authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-09 16:38:41-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-09 16:38:41-07:00
log58916b6554f45ac518bd8150dd10f1f536e40aac
tree5fc37b4d3d247bf4c10b768684785d0183d2804d
parent51347aca3aa73112bc1e288ae45507ce515f42a2
signature Signed by PGP key B5690EEEBB952194

refactor: home directory comes from a HostProcessHomeDirectory reference (#17628)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

74 files changed, 442 insertions(+), 308 deletions(-)

apps/desktop/src/app/DesktopApp.ts+4-1
......@@ -35,6 +35,7 @@ import * as DesktopUpdates from "../updates/DesktopUpdates.ts";
3535import * as DesktopSnapShot from "../snapShot/DesktopSnapShot.ts";
3636import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts";
3737import * as DesktopRendererHistory from "../telemetry/DesktopRendererHistory.ts";
38import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
3839
3940const DEFAULT_DESKTOP_BACKEND_PORT = 3773;
4041const MAX_TCP_PORT = 65_535;
......@@ -284,7 +285,9 @@ const startup = Effect.gen(function* () {
284285 preReadyElectronOptions.linuxPasswordStoreCommandLine !== null;
285286 const linuxElectronOptions =
286287 environment.platform === "linux" && !hasCommandLinePasswordStore
287 ? DesktopPreReadyPlatform.resolveEarlyLinuxElectronOptionsFromProcess()
288 ? DesktopPreReadyPlatform.resolveEarlyLinuxElectronOptionsFromProcess(
289 yield* HostProcessHomeDirectory,
290 )
288291 : preReadyElectronOptions.linux;
289292 if (linuxElectronOptions !== null && !hasCommandLinePasswordStore) {
290293 if (
apps/desktop/src/app/DesktopPreReadyPlatform.ts+14-12
......@@ -1,13 +1,12 @@
11// @effect-diagnostics nodeBuiltinImport:off - pre-ready Electron setup reads settings and prepares the Linux desktop entry synchronously before app services are available.
22import * as NodeFS from "node:fs";
3import * as NodeOS from "node:os";
43import * as NodePath from "node:path";
54import * as Context from "effect/Context";
65import * as Effect from "effect/Effect";
76import * as Layer from "effect/Layer";
87
98import * as Electron from "electron";
10import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
9import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
1110
1211import * as DesktopEarlyElectronStartup from "./DesktopEarlyElectronStartup.ts";
1312import { resolveDesktopAppBranding } from "./DesktopEnvironment.ts";
......@@ -31,14 +30,15 @@ function readCommandLineSwitchValue(
3130 return value.length > 0 ? value : null;
3231}
3332
34export const resolveEarlyLinuxElectronOptionsFromProcess =
35 (): DesktopEarlyElectronStartup.EarlyLinuxElectronOptions =>
36 DesktopEarlyElectronStartup.resolveEarlyLinuxElectronOptions({
37 env: process.env,
38 homeDirectory: NodeOS.homedir(),
39 joinPath: NodePath.posix.join,
40 readFileString: (path) => NodeFS.readFileSync(path, "utf8"),
41 });
33export const resolveEarlyLinuxElectronOptionsFromProcess = (
34 homeDirectory: string,
35): DesktopEarlyElectronStartup.EarlyLinuxElectronOptions =>
36 DesktopEarlyElectronStartup.resolveEarlyLinuxElectronOptions({
37 env: process.env,
38 homeDirectory,
39 joinPath: NodePath.posix.join,
40 readFileString: (path) => NodeFS.readFileSync(path, "utf8"),
41 });
4242
4343export class DesktopPreReadyElectronOptions extends Context.Service<
4444 DesktopPreReadyElectronOptions,
......@@ -51,12 +51,14 @@ export class DesktopPreReadyElectronOptions extends Context.Service<
5151/** @public Service construction is part of the canonical Effect module API. */
5252export const make = Effect.gen(function* () {
5353 const platform = yield* HostProcessPlatform;
54 const homeDirectory = yield* HostProcessHomeDirectory;
5455 return yield* Effect.sync((): DesktopPreReadyElectronOptions["Service"] => {
5556 const linuxPasswordStoreCommandLine =
5657 platform === "linux"
5758 ? readCommandLineSwitchValue(Electron.app.commandLine, "password-store")
5859 : null;
59 const linux = platform === "linux" ? resolveEarlyLinuxElectronOptionsFromProcess() : null;
60 const linux =
61 platform === "linux" ? resolveEarlyLinuxElectronOptionsFromProcess(homeDirectory) : null;
6062
6163 if (linux !== null) {
6264 // The portal also requires a valid desktop entry. An AppImage update may
......@@ -64,7 +66,7 @@ export const make = Effect.gen(function* () {
6466 try {
6567 const applicationsDir = NodePath.posix.join(
6668 process.env.XDG_DATA_HOME?.trim() ||
67 NodePath.posix.join(NodeOS.homedir(), ".local", "share"),
69 NodePath.posix.join(homeDirectory, ".local", "share"),
6870 "applications",
6971 );
7072 NodeFS.mkdirSync(applicationsDir, { recursive: true });
apps/desktop/src/ipc/methods/window.ts+2-2
......@@ -16,7 +16,6 @@ import {
1616import { WORKSPACE_IMAGE_PREVIEW_EXTENSIONS } from "@t3tools/shared/filePreview";
1717import { resolveEditorCommand } from "@t3tools/shared/editor";
1818import * as HostProcess from "@t3tools/shared/hostProcess";
19import * as NodeOS from "node:os";
2019import * as FileSystem from "effect/FileSystem";
2120import * as Path from "effect/Path";
2221import * as Effect from "effect/Effect";
......@@ -46,6 +45,7 @@ import {
4645 resolveWslPickFolderDefaultPath,
4746 wslUncPathToLinuxPath,
4847} from "../../wsl/wslPathParsing.ts";
48import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
4949
5050const ContextMenuPosition = Schema.Struct({
5151 x: Schema.Number,
......@@ -398,7 +398,7 @@ export const pickThemeFiles = DesktopIpc.makeIpcMethod({
398398 // The VS Code extensions directory is the same dotfolder on Windows,
399399 // macOS, and Linux; when it is missing the picker opens wherever the
400400 // platform would by default.
401 const extensionsDir = path.join(NodeOS.homedir(), ".vscode", "extensions");
401 const extensionsDir = path.join(yield* HostProcessHomeDirectory, ".vscode", "extensions");
402402 const defaultPath = yield* fileSystem
403403 .exists(extensionsDir)
404404 .pipe(Effect.orElseSucceed(() => false));
apps/desktop/src/snapShot/CaptureShortcutConfig.test.ts+8-8
......@@ -29,7 +29,7 @@ beforeEach(async () => {
2929 tools.validateNiri.mockReset().mockResolvedValue(undefined);
3030 tools.hyprlandBindings.mockReset().mockResolvedValue([]);
3131 tools.reloadHyprland.mockReset().mockResolvedValue(undefined);
32 setup = new CaptureShortcutConfig(tools);
32 setup = new CaptureShortcutConfig(NodeOS.homedir(), tools);
3333});
3434afterEach(async () => {
3535 await NodeFSP.rm(directory, { recursive: true, force: true });
......@@ -219,15 +219,15 @@ it("does not edit Omarchy's shipped defaults", async () => {
219219});
220220it("honors NIRI_CONFIG and XDG_CONFIG_HOME", () => {
221221 expect(
222 niriCaptureConfigPath(
223 { NIRI_CONFIG: "/custom/test.kdl", XDG_CONFIG_HOME: "/config" },
224 "/home/test",
225 ),
222 niriCaptureConfigPath("/home/test", {
223 NIRI_CONFIG: "/custom/test.kdl",
224 XDG_CONFIG_HOME: "/config",
225 }),
226226 ).toBe("/custom/test.kdl");
227 expect(niriCaptureConfigPath({ XDG_CONFIG_HOME: "/config" }, "/home/test")).toBe(
227 expect(niriCaptureConfigPath("/home/test", { XDG_CONFIG_HOME: "/config" })).toBe(
228228 "/config/niri/config.kdl",
229229 );
230 expect(niriCaptureConfigPath({}, "/home/test")).toBe("/home/test/.config/niri/config.kdl");
230 expect(niriCaptureConfigPath("/home/test", {})).toBe("/home/test/.config/niri/config.kdl");
231231});
232232it("preserves a UTF-8 BOM through preview and apply", async () => {
233233 await NodeFSP.writeFile(path, "\uFEFFbinds {}\r\n");
......@@ -251,7 +251,7 @@ it("the installed Niri parser accepts the generated edit and its relative includ
251251 NodePath.join(directory, "keys.kdl"),
252252 "binds {\n Ctrl+Alt+Q { quit; }\n}\n",
253253 );
254 const checked = new CaptureShortcutConfig({
254 const checked = new CaptureShortcutConfig(NodeOS.homedir(), {
255255 ...tools,
256256 validateNiri: async (file) => {
257257 await NodeUtil.promisify(NodeChildProcess.execFile)("niri", ["validate", "--config", file]);
apps/desktop/src/snapShot/CaptureShortcutConfig.ts+5-4
......@@ -2,7 +2,6 @@
22import * as NodeFSP from "node:fs/promises";
33import * as NodeFS from "node:fs";
44import * as NodePath from "node:path";
5import * as NodeOS from "node:os";
65import * as NodeCrypto from "node:crypto";
76import * as NodeChildProcess from "node:child_process";
87import * as NodeUtil from "node:util";
......@@ -39,7 +38,7 @@ const decodeHyprlandBindings = Schema.decodeUnknownSync(
3938 ),
4039);
4140
42export function niriCaptureConfigPath(env = process.env, home = NodeOS.homedir()) {
41export function niriCaptureConfigPath(home: string, env = process.env) {
4342 return (
4443 env.NIRI_CONFIG ||
4544 NodePath.join(env.XDG_CONFIG_HOME || NodePath.join(home, ".config"), "niri", "config.kdl")
......@@ -99,8 +98,10 @@ export class CaptureShortcutConfig {
9998 | { preview: DesktopCaptureConfigPreview; target: Target; files: Snapshot[]; missing: string[] }
10099 | undefined;
101100 private applying = false;
101 private readonly home: string;
102102 private readonly tools: typeof defaultTools;
103 constructor(tools = defaultTools) {
103 constructor(home: string, tools = defaultTools) {
104 this.home = home;
104105 this.tools = tools;
105106 }
106107
......@@ -166,7 +167,7 @@ export class CaptureShortcutConfig {
166167 if (include.path.includes("$") || /[*?[\]]/.test(include.path))
167168 throw new Error("This config uses a dynamic include. Use manual setup in Advanced.");
168169 const path = include.path.startsWith("~/")
169 ? NodePath.join(NodeOS.homedir(), include.path.slice(2))
170 ? NodePath.join(this.home, include.path.slice(2))
170171 : NodePath.resolve(NodePath.dirname(file.path), include.path);
171172 let child: Snapshot;
172173 try {
apps/desktop/src/snapShot/DesktopSnapShot.ts+8-6
......@@ -88,6 +88,7 @@ import {
8888 snapShotShortcutRegistrationFailureMessage,
8989 snapShotShortcutSystemConflict,
9090} from "./snapShot.ts";
91import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
9192
9293const MAX_CAPTURE_WIDTH = 2_560;
9394const MAX_CAPTURE_HEIGHT = 1_600;
......@@ -727,6 +728,7 @@ export const make = Effect.gen(function* () {
727728 const runPromise = Effect.runPromiseWith(context);
728729 const captureDirectory = path.join(environment.stateDir, "snap-shots");
729730 const linuxAppId = environment.linuxDesktopEntryName.replace(/\.desktop$/, "");
731 const home = yield* HostProcessHomeDirectory;
730732 let shortcutVerified = false;
731733 const gnomeSetupPaths = {
732734 bundle: environment.isPackaged
......@@ -757,7 +759,7 @@ export const make = Effect.gen(function* () {
757759 dataHome: path.dirname(environment.linuxApplicationsDir),
758760 };
759761 const shiftShortcutWorkerPath = path.join(__dirname, "snapShot", "GlobalShiftShortcutWorker.cjs");
760 const shortcutConfig = new CaptureShortcutConfig();
762 const shortcutConfig = new CaptureShortcutConfig(home);
761763 const accessibilityWorkerPath = path.join(
762764 __dirname,
763765 "snapShot",
......@@ -1184,7 +1186,7 @@ export const make = Effect.gen(function* () {
11841186 shortcut,
11851187 shortcutRegistered: false,
11861188 shortcutBinding: niriCaptureBinding(linuxAppId),
1187 shortcutConfigPath: niriCaptureConfigPath(),
1189 shortcutConfigPath: niriCaptureConfigPath(home),
11881190 shortcutActionRegistered: registered,
11891191 shortcutMessage: registered
11901192 ? "Set up the shortcut to add it to your Niri config."
......@@ -1474,8 +1476,8 @@ export const make = Effect.gen(function* () {
14741476 const configPath =
14751477 selectedPath ??
14761478 (desktop === "niri"
1477 ? niriCaptureConfigPath()
1478 : (await hyprlandCaptureShortcut(linuxAppId)).shortcutConfigPath);
1479 ? niriCaptureConfigPath(home)
1480 : (await hyprlandCaptureShortcut(linuxAppId, home)).shortcutConfigPath);
14791481 return shortcutConfig.preview({ desktop, path: configPath, appId: linuxAppId }, request);
14801482 },
14811483 catch: (cause) =>
......@@ -1593,7 +1595,7 @@ export const make = Effect.gen(function* () {
15931595 : undefined;
15941596 const hyprlandShortcut =
15951597 state.linuxBackend === "hyprland"
1596 ? yield* Effect.promise(() => hyprlandCaptureShortcut(linuxAppId))
1598 ? yield* Effect.promise(() => hyprlandCaptureShortcut(linuxAppId, home))
15971599 : undefined;
15981600 return {
15991601 ...state,
......@@ -1609,7 +1611,7 @@ export const make = Effect.gen(function* () {
16091611 : {}),
16101612 ...hyprlandShortcut,
16111613 ...(state.linuxBackend === "niri"
1612 ? { shortcutConfigPath: niriCaptureConfigPath() }
1614 ? { shortcutConfigPath: niriCaptureConfigPath(home) }
16131615 : {}),
16141616 ...(kdeHelper
16151617 ? {
apps/desktop/src/snapShot/HyprlandSnapShot.test.ts+1-1
......@@ -121,7 +121,7 @@ it.each([true, false])("uses the active config syntax and user bindings (Lua: %s
121121 NodePath.join(config, lua ? "bindings.lua" : "bindings.conf"),
122122 "custom bindings",
123123 );
124 const result = await hyprlandCaptureShortcut("com.t3tools.T3Code", directory);
124 const result = await hyprlandCaptureShortcut("com.t3tools.T3Code", "/home/test", directory);
125125 expect(result.shortcutConfigPath).toBe(
126126 NodePath.join(config, lua ? "bindings.lua" : "bindings.conf"),
127127 );
apps/desktop/src/snapShot/HyprlandSnapShot.ts+2-1
......@@ -29,7 +29,8 @@ function hyprlandCaptureBinding(appId: string, lua: boolean): string {
2929/** Omarchy owns its defaults; instructions always point at a user-owned config. */
3030export async function hyprlandCaptureShortcut(
3131 appId: string,
32 configHome = process.env.XDG_CONFIG_HOME || NodePath.join(NodeOS.homedir(), ".config"),
32 home: string,
33 configHome = process.env.XDG_CONFIG_HOME || NodePath.join(home, ".config"),
3334) {
3435 const directory = NodePath.join(configHome, "hypr");
3536 const exists = async (name: string) =>
apps/server/src/assets/AssetAccess.test.ts+3-11
......@@ -2,7 +2,6 @@
22import * as NodeServices from "@effect/platform-node/NodeServices";
33import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform";
44import * as NodeFSP from "node:fs/promises";
5import * as NodeOS from "node:os";
65import {
76 AssetAccessError,
87 AssetPreviewTypeValidationError,
......@@ -37,17 +36,13 @@ import { openMediaFile } from "./MediaFile.ts";
3736import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
3837import * as GitHubCredentials from "@t3tools/source-control-github/server/GitHubCredentials";
3938import { githubMediaResponse } from "./GitHubMediaFetch.ts";
39import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
4040
4141vi.mock("node:fs/promises", async (importOriginal) => {
4242 const actual = await importOriginal<typeof NodeFSP>();
4343 return { ...actual, open: vi.fn(actual.open), realpath: vi.fn(actual.realpath) };
4444});
4545
46vi.mock("node:os", async (importOriginal) => {
47 const actual = await importOriginal<typeof NodeOS>();
48 return { ...actual, homedir: vi.fn(actual.homedir) };
49});
50
5146const layerConfig = ServerConfig.ServerConfig.layerTest(process.cwd(), {
5247 prefix: "t3-asset-access-test-",
5348});
......@@ -299,8 +294,7 @@ describe("AssetAccess", () => {
299294 yield* fs.makeDirectory(path.dirname(filePath), { recursive: true });
300295 yield* fs.writeFileString(filePath, "recording bytes");
301296 const canonicalFile = yield* fs.realPath(filePath);
302 const homeSpy = vi.mocked(NodeOS.homedir).mockReturnValue(home);
303 try {
297 yield* Effect.gen(function* () {
304298 for (const workspaceRoot of [
305299 path.join(home, "project"),
306300 path.join(directory, "srv", "project"),
......@@ -332,9 +326,7 @@ describe("AssetAccess", () => {
332326 expect(yield* Effect.promise(() => response.text())).toBe("recording bytes");
333327 }
334328 }
335 } finally {
336 homeSpy.mockRestore();
337 }
329 }).pipe(Effect.provideService(HostProcessHomeDirectory, home));
338330 }).pipe(Effect.provide(layerTest)),
339331 );
340332
apps/server/src/assets/AssetAccess.ts+2-1
......@@ -58,6 +58,7 @@ import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts";
5858import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";
5959import * as NativeAppIconResolver from "./NativeAppIconResolver.ts";
6060import { openMediaFile, readMediaFileHeader, type OpenMediaFile } from "./MediaFile.ts";
61import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
6162
6263export const ASSET_ROUTE_PREFIX = "/api/assets";
6364
......@@ -469,7 +470,7 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i
469470
470471 switch (input.resource._tag) {
471472 case "media-file": {
472 let requestedPath = expandHomePath(input.resource.path);
473 let requestedPath = expandHomePath(input.resource.path, yield* HostProcessHomeDirectory);
473474 if (!path.isAbsolute(requestedPath)) {
474475 if (!input.workspaceRoot) {
475476 return yield* new AssetWorkspaceContextNotFoundError({ resource: input.resource });
apps/server/src/cli/app.test.ts+6-16
......@@ -9,6 +9,7 @@ import { it } from "@effect/vitest";
99import type { DesktopAppActivationRequest } from "@t3tools/contracts";
1010import { resolveDesktopAppControlAddress } from "@t3tools/shared/desktopAppControl";
1111import {
12 HostProcessHomeDirectory,
1213 HostProcessPlatform,
1314 HostProcessUserId,
1415 HostProcessWorkingDirectory,
......@@ -19,18 +20,11 @@ import * as Effect from "effect/Effect";
1920import * as Layer from "effect/Layer";
2021import * as Schema from "effect/Schema";
2122import { Command } from "effect/cli";
22import { afterEach, describe, expect, vi } from "vite-plus/test";
23import { describe, expect } from "vite-plus/test";
2324
2425import { makeCli } from "../binCli.ts";
2526import { PersistedServerRuntimeState } from "../serverRuntimeState.ts";
2627
27vi.mock("node:os", async (importOriginal) => {
28 const os = await importOriginal<typeof import("node:os")>();
29 return { ...os, homedir: vi.fn(os.homedir) };
30});
31
32afterEach(() => vi.mocked(NodeOS.homedir).mockReset());
33
3428const encodeRuntimeState = Schema.encodeEffect(Schema.fromJsonString(PersistedServerRuntimeState));
3529
3630const runCli = (args: ReadonlyArray<string>, env: Record<string, string> = {}) =>
......@@ -296,7 +290,6 @@ describe("t3 app", () => {
296290 it.effect("prefers the installed desktop app when a dev desktop is also running", () =>
297291 withTempDirectory("t3-app-preferred-test-", (root) =>
298292 Effect.gen(function* () {
299 vi.mocked(NodeOS.homedir).mockReturnValue(root);
300293 const baseDir = NodePath.join(root, ".t3");
301294 const desktop = yield* fakeDesktop({ baseDir });
302295 const development = yield* fakeDesktop({ baseDir, stateSubdirectory: "dev" });
......@@ -305,14 +298,13 @@ describe("t3 app", () => {
305298
306299 expect(desktop.received).toHaveLength(1);
307300 expect(development.received).toHaveLength(0);
308 }).pipe(Effect.scoped),
301 }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped),
309302 ),
310303 );
311304
312305 it.effect("finds the dev desktop when the default desktop socket is absent", () =>
313306 withTempDirectory("t3-app-dev-test-", (root) =>
314307 Effect.gen(function* () {
315 vi.mocked(NodeOS.homedir).mockReturnValue(root);
316308 const baseDir = NodePath.join(root, ".t3");
317309 const development = yield* fakeDesktop({ baseDir, stateSubdirectory: "dev" });
318310
......@@ -321,14 +313,13 @@ describe("t3 app", () => {
321313
322314 expect(development.received).toHaveLength(2);
323315 expect(yield* pathExists(baseDir)).toBe(false);
324 }).pipe(Effect.scoped),
316 }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped),
325317 ),
326318 );
327319
328320 it.effect("never searches a dev state directory for an explicit T3 home", () =>
329321 withTempDirectory("t3-app-explicit-test-", (root) =>
330322 Effect.gen(function* () {
331 vi.mocked(NodeOS.homedir).mockReturnValue(root);
332323 const baseDir = NodePath.join(root, ".t3");
333324 const development = yield* fakeDesktop({ baseDir, stateSubdirectory: "dev" });
334325
......@@ -338,7 +329,7 @@ describe("t3 app", () => {
338329 expect(flagError).toMatchObject({ _tag: "DesktopAppUnreachableError" });
339330 expect(envError).toMatchObject({ _tag: "DesktopAppUnreachableError" });
340331 expect(development.received).toHaveLength(0);
341 }).pipe(Effect.scoped),
332 }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped),
342333 ),
343334 );
344335
......@@ -347,7 +338,6 @@ describe("t3 app", () => {
347338 (responseKind) =>
348339 withTempDirectory("t3-app-response-test-", (root) =>
349340 Effect.gen(function* () {
350 vi.mocked(NodeOS.homedir).mockReturnValue(root);
351341 const baseDir = NodePath.join(root, ".t3");
352342 const desktop = yield* fakeDesktop({
353343 baseDir,
......@@ -387,7 +377,7 @@ describe("t3 app", () => {
387377 cause: { message: "The desktop app response is invalid." },
388378 });
389379 }
390 }).pipe(Effect.scoped),
380 }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped),
391381 ),
392382 );
393383});
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts+3-3
......@@ -31,7 +31,7 @@ import type {
3131 WebSearchOutput,
3232} from "@anthropic-ai/claude-agent-sdk/sdk-tools";
3333import { parseCliArgs } from "@t3tools/shared/cliArgs";
34import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";
34import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
3535import { applyClaudePromptEffortPrefix } from "@t3tools/shared/model";
3636import {
3737 CLAUDE_RESUME_COMPACTION_NEVER_ANSWER,
......@@ -8181,12 +8181,12 @@ export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")(
81818181 const queryRunner = yield* ClaudeAgentSdkQueryRunner;
81828182 const serverConfig = yield* ServerConfig.ServerConfig;
81838183 const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
8184 const baseEnvironment = mergeProviderInstanceEnvironment(environment, hostEnvironment);
8184 const baseEnvironment = yield* mergeProviderInstanceEnvironment(environment, hostEnvironment);
81858185 const claudeEnvironment = yield* makeClaudeEnvironment(config, baseEnvironment);
81868186 const path = yield* Path.Path;
81878187 const crypto = yield* Crypto.Crypto;
81888188 const binaryPath = yield* resolveClaudeSdkExecutablePath(
8189 expandHomePath(config.binaryPath),
8189 expandHomePath(config.binaryPath, yield* HostProcessHomeDirectory),
81908190 claudeEnvironment,
81918191 );
81928192 return yield* makeClaudeAdapterV2({
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts+3-3
......@@ -30,7 +30,7 @@ import {
3030 type ProviderSetupError,
3131} from "@t3tools/contracts";
3232import { SKILL_MENTION_PATTERN } from "@t3tools/shared/composerInlineTokens";
33import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";
33import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
3434import { dynamicToolTitle } from "@t3tools/shared/toolActivity";
3535import { getModelSelectionStringOptionValue, modelSelectionsEqual } from "@t3tools/shared/model";
3636import { resolveSpawnCommand } from "@t3tools/shared/shell";
......@@ -1582,14 +1582,14 @@ export const createCodexAdapterV2 = (
15821582 const settings = {
15831583 ...config,
15841584 enabled,
1585 binaryPath: expandHomePath(config.binaryPath),
1585 binaryPath: expandHomePath(config.binaryPath, yield* HostProcessHomeDirectory),
15861586 homePath: homeLayout.effectiveHomePath ?? "",
15871587 } satisfies CodexSettings;
15881588
15891589 return yield* makeCodexAdapterV2({
15901590 instanceId,
15911591 settings,
1592 environment: mergeProviderInstanceEnvironment(environment, hostEnvironment),
1592 environment: yield* mergeProviderInstanceEnvironment(environment, hostEnvironment),
15931593 clientFactory,
15941594 crypto,
15951595 fileSystem,
apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts+24-12
......@@ -94,9 +94,12 @@ describe("acpSubagentStatusBlocksTurnSettlement", () => {
9494
9595describe("GrokAdapterV2 capabilities", () => {
9696 it("preserves Grok's rate-limit stop and distinguishes other prompt failures", () => {
97 const flavor = makeGrokAcpAdapterFlavor({
98 makeRuntime: () => Effect.never,
99 } as unknown as Parameters<typeof makeGrokAcpAdapterFlavor>[0]);
97 const flavor = makeGrokAcpAdapterFlavor(
98 {
99 makeRuntime: () => Effect.never,
100 } as unknown as Parameters<typeof makeGrokAcpAdapterFlavor>[0],
101 "/home/ada",
102 );
100103 const limit = flavor.promptFailure?.(
101104 new EffectAcpErrors.AcpRequestError({
102105 code: xAiRateLimitedErrorCode,
......@@ -122,9 +125,12 @@ describe("GrokAdapterV2 capabilities", () => {
122125 });
123126
124127 it("wires hard Stop teardown but soft non-Stop interrupts in the constructor flavor", () => {
125 const flavor = makeGrokAcpAdapterFlavor({
126 makeRuntime: () => Effect.never,
127 } as unknown as Parameters<typeof makeGrokAcpAdapterFlavor>[0]);
128 const flavor = makeGrokAcpAdapterFlavor(
129 {
130 makeRuntime: () => Effect.never,
131 } as unknown as Parameters<typeof makeGrokAcpAdapterFlavor>[0],
132 "/home/ada",
133 );
128134
129135 assert.isFalse(flavor.interruptPromptOnCancel);
130136 // User Stop (requestRuntimeRestart) keeps the hard process-group kill and
......@@ -139,9 +145,12 @@ describe("GrokAdapterV2 capabilities", () => {
139145 });
140146
141147 it("terminalizes only foreground tools under the actual Grok flavor", () => {
142 const flavor = makeGrokAcpAdapterFlavor({
143 makeRuntime: () => Effect.never,
144 } as unknown as Parameters<typeof makeGrokAcpAdapterFlavor>[0]);
148 const flavor = makeGrokAcpAdapterFlavor(
149 {
150 makeRuntime: () => Effect.never,
151 } as unknown as Parameters<typeof makeGrokAcpAdapterFlavor>[0],
152 "/home/ada",
153 );
145154 const foreground = {
146155 toolCallId: "foreground-1",
147156 title: "Terminal",
......@@ -284,9 +293,12 @@ describe("ACP permission policy", () => {
284293});
285294
286295describe("Grok permission prompts", () => {
287 const disposition = makeGrokAcpAdapterFlavor({
288 makeRuntime: () => Effect.never,
289 } as unknown as Parameters<typeof makeGrokAcpAdapterFlavor>[0]).permissionDisposition;
296 const disposition = makeGrokAcpAdapterFlavor(
297 {
298 makeRuntime: () => Effect.never,
299 } as unknown as Parameters<typeof makeGrokAcpAdapterFlavor>[0],
300 "/home/ada",
301 ).permissionDisposition;
290302
291303 // grok_auto_blocked_command replays Auto end to end. When an explicit policy
292304 // launches Grok asking instead, T3's policy still answers its prompts.
apps/server/src/orchestration-v2/workflowScriptQuery.ts+5-4
......@@ -14,16 +14,16 @@
1414 * never trusted beyond these checks.
1515 */
1616import * as NodeFSP from "node:fs/promises";
17import * as NodeOS from "node:os";
1817import * as NodePath from "node:path";
1918
2019import { OrchestrationGetWorkflowScriptError } from "@t3tools/contracts";
2120import * as Effect from "effect/Effect";
21import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
2222
2323const SCRIPT_BYTE_CAP = 256 * 1024;
2424
25function scriptsRoot(): string {
26 return NodePath.join(NodeOS.homedir(), ".claude", "projects");
25function scriptsRoot(home: string): string {
26 return NodePath.join(home, ".claude", "projects");
2727}
2828
2929export const readWorkflowScript = Effect.fn("orchestration.readWorkflowScript")(function* (input: {
......@@ -38,8 +38,9 @@ export const readWorkflowScript = Effect.fn("orchestration.readWorkflowScript")(
3838 });
3939 }
4040
41 const home = yield* HostProcessHomeDirectory;
4142 const root = yield* Effect.tryPromise({
42 try: () => NodeFSP.realpath(scriptsRoot()),
43 try: () => NodeFSP.realpath(scriptsRoot(home)),
4344 catch: (cause) =>
4445 new OrchestrationGetWorkflowScriptError({
4546 reason: "root-unavailable",
apps/server/src/os-jank.ts+9-4
......@@ -1,4 +1,8 @@
1import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";
1import {
2 HostProcessEnvironment,
3 HostProcessHomeDirectory,
4 HostProcessPlatform,
5} from "@t3tools/shared/hostProcess";
26import {
37 listLoginShellCandidates,
48 mergePathEntries,
......@@ -93,11 +97,12 @@ export const fixPath = Effect.fn("fixPath")(function* (): Effect.fn.Return<
9397
9498export const expandHomePath = Effect.fn(function* (input: string) {
9599 const { join } = yield* Path.Path;
100 const home = yield* HostProcessHomeDirectory;
96101 if (input === "~") {
97 return NodeOS.homedir();
102 return home;
98103 }
99104 if (input.startsWith("~/") || input.startsWith("~\\")) {
100 return join(NodeOS.homedir(), input.slice(2));
105 return join(home, input.slice(2));
101106 }
102107 return input;
103108});
......@@ -105,7 +110,7 @@ export const expandHomePath = Effect.fn(function* (input: string) {
105110export const resolveBaseDir = Effect.fn(function* (raw: string | undefined) {
106111 const { join, resolve } = yield* Path.Path;
107112 if (!raw || raw.trim().length === 0) {
108 return join(NodeOS.homedir(), ".t3");
113 return join(yield* HostProcessHomeDirectory, ".t3");
109114 }
110115 return resolve(yield* expandHomePath(raw.trim()));
111116});
apps/server/src/project/AgentSessionScanner.ts+14-10
......@@ -44,7 +44,11 @@ import {
4444 parseGitHubRepositoryNameWithOwnerFromRemoteUrl,
4545 parseOriginUrlFromGitConfig,
4646} from "@t3tools/shared/git";
47import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";
47import {
48 HostProcessEnvironment,
49 HostProcessHomeDirectory,
50 HostProcessPlatform,
51} from "@t3tools/shared/hostProcess";
4852import { normalizeProjectPathForComparison } from "@t3tools/shared/path";
4953
5054import * as ServerConfig from "../config.ts";
......@@ -631,7 +635,7 @@ export const make = Effect.gen(function* () {
631635 // must case fold.
632636 const foldWorktreeCase = (yield* HostProcessPlatform) === "win32";
633637 const hostEnvironment = yield* HostProcessEnvironment;
634 const homeDir = NodeOS.homedir();
638 const homeDir = yield* HostProcessHomeDirectory;
635639 // `/private/tmp` is what macOS reports for sessions started in `/tmp`.
636640 const excludedProjectRoots = new Set(
637641 [homeDir, NodeOS.tmpdir(), "/tmp", "/private/tmp"].map((directory) =>
......@@ -912,13 +916,13 @@ export const make = Effect.gen(function* () {
912916 const resolveClaudeConfigDir = (homePath: string, environmentHome?: string): string => {
913917 const configured = homePath.trim();
914918 if (configured.length > 0) {
915 return path.resolve(expandHomePath(configured));
919 return path.resolve(expandHomePath(configured, homeDir));
916920 }
917921 const fromEnvironment = environmentHome?.trim() ?? "";
918922 if (fromEnvironment.length > 0) {
919 return path.resolve(expandHomePath(fromEnvironment));
923 return path.resolve(expandHomePath(fromEnvironment, homeDir));
920924 }
921 return path.join(NodeOS.homedir(), ".claude");
925 return path.join(homeDir, ".claude");
922926 };
923927
924928 const discoverClaudeTranscripts = Effect.fn("AgentSessionScanner.discoverClaudeTranscripts")(
......@@ -1217,7 +1221,7 @@ export const make = Effect.gen(function* () {
12171221 const gitIdentities = new Map<string, AgentSessionProjectGit | null>();
12181222
12191223 for (const candidate of raw) {
1220 const expanded = expandHomePath(candidate.cwd.trim());
1224 const expanded = expandHomePath(candidate.cwd.trim(), homeDir);
12211225 if (!path.isAbsolute(expanded)) continue;
12221226 const resolved = path.resolve(expanded);
12231227 if (isExcludedProjectPath(resolved)) continue;
......@@ -1281,7 +1285,7 @@ export const make = Effect.gen(function* () {
12811285 );
12821286 const importedProjectsByRoot = new Map<string, (typeof importedProjects)[number]>();
12831287 for (const project of importedProjects) {
1284 const projectRoot = path.resolve(expandHomePath(project.workspaceRoot));
1288 const projectRoot = path.resolve(expandHomePath(project.workspaceRoot, homeDir));
12851289 importedProjectsByRoot.set(normalizeProjectPathForComparison(projectRoot), project);
12861290 importedProjectsByRoot.set(yield* directoryIdentity(projectRoot), project);
12871291 }
......@@ -1328,7 +1332,7 @@ export const make = Effect.gen(function* () {
13281332 workspaceRoot: string,
13291333 completedSources: ReadonlyArray<AgentSessionImportSource>,
13301334 ) {
1331 const root = path.resolve(expandHomePath(workspaceRoot));
1335 const root = path.resolve(expandHomePath(workspaceRoot, homeDir));
13321336 const realRoot = yield* fileSystem.realPath(root).pipe(Effect.orElseSucceed(() => root));
13331337 if (isExcludedProjectPath(root) || isExcludedProjectPath(realRoot)) return Stream.empty;
13341338 const rootIdentity = yield* directoryIdentity(root);
......@@ -1343,7 +1347,7 @@ export const make = Effect.gen(function* () {
13431347 readonly transcript: RawCandidate["transcripts"][number] & { readonly mtimeMs: number };
13441348 }> = [];
13451349 for (const candidate of candidates) {
1346 const expanded = expandHomePath(candidate.cwd.trim());
1350 const expanded = expandHomePath(candidate.cwd.trim(), homeDir);
13471351 if (!path.isAbsolute(expanded)) continue;
13481352 const resolved = path.resolve(expanded);
13491353 if ((yield* directoryIdentity(resolved)) !== rootIdentity) continue;
......@@ -1439,7 +1443,7 @@ export const make = Effect.gen(function* () {
14391443 if (snapshotCwd === null) {
14401444 return Option.some<AgentSessionRecentThread>({ _tag: "Skipped" });
14411445 }
1442 const expandedCwd = expandHomePath(snapshotCwd.trim());
1446 const expandedCwd = expandHomePath(snapshotCwd.trim(), homeDir);
14431447 if (
14441448 !path.isAbsolute(expandedCwd) ||
14451449 (yield* directoryIdentity(path.resolve(expandedCwd))) !== rootIdentity
apps/server/src/provider/CodexProvider.ts+4-1
......@@ -47,6 +47,7 @@ import {
4747 type CodexRateLimitSnapshot,
4848 type CodexResetCreditsSummary,
4949} from "./codexUsageLimits.ts";
50import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
5051import packageJson from "../../package.json" with { type: "json" };
5152const isCodexAppServerSpawnError = Schema.is(CodexErrors.CodexAppServerSpawnError);
5253const RATE_LIMITS_PROBE_TIMEOUT_MS = 3_000;
......@@ -374,7 +375,9 @@ export const withCodexAppServerClient = Effect.fn("withCodexAppServerClient")(fu
374375 // so `CODEX_HOME=~/.codex_work` would reach codex verbatim and trip
375376 // "CODEX_HOME points to '~/.codex_work', but that path does not exist".
376377 // Expand here for parity with `CodexTextGeneration`.
377 const resolvedHomePath = input.homePath ? expandHomePath(input.homePath) : undefined;
378 const resolvedHomePath = input.homePath
379 ? expandHomePath(input.homePath, yield* HostProcessHomeDirectory)
380 : undefined;
378381 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
379382 const environment = {
380383 ...input.environment,
apps/server/src/provider/Drivers/AntigravityDriver.ts+7-3
......@@ -1,7 +1,7 @@
11import { withAgentDeviceEnvironment } from "@t3tools/provider-core/server/mcpSession";
22import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
33import { AntigravitySettings, ProviderDriverKind, ProviderSetupError } from "@t3tools/contracts";
4import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
4import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
55import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime";
66import {
77 NodeRuntimeUnavailableError,
......@@ -113,8 +113,12 @@ export const AntigravityDriver: ProviderDriver<
113113 gcpLocation: settings.gcpLocation,
114114 };
115115 const authConfigIssue = antigravityAuthConfigIssue(auth);
116 const processEnvironment = mergeProviderInstanceEnvironment(environment);
117 const userHome = resolveAntigravityUserHome(yield* HostProcessPlatform, processEnvironment);
116 const processEnvironment = yield* mergeProviderInstanceEnvironment(environment);
117 const userHome = resolveAntigravityUserHome(
118 yield* HostProcessPlatform,
119 processEnvironment,
120 yield* HostProcessHomeDirectory,
121 );
118122 const directories = yield* resolveAntigravityInstanceDirectories(
119123 host.paths.stateDir,
120124 instanceId,
apps/server/src/provider/Drivers/AntigravitySkills.test.ts+23-5
......@@ -344,17 +344,35 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => {
344344
345345it("resolves the home the agent expands ~ against", () => {
346346 assert.equal(
347 resolveAntigravityUserHome("linux", { HOME: "/home/user", USERPROFILE: "C:\\Users\\user" }),
347 resolveAntigravityUserHome(
348 "linux",
349 { HOME: "/home/user", USERPROFILE: "C:\\Users\\user" },
350 "/home/fallback",
351 ),
348352 "/home/user",
349353 );
350354 assert.equal(
351 resolveAntigravityUserHome("win32", { HOME: "/home/user", USERPROFILE: "C:\\Users\\user" }),
355 resolveAntigravityUserHome(
356 "win32",
357 { HOME: "/home/user", USERPROFILE: "C:\\Users\\user" },
358 "/home/fallback",
359 ),
352360 "C:\\Users\\user",
353361 );
354362 assert.equal(
355 resolveAntigravityUserHome("win32", { HOMEDRIVE: "D:", HOMEPATH: "\\Users\\alice" }),
363 resolveAntigravityUserHome(
364 "win32",
365 { HOMEDRIVE: "D:", HOMEPATH: "\\Users\\alice" },
366 "/home/fallback",
367 ),
356368 "D:\\Users\\alice",
357369 );
358 assert.equal(resolveAntigravityUserHome("darwin", { HOME: "/Users/a b " }), "/Users/a b ");
359 assert.equal(resolveAntigravityUserHome("darwin", { HOME: "" }).length > 0, true);
370 assert.equal(
371 resolveAntigravityUserHome("darwin", { HOME: "/Users/a b " }, "/home/fallback"),
372 "/Users/a b ",
373 );
374 assert.equal(
375 resolveAntigravityUserHome("darwin", { HOME: "" }, "/home/fallback"),
376 "/home/fallback",
377 );
360378});
apps/server/src/provider/Drivers/AntigravitySkills.ts+3-4
......@@ -1,5 +1,3 @@
1import * as NodeOS from "node:os";
2
31import type { ServerProviderSkill } from "@t3tools/contracts";
42import * as Effect from "effect/Effect";
53import * as FileSystem from "effect/FileSystem";
......@@ -18,15 +16,16 @@ import { parse as parseYamlDocument } from "yaml";
1816export function resolveAntigravityUserHome(
1917 platform: NodeJS.Platform,
2018 environment: NodeJS.ProcessEnv,
19 home: string,
2120): string {
2221 if (platform === "win32") {
2322 if (environment.USERPROFILE) return environment.USERPROFILE;
2423 if (environment.HOMEDRIVE && environment.HOMEPATH) {
2524 return `${environment.HOMEDRIVE}${environment.HOMEPATH}`;
2625 }
27 return NodeOS.homedir();
26 return home;
2827 }
29 return environment.HOME || NodeOS.homedir();
28 return environment.HOME || home;
3029}
3130
3231/**
apps/server/src/provider/Drivers/AntigravityUsage.ts+3-4
......@@ -8,7 +8,6 @@
88 *
99 * @module provider/Drivers/AntigravityUsage
1010 */
11import * as NodeOS from "node:os";
1211
1312import { type AntigravitySettings, UsageReadError } from "@t3tools/contracts";
1413import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
......@@ -18,7 +17,7 @@ import type {
1817 ProviderUsageReader,
1918 ProviderUsageScan,
2019} from "@t3tools/provider-core/server/usage";
21import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";
20import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
2221import * as Context from "effect/Context";
2322import * as Crypto from "effect/Crypto";
2423import * as Effect from "effect/Effect";
......@@ -50,7 +49,7 @@ const make = Effect.gen(function* () {
5049
5150 /** `ANTIGRAVITY_DATA_DIR` (comma-separated) or the defaults, canonicalized. */
5251 const dataRoots = Effect.gen(function* () {
53 const home = NodeOS.homedir();
52 const home = yield* HostProcessHomeDirectory;
5453 const configured = hostEnvironment["ANTIGRAVITY_DATA_DIR"]
5554 ?.split(",")
5655 .map((value) => value.trim())
......@@ -63,7 +62,7 @@ const make = Effect.gen(function* () {
6362 ];
6463 const canonical = new Set<string>();
6564 for (const root of configured?.length ? configured : defaults) {
66 const resolved = path.resolve(expandHomePath(root));
65 const resolved = path.resolve(expandHomePath(root, home));
6766 canonical.add(
6867 yield* fileSystem.realPath(resolved).pipe(Effect.orElseSucceed(() => resolved)),
6968 );
apps/server/src/provider/Drivers/ClaudeDriver.ts+3-2
......@@ -71,6 +71,7 @@ import {
7171 makeClaudeContinuationGroupKey,
7272 resolveClaudeHomePath,
7373} from "./ClaudeHome.ts";
74import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
7475const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);
7576
7677const DRIVER_KIND = ProviderDriverKind.make("claudeAgent");
......@@ -130,7 +131,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
130131 const modelCatalog = catalogService
131132 .current(DRIVER_KIND)
132133 .pipe(Effect.map(resolveClaudeModelCatalog));
133 const processEnv = mergeProviderInstanceEnvironment(environment);
134 const processEnv = yield* mergeProviderInstanceEnvironment(environment);
134135 const fallbackContinuationIdentity = defaultProviderContinuationIdentity({
135136 driverKind: DRIVER_KIND,
136137 instanceId,
......@@ -138,7 +139,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
138139 const effectiveConfig = {
139140 ...config,
140141 enabled,
141 binaryPath: expandHomePath(config.binaryPath),
142 binaryPath: expandHomePath(config.binaryPath, yield* HostProcessHomeDirectory),
142143 } satisfies ClaudeSettings;
143144 const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution(
144145 resolveProviderMaintenanceCapabilitiesEffect(UPDATE, {
apps/server/src/provider/Drivers/ClaudeHome.ts+4-4
......@@ -1,11 +1,10 @@
1import * as NodeOS from "node:os";
2
31import type { ClaudeSettings } from "@t3tools/contracts";
42import * as Effect from "effect/Effect";
53import * as Path from "effect/Path";
64import * as Schema from "effect/Schema";
75
86import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
7import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
98
109const quotePath = Schema.encodeSync(Schema.fromJsonString(Schema.String));
1110
......@@ -21,16 +20,17 @@ export const resolveClaudeHomePath = Effect.fn("resolveClaudeHomePath")(function
2120 environment?: NodeJS.ProcessEnv,
2221): Effect.fn.Return<string, never, Path.Path> {
2322 const path = yield* Path.Path;
23 const home = yield* HostProcessHomeDirectory;
2424 const homePath = config.homePath.trim();
2525 if (homePath.length > 0) {
26 return path.resolve(expandHomePath(homePath));
26 return path.resolve(expandHomePath(homePath, home));
2727 }
2828 // Inherited env vars are not shell-expanded, so a literal `~` stays literal.
2929 const inherited = environment?.CLAUDE_CONFIG_DIR?.trim() ?? "";
3030 if (inherited.length > 0) {
3131 return path.resolve(inherited);
3232 }
33 return path.resolve(path.join(NodeOS.homedir(), ".claude"));
33 return path.resolve(path.join(home, ".claude"));
3434});
3535
3636export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function* (
apps/server/src/provider/Drivers/ClaudeSkills.ts+4-4
......@@ -13,14 +13,13 @@
1313 *
1414 * @module provider/Drivers/ClaudeSkills
1515 */
16import * as NodeOS from "node:os";
1716
1817import type { ClaudeSettings, ServerProviderSkill } from "@t3tools/contracts";
1918import * as Effect from "effect/Effect";
2019import * as FileSystem from "effect/FileSystem";
2120import * as Path from "effect/Path";
2221import * as Schema from "effect/Schema";
23import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
22import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
2423import { fromLenientJson } from "@t3tools/shared/schemaJson";
2524import { parse as parseYamlDocument } from "yaml";
2625
......@@ -296,9 +295,10 @@ const resolveClaudeConfigDirPath = Effect.fn("resolveClaudeConfigDirPath")(funct
296295 cwd?: string,
297296): Effect.fn.Return<string, never, Path.Path> {
298297 const path = yield* Path.Path;
298 const home = yield* HostProcessHomeDirectory;
299299 const homePath = config.homePath.trim();
300300 if (homePath.length > 0) {
301 return path.resolve(expandHomePath(homePath));
301 return path.resolve(expandHomePath(homePath, home));
302302 }
303303 // No tilde expansion here: the spawned CLI receives this env var verbatim
304304 // (env vars are never shell-expanded), so a literal `~` must stay literal
......@@ -309,7 +309,7 @@ const resolveClaudeConfigDirPath = Effect.fn("resolveClaudeConfigDirPath")(funct
309309 if (environmentConfigDir.length > 0) {
310310 return cwd ? path.resolve(cwd, environmentConfigDir) : path.resolve(environmentConfigDir);
311311 }
312 return path.join(NodeOS.homedir(), ".claude");
312 return path.join(home, ".claude");
313313});
314314
315315/**
apps/server/src/provider/Drivers/CodexDriver.ts+3-2
......@@ -77,6 +77,7 @@ import { codexUsageReader } from "./codexUsage.ts";
7777import * as CodexInstallation from "../CodexInstallation.ts";
7878import * as ServerSecretStore from "../../auth/ServerSecretStore.ts";
7979import * as ServerEnvironment from "../../environment/ServerEnvironment.ts";
80import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
8081const decodeCodexSettings = Schema.decodeSync(CodexSettings);
8182
8283const DRIVER_KIND = ProviderDriverKind.make("codex");
......@@ -154,7 +155,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv, Path.Pat
154155 const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions;
155156 const modelCatalog = yield* ModelCatalog.ModelCatalog;
156157 const currentCatalog = modelCatalog.current(DRIVER_KIND);
157 const processEnv = mergeProviderInstanceEnvironment(environment);
158 const processEnv = yield* mergeProviderInstanceEnvironment(environment);
158159 const homeLayout = yield* resolveCodexHomeLayout(config);
159160 const continuationIdentity = codexContinuationIdentity(homeLayout);
160161 const stampIdentity = withInstanceIdentity({
......@@ -178,7 +179,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv, Path.Pat
178179 const effectiveConfig = {
179180 ...config,
180181 enabled,
181 binaryPath: expandHomePath(config.binaryPath),
182 binaryPath: expandHomePath(config.binaryPath, yield* HostProcessHomeDirectory),
182183 homePath: homeLayout.effectiveHomePath ?? "",
183184 } satisfies CodexSettings;
184185 const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution(
apps/server/src/provider/Drivers/CodexHomeLayout.ts+6-8
......@@ -1,5 +1,3 @@
1import * as NodeOS from "node:os";
2
31import { ProviderDriverKind, type CodexSettings } from "@t3tools/contracts";
42import * as Effect from "effect/Effect";
53import * as FileSystem from "effect/FileSystem";
......@@ -8,6 +6,7 @@ import * as Schema from "effect/Schema";
86import * as PlatformError from "effect/PlatformError";
97
108import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
9import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
1110
1211export interface CodexHomeLayout {
1312 readonly mode: "direct" | "authOverlay";
......@@ -33,11 +32,9 @@ const PRIVATE_ENTRY_NAMES = new Set(["auth.json", "models_cache.json"]);
3332const SHADOW_LOCAL_ENTRY_NAMES = new Set(["log", "memories", "tmp"]);
3433const REPLACEABLE_SHARED_RUNTIME_DIRECTORIES = new Set(["mcp-oauth-locks"]);
3534
36function resolveHomePath(path: Path.Path, value: string | undefined): string {
35function resolveHomePath(path: Path.Path, home: string, value: string | undefined): string {
3736 const expanded =
38 value && value.trim().length > 0
39 ? expandHomePath(value)
40 : path.join(NodeOS.homedir(), ".codex");
37 value && value.trim().length > 0 ? expandHomePath(value, home) : path.join(home, ".codex");
4138 return path.resolve(expanded);
4239}
4340
......@@ -45,7 +42,8 @@ export const resolveCodexHomeLayout = Effect.fn("resolveCodexHomeLayout")(functi
4542 config: CodexSettings,
4643): Effect.fn.Return<CodexHomeLayout, never, Path.Path> {
4744 const path = yield* Path.Path;
48 const sharedHomePath = resolveHomePath(path, config.homePath);
45 const home = yield* HostProcessHomeDirectory;
46 const sharedHomePath = resolveHomePath(path, home, config.homePath);
4947 const shadowHomePath = config.shadowHomePath.trim();
5048 if (shadowHomePath.length === 0) {
5149 return {
......@@ -56,7 +54,7 @@ export const resolveCodexHomeLayout = Effect.fn("resolveCodexHomeLayout")(functi
5654 };
5755 }
5856
59 const effectiveHomePath = path.resolve(expandHomePath(shadowHomePath));
57 const effectiveHomePath = path.resolve(expandHomePath(shadowHomePath, home));
6058 return {
6159 mode: "authOverlay",
6260 sharedHomePath,
apps/server/src/provider/Drivers/CodexManagedProvider.ts+1-1
......@@ -36,7 +36,7 @@ export const makeManagedCodexProvider = Effect.fn("makeManagedCodexProvider")(fu
3636 instanceId,
3737 enabled,
3838 config,
39 environment: mergeProviderInstanceEnvironment(input.environment),
39 environment: yield* mergeProviderInstanceEnvironment(input.environment),
4040 });
4141 const continuationIdentity = codexContinuationIdentity(runtime.homeLayout);
4242 const stamp = withInstanceIdentity({
apps/server/src/provider/Drivers/claudeUsage.ts+4-3
......@@ -4,7 +4,6 @@
44 *
55 * @module provider/Drivers/claudeUsage
66 */
7import * as NodeOS from "node:os";
87
98import type { ClaudeSettings } from "@t3tools/contracts";
109import * as Effect from "effect/Effect";
......@@ -18,6 +17,7 @@ import {
1817 type TranscriptUsageFormat,
1918 type UsageRecord,
2019} from "@t3tools/provider-core/server/usage";
20import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
2121
2222/**
2323 * Parses one line of a Claude Code transcript.
......@@ -111,10 +111,11 @@ export const claudeUsageReader: ProviderUsageReader<ClaudeSettings, Path.Path> =
111111 // An undecodable config has no trustworthy home to read.
112112 if (config === undefined) return [];
113113 const path = yield* Path.Path;
114 const homeDirectory = yield* HostProcessHomeDirectory;
114115 const configured = config.homePath.trim();
115116 const home = configured
116 ? expandHomePath(configured)
117 : environment.CLAUDE_CONFIG_DIR?.trim() || path.join(NodeOS.homedir(), ".claude");
117 ? expandHomePath(configured, homeDirectory)
118 : environment.CLAUDE_CONFIG_DIR?.trim() || path.join(homeDirectory, ".claude");
118119 return [{ dir: path.resolve(home, "projects") }];
119120 }),
120121};
apps/server/src/provider/ProviderInstanceRegistry.test.ts+6-3
......@@ -39,7 +39,11 @@ import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderL
3939import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
4040import type { GrokSettings } from "@t3tools/provider-grok/settings";
4141import type { CursorSettings } from "@t3tools/provider-cursor/settings";
42import { HostProcessPlatform, isHostWindows } from "@t3tools/shared/hostProcess";
42import {
43 HostProcessHomeDirectory,
44 HostProcessPlatform,
45 isHostWindows,
46} from "@t3tools/shared/hostProcess";
4347import * as DateTime from "effect/DateTime";
4448import * as Effect from "effect/Effect";
4549import * as FileSystem from "effect/FileSystem";
......@@ -51,7 +55,6 @@ import { HttpClient, HttpClientResponse } from "effect/http";
5155import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
5256import * as AntigravityInstallation from "./AntigravityInstallation.ts";
5357import * as ServerConfig from "../config.ts";
54import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
5558import * as ServerSettings from "../serverSettings.ts";
5659import { ClaudeDriver, type ClaudeDriverEnv } from "./Drivers/ClaudeDriver.ts";
5760import { CodexDriver, type CodexDriverEnv } from "./Drivers/CodexDriver.ts";
......@@ -153,7 +156,7 @@ const makeTildeProviderFixtures = Effect.fn(
153156)(function* () {
154157 const fileSystem = yield* FileSystem.FileSystem;
155158 const path = yield* Path.Path;
156 const homePath = expandHomePath("~");
159 const homePath = yield* HostProcessHomeDirectory;
157160 const fixtureDir = yield* fileSystem.makeTempDirectoryScoped({
158161 directory: homePath,
159162 prefix: ".t3-provider-path-test-",
apps/server/src/provider/antigravityAuthSupport.ts+7-2
......@@ -2,7 +2,7 @@
22import * as NodeFSP from "node:fs/promises";
33
44import type { AntigravityAuthMethod, ProviderInstanceId } from "@t3tools/contracts";
5import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
5import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
66import { resolveNodeExecutable, nodeRuntimeUnavailableMessage } from "@t3tools/shared/nodeRuntime";
77import * as Crypto from "effect/Crypto";
88import * as Effect from "effect/Effect";
......@@ -317,7 +317,12 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(
317317 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
318318 const platform = input.platform ?? (yield* HostProcessPlatform);
319319 const userHome =
320 input.userHome ?? resolveAntigravityUserHome(platform, input.baseEnv ?? process.env);
320 input.userHome ??
321 resolveAntigravityUserHome(
322 platform,
323 input.baseEnv ?? process.env,
324 yield* HostProcessHomeDirectory,
325 );
321326 const runtimeExecutablePath =
322327 input.runtimeExecutablePath ??
323328 (yield* resolveNodeExecutable("Antigravity sign-in", input.baseEnv).pipe(
apps/server/src/provider/claudeResetCredits.ts+9-6
......@@ -7,12 +7,11 @@
77 *
88 * @module provider/claudeResetCredits
99 */
10import * as NodeOS from "node:os";
1110import type {
1211 ProviderConsumeResetCreditOutcome,
1312 ServerProviderResetCredits,
1413} from "@t3tools/contracts";
15import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
14import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
1615import * as DateTime from "effect/DateTime";
1716import * as Effect from "effect/Effect";
1817import * as FileSystem from "effect/FileSystem";
......@@ -190,10 +189,14 @@ export const readClaudeResetCredits = Effect.fn("readClaudeResetCredits")(
190189);
191190
192191/** The CLI keeps the account record beside its settings, or in the home directory by default. */
193export const claudeAccountConfigPath = (configDir: string | undefined) =>
194 Effect.map(Path.Path, (path) =>
195 configDir ? path.join(configDir, ".claude.json") : path.join(NodeOS.homedir(), ".claude.json"),
196 );
192export const claudeAccountConfigPath = Effect.fn("claudeAccountConfigPath")(function* (
193 configDir: string | undefined,
194) {
195 const path = yield* Path.Path;
196 return configDir
197 ? path.join(configDir, ".claude.json")
198 : path.join(yield* HostProcessHomeDirectory, ".claude.json");
199});
197200
198201const CLAIM_OUTCOMES = {
199202 reset: "reset",
apps/server/src/provider/providerInstallation.ts+15-12
......@@ -134,18 +134,21 @@ export const makeProviderInstallation = Effect.fn("makeProviderInstallation")(fu
134134 true,
135135 );
136136 const entries = yield* readEntries(input.instanceId, "remove-install");
137 const protectedPaths = yield* Effect.forEach(Object.values(entries), (entry) => {
138 if (!hasBinaryPath(entry.config) || !entry.config.binaryPath.trim()) {
139 return Effect.succeed([]);
140 }
141 const binaryPath = entry.config.binaryPath.trim();
142 return resolveCommandPath(binaryPath, {
143 env: mergeProviderInstanceEnvironment(entry.environment),
144 }).pipe(
145 Effect.map((resolved) => [binaryPath, resolved]),
146 Effect.orElseSucceed(() => [binaryPath]),
147 );
148 });
137 const protectedPaths = yield* Effect.forEach(
138 Object.values(entries),
139 Effect.fnUntraced(function* (entry) {
140 if (!hasBinaryPath(entry.config) || !entry.config.binaryPath.trim()) {
141 return [];
142 }
143 const binaryPath = entry.config.binaryPath.trim();
144 return yield* resolveCommandPath(binaryPath, {
145 env: yield* mergeProviderInstanceEnvironment(entry.environment),
146 }).pipe(
147 Effect.map((resolved) => [binaryPath, resolved]),
148 Effect.orElseSucceed(() => [binaryPath]),
149 );
150 }),
151 );
149152 yield* installation
150153 .remove(protectedPaths.flat())
151154 .pipe(Effect.mapError(failure(input.instanceId)));
apps/server/src/review/ReviewService.ts+3-1
......@@ -20,6 +20,7 @@ import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
2020import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
2121import * as ServerSettings from "../serverSettings.ts";
2222import { isFilesystemRoot, managedWorktreesDirectories } from "../worktreesDirectory.ts";
23import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
2324
2425export class ReviewService extends Context.Service<
2526 ReviewService,
......@@ -73,13 +74,14 @@ export const make = Effect.gen(function* () {
7374 const worktreesDirectories = yield* settings.getSettings.pipe(
7475 Effect.orElseSucceed(() => ({ worktreesDirectory: "", previousWorktreesDirectories: [] })),
7576 );
77 const home = yield* HostProcessHomeDirectory;
7678 const [candidate, workspaceRoot, worktreesRoots] = yield* Effect.all([
7779 canonicalizePath(cwd),
7880 canonicalizePath(config.cwd),
7981 // A managed root that cannot be resolved, or resolves to a filesystem
8082 // root through a symlink, is skipped rather than failing every review.
8183 Effect.forEach(
82 managedWorktreesDirectories(worktreesDirectories, config.worktreesDir, path),
84 managedWorktreesDirectories(worktreesDirectories, config.worktreesDir, path, home),
8385 (directory) => canonicalizePath(directory).pipe(Effect.orElseSucceed(() => null)),
8486 ).pipe(
8587 Effect.map((roots) =>
apps/server/src/sourceControl/SourceControlRepositoryService.ts+2-1
......@@ -29,6 +29,7 @@ import {
2929import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
3030import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi";
3131import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";
32import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
3233const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError);
3334const isSourceControlProviderError = Schema.is(SourceControlProviderError);
3435const isBitbucketRepositoryLocatorError = Schema.is(BitbucketApi.BitbucketRepositoryLocatorError);
......@@ -213,7 +214,7 @@ export const make = Effect.gen(function* () {
213214 });
214215 }
215216
216 return path.resolve(expandHomePath(trimmed));
217 return path.resolve(expandHomePath(trimmed, yield* HostProcessHomeDirectory));
217218 },
218219 );
219220
apps/server/src/storageCleanup.ts+2
......@@ -38,6 +38,7 @@ import * as TerminalManager from "./terminal/Manager.ts";
3838import { isFilesystemRoot, managedWorktreesDirectories } from "./worktreesDirectory.ts";
3939import * as GitVcsDriver from "./vcs/GitVcsDriver.ts";
4040import { withWorkspaceLease } from "./workspace/workspaceLease.ts";
41import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
4142
4243const decodeCleanupThread = Schema.decodeUnknownEffect(
4344 Schema.fromJsonString(OrchestrationV2AppThreadJson),
......@@ -213,6 +214,7 @@ export const make = Effect.gen(function* () {
213214 serverSettings,
214215 config.worktreesDir,
215216 path,
217 yield* HostProcessHomeDirectory,
216218 )) {
217219 // An unmounted drive only skips its own worktrees.
218220 const root = yield* fs.exists(directory).pipe(
apps/server/src/telemetry/Identify.ts+4-3
......@@ -1,4 +1,3 @@
1import * as NodeOS from "node:os";
21import * as Crypto from "effect/Crypto";
32import * as Effect from "effect/Effect";
43import * as Hex from "effect/encoding/Hex";
......@@ -10,6 +9,7 @@ import * as Schema from "effect/Schema";
109
1110import { writeFileStringAtomically } from "@t3tools/shared/atomicWrite";
1211import * as ServerConfig from "../config.ts";
12import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
1313
1414/**
1515 * Codex writes `tokens` only for ChatGPT logins and omits the key for API-key,
......@@ -311,6 +311,7 @@ export const getTelemetryIdentifierForHome = Effect.fn("getTelemetryIdentifierFo
311311 Effect.orElseSucceed(() => null),
312312);
313313
314export const getTelemetryIdentifier = Effect.suspend(() =>
315 getTelemetryIdentifierForHome(NodeOS.homedir()),
314export const getTelemetryIdentifier = Effect.flatMap(
315 HostProcessHomeDirectory,
316 getTelemetryIdentifierForHome,
316317);
apps/server/src/terminal/Manager.ts+14-4
......@@ -42,7 +42,11 @@ import {
4242} from "@t3tools/contracts";
4343import { makeKeyedCoalescingWorker } from "@t3tools/shared/KeyedCoalescingWorker";
4444import * as KeyedLock from "@t3tools/shared/KeyedLock";
45import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess";
45import {
46 HostProcessArchitecture,
47 HostProcessHomeDirectory,
48 HostProcessPlatform,
49} from "@t3tools/shared/hostProcess";
4650import { mergePathEntries } from "@t3tools/shared/shell";
4751
4852import { acpRegistryManagedBinaryDirectories } from "@t3tools/provider-acp-registry/server";
......@@ -1317,6 +1321,7 @@ function createTerminalSpawnEnv(
13171321 baseEnv: NodeJS.ProcessEnv,
13181322 runtimeEnv: Record<string, string> | null | undefined,
13191323 platform: NodeJS.Platform,
1324 home: string,
13201325): NodeJS.ProcessEnv {
13211326 const spawnEnv: NodeJS.ProcessEnv = {};
13221327 for (const [key, value] of Object.entries(baseEnv)) {
......@@ -1331,7 +1336,7 @@ function createTerminalSpawnEnv(
13311336 ? Object.keys(spawnEnv).find((candidate) => candidate.toLowerCase() === key.toLowerCase())
13321337 : undefined;
13331338 spawnEnv[existingKey ?? key] =
1334 key === "CODEX_HOME" || key === "CLAUDE_CONFIG_DIR" ? expandHomePath(value) : value;
1339 key === "CODEX_HOME" || key === "CLAUDE_CONFIG_DIR" ? expandHomePath(value, home) : value;
13351340 }
13361341 }
13371342 // An explicit empty override opts out for terminals started without a client.
......@@ -1408,7 +1413,7 @@ export const resolveProviderInstanceTerminalEnvironment = Effect.fn(
14081413 return yield* new TerminalProviderInstanceNotFoundError({ providerInstanceId });
14091414 }
14101415
1411 let resolved = mergeProviderInstanceEnvironment(instance.environment, input.env ?? {});
1416 let resolved = yield* mergeProviderInstanceEnvironment(instance.environment, input.env ?? {});
14121417 if (instance.driver === "codex") {
14131418 const config = decodeCodexSettings(instance.config ?? {});
14141419 if (Option.isSome(config)) {
......@@ -2244,7 +2249,12 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func
22442249 Effect.andThen(
22452250 Effect.gen(function* () {
22462251 const shellCandidates = resolveShellCandidates(shellResolver, platform, baseEnv);
2247 const terminalEnv = createTerminalSpawnEnv(baseEnv, session.runtimeEnv, platform);
2252 const terminalEnv = createTerminalSpawnEnv(
2253 baseEnv,
2254 session.runtimeEnv,
2255 platform,
2256 yield* HostProcessHomeDirectory,
2257 );
22482258 // Append (never prepend) managed ACP agent install directories so
22492259 // `kimi login` and friends resolve by name without shadowing any
22502260 // system or user tool of the same name.
apps/server/src/textGeneration/CodexTextGeneration.ts+7-1
......@@ -26,6 +26,7 @@ import {
2626} from "@t3tools/provider-core/server/textGenerationUtils";
2727import { codexModelFamily, getModelSelectionStringOptionValue } from "@t3tools/shared/model";
2828import { getCodexServiceTierOptionValue } from "../codexModelOptions.ts";
29import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
2930
3031const CODEX_TIMEOUT_MS = 180_000;
3132const encodeJsonString = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
......@@ -214,7 +215,12 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
214215 env: {
215216 ...effectiveEnvironment,
216217 ...(effectiveConfig.homePath
217 ? { CODEX_HOME: expandHomePath(effectiveConfig.homePath) }
218 ? {
219 CODEX_HOME: expandHomePath(
220 effectiveConfig.homePath,
221 yield* HostProcessHomeDirectory,
222 ),
223 }
218224 : {}),
219225 },
220226 cwd,
apps/server/src/usage/UsageService.ts+27-19
......@@ -339,10 +339,10 @@ export const make = Effect.gen(function* () {
339339 * not decode. Disabled accounts still have history. An unconfigured default
340340 * slot runs with default config, just as it does in the provider registry.
341341 */
342 const usageInstances = <Config>(
342 const usageInstances = Effect.fn("UsageService.usageInstances")(function* <Config>(
343343 driver: ProviderDriver<Config, unknown, unknown>,
344344 settings: ServerSettingsValue,
345 ): Array<ProviderUsageInstance<Config>> => {
345 ) {
346346 const entries: Array<
347347 readonly [ProviderInstanceId, Pick<ProviderInstanceConfig, "config" | "environment">, boolean]
348348 > = Object.entries(settings.providerInstances)
......@@ -352,13 +352,22 @@ export const make = Effect.gen(function* () {
352352 entries.push([ProviderInstanceId.make(driver.driverKind), {}, false]);
353353 }
354354 const decodeConfig = Schema.decodeUnknownOption(driver.configSchema);
355 return entries.map(([instanceId, instance, configured]) => ({
356 instanceId,
357 config: Option.getOrUndefined(decodeConfig(instance.config ?? {})),
358 environment: mergeProviderInstanceEnvironment(instance.environment, hostEnvironment),
359 configured,
360 }));
361 };
355 return yield* Effect.forEach(
356 entries,
357 Effect.fnUntraced(function* ([instanceId, instance, configured]) {
358 const instanceConfig: ProviderUsageInstance<Config> = {
359 instanceId,
360 config: Option.getOrUndefined(decodeConfig(instance.config ?? {})),
361 environment: yield* mergeProviderInstanceEnvironment(
362 instance.environment,
363 hostEnvironment,
364 ),
365 configured,
366 };
367 return instanceConfig;
368 }),
369 );
370 });
362371
363372 /** Resolves every transcript directory the usage readers point at. */
364373 const resolveTranscriptDirs = Effect.fn("UsageService.resolveTranscriptDirs")(function* (
......@@ -369,8 +378,9 @@ export const make = Effect.gen(function* () {
369378 const seen = new Set<string>();
370379 for (const { driver, reader } of transcriptReaders) {
371380 const { provider, format } = reader;
372 const directories = yield* Effect.forEach(usageInstances(driver, settings), (instance) =>
373 reader.directories(instance),
381 const directories = yield* Effect.forEach(
382 yield* usageInstances(driver, settings),
383 (instance) => reader.directories(instance),
374384 );
375385 for (const { dir: directory, fileName } of directories.flat()) {
376386 const sourceKey = provider + "\0" + directory;
......@@ -635,14 +645,12 @@ export const make = Effect.gen(function* () {
635645 const scans = Effect.forEach(
636646 scanReaders,
637647 ({ driver, reader }) =>
638 reader
639 .scan({
640 instances: usageInstances(driver, settings),
641 settings,
642 windowStartMs,
643 retentionCutoffMs,
644 awaitRefresh,
645 })
648 usageInstances(driver, settings)
649 .pipe(
650 Effect.flatMap((instances) =>
651 reader.scan({ instances, settings, windowStartMs, retentionCutoffMs, awaitRefresh }),
652 ),
653 )
646654 .pipe(
647655 Effect.flatMap((sources) =>
648656 Effect.forEach(sources, ({ volumeId, ...source }) =>
apps/server/src/vcs/GitVcsDriverCore.ts+2-1
......@@ -29,7 +29,7 @@ import {
2929 type VcsRef,
3030} from "@t3tools/contracts";
3131import { dedupeRemoteBranchesWithLocalMatches, normalizeGitRemoteUrl } from "@t3tools/shared/git";
32import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
32import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
3333import { compactTraceAttributes } from "@t3tools/shared/observability";
3434import { decodeJsonResult } from "@t3tools/shared/schemaJson";
3535import { parseT3ProjectFile } from "@t3tools/shared/t3ProjectFile";
......@@ -3403,6 +3403,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
34033403 options?.worktreesDirectory ?? "",
34043404 worktreesDir,
34053405 path,
3406 yield* HostProcessHomeDirectory,
34063407 );
34073408 if (parentDir === null) {
34083409 return yield* new GitCommandError({
apps/server/src/workspace/WorkspaceEntries.ts+6-3
......@@ -19,7 +19,7 @@ import type {
1919 ProjectSearchEntriesInput,
2020 ProjectSearchEntriesResult,
2121} from "@t3tools/contracts";
22import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
22import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
2323import { isExplicitRelativePath, isWindowsAbsolutePath } from "@t3tools/shared/path";
2424import { normalizeSearchQuery } from "@t3tools/shared/searchRanking";
2525
......@@ -120,7 +120,7 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu
120120 }
121121
122122 if (!isExplicitRelativePath(input.partialPath)) {
123 return path.resolve(expandHomePath(input.partialPath));
123 return path.resolve(expandHomePath(input.partialPath, yield* HostProcessHomeDirectory));
124124 }
125125
126126 if (!input.cwd) {
......@@ -128,7 +128,10 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu
128128 partialPath: input.partialPath,
129129 });
130130 }
131 return path.resolve(expandHomePath(input.cwd), input.partialPath);
131 return path.resolve(
132 expandHomePath(input.cwd, yield* HostProcessHomeDirectory),
133 input.partialPath,
134 );
132135});
133136
134137/** @public Service construction is part of the canonical Effect module API. */
apps/server/src/workspace/WorkspacePaths.ts+4-1
......@@ -15,6 +15,7 @@ import * as Path from "effect/Path";
1515import * as Schema from "effect/Schema";
1616
1717import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
18import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
1819
1920export class WorkspaceRootNotExistsError extends Schema.TaggedError<WorkspaceRootNotExistsError>()(
2021 "WorkspaceRootNotExistsError",
......@@ -152,7 +153,9 @@ export const make = Effect.gen(function* () {
152153 const normalizeWorkspaceRoot: WorkspacePaths["Service"]["normalizeWorkspaceRoot"] = Effect.fn(
153154 "WorkspacePaths.normalizeWorkspaceRoot",
154155 )(function* (workspaceRoot, options) {
155 const normalizedWorkspaceRoot = path.resolve(expandHomePath(workspaceRoot.trim()));
156 const normalizedWorkspaceRoot = path.resolve(
157 expandHomePath(workspaceRoot.trim(), yield* HostProcessHomeDirectory),
158 );
156159 let workspaceStat = yield* statWorkspaceRoot(
157160 workspaceRoot,
158161 normalizedWorkspaceRoot,
apps/server/src/worktreesDirectory.ts+6-3
......@@ -4,7 +4,8 @@ import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
44
55/**
66 * Directory new worktrees are created under: the `worktreesDirectory`
7 * setting, or `defaultDir` (`<T3 home>/worktrees`) when it is empty. Null when
7 * setting (`~` expands to `home`), or `defaultDir` (`<T3 home>/worktrees`)
8 * when it is empty. Null when
89 * the setting is not an absolute path on this machine, such as `D:\worktrees`
910 * configured for a Windows server and synced to a Linux one, or when it is a
1011 * filesystem root, which would make every path on that drive look managed.
......@@ -13,9 +14,10 @@ export function resolveWorktreesDirectory(
1314 setting: string,
1415 defaultDir: string,
1516 path: Path.Path,
17 home: string,
1618): string | null {
1719 if (setting === "") return defaultDir;
18 const expanded = expandHomePath(setting);
20 const expanded = expandHomePath(setting, home);
1921 if (!path.isAbsolute(expanded)) return null;
2022 const resolved = path.resolve(expanded);
2123 return isFilesystemRoot(resolved, path) ? null : resolved;
......@@ -34,10 +36,11 @@ export function managedWorktreesDirectories(
3436 },
3537 defaultDir: string,
3638 path: Path.Path,
39 home: string,
3740): ReadonlyArray<string> {
3841 const directories = new Set([defaultDir]);
3942 for (const setting of [settings.worktreesDirectory, ...settings.previousWorktreesDirectories]) {
40 const directory = resolveWorktreesDirectory(setting, defaultDir, path);
43 const directory = resolveWorktreesDirectory(setting, defaultDir, path, home);
4144 if (directory !== null) directories.add(directory);
4245 }
4346 return [...directories];
packages/provider-acp-registry/src/server/adapter.ts+1-1
......@@ -278,7 +278,7 @@ export const AcpRegistryAdapterV2Driver: ProviderAdapterDriver<
278278 return yield* makeAcpRegistryAdapterV2({
279279 instanceId: input.instanceId,
280280 settings: { ...input.config, enabled: input.enabled },
281 environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
281 environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
282282 selfInvocation,
283283 nativeLogging: (threadId) =>
284284 makeNativeLogger({
packages/provider-acp-registry/src/server/driver.ts+4-1
......@@ -497,7 +497,10 @@ export const AcpRegistryDriver: ProviderDriver<AcpRegistrySettings, AcpRegistryD
497497 continuationKey: continuationIdentity.continuationKey,
498498 };
499499 const effectiveConfig = { ...config, enabled } satisfies AcpRegistrySettings;
500 const processEnvironment = mergeProviderInstanceEnvironment(environment, hostEnvironment);
500 const processEnvironment = yield* mergeProviderInstanceEnvironment(
501 environment,
502 hostEnvironment,
503 );
501504 const orchestrationAdapter = yield* AcpRegistryAdapterV2Driver.create({
502505 instanceId,
503506 displayName,
packages/provider-acp/src/server/AcpSessionRuntime.ts+3-2
......@@ -27,7 +27,7 @@ import * as EffectAcpErrors from "effect-acp/errors";
2727import type * as EffectAcpSchema from "effect-acp/compat";
2828import type * as EffectAcpProtocol from "effect-acp/protocol";
2929import { resolveSpawnCommand } from "@t3tools/shared/shell";
30import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
30import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
3131
3232import { signalProcessGroup } from "@t3tools/provider-core/server/processGroup";
3333import { appendAcpStderrTail, sanitizeAcpStderrExcerpt } from "./stderr.ts";
......@@ -1408,6 +1408,7 @@ export const make = (
14081408 const stoppingRef = yield* Ref.make(false);
14091409 const stderrFailure = yield* Deferred.make<never, EffectAcpErrors.AcpError>();
14101410 const stderrTailRef = yield* Ref.make("");
1411 const homeDirectory = yield* HostProcessHomeDirectory;
14111412 const stderrDrained = yield* Deferred.make<void>();
14121413 const runtimeClosed = yield* Deferred.make<void>();
14131414 const promptSerializationSemaphore = yield* Semaphore.make(1);
......@@ -1440,7 +1441,7 @@ export const make = (
14401441 Effect.ignore,
14411442 Effect.andThen(Ref.get(stderrTailRef)),
14421443 Effect.map((tail) => {
1443 const stderr = sanitizeAcpStderrExcerpt(tail);
1444 const stderr = sanitizeAcpStderrExcerpt(tail, homeDirectory);
14441445 return stderr.length === 0
14451446 ? error
14461447 : new EffectAcpErrors.AcpProcessExitedError({
packages/provider-acp/src/server/stderr.test.ts+3
......@@ -20,6 +20,7 @@ describe("AcpStderr", () => {
2020 "Visit http://localhost:5733/pair#token=ABCDEF for pairing",
2121 "key=sk-abcdefghijklmnopqrstuv",
2222 ].join("\n"),
23 "/home/server",
2324 { HOME: "/Users/ada" },
2425 );
2526
......@@ -41,6 +42,8 @@ describe("AcpStderr", () => {
4142 "Authorization: Basic dXNlcjpwYXNz",
4243 "x-api-key: ant-api-key-value",
4344 ].join("\n"),
45 "/Users/ada",
46 {},
4447 );
4548
4649 expect(excerpt).toContain("[redacted]");
packages/provider-acp/src/server/stderr.ts+2-4
......@@ -1,6 +1,3 @@
1// @effect-diagnostics nodeBuiltinImport:off -- The excerpt sanitizer masks the home directory, which only the Node os module can resolve.
2import * as NodeOS from "node:os";
3
41/** Last few KiB of ACP child stderr kept for startup / exit diagnostics. */
52export const ACP_STDERR_TAIL_MAX_CHARS = 4_096;
63
......@@ -19,10 +16,11 @@ export function appendAcpStderrTail(current: string, chunk: string): string {
1916/** Bounded, redacted excerpt safe to put on user-facing adapter errors. */
2017export function sanitizeAcpStderrExcerpt(
2118 text: string,
19 homeDirectory: string,
2220 environment: NodeJS.ProcessEnv = process.env,
2321): string {
2422 let result = text.replaceAll("\0", "");
25 const homes = [environment.HOME, environment.USERPROFILE, NodeOS.homedir()].filter(
23 const homes = [environment.HOME, environment.USERPROFILE, homeDirectory].filter(
2624 (value): value is string => typeof value === "string" && value.length > 1,
2725 );
2826 for (const home of new Set(homes)) {
packages/provider-core/src/server/instanceEnvironment.test.ts+36-30
......@@ -1,9 +1,8 @@
1import * as NodeOS from "node:os";
2
31import * as NodeServices from "@effect/platform-node/NodeServices";
42import { describe, expect, it } from "@effect/vitest";
53import * as Effect from "effect/Effect";
64import * as Path from "effect/Path";
5import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
76
87import { mergeProviderInstanceEnvironment } from "./instanceEnvironment.ts";
98
......@@ -18,7 +17,7 @@ describe("mergeProviderInstanceEnvironment", () => {
1817 CODEX_HOME: "~/.inherited-codex",
1918 CLAUDE_CONFIG_DIR: "~/.inherited-claude",
2019 };
21 const environment = mergeProviderInstanceEnvironment(
20 const environment = yield* mergeProviderInstanceEnvironment(
2221 [
2322 { name: "CODEX_HOME", value, sensitive: false },
2423 { name: "CLAUDE_CONFIG_DIR", value, sensitive: false },
......@@ -28,41 +27,48 @@ describe("mergeProviderInstanceEnvironment", () => {
2827 );
2928
3029 expect(environment).toEqual({
31 CODEX_HOME: path.join(NodeOS.homedir(), tail),
32 CLAUDE_CONFIG_DIR: path.join(NodeOS.homedir(), tail),
30 CODEX_HOME: path.join("/home/ada", tail),
31 CLAUDE_CONFIG_DIR: path.join("/home/ada", tail),
3332 CUSTOM_VALUE: value,
3433 });
3534 expect(baseEnv).toEqual({
3635 CODEX_HOME: "~/.inherited-codex",
3736 CLAUDE_CONFIG_DIR: "~/.inherited-claude",
3837 });
39 }).pipe(Effect.provide(NodeServices.layer)),
38 }).pipe(
39 Effect.provideService(HostProcessHomeDirectory, "/home/ada"),
40 Effect.provide(NodeServices.layer),
41 ),
4042 );
4143
42 it("leaves inherited provider homes unchanged", () => {
43 const baseEnv = { CODEX_HOME: "~/.codex", CLAUDE_CONFIG_DIR: "~\\.claude" };
44 it.effect("leaves inherited provider homes unchanged", () =>
45 Effect.gen(function* () {
46 const baseEnv = { CODEX_HOME: "~/.codex", CLAUDE_CONFIG_DIR: "~\\.claude" };
4447
45 expect(
46 mergeProviderInstanceEnvironment(
47 [{ name: "CUSTOM_VALUE", value: "~/.custom", sensitive: false }],
48 baseEnv,
49 ),
50 ).toEqual({ ...baseEnv, CUSTOM_VALUE: "~/.custom" });
51 });
48 expect(
49 yield* mergeProviderInstanceEnvironment(
50 [{ name: "CUSTOM_VALUE", value: "~/.custom", sensitive: false }],
51 baseEnv,
52 ),
53 ).toEqual({ ...baseEnv, CUSTOM_VALUE: "~/.custom" });
54 }),
55 );
5256
53 it("overrides inherited environment values and preserves empty strings", () => {
54 expect(
55 mergeProviderInstanceEnvironment(
56 [
57 { name: "OPENROUTER_API_KEY", value: "sk-or-test", sensitive: true },
58 { name: "ANTHROPIC_API_KEY", value: "", sensitive: false },
59 ],
60 { ANTHROPIC_API_KEY: "inherited", PATH: "/bin" },
61 ),
62 ).toMatchObject({
63 OPENROUTER_API_KEY: "sk-or-test",
64 ANTHROPIC_API_KEY: "",
65 PATH: "/bin",
66 });
67 });
57 it.effect("overrides inherited environment values and preserves empty strings", () =>
58 Effect.gen(function* () {
59 expect(
60 yield* mergeProviderInstanceEnvironment(
61 [
62 { name: "OPENROUTER_API_KEY", value: "sk-or-test", sensitive: true },
63 { name: "ANTHROPIC_API_KEY", value: "", sensitive: false },
64 ],
65 { ANTHROPIC_API_KEY: "inherited", PATH: "/bin" },
66 ),
67 ).toMatchObject({
68 OPENROUTER_API_KEY: "sk-or-test",
69 ANTHROPIC_API_KEY: "",
70 PATH: "/bin",
71 });
72 }),
73 );
6874});
packages/provider-core/src/server/instanceEnvironment.ts+7-4
......@@ -1,22 +1,25 @@
11import type { ProviderInstanceEnvironment } from "@t3tools/contracts";
2import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
3import * as Effect from "effect/Effect";
24
35import { expandHomePath } from "./pathExpansion.ts";
46
5export function mergeProviderInstanceEnvironment(
7export const mergeProviderInstanceEnvironment = Effect.fn(function* (
68 environment: ProviderInstanceEnvironment | undefined,
79 baseEnv: NodeJS.ProcessEnv = process.env,
8): NodeJS.ProcessEnv {
10) {
911 if (!environment || environment.length === 0) {
1012 return baseEnv;
1113 }
1214
15 const home = yield* HostProcessHomeDirectory;
1316 const next: NodeJS.ProcessEnv = { ...baseEnv };
1417 for (const variable of environment) {
1518 // Child processes do not apply shell expansion to environment values.
1619 next[variable.name] =
1720 variable.name === "CODEX_HOME" || variable.name === "CLAUDE_CONFIG_DIR"
18 ? expandHomePath(variable.value)
21 ? expandHomePath(variable.value, home)
1922 : variable.value;
2023 }
2124 return next;
22}
25});
packages/provider-core/src/server/pathExpansion.test.ts+10-9
......@@ -1,34 +1,35 @@
11// @effect-diagnostics nodeBuiltinImport:off
2import * as NodeOS from "node:os";
32import * as NodePath from "node:path";
43import { describe, expect, it } from "vite-plus/test";
54
65import { expandHomePath } from "./pathExpansion.ts";
76
7const home = "/home/ada";
8
89describe("expandHomePath", () => {
910 it("returns an empty string unchanged", () => {
10 expect(expandHomePath("")).toBe("");
11 expect(expandHomePath("", home)).toBe("");
1112 });
1213
1314 it("returns paths without a leading tilde unchanged", () => {
14 expect(expandHomePath("/absolute/path")).toBe("/absolute/path");
15 expect(expandHomePath("relative/path")).toBe("relative/path");
16 expect(expandHomePath("some~weird~path")).toBe("some~weird~path");
15 expect(expandHomePath("/absolute/path", home)).toBe("/absolute/path");
16 expect(expandHomePath("relative/path", home)).toBe("relative/path");
17 expect(expandHomePath("some~weird~path", home)).toBe("some~weird~path");
1718 });
1819
1920 it("expands a lone tilde to the home directory", () => {
20 expect(expandHomePath("~")).toBe(NodeOS.homedir());
21 expect(expandHomePath("~", home)).toBe(home);
2122 });
2223
2324 it("expands ~/ to a subpath of the home directory", () => {
24 expect(expandHomePath("~/.codex-work")).toBe(NodePath.join(NodeOS.homedir(), ".codex-work"));
25 expect(expandHomePath("~/.codex-work", home)).toBe(NodePath.join(home, ".codex-work"));
2526 });
2627
2728 it("expands a Windows-style ~\\ prefix", () => {
28 expect(expandHomePath("~\\.codex")).toBe(NodePath.join(NodeOS.homedir(), ".codex"));
29 expect(expandHomePath("~\\.codex", home)).toBe(NodePath.join(home, ".codex"));
2930 });
3031
3132 it("does not expand ~user paths", () => {
32 expect(expandHomePath("~alice/foo")).toBe("~alice/foo");
33 expect(expandHomePath("~alice/foo", home)).toBe("~alice/foo");
3334 });
3435});
packages/provider-core/src/server/pathExpansion.ts+8-8
......@@ -1,12 +1,12 @@
11// @effect-diagnostics nodeBuiltinImport:off
2import * as NodeOS from "node:os";
32import * as NodePath from "node:path";
43
54/**
6 * Expand a leading `~` (or `~/…`, `~\…`) in a user-supplied path to the
7 * current user's home directory. Spawned processes don't get shell
8 * expansion, so env vars like `CODEX_HOME=~/.codex-work` would be passed
9 * verbatim and treated as relative paths by the receiver.
5 * Expand a leading `~` (or `~/…`, `~\…`) in a user-supplied path to `home`,
6 * the current user's home directory (`HostProcessHomeDirectory`). Spawned
7 * processes don't get shell expansion, so env vars like
8 * `CODEX_HOME=~/.codex-work` would be passed verbatim and treated as relative
9 * paths by the receiver.
1010 *
1111 * Matches the behavior of the other `expandHomePath` helpers in the
1212 * workspace layers and CLI bootstrap: `~` alone and both `~/` and `~\`
......@@ -14,11 +14,11 @@ import * as NodePath from "node:path";
1414 * start with `~` or is empty. Does not handle `~user` (other-user)
1515 * expansion.
1616 */
17export function expandHomePath(value: string): string {
17export function expandHomePath(value: string, home: string): string {
1818 if (!value) return value;
19 if (value === "~") return NodeOS.homedir();
19 if (value === "~") return home;
2020 if (value.startsWith("~/") || value.startsWith("~\\")) {
21 return NodePath.join(NodeOS.homedir(), value.slice(2));
21 return NodePath.join(home, value.slice(2));
2222 }
2323 return value;
2424}
packages/provider-cursor/src/server/CursorUsageAccounts.ts+6-3
......@@ -10,12 +10,15 @@
1010 *
1111 * @module provider-cursor/server/CursorUsageAccounts
1212 */
13import * as NodeOS from "node:os";
1413
1514import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
1615import type { ProviderUsageScan } from "@t3tools/provider-core/server/usage";
1716import { writeFileStringAtomically } from "@t3tools/shared/atomicWrite";
18import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";
17import {
18 HostProcessEnvironment,
19 HostProcessHomeDirectory,
20 HostProcessPlatform,
21} from "@t3tools/shared/hostProcess";
1922import * as Clock from "effect/Clock";
2023import * as Context from "effect/Context";
2124import * as Deferred from "effect/Deferred";
......@@ -283,7 +286,7 @@ const make = Effect.gen(function* () {
283286
284287 const scan: CursorUsageAccounts["Service"]["scan"] = Effect.fn("CursorUsageAccounts.scan")(
285288 function* ({ keychainUsageEnabled, windowStartMs, retentionCutoffMs, awaitRefresh }) {
286 const home = NodeOS.homedir();
289 const home = yield* HostProcessHomeDirectory;
287290 const userHome =
288291 (platform === "win32" ? hostEnvironment["USERPROFILE"] : hostEnvironment["HOME"]) || home;
289292 const configHome = hostEnvironment["XDG_CONFIG_HOME"]?.trim();
packages/provider-cursor/src/server/adapter.ts+1-1
......@@ -2627,7 +2627,7 @@ export const CursorAdapterV2Driver: ProviderAdapterDriver<
26272627 ...input.config,
26282628 enabled: input.enabled,
26292629 },
2630 environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
2630 environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
26312631 });
26322632 },
26332633 (effect, input) =>
packages/provider-cursor/src/server/driver.ts+1-1
......@@ -99,7 +99,7 @@ export const CursorDriver: ProviderDriver<
9999 const httpClient = yield* HttpClient.HttpClient;
100100 const keychain = yield* CursorKeychain.CursorKeychain;
101101 const sdkRunner = yield* CursorAgentSdk.CursorAgentSdkRunner;
102 const processEnv = mergeProviderInstanceEnvironment(environment);
102 const processEnv = yield* mergeProviderInstanceEnvironment(environment);
103103 const continuationIdentity = defaultProviderContinuationIdentity({
104104 driverKind: DRIVER_KIND,
105105 instanceId,
packages/provider-cursor/src/server/skills.ts+5-2
......@@ -8,7 +8,6 @@
88 *
99 * @module provider/Drivers/CursorSkills
1010 */
11import * as NodeOS from "node:os";
1211
1312import type { ServerProviderSkill } from "@t3tools/contracts";
1413import * as ByteSize from "effect/ByteSize";
......@@ -18,6 +17,7 @@ import * as Path from "effect/Path";
1817import * as PlatformError from "effect/PlatformError";
1918import * as Schema from "effect/Schema";
2019import { parse as parseYamlDocument } from "yaml";
20import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
2121
2222const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
2323const SKILL_MENTION_PATTERN =
......@@ -221,7 +221,10 @@ const inspectCursorSkills = Effect.fn("inspectCursorSkills")(function* (
221221 environment: NodeJS.ProcessEnv = process.env,
222222) {
223223 const path = yield* Path.Path;
224 const userHome = environment.HOME?.trim() || environment.USERPROFILE?.trim() || NodeOS.homedir();
224 const userHome =
225 environment.HOME?.trim() ||
226 environment.USERPROFILE?.trim() ||
227 (yield* HostProcessHomeDirectory);
225228 const rootsBelow = (base: string, scope: "user" | "project") => [
226229 { directory: path.join(base, ".cursor", "skills"), scope },
227230 { directory: path.join(base, ".agents", "skills"), scope },
packages/provider-cursor/src/server/textGeneration.ts+2-2
......@@ -1,4 +1,3 @@
1import * as NodeOS from "node:os";
21import * as FileSystem from "effect/FileSystem";
32
43import type { AgentOptions, RunResult } from "@cursor/sdk";
......@@ -13,6 +12,7 @@ import * as TextGenerationOperations from "@t3tools/provider-core/server/textGen
1312import { cursorSdkModelSelection } from "./sdkModel.ts";
1413import type { CursorAuth } from "./auth.ts";
1514import * as CursorSdk from "./CursorSdk.ts";
15import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
1616
1717const CURSOR_TIMEOUT_MS = 180_000;
1818
......@@ -90,7 +90,7 @@ export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(fu
9090 const apiKey = yield* resolveCursorApiKey(operation);
9191 // The SDK loads sandbox.json independently of settingSources and lets it
9292 // expand the writable paths. Its public API cannot override that policy.
93 if (yield* fs.exists(`${NodeOS.homedir()}/.cursor/sandbox.json`)) {
93 if (yield* fs.exists(`${yield* HostProcessHomeDirectory}/.cursor/sandbox.json`)) {
9494 return yield* new TextGenerationError({
9595 operation,
9696 detail:
packages/provider-cursor/src/server/usageLimits.ts+3-3
......@@ -1,7 +1,6 @@
1import * as NodeOS from "node:os";
21import type { ServerProviderUsageWindow } from "@t3tools/contracts";
32import type { CursorSettings } from "../settings.ts";
4import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
3import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
54import { CURSOR_USAGE_WINDOWS } from "@t3tools/shared/usageLimits";
65import * as DateTime from "effect/DateTime";
76import * as Effect from "effect/Effect";
......@@ -109,7 +108,8 @@ export const readCursorUsageLimits = Effect.fn("readCursorUsageLimits")(function
109108 token = (yield* keychain.accessToken)?.trim();
110109 } else if (!token) {
111110 const home =
112 (platform === "win32" ? environment.USERPROFILE : environment.HOME) || NodeOS.homedir();
111 (platform === "win32" ? environment.USERPROFILE : environment.HOME) ||
112 (yield* HostProcessHomeDirectory);
113113 const directory =
114114 platform === "win32"
115115 ? path.join(environment.APPDATA || path.join(home, "AppData", "Roaming"), "Cursor")
packages/provider-grok/src/server/adapter.ts+12-4
......@@ -4,7 +4,11 @@ import {
44 isXAiTaskCompletedWakeNotification,
55 xAiRateLimitedErrorCode,
66} from "./xaiAcpExtension.ts";
7import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";
7import {
8 HostProcessEnvironment,
9 HostProcessHomeDirectory,
10 HostProcessPlatform,
11} from "@t3tools/shared/hostProcess";
812import { resolveSelfInvocation, type SelfInvocation } from "@t3tools/shared/nodeRuntime";
913import {
1014 defaultInstanceIdForDriver,
......@@ -227,7 +231,10 @@ export function grokLaunchRuntimeMode(
227231}
228232
229233/** The flavor runs Grok's launcher through the adapter's spawner. */
230export function makeGrokAcpAdapterFlavor(options: GrokAdapterV2Options): AcpAdapterV2Flavor {
234export function makeGrokAcpAdapterFlavor(
235 options: GrokAdapterV2Options,
236 homeDirectory: string,
237): AcpAdapterV2Flavor {
231238 return {
232239 driver: GROK_PROVIDER,
233240 runtimeHarness: "Grok",
......@@ -311,6 +318,7 @@ export function makeGrokAcpAdapterFlavor(options: GrokAdapterV2Options): AcpAdap
311318 extractGrokPlanMarkdownFromToolCallData(toolCall.data, {
312319 platform: options.hostPlatform,
313320 environment: options.environment,
321 homeDirectory,
314322 }),
315323 extractBackgroundTaskId: extractXAiMonitorTaskId,
316324 extractBackgroundToolMutation: extractXAiAcpBackgroundToolMutation,
......@@ -331,7 +339,7 @@ export const makeGrokAdapterV2 = Effect.fn("makeGrokAdapterV2")(function* (
331339) {
332340 return yield* makeAcpAdapterV2({
333341 instanceId: options.instanceId,
334 flavor: makeGrokAcpAdapterFlavor(options),
342 flavor: makeGrokAcpAdapterFlavor(options, yield* HostProcessHomeDirectory),
335343 selfInvocation: options.selfInvocation,
336344 ...(options.nativeLogging === undefined ? {} : { nativeLogging: options.nativeLogging }),
337345 ...(options.continuationRequests === undefined
......@@ -366,7 +374,7 @@ export const GrokAdapterV2Driver: ProviderAdapterDriver<GrokSettings, GrokAdapte
366374 return yield* makeGrokAdapterV2({
367375 instanceId: input.instanceId,
368376 settings: { ...input.config, enabled: input.enabled },
369 environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
377 environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
370378 hostPlatform,
371379 selfInvocation,
372380 continuationRequests,
packages/provider-grok/src/server/driver.ts+1-1
......@@ -102,7 +102,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv, Path.Path>
102102 const path = yield* Path.Path;
103103 const host = yield* ProviderHost.ProviderHost;
104104 const { cwd } = host.paths;
105 const processEnv = mergeProviderInstanceEnvironment(environment);
105 const processEnv = yield* mergeProviderInstanceEnvironment(environment);
106106 const continuationIdentity = defaultProviderContinuationIdentity({
107107 driverKind: DRIVER_KIND,
108108 instanceId,
packages/provider-grok/src/server/usage.ts+4-2
......@@ -4,7 +4,6 @@
44 *
55 * @module provider-grok/server/usage
66 */
7import * as NodeOS from "node:os";
87
98import type { UsageTokenTotals } from "@t3tools/contracts";
109import * as Effect from "effect/Effect";
......@@ -20,6 +19,7 @@ import {
2019} from "@t3tools/provider-core/server/usage";
2120
2221import type { GrokSettings } from "../settings.ts";
22import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
2323
2424/**
2525 * Grok reports cost in integer ticks where `1 USD = 10^10` ticks. See Grok
......@@ -229,8 +229,10 @@ export const grokUsageReader: ProviderUsageReader<GrokSettings, Path.Path> = {
229229 format: grokUsageFormat,
230230 directories: Effect.fn("grokUsageReader.directories")(function* ({ environment }) {
231231 const path = yield* Path.Path;
232 const homeDirectory = yield* HostProcessHomeDirectory;
232233 const home = expandHomePath(
233 environment.GROK_HOME?.trim() || path.join(NodeOS.homedir(), ".grok"),
234 environment.GROK_HOME?.trim() || path.join(homeDirectory, ".grok"),
235 homeDirectory,
234236 );
235237 // Sessions also ship multi-megabyte `chat_history` and `events` logs that
236238 // never carry usage; only `updates.jsonl` does.
packages/provider-grok/src/server/usageLimits.ts+5-2
......@@ -1,4 +1,3 @@
1import * as NodeOS from "node:os";
21import type { ServerProviderUsageWindow } from "@t3tools/contracts";
32import * as DateTime from "effect/DateTime";
43import * as Effect from "effect/Effect";
......@@ -14,6 +13,7 @@ import {
1413 makeUnavailableUsageLimits,
1514 makeUsageLimits,
1615} from "@t3tools/provider-core/server/usageLimits";
16import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
1717
1818const GrokCredentials = Schema.Record(
1919 Schema.String,
......@@ -100,7 +100,10 @@ const readGrokCredential = Effect.fn("readGrokCredential")(function* (
100100 const path = yield* Path.Path;
101101 const home =
102102 environment.GROK_HOME?.trim() ||
103 path.join(environment.HOME || environment.USERPROFILE || NodeOS.homedir(), ".grok");
103 path.join(
104 environment.HOME || environment.USERPROFILE || (yield* HostProcessHomeDirectory),
105 ".grok",
106 );
104107 for (const configPath of [
105108 path.join(home, "config.toml"),
106109 path.join(home, "managed_config.toml"),
packages/provider-grok/src/server/xaiAcpExtension.test.ts+5-5
......@@ -1,5 +1,3 @@
1// @effect-diagnostics nodeBuiltinImport:off -- plan-path tests resolve the OS home dir.
2import * as NodeOS from "node:os";
31import { it } from "@effect/vitest";
42import * as Deferred from "effect/Deferred";
53import * as Effect from "effect/Effect";
......@@ -1902,13 +1900,14 @@ describe("Grok exit_plan_mode capture (#8358)", () => {
19021900 expect(extractXAiExitPlanMarkdown(wrapped)).toBe(XAI_EMPTY_PLAN_MARKDOWN);
19031901 });
19041902 it("identifies Grok plan.md paths and extracts markdown from tool call data", () => {
1905 const linuxHost = { platform: "linux" as const, environment: {} };
1906 const windowsHost = { platform: "win32" as const, environment: {} };
1903 const home = "/home/ada";
1904 const linuxHost = { platform: "linux" as const, environment: {}, homeDirectory: home };
1905 const windowsHost = { platform: "win32" as const, environment: {}, homeDirectory: home };
19071906 const grokHomeHost = {
19081907 platform: "linux" as const,
19091908 environment: { GROK_HOME: "/opt/grok-data" },
1909 homeDirectory: home,
19101910 };
1911 const home = NodeOS.homedir().replace(/\\/g, "/");
19121911 const sessionPlan = `${home}/.grok/sessions/abc/plan.md`;
19131912 const nestedSessionPlan = `${home}/.grok/sessions/%2Fhome%2Fproj/019fd20e-c563-70a0-b801-a6bc51815a9b/plan.md`;
19141913 expect(isGrokPlanMarkdownPath(sessionPlan, linuxHost)).toBe(true);
......@@ -1935,6 +1934,7 @@ describe("Grok exit_plan_mode capture (#8358)", () => {
19351934 isGrokPlanMarkdownPath("/OPT/GROK-DATA/sessions/sess/plan.md", {
19361935 platform: "win32",
19371936 environment: { GROK_HOME: "/opt/grok-data" },
1937 homeDirectory: home,
19381938 }),
19391939 ).toBe(true);
19401940 expect(isGrokPlanMarkdownPath("/OPT/GROK-DATA/sessions/sess/plan.md", grokHomeHost)).toBe(
packages/provider-grok/src/server/xaiAcpExtension.ts+5-6
......@@ -1,6 +1,3 @@
1// @effect-diagnostics nodeBuiltinImport:off -- Grok's plan file lives under the OS home dir.
2import * as NodeOS from "node:os";
3
41import type { ProviderUserInputAnswers, UserInputQuestion } from "@t3tools/contracts";
52import * as Cause from "effect/Cause";
63import * as Deferred from "effect/Deferred";
......@@ -1088,11 +1085,13 @@ function addGrokSessionPrefix(
10881085export interface GrokPlanPathHost {
10891086 readonly platform: NodeJS.Platform;
10901087 readonly environment: NodeJS.ProcessEnv;
1088 readonly homeDirectory: string;
10911089}
10921090
1093function grokPlanSessionPrefixes(environment: NodeJS.ProcessEnv): ReadonlySet<string> {
1091function grokPlanSessionPrefixes(host: GrokPlanPathHost): ReadonlySet<string> {
1092 const { environment } = host;
10941093 const prefixes = new Set<string>();
1095 addGrokSessionPrefix(prefixes, NodeOS.homedir(), true);
1094 addGrokSessionPrefix(prefixes, host.homeDirectory, true);
10961095 addGrokSessionPrefix(prefixes, "~", true);
10971096 addGrokSessionPrefix(prefixes, environment.HOME ?? "", true);
10981097 addGrokSessionPrefix(prefixes, environment.USERPROFILE ?? "", true);
......@@ -1134,7 +1133,7 @@ export function isGrokPlanMarkdownPath(
11341133 ) {
11351134 return false;
11361135 }
1137 for (const prefix of grokPlanSessionPrefixes(host.environment)) {
1136 for (const prefix of grokPlanSessionPrefixes(host)) {
11381137 const needle = win32 ? prefix.toLowerCase() : prefix;
11391138 if (!haystack.startsWith(needle)) {
11401139 continue;
packages/provider-muse/src/server/driver.ts+3-3
......@@ -1,6 +1,6 @@
11import { ProviderDriverKind } from "@t3tools/contracts";
22import { MuseSettings } from "../settings.ts";
3import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";
3import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
44import * as Effect from "effect/Effect";
55import * as FileSystem from "effect/FileSystem";
66import * as Path from "effect/Path";
......@@ -71,14 +71,14 @@ export const MuseDriver: ProviderDriver<MuseSettings, MuseDriverEnv> = {
7171 const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
7272 const hostEnvironment = yield* HostProcessEnvironment;
7373 // Drop an inherited META_API_KEY so Muse uses its login; an instance value still wins.
74 const processEnvironment = mergeProviderInstanceEnvironment(
74 const processEnvironment = yield* mergeProviderInstanceEnvironment(
7575 environment,
7676 makeMuseEnvironment(hostEnvironment),
7777 );
7878 const effectiveConfig = {
7979 ...config,
8080 enabled,
81 binaryPath: expandHomePath(config.binaryPath),
81 binaryPath: expandHomePath(config.binaryPath, yield* HostProcessHomeDirectory),
8282 } satisfies MuseSettings;
8383 const continuationIdentity = defaultProviderContinuationIdentity({
8484 driverKind: DRIVER_KIND,
packages/provider-opencode/src/server/adapter.ts+1-1
......@@ -3744,7 +3744,7 @@ export const OpenCodeAdapterV2Driver: ProviderAdapterDriver<
37443744 return yield* makeOpenCodeAdapterV2({
37453745 instanceId: input.instanceId,
37463746 settings: { ...input.config, enabled: input.enabled },
3747 environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
3747 environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
37483748 ...(providerEventLoggers.native === undefined
37493749 ? {}
37503750 : { nativeEventLogger: providerEventLoggers.native }),
packages/provider-opencode/src/server/driver.ts+1-1
......@@ -206,7 +206,7 @@ export const OpenCodeDriver: ProviderDriver<
206206 const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions;
207207 const crypto = yield* Crypto.Crypto;
208208 const host = yield* ProviderHost.ProviderHost;
209 const processEnv = mergeProviderInstanceEnvironment(environment);
209 const processEnv = yield* mergeProviderInstanceEnvironment(environment);
210210 const continuationIdentity = defaultProviderContinuationIdentity({
211211 driverKind: DRIVER_KIND,
212212 instanceId,
packages/provider-opencode/src/server/usage.ts+4-4
......@@ -4,7 +4,6 @@
44 *
55 * @module provider-opencode/server/usage
66 */
7import * as NodeOS from "node:os";
87
98import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
109import {
......@@ -12,7 +11,7 @@ import {
1211 type ProviderUsageReader,
1312 type UsageRecord,
1413} from "@t3tools/provider-core/server/usage";
15import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";
14import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
1615import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
1716import * as Effect from "effect/Effect";
1817import * as Exit from "effect/Exit";
......@@ -259,6 +258,7 @@ const resolveOpenCodeDataDirs = Effect.fn("resolveOpenCodeDataDirs")(function* (
259258 const fileSystem = yield* FileSystem.FileSystem;
260259 const path = yield* Path.Path;
261260 const environment = yield* HostProcessEnvironment;
261 const homeDirectory = yield* HostProcessHomeDirectory;
262262 const roots = environment["OPENCODE_DATA_DIR"]
263263 ?.split(",")
264264 .map((value) => value.trim())
......@@ -268,13 +268,13 @@ const resolveOpenCodeDataDirs = Effect.fn("resolveOpenCodeDataDirs")(function* (
268268 path.join(
269269 dataHome && path.isAbsolute(dataHome)
270270 ? dataHome
271 : path.join(NodeOS.homedir(), ".local", "share"),
271 : path.join(homeDirectory, ".local", "share"),
272272 "opencode",
273273 ),
274274 ];
275275 const canonical = new Set<string>();
276276 for (const root of roots?.length ? roots : defaults) {
277 const resolved = path.resolve(expandHomePath(root));
277 const resolved = path.resolve(expandHomePath(root, homeDirectory));
278278 canonical.add(yield* fileSystem.realPath(resolved).pipe(Effect.orElseSucceed(() => resolved)));
279279 }
280280 return [...canonical];
packages/provider-opencode/src/server/usageLimits.ts+6-3
......@@ -1,5 +1,3 @@
1import * as NodeOS from "node:os";
2
31import type { ServerProviderUsageWindow } from "@t3tools/contracts";
42import * as Crypto from "effect/Crypto";
53import * as DateTime from "effect/DateTime";
......@@ -18,6 +16,7 @@ import {
1816 makeUnavailableUsageLimits,
1917 makeUsageLimits,
2018} from "@t3tools/provider-core/server/usageLimits";
19import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
2120
2221const AuthFile = Schema.Struct({ "opencode-go": Schema.optionalKey(Schema.Unknown) });
2322const ApiAuth = Schema.Struct({ type: Schema.Literal("api"), key: Schema.String });
......@@ -47,7 +46,11 @@ export const readOpenCodeGoUsageLimits = Effect.fn("readOpenCodeGoUsageLimits")(
4746 const env = input.environment;
4847 const dataHome =
4948 env.XDG_DATA_HOME ||
50 path.join(env.HOME || env.USERPROFILE || NodeOS.homedir(), ".local", "share");
49 path.join(
50 env.HOME || env.USERPROFILE || (yield* HostProcessHomeDirectory),
51 ".local",
52 "share",
53 );
5154 const authPath = path.join(dataHome, "opencode", "auth.json");
5255 const contents =
5356 env.OPENCODE_AUTH_CONTENT ||
packages/provider-pi/src/server/adapter.ts+1-1
......@@ -3276,7 +3276,7 @@ export const PiAdapterV2Driver: ProviderAdapterDriver<PiSettings, PiAdapterV2Dri
32763276 return yield* makePiAdapterV2({
32773277 instanceId: input.instanceId,
32783278 settings: { ...input.config, enabled: input.enabled },
3279 environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
3279 environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
32803280 continuationRequests,
32813281 });
32823282 },
packages/provider-pi/src/server/driver.ts+1-1
......@@ -97,7 +97,7 @@ export const PiDriver: ProviderDriver<PiSettings, PiDriverEnv> = {
9797 const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions;
9898 const host = yield* ProviderHost.ProviderHost;
9999 const { cwd } = host.paths;
100 const processEnv = mergeProviderInstanceEnvironment(environment);
100 const processEnv = yield* mergeProviderInstanceEnvironment(environment);
101101 const continuationIdentity = defaultProviderContinuationIdentity({
102102 driverKind: DRIVER_KIND,
103103 instanceId,
packages/shared/src/hostProcess.ts+7
......@@ -25,6 +25,13 @@ export const HostProcessHostname = Context.Reference<string>(
2525 },
2626);
2727
28export const HostProcessHomeDirectory = Context.Reference<string>(
29 "@t3tools/shared/hostProcess/HostProcessHomeDirectory",
30 {
31 defaultValue: () => NodeOS.homedir(),
32 },
33);
34
2835export const HostProcessEnvironment = Context.Reference<NodeJS.ProcessEnv>(
2936 "@t3tools/shared/hostProcess/HostProcessEnvironment",
3037 {
packages/source-control-forgejo/src/server/ForgejoCli.ts+2-3
......@@ -6,13 +6,12 @@ import * as Result from "effect/Result";
66import * as Clock from "effect/Clock";
77import * as FileSystem from "effect/FileSystem";
88import * as Semaphore from "effect/Semaphore";
9import * as NodeOS from "node:os";
109// @effect-diagnostics-next-line nodeBuiltinImport:off - fj storage paths use explicit Windows and POSIX layouts, independently of this process's platform.
1110import * as NodePath from "node:path";
1211import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";
1312import { ChildProcessSpawner } from "effect/process";
1413import { decodeJsonResult } from "@t3tools/shared/schemaJson";
15import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
14import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
1615import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost";
1716import { collectUint8StreamText } from "@t3tools/provider-core/server/collectStreamText";
1817import type { SourceControlProviderContext } from "@t3tools/source-control-core/server/SourceControlProvider";
......@@ -250,7 +249,7 @@ export const make = Effect.gen(function* () {
250249 const readKeys = Effect.fn("ForgejoCli.readKeys")(function* (cwd: string) {
251250 for (const path of forgejoKeysPaths({
252251 platform: yield* HostProcessPlatform,
253 home: NodeOS.homedir(),
252 home: yield* HostProcessHomeDirectory,
254253 ...(globalThis.process.env.XDG_DATA_HOME
255254 ? { dataHome: globalThis.process.env.XDG_DATA_HOME }
256255 : {}),