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";...@@ -35,6 +35,7 @@ import * as DesktopUpdates from "../updates/DesktopUpdates.ts";
35import * as DesktopSnapShot from "../snapShot/DesktopSnapShot.ts";35import * as DesktopSnapShot from "../snapShot/DesktopSnapShot.ts";
36import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts";36import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts";
37import * as DesktopRendererHistory from "../telemetry/DesktopRendererHistory.ts";37import * as DesktopRendererHistory from "../telemetry/DesktopRendererHistory.ts";
38import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
3839
39const DEFAULT_DESKTOP_BACKEND_PORT = 3773;40const DEFAULT_DESKTOP_BACKEND_PORT = 3773;
40const MAX_TCP_PORT = 65_535;41const MAX_TCP_PORT = 65_535;
...@@ -284,7 +285,9 @@ const startup = Effect.gen(function* () {...@@ -284,7 +285,9 @@ const startup = Effect.gen(function* () {
284 preReadyElectronOptions.linuxPasswordStoreCommandLine !== null;285 preReadyElectronOptions.linuxPasswordStoreCommandLine !== null;
285 const linuxElectronOptions =286 const linuxElectronOptions =
286 environment.platform === "linux" && !hasCommandLinePasswordStore287 environment.platform === "linux" && !hasCommandLinePasswordStore
287 ? DesktopPreReadyPlatform.resolveEarlyLinuxElectronOptionsFromProcess()288 ? DesktopPreReadyPlatform.resolveEarlyLinuxElectronOptionsFromProcess(
289 yield* HostProcessHomeDirectory,
290 )
288 : preReadyElectronOptions.linux;291 : preReadyElectronOptions.linux;
289 if (linuxElectronOptions !== null && !hasCommandLinePasswordStore) {292 if (linuxElectronOptions !== null && !hasCommandLinePasswordStore) {
290 if (293 if (
apps/desktop/src/app/DesktopPreReadyPlatform.ts+14-12
...@@ -1,13 +1,12 @@...@@ -1,13 +1,12 @@
1// @effect-diagnostics nodeBuiltinImport:off - pre-ready Electron setup reads settings and prepares the Linux desktop entry synchronously before app services are available.1// @effect-diagnostics nodeBuiltinImport:off - pre-ready Electron setup reads settings and prepares the Linux desktop entry synchronously before app services are available.
2import * as NodeFS from "node:fs";2import * as NodeFS from "node:fs";
3import * as NodeOS from "node:os";
4import * as NodePath from "node:path";3import * as NodePath from "node:path";
5import * as Context from "effect/Context";4import * as Context from "effect/Context";
6import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";6import * as Layer from "effect/Layer";
87
9import * as Electron from "electron";8import * as Electron from "electron";
10import { HostProcessPlatform } from "@t3tools/shared/hostProcess";9import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
1110
12import * as DesktopEarlyElectronStartup from "./DesktopEarlyElectronStartup.ts";11import * as DesktopEarlyElectronStartup from "./DesktopEarlyElectronStartup.ts";
13import { resolveDesktopAppBranding } from "./DesktopEnvironment.ts";12import { resolveDesktopAppBranding } from "./DesktopEnvironment.ts";
...@@ -31,14 +30,15 @@ function readCommandLineSwitchValue(...@@ -31,14 +30,15 @@ function readCommandLineSwitchValue(
31 return value.length > 0 ? value : null;30 return value.length > 0 ? value : null;
32}31}
3332
34export const resolveEarlyLinuxElectronOptionsFromProcess =33export const resolveEarlyLinuxElectronOptionsFromProcess = (
35 (): DesktopEarlyElectronStartup.EarlyLinuxElectronOptions =>34 homeDirectory: string,
36 DesktopEarlyElectronStartup.resolveEarlyLinuxElectronOptions({35): DesktopEarlyElectronStartup.EarlyLinuxElectronOptions =>
37 env: process.env,36 DesktopEarlyElectronStartup.resolveEarlyLinuxElectronOptions({
38 homeDirectory: NodeOS.homedir(),37 env: process.env,
39 joinPath: NodePath.posix.join,38 homeDirectory,
40 readFileString: (path) => NodeFS.readFileSync(path, "utf8"),39 joinPath: NodePath.posix.join,
41 });40 readFileString: (path) => NodeFS.readFileSync(path, "utf8"),
41 });
4242
43export class DesktopPreReadyElectronOptions extends Context.Service<43export class DesktopPreReadyElectronOptions extends Context.Service<
44 DesktopPreReadyElectronOptions,44 DesktopPreReadyElectronOptions,
...@@ -51,12 +51,14 @@ export class DesktopPreReadyElectronOptions extends Context.Service<...@@ -51,12 +51,14 @@ export class DesktopPreReadyElectronOptions extends Context.Service<
51/** @public Service construction is part of the canonical Effect module API. */51/** @public Service construction is part of the canonical Effect module API. */
52export const make = Effect.gen(function* () {52export const make = Effect.gen(function* () {
53 const platform = yield* HostProcessPlatform;53 const platform = yield* HostProcessPlatform;
54 const homeDirectory = yield* HostProcessHomeDirectory;
54 return yield* Effect.sync((): DesktopPreReadyElectronOptions["Service"] => {55 return yield* Effect.sync((): DesktopPreReadyElectronOptions["Service"] => {
55 const linuxPasswordStoreCommandLine =56 const linuxPasswordStoreCommandLine =
56 platform === "linux"57 platform === "linux"
57 ? readCommandLineSwitchValue(Electron.app.commandLine, "password-store")58 ? readCommandLineSwitchValue(Electron.app.commandLine, "password-store")
58 : null;59 : null;
59 const linux = platform === "linux" ? resolveEarlyLinuxElectronOptionsFromProcess() : null;60 const linux =
61 platform === "linux" ? resolveEarlyLinuxElectronOptionsFromProcess(homeDirectory) : null;
6062
61 if (linux !== null) {63 if (linux !== null) {
62 // The portal also requires a valid desktop entry. An AppImage update may64 // The portal also requires a valid desktop entry. An AppImage update may
...@@ -64,7 +66,7 @@ export const make = Effect.gen(function* () {...@@ -64,7 +66,7 @@ export const make = Effect.gen(function* () {
64 try {66 try {
65 const applicationsDir = NodePath.posix.join(67 const applicationsDir = NodePath.posix.join(
66 process.env.XDG_DATA_HOME?.trim() ||68 process.env.XDG_DATA_HOME?.trim() ||
67 NodePath.posix.join(NodeOS.homedir(), ".local", "share"),69 NodePath.posix.join(homeDirectory, ".local", "share"),
68 "applications",70 "applications",
69 );71 );
70 NodeFS.mkdirSync(applicationsDir, { recursive: true });72 NodeFS.mkdirSync(applicationsDir, { recursive: true });
apps/desktop/src/ipc/methods/window.ts+2-2
...@@ -16,7 +16,6 @@ import {...@@ -16,7 +16,6 @@ import {
16import { WORKSPACE_IMAGE_PREVIEW_EXTENSIONS } from "@t3tools/shared/filePreview";16import { WORKSPACE_IMAGE_PREVIEW_EXTENSIONS } from "@t3tools/shared/filePreview";
17import { resolveEditorCommand } from "@t3tools/shared/editor";17import { resolveEditorCommand } from "@t3tools/shared/editor";
18import * as HostProcess from "@t3tools/shared/hostProcess";18import * as HostProcess from "@t3tools/shared/hostProcess";
19import * as NodeOS from "node:os";
20import * as FileSystem from "effect/FileSystem";19import * as FileSystem from "effect/FileSystem";
21import * as Path from "effect/Path";20import * as Path from "effect/Path";
22import * as Effect from "effect/Effect";21import * as Effect from "effect/Effect";
...@@ -46,6 +45,7 @@ import {...@@ -46,6 +45,7 @@ import {
46 resolveWslPickFolderDefaultPath,45 resolveWslPickFolderDefaultPath,
47 wslUncPathToLinuxPath,46 wslUncPathToLinuxPath,
48} from "../../wsl/wslPathParsing.ts";47} from "../../wsl/wslPathParsing.ts";
48import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
4949
50const ContextMenuPosition = Schema.Struct({50const ContextMenuPosition = Schema.Struct({
51 x: Schema.Number,51 x: Schema.Number,
...@@ -398,7 +398,7 @@ export const pickThemeFiles = DesktopIpc.makeIpcMethod({...@@ -398,7 +398,7 @@ export const pickThemeFiles = DesktopIpc.makeIpcMethod({
398 // The VS Code extensions directory is the same dotfolder on Windows,398 // The VS Code extensions directory is the same dotfolder on Windows,
399 // macOS, and Linux; when it is missing the picker opens wherever the399 // macOS, and Linux; when it is missing the picker opens wherever the
400 // platform would by default.400 // platform would by default.
401 const extensionsDir = path.join(NodeOS.homedir(), ".vscode", "extensions");401 const extensionsDir = path.join(yield* HostProcessHomeDirectory, ".vscode", "extensions");
402 const defaultPath = yield* fileSystem402 const defaultPath = yield* fileSystem
403 .exists(extensionsDir)403 .exists(extensionsDir)
404 .pipe(Effect.orElseSucceed(() => false));404 .pipe(Effect.orElseSucceed(() => false));
apps/desktop/src/snapShot/CaptureShortcutConfig.test.ts+8-8
...@@ -29,7 +29,7 @@ beforeEach(async () => {...@@ -29,7 +29,7 @@ beforeEach(async () => {
29 tools.validateNiri.mockReset().mockResolvedValue(undefined);29 tools.validateNiri.mockReset().mockResolvedValue(undefined);
30 tools.hyprlandBindings.mockReset().mockResolvedValue([]);30 tools.hyprlandBindings.mockReset().mockResolvedValue([]);
31 tools.reloadHyprland.mockReset().mockResolvedValue(undefined);31 tools.reloadHyprland.mockReset().mockResolvedValue(undefined);
32 setup = new CaptureShortcutConfig(tools);32 setup = new CaptureShortcutConfig(NodeOS.homedir(), tools);
33});33});
34afterEach(async () => {34afterEach(async () => {
35 await NodeFSP.rm(directory, { recursive: true, force: true });35 await NodeFSP.rm(directory, { recursive: true, force: true });
...@@ -219,15 +219,15 @@ it("does not edit Omarchy's shipped defaults", async () => {...@@ -219,15 +219,15 @@ it("does not edit Omarchy's shipped defaults", async () => {
219});219});
220it("honors NIRI_CONFIG and XDG_CONFIG_HOME", () => {220it("honors NIRI_CONFIG and XDG_CONFIG_HOME", () => {
221 expect(221 expect(
222 niriCaptureConfigPath(222 niriCaptureConfigPath("/home/test", {
223 { NIRI_CONFIG: "/custom/test.kdl", XDG_CONFIG_HOME: "/config" },223 NIRI_CONFIG: "/custom/test.kdl",
224 "/home/test",224 XDG_CONFIG_HOME: "/config",
225 ),225 }),
226 ).toBe("/custom/test.kdl");226 ).toBe("/custom/test.kdl");
227 expect(niriCaptureConfigPath({ XDG_CONFIG_HOME: "/config" }, "/home/test")).toBe(227 expect(niriCaptureConfigPath("/home/test", { XDG_CONFIG_HOME: "/config" })).toBe(
228 "/config/niri/config.kdl",228 "/config/niri/config.kdl",
229 );229 );
230 expect(niriCaptureConfigPath({}, "/home/test")).toBe("/home/test/.config/niri/config.kdl");230 expect(niriCaptureConfigPath("/home/test", {})).toBe("/home/test/.config/niri/config.kdl");
231});231});
232it("preserves a UTF-8 BOM through preview and apply", async () => {232it("preserves a UTF-8 BOM through preview and apply", async () => {
233 await NodeFSP.writeFile(path, "\uFEFFbinds {}\r\n");233 await NodeFSP.writeFile(path, "\uFEFFbinds {}\r\n");
...@@ -251,7 +251,7 @@ it("the installed Niri parser accepts the generated edit and its relative includ...@@ -251,7 +251,7 @@ it("the installed Niri parser accepts the generated edit and its relative includ
251 NodePath.join(directory, "keys.kdl"),251 NodePath.join(directory, "keys.kdl"),
252 "binds {\n Ctrl+Alt+Q { quit; }\n}\n",252 "binds {\n Ctrl+Alt+Q { quit; }\n}\n",
253 );253 );
254 const checked = new CaptureShortcutConfig({254 const checked = new CaptureShortcutConfig(NodeOS.homedir(), {
255 ...tools,255 ...tools,
256 validateNiri: async (file) => {256 validateNiri: async (file) => {
257 await NodeUtil.promisify(NodeChildProcess.execFile)("niri", ["validate", "--config", file]);257 await NodeUtil.promisify(NodeChildProcess.execFile)("niri", ["validate", "--config", file]);
apps/desktop/src/snapShot/CaptureShortcutConfig.ts+5-4
...@@ -2,7 +2,6 @@...@@ -2,7 +2,6 @@
2import * as NodeFSP from "node:fs/promises";2import * as NodeFSP from "node:fs/promises";
3import * as NodeFS from "node:fs";3import * as NodeFS from "node:fs";
4import * as NodePath from "node:path";4import * as NodePath from "node:path";
5import * as NodeOS from "node:os";
6import * as NodeCrypto from "node:crypto";5import * as NodeCrypto from "node:crypto";
7import * as NodeChildProcess from "node:child_process";6import * as NodeChildProcess from "node:child_process";
8import * as NodeUtil from "node:util";7import * as NodeUtil from "node:util";
...@@ -39,7 +38,7 @@ const decodeHyprlandBindings = Schema.decodeUnknownSync(...@@ -39,7 +38,7 @@ const decodeHyprlandBindings = Schema.decodeUnknownSync(
39 ),38 ),
40);39);
4140
42export function niriCaptureConfigPath(env = process.env, home = NodeOS.homedir()) {41export function niriCaptureConfigPath(home: string, env = process.env) {
43 return (42 return (
44 env.NIRI_CONFIG ||43 env.NIRI_CONFIG ||
45 NodePath.join(env.XDG_CONFIG_HOME || NodePath.join(home, ".config"), "niri", "config.kdl")44 NodePath.join(env.XDG_CONFIG_HOME || NodePath.join(home, ".config"), "niri", "config.kdl")
...@@ -99,8 +98,10 @@ export class CaptureShortcutConfig {...@@ -99,8 +98,10 @@ export class CaptureShortcutConfig {
99 | { preview: DesktopCaptureConfigPreview; target: Target; files: Snapshot[]; missing: string[] }98 | { preview: DesktopCaptureConfigPreview; target: Target; files: Snapshot[]; missing: string[] }
100 | undefined;99 | undefined;
101 private applying = false;100 private applying = false;
101 private readonly home: string;
102 private readonly tools: typeof defaultTools;102 private readonly tools: typeof defaultTools;
103 constructor(tools = defaultTools) {103 constructor(home: string, tools = defaultTools) {
104 this.home = home;
104 this.tools = tools;105 this.tools = tools;
105 }106 }
106107
...@@ -166,7 +167,7 @@ export class CaptureShortcutConfig {...@@ -166,7 +167,7 @@ export class CaptureShortcutConfig {
166 if (include.path.includes("$") || /[*?[\]]/.test(include.path))167 if (include.path.includes("$") || /[*?[\]]/.test(include.path))
167 throw new Error("This config uses a dynamic include. Use manual setup in Advanced.");168 throw new Error("This config uses a dynamic include. Use manual setup in Advanced.");
168 const path = include.path.startsWith("~/")169 const path = include.path.startsWith("~/")
169 ? NodePath.join(NodeOS.homedir(), include.path.slice(2))170 ? NodePath.join(this.home, include.path.slice(2))
170 : NodePath.resolve(NodePath.dirname(file.path), include.path);171 : NodePath.resolve(NodePath.dirname(file.path), include.path);
171 let child: Snapshot;172 let child: Snapshot;
172 try {173 try {
apps/desktop/src/snapShot/DesktopSnapShot.ts+8-6
...@@ -88,6 +88,7 @@ import {...@@ -88,6 +88,7 @@ import {
88 snapShotShortcutRegistrationFailureMessage,88 snapShotShortcutRegistrationFailureMessage,
89 snapShotShortcutSystemConflict,89 snapShotShortcutSystemConflict,
90} from "./snapShot.ts";90} from "./snapShot.ts";
91import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
9192
92const MAX_CAPTURE_WIDTH = 2_560;93const MAX_CAPTURE_WIDTH = 2_560;
93const MAX_CAPTURE_HEIGHT = 1_600;94const MAX_CAPTURE_HEIGHT = 1_600;
...@@ -727,6 +728,7 @@ export const make = Effect.gen(function* () {...@@ -727,6 +728,7 @@ export const make = Effect.gen(function* () {
727 const runPromise = Effect.runPromiseWith(context);728 const runPromise = Effect.runPromiseWith(context);
728 const captureDirectory = path.join(environment.stateDir, "snap-shots");729 const captureDirectory = path.join(environment.stateDir, "snap-shots");
729 const linuxAppId = environment.linuxDesktopEntryName.replace(/\.desktop$/, "");730 const linuxAppId = environment.linuxDesktopEntryName.replace(/\.desktop$/, "");
731 const home = yield* HostProcessHomeDirectory;
730 let shortcutVerified = false;732 let shortcutVerified = false;
731 const gnomeSetupPaths = {733 const gnomeSetupPaths = {
732 bundle: environment.isPackaged734 bundle: environment.isPackaged
...@@ -757,7 +759,7 @@ export const make = Effect.gen(function* () {...@@ -757,7 +759,7 @@ export const make = Effect.gen(function* () {
757 dataHome: path.dirname(environment.linuxApplicationsDir),759 dataHome: path.dirname(environment.linuxApplicationsDir),
758 };760 };
759 const shiftShortcutWorkerPath = path.join(__dirname, "snapShot", "GlobalShiftShortcutWorker.cjs");761 const shiftShortcutWorkerPath = path.join(__dirname, "snapShot", "GlobalShiftShortcutWorker.cjs");
760 const shortcutConfig = new CaptureShortcutConfig();762 const shortcutConfig = new CaptureShortcutConfig(home);
761 const accessibilityWorkerPath = path.join(763 const accessibilityWorkerPath = path.join(
762 __dirname,764 __dirname,
763 "snapShot",765 "snapShot",
...@@ -1184,7 +1186,7 @@ export const make = Effect.gen(function* () {...@@ -1184,7 +1186,7 @@ export const make = Effect.gen(function* () {
1184 shortcut,1186 shortcut,
1185 shortcutRegistered: false,1187 shortcutRegistered: false,
1186 shortcutBinding: niriCaptureBinding(linuxAppId),1188 shortcutBinding: niriCaptureBinding(linuxAppId),
1187 shortcutConfigPath: niriCaptureConfigPath(),1189 shortcutConfigPath: niriCaptureConfigPath(home),
1188 shortcutActionRegistered: registered,1190 shortcutActionRegistered: registered,
1189 shortcutMessage: registered1191 shortcutMessage: registered
1190 ? "Set up the shortcut to add it to your Niri config."1192 ? "Set up the shortcut to add it to your Niri config."
...@@ -1474,8 +1476,8 @@ export const make = Effect.gen(function* () {...@@ -1474,8 +1476,8 @@ export const make = Effect.gen(function* () {
1474 const configPath =1476 const configPath =
1475 selectedPath ??1477 selectedPath ??
1476 (desktop === "niri"1478 (desktop === "niri"
1477 ? niriCaptureConfigPath()1479 ? niriCaptureConfigPath(home)
1478 : (await hyprlandCaptureShortcut(linuxAppId)).shortcutConfigPath);1480 : (await hyprlandCaptureShortcut(linuxAppId, home)).shortcutConfigPath);
1479 return shortcutConfig.preview({ desktop, path: configPath, appId: linuxAppId }, request);1481 return shortcutConfig.preview({ desktop, path: configPath, appId: linuxAppId }, request);
1480 },1482 },
1481 catch: (cause) =>1483 catch: (cause) =>
...@@ -1593,7 +1595,7 @@ export const make = Effect.gen(function* () {...@@ -1593,7 +1595,7 @@ export const make = Effect.gen(function* () {
1593 : undefined;1595 : undefined;
1594 const hyprlandShortcut =1596 const hyprlandShortcut =
1595 state.linuxBackend === "hyprland"1597 state.linuxBackend === "hyprland"
1596 ? yield* Effect.promise(() => hyprlandCaptureShortcut(linuxAppId))1598 ? yield* Effect.promise(() => hyprlandCaptureShortcut(linuxAppId, home))
1597 : undefined;1599 : undefined;
1598 return {1600 return {
1599 ...state,1601 ...state,
...@@ -1609,7 +1611,7 @@ export const make = Effect.gen(function* () {...@@ -1609,7 +1611,7 @@ export const make = Effect.gen(function* () {
1609 : {}),1611 : {}),
1610 ...hyprlandShortcut,1612 ...hyprlandShortcut,
1611 ...(state.linuxBackend === "niri"1613 ...(state.linuxBackend === "niri"
1612 ? { shortcutConfigPath: niriCaptureConfigPath() }1614 ? { shortcutConfigPath: niriCaptureConfigPath(home) }
1613 : {}),1615 : {}),
1614 ...(kdeHelper1616 ...(kdeHelper
1615 ? {1617 ? {
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...@@ -121,7 +121,7 @@ it.each([true, false])("uses the active config syntax and user bindings (Lua: %s
121 NodePath.join(config, lua ? "bindings.lua" : "bindings.conf"),121 NodePath.join(config, lua ? "bindings.lua" : "bindings.conf"),
122 "custom bindings",122 "custom bindings",
123 );123 );
124 const result = await hyprlandCaptureShortcut("com.t3tools.T3Code", directory);124 const result = await hyprlandCaptureShortcut("com.t3tools.T3Code", "/home/test", directory);
125 expect(result.shortcutConfigPath).toBe(125 expect(result.shortcutConfigPath).toBe(
126 NodePath.join(config, lua ? "bindings.lua" : "bindings.conf"),126 NodePath.join(config, lua ? "bindings.lua" : "bindings.conf"),
127 );127 );
apps/desktop/src/snapShot/HyprlandSnapShot.ts+2-1
...@@ -29,7 +29,8 @@ function hyprlandCaptureBinding(appId: string, lua: boolean): string {...@@ -29,7 +29,8 @@ function hyprlandCaptureBinding(appId: string, lua: boolean): string {
29/** Omarchy owns its defaults; instructions always point at a user-owned config. */29/** Omarchy owns its defaults; instructions always point at a user-owned config. */
30export async function hyprlandCaptureShortcut(30export async function hyprlandCaptureShortcut(
31 appId: string,31 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"),
33) {34) {
34 const directory = NodePath.join(configHome, "hypr");35 const directory = NodePath.join(configHome, "hypr");
35 const exists = async (name: string) =>36 const exists = async (name: string) =>
apps/server/src/assets/AssetAccess.test.ts+3-11
...@@ -2,7 +2,6 @@...@@ -2,7 +2,6 @@
2import * as NodeServices from "@effect/platform-node/NodeServices";2import * as NodeServices from "@effect/platform-node/NodeServices";
3import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform";3import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform";
4import * as NodeFSP from "node:fs/promises";4import * as NodeFSP from "node:fs/promises";
5import * as NodeOS from "node:os";
6import {5import {
7 AssetAccessError,6 AssetAccessError,
8 AssetPreviewTypeValidationError,7 AssetPreviewTypeValidationError,
...@@ -37,17 +36,13 @@ import { openMediaFile } from "./MediaFile.ts";...@@ -37,17 +36,13 @@ import { openMediaFile } from "./MediaFile.ts";
37import { symlinksSupported } from "@t3tools/shared/testing/symlinks";36import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
38import * as GitHubCredentials from "@t3tools/source-control-github/server/GitHubCredentials";37import * as GitHubCredentials from "@t3tools/source-control-github/server/GitHubCredentials";
39import { githubMediaResponse } from "./GitHubMediaFetch.ts";38import { githubMediaResponse } from "./GitHubMediaFetch.ts";
39import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
4040
41vi.mock("node:fs/promises", async (importOriginal) => {41vi.mock("node:fs/promises", async (importOriginal) => {
42 const actual = await importOriginal<typeof NodeFSP>();42 const actual = await importOriginal<typeof NodeFSP>();
43 return { ...actual, open: vi.fn(actual.open), realpath: vi.fn(actual.realpath) };43 return { ...actual, open: vi.fn(actual.open), realpath: vi.fn(actual.realpath) };
44});44});
4545
46vi.mock("node:os", async (importOriginal) => {
47 const actual = await importOriginal<typeof NodeOS>();
48 return { ...actual, homedir: vi.fn(actual.homedir) };
49});
50
51const layerConfig = ServerConfig.ServerConfig.layerTest(process.cwd(), {46const layerConfig = ServerConfig.ServerConfig.layerTest(process.cwd(), {
52 prefix: "t3-asset-access-test-",47 prefix: "t3-asset-access-test-",
53});48});
...@@ -299,8 +294,7 @@ describe("AssetAccess", () => {...@@ -299,8 +294,7 @@ describe("AssetAccess", () => {
299 yield* fs.makeDirectory(path.dirname(filePath), { recursive: true });294 yield* fs.makeDirectory(path.dirname(filePath), { recursive: true });
300 yield* fs.writeFileString(filePath, "recording bytes");295 yield* fs.writeFileString(filePath, "recording bytes");
301 const canonicalFile = yield* fs.realPath(filePath);296 const canonicalFile = yield* fs.realPath(filePath);
302 const homeSpy = vi.mocked(NodeOS.homedir).mockReturnValue(home);297 yield* Effect.gen(function* () {
303 try {
304 for (const workspaceRoot of [298 for (const workspaceRoot of [
305 path.join(home, "project"),299 path.join(home, "project"),
306 path.join(directory, "srv", "project"),300 path.join(directory, "srv", "project"),
...@@ -332,9 +326,7 @@ describe("AssetAccess", () => {...@@ -332,9 +326,7 @@ describe("AssetAccess", () => {
332 expect(yield* Effect.promise(() => response.text())).toBe("recording bytes");326 expect(yield* Effect.promise(() => response.text())).toBe("recording bytes");
333 }327 }
334 }328 }
335 } finally {329 }).pipe(Effect.provideService(HostProcessHomeDirectory, home));
336 homeSpy.mockRestore();
337 }
338 }).pipe(Effect.provide(layerTest)),330 }).pipe(Effect.provide(layerTest)),
339 );331 );
340332
apps/server/src/assets/AssetAccess.ts+2-1
...@@ -58,6 +58,7 @@ import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts";...@@ -58,6 +58,7 @@ import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts";
58import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";58import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";
59import * as NativeAppIconResolver from "./NativeAppIconResolver.ts";59import * as NativeAppIconResolver from "./NativeAppIconResolver.ts";
60import { openMediaFile, readMediaFileHeader, type OpenMediaFile } from "./MediaFile.ts";60import { openMediaFile, readMediaFileHeader, type OpenMediaFile } from "./MediaFile.ts";
61import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
6162
62export const ASSET_ROUTE_PREFIX = "/api/assets";63export const ASSET_ROUTE_PREFIX = "/api/assets";
6364
...@@ -469,7 +470,7 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i...@@ -469,7 +470,7 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i
469470
470 switch (input.resource._tag) {471 switch (input.resource._tag) {
471 case "media-file": {472 case "media-file": {
472 let requestedPath = expandHomePath(input.resource.path);473 let requestedPath = expandHomePath(input.resource.path, yield* HostProcessHomeDirectory);
473 if (!path.isAbsolute(requestedPath)) {474 if (!path.isAbsolute(requestedPath)) {
474 if (!input.workspaceRoot) {475 if (!input.workspaceRoot) {
475 return yield* new AssetWorkspaceContextNotFoundError({ resource: input.resource });476 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";...@@ -9,6 +9,7 @@ import { it } from "@effect/vitest";
9import type { DesktopAppActivationRequest } from "@t3tools/contracts";9import type { DesktopAppActivationRequest } from "@t3tools/contracts";
10import { resolveDesktopAppControlAddress } from "@t3tools/shared/desktopAppControl";10import { resolveDesktopAppControlAddress } from "@t3tools/shared/desktopAppControl";
11import {11import {
12 HostProcessHomeDirectory,
12 HostProcessPlatform,13 HostProcessPlatform,
13 HostProcessUserId,14 HostProcessUserId,
14 HostProcessWorkingDirectory,15 HostProcessWorkingDirectory,
...@@ -19,18 +20,11 @@ import * as Effect from "effect/Effect";...@@ -19,18 +20,11 @@ import * as Effect from "effect/Effect";
19import * as Layer from "effect/Layer";20import * as Layer from "effect/Layer";
20import * as Schema from "effect/Schema";21import * as Schema from "effect/Schema";
21import { Command } from "effect/cli";22import { Command } from "effect/cli";
22import { afterEach, describe, expect, vi } from "vite-plus/test";23import { describe, expect } from "vite-plus/test";
2324
24import { makeCli } from "../binCli.ts";25import { makeCli } from "../binCli.ts";
25import { PersistedServerRuntimeState } from "../serverRuntimeState.ts";26import { 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
34const encodeRuntimeState = Schema.encodeEffect(Schema.fromJsonString(PersistedServerRuntimeState));28const encodeRuntimeState = Schema.encodeEffect(Schema.fromJsonString(PersistedServerRuntimeState));
3529
36const runCli = (args: ReadonlyArray<string>, env: Record<string, string> = {}) =>30const runCli = (args: ReadonlyArray<string>, env: Record<string, string> = {}) =>
...@@ -296,7 +290,6 @@ describe("t3 app", () => {...@@ -296,7 +290,6 @@ describe("t3 app", () => {
296 it.effect("prefers the installed desktop app when a dev desktop is also running", () =>290 it.effect("prefers the installed desktop app when a dev desktop is also running", () =>
297 withTempDirectory("t3-app-preferred-test-", (root) =>291 withTempDirectory("t3-app-preferred-test-", (root) =>
298 Effect.gen(function* () {292 Effect.gen(function* () {
299 vi.mocked(NodeOS.homedir).mockReturnValue(root);
300 const baseDir = NodePath.join(root, ".t3");293 const baseDir = NodePath.join(root, ".t3");
301 const desktop = yield* fakeDesktop({ baseDir });294 const desktop = yield* fakeDesktop({ baseDir });
302 const development = yield* fakeDesktop({ baseDir, stateSubdirectory: "dev" });295 const development = yield* fakeDesktop({ baseDir, stateSubdirectory: "dev" });
...@@ -305,14 +298,13 @@ describe("t3 app", () => {...@@ -305,14 +298,13 @@ describe("t3 app", () => {
305298
306 expect(desktop.received).toHaveLength(1);299 expect(desktop.received).toHaveLength(1);
307 expect(development.received).toHaveLength(0);300 expect(development.received).toHaveLength(0);
308 }).pipe(Effect.scoped),301 }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped),
309 ),302 ),
310 );303 );
311304
312 it.effect("finds the dev desktop when the default desktop socket is absent", () =>305 it.effect("finds the dev desktop when the default desktop socket is absent", () =>
313 withTempDirectory("t3-app-dev-test-", (root) =>306 withTempDirectory("t3-app-dev-test-", (root) =>
314 Effect.gen(function* () {307 Effect.gen(function* () {
315 vi.mocked(NodeOS.homedir).mockReturnValue(root);
316 const baseDir = NodePath.join(root, ".t3");308 const baseDir = NodePath.join(root, ".t3");
317 const development = yield* fakeDesktop({ baseDir, stateSubdirectory: "dev" });309 const development = yield* fakeDesktop({ baseDir, stateSubdirectory: "dev" });
318310
...@@ -321,14 +313,13 @@ describe("t3 app", () => {...@@ -321,14 +313,13 @@ describe("t3 app", () => {
321313
322 expect(development.received).toHaveLength(2);314 expect(development.received).toHaveLength(2);
323 expect(yield* pathExists(baseDir)).toBe(false);315 expect(yield* pathExists(baseDir)).toBe(false);
324 }).pipe(Effect.scoped),316 }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped),
325 ),317 ),
326 );318 );
327319
328 it.effect("never searches a dev state directory for an explicit T3 home", () =>320 it.effect("never searches a dev state directory for an explicit T3 home", () =>
329 withTempDirectory("t3-app-explicit-test-", (root) =>321 withTempDirectory("t3-app-explicit-test-", (root) =>
330 Effect.gen(function* () {322 Effect.gen(function* () {
331 vi.mocked(NodeOS.homedir).mockReturnValue(root);
332 const baseDir = NodePath.join(root, ".t3");323 const baseDir = NodePath.join(root, ".t3");
333 const development = yield* fakeDesktop({ baseDir, stateSubdirectory: "dev" });324 const development = yield* fakeDesktop({ baseDir, stateSubdirectory: "dev" });
334325
...@@ -338,7 +329,7 @@ describe("t3 app", () => {...@@ -338,7 +329,7 @@ describe("t3 app", () => {
338 expect(flagError).toMatchObject({ _tag: "DesktopAppUnreachableError" });329 expect(flagError).toMatchObject({ _tag: "DesktopAppUnreachableError" });
339 expect(envError).toMatchObject({ _tag: "DesktopAppUnreachableError" });330 expect(envError).toMatchObject({ _tag: "DesktopAppUnreachableError" });
340 expect(development.received).toHaveLength(0);331 expect(development.received).toHaveLength(0);
341 }).pipe(Effect.scoped),332 }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped),
342 ),333 ),
343 );334 );
344335
...@@ -347,7 +338,6 @@ describe("t3 app", () => {...@@ -347,7 +338,6 @@ describe("t3 app", () => {
347 (responseKind) =>338 (responseKind) =>
348 withTempDirectory("t3-app-response-test-", (root) =>339 withTempDirectory("t3-app-response-test-", (root) =>
349 Effect.gen(function* () {340 Effect.gen(function* () {
350 vi.mocked(NodeOS.homedir).mockReturnValue(root);
351 const baseDir = NodePath.join(root, ".t3");341 const baseDir = NodePath.join(root, ".t3");
352 const desktop = yield* fakeDesktop({342 const desktop = yield* fakeDesktop({
353 baseDir,343 baseDir,
...@@ -387,7 +377,7 @@ describe("t3 app", () => {...@@ -387,7 +377,7 @@ describe("t3 app", () => {
387 cause: { message: "The desktop app response is invalid." },377 cause: { message: "The desktop app response is invalid." },
388 });378 });
389 }379 }
390 }).pipe(Effect.scoped),380 }).pipe(Effect.provideService(HostProcessHomeDirectory, root), Effect.scoped),
391 ),381 ),
392 );382 );
393});383});
apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts+3-3
...@@ -31,7 +31,7 @@ import type {...@@ -31,7 +31,7 @@ import type {
31 WebSearchOutput,31 WebSearchOutput,
32} from "@anthropic-ai/claude-agent-sdk/sdk-tools";32} from "@anthropic-ai/claude-agent-sdk/sdk-tools";
33import { parseCliArgs } from "@t3tools/shared/cliArgs";33import { parseCliArgs } from "@t3tools/shared/cliArgs";
34import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";34import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
35import { applyClaudePromptEffortPrefix } from "@t3tools/shared/model";35import { applyClaudePromptEffortPrefix } from "@t3tools/shared/model";
36import {36import {
37 CLAUDE_RESUME_COMPACTION_NEVER_ANSWER,37 CLAUDE_RESUME_COMPACTION_NEVER_ANSWER,
...@@ -8181,12 +8181,12 @@ export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")(...@@ -8181,12 +8181,12 @@ export const createClaudeAdapterV2 = Effect.fn("ClaudeAdapterV2Driver.create")(
8181 const queryRunner = yield* ClaudeAgentSdkQueryRunner;8181 const queryRunner = yield* ClaudeAgentSdkQueryRunner;
8182 const serverConfig = yield* ServerConfig.ServerConfig;8182 const serverConfig = yield* ServerConfig.ServerConfig;
8183 const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;8183 const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
8184 const baseEnvironment = mergeProviderInstanceEnvironment(environment, hostEnvironment);8184 const baseEnvironment = yield* mergeProviderInstanceEnvironment(environment, hostEnvironment);
8185 const claudeEnvironment = yield* makeClaudeEnvironment(config, baseEnvironment);8185 const claudeEnvironment = yield* makeClaudeEnvironment(config, baseEnvironment);
8186 const path = yield* Path.Path;8186 const path = yield* Path.Path;
8187 const crypto = yield* Crypto.Crypto;8187 const crypto = yield* Crypto.Crypto;
8188 const binaryPath = yield* resolveClaudeSdkExecutablePath(8188 const binaryPath = yield* resolveClaudeSdkExecutablePath(
8189 expandHomePath(config.binaryPath),8189 expandHomePath(config.binaryPath, yield* HostProcessHomeDirectory),
8190 claudeEnvironment,8190 claudeEnvironment,
8191 );8191 );
8192 return yield* makeClaudeAdapterV2({8192 return yield* makeClaudeAdapterV2({
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts+3-3
...@@ -30,7 +30,7 @@ import {...@@ -30,7 +30,7 @@ import {
30 type ProviderSetupError,30 type ProviderSetupError,
31} from "@t3tools/contracts";31} from "@t3tools/contracts";
32import { SKILL_MENTION_PATTERN } from "@t3tools/shared/composerInlineTokens";32import { SKILL_MENTION_PATTERN } from "@t3tools/shared/composerInlineTokens";
33import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";33import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
34import { dynamicToolTitle } from "@t3tools/shared/toolActivity";34import { dynamicToolTitle } from "@t3tools/shared/toolActivity";
35import { getModelSelectionStringOptionValue, modelSelectionsEqual } from "@t3tools/shared/model";35import { getModelSelectionStringOptionValue, modelSelectionsEqual } from "@t3tools/shared/model";
36import { resolveSpawnCommand } from "@t3tools/shared/shell";36import { resolveSpawnCommand } from "@t3tools/shared/shell";
...@@ -1582,14 +1582,14 @@ export const createCodexAdapterV2 = (...@@ -1582,14 +1582,14 @@ export const createCodexAdapterV2 = (
1582 const settings = {1582 const settings = {
1583 ...config,1583 ...config,
1584 enabled,1584 enabled,
1585 binaryPath: expandHomePath(config.binaryPath),1585 binaryPath: expandHomePath(config.binaryPath, yield* HostProcessHomeDirectory),
1586 homePath: homeLayout.effectiveHomePath ?? "",1586 homePath: homeLayout.effectiveHomePath ?? "",
1587 } satisfies CodexSettings;1587 } satisfies CodexSettings;
15881588
1589 return yield* makeCodexAdapterV2({1589 return yield* makeCodexAdapterV2({
1590 instanceId,1590 instanceId,
1591 settings,1591 settings,
1592 environment: mergeProviderInstanceEnvironment(environment, hostEnvironment),1592 environment: yield* mergeProviderInstanceEnvironment(environment, hostEnvironment),
1593 clientFactory,1593 clientFactory,
1594 crypto,1594 crypto,
1595 fileSystem,1595 fileSystem,
apps/server/src/orchestration-v2/Adapters/GrokAdapterV2.test.ts+24-12
...@@ -94,9 +94,12 @@ describe("acpSubagentStatusBlocksTurnSettlement", () => {...@@ -94,9 +94,12 @@ describe("acpSubagentStatusBlocksTurnSettlement", () => {
9494
95describe("GrokAdapterV2 capabilities", () => {95describe("GrokAdapterV2 capabilities", () => {
96 it("preserves Grok's rate-limit stop and distinguishes other prompt failures", () => {96 it("preserves Grok's rate-limit stop and distinguishes other prompt failures", () => {
97 const flavor = makeGrokAcpAdapterFlavor({97 const flavor = makeGrokAcpAdapterFlavor(
98 makeRuntime: () => Effect.never,98 {
99 } as unknown as Parameters<typeof makeGrokAcpAdapterFlavor>[0]);99 makeRuntime: () => Effect.never,
100 } as unknown as Parameters<typeof makeGrokAcpAdapterFlavor>[0],
101 "/home/ada",
102 );
100 const limit = flavor.promptFailure?.(103 const limit = flavor.promptFailure?.(
101 new EffectAcpErrors.AcpRequestError({104 new EffectAcpErrors.AcpRequestError({
102 code: xAiRateLimitedErrorCode,105 code: xAiRateLimitedErrorCode,
...@@ -122,9 +125,12 @@ describe("GrokAdapterV2 capabilities", () => {...@@ -122,9 +125,12 @@ describe("GrokAdapterV2 capabilities", () => {
122 });125 });
123126
124 it("wires hard Stop teardown but soft non-Stop interrupts in the constructor flavor", () => {127 it("wires hard Stop teardown but soft non-Stop interrupts in the constructor flavor", () => {
125 const flavor = makeGrokAcpAdapterFlavor({128 const flavor = makeGrokAcpAdapterFlavor(
126 makeRuntime: () => Effect.never,129 {
127 } as unknown as Parameters<typeof makeGrokAcpAdapterFlavor>[0]);130 makeRuntime: () => Effect.never,
131 } as unknown as Parameters<typeof makeGrokAcpAdapterFlavor>[0],
132 "/home/ada",
133 );
128134
129 assert.isFalse(flavor.interruptPromptOnCancel);135 assert.isFalse(flavor.interruptPromptOnCancel);
130 // User Stop (requestRuntimeRestart) keeps the hard process-group kill and136 // User Stop (requestRuntimeRestart) keeps the hard process-group kill and
...@@ -139,9 +145,12 @@ describe("GrokAdapterV2 capabilities", () => {...@@ -139,9 +145,12 @@ describe("GrokAdapterV2 capabilities", () => {
139 });145 });
140146
141 it("terminalizes only foreground tools under the actual Grok flavor", () => {147 it("terminalizes only foreground tools under the actual Grok flavor", () => {
142 const flavor = makeGrokAcpAdapterFlavor({148 const flavor = makeGrokAcpAdapterFlavor(
143 makeRuntime: () => Effect.never,149 {
144 } as unknown as Parameters<typeof makeGrokAcpAdapterFlavor>[0]);150 makeRuntime: () => Effect.never,
151 } as unknown as Parameters<typeof makeGrokAcpAdapterFlavor>[0],
152 "/home/ada",
153 );
145 const foreground = {154 const foreground = {
146 toolCallId: "foreground-1",155 toolCallId: "foreground-1",
147 title: "Terminal",156 title: "Terminal",
...@@ -284,9 +293,12 @@ describe("ACP permission policy", () => {...@@ -284,9 +293,12 @@ describe("ACP permission policy", () => {
284});293});
285294
286describe("Grok permission prompts", () => {295describe("Grok permission prompts", () => {
287 const disposition = makeGrokAcpAdapterFlavor({296 const disposition = makeGrokAcpAdapterFlavor(
288 makeRuntime: () => Effect.never,297 {
289 } as unknown as Parameters<typeof makeGrokAcpAdapterFlavor>[0]).permissionDisposition;298 makeRuntime: () => Effect.never,
299 } as unknown as Parameters<typeof makeGrokAcpAdapterFlavor>[0],
300 "/home/ada",
301 ).permissionDisposition;
290302
291 // grok_auto_blocked_command replays Auto end to end. When an explicit policy303 // grok_auto_blocked_command replays Auto end to end. When an explicit policy
292 // launches Grok asking instead, T3's policy still answers its prompts.304 // launches Grok asking instead, T3's policy still answers its prompts.
apps/server/src/orchestration-v2/workflowScriptQuery.ts+5-4
...@@ -14,16 +14,16 @@...@@ -14,16 +14,16 @@
14 * never trusted beyond these checks.14 * never trusted beyond these checks.
15 */15 */
16import * as NodeFSP from "node:fs/promises";16import * as NodeFSP from "node:fs/promises";
17import * as NodeOS from "node:os";
18import * as NodePath from "node:path";17import * as NodePath from "node:path";
1918
20import { OrchestrationGetWorkflowScriptError } from "@t3tools/contracts";19import { OrchestrationGetWorkflowScriptError } from "@t3tools/contracts";
21import * as Effect from "effect/Effect";20import * as Effect from "effect/Effect";
21import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
2222
23const SCRIPT_BYTE_CAP = 256 * 1024;23const SCRIPT_BYTE_CAP = 256 * 1024;
2424
25function scriptsRoot(): string {25function scriptsRoot(home: string): string {
26 return NodePath.join(NodeOS.homedir(), ".claude", "projects");26 return NodePath.join(home, ".claude", "projects");
27}27}
2828
29export const readWorkflowScript = Effect.fn("orchestration.readWorkflowScript")(function* (input: {29export const readWorkflowScript = Effect.fn("orchestration.readWorkflowScript")(function* (input: {
...@@ -38,8 +38,9 @@ export const readWorkflowScript = Effect.fn("orchestration.readWorkflowScript")(...@@ -38,8 +38,9 @@ export const readWorkflowScript = Effect.fn("orchestration.readWorkflowScript")(
38 });38 });
39 }39 }
4040
41 const home = yield* HostProcessHomeDirectory;
41 const root = yield* Effect.tryPromise({42 const root = yield* Effect.tryPromise({
42 try: () => NodeFSP.realpath(scriptsRoot()),43 try: () => NodeFSP.realpath(scriptsRoot(home)),
43 catch: (cause) =>44 catch: (cause) =>
44 new OrchestrationGetWorkflowScriptError({45 new OrchestrationGetWorkflowScriptError({
45 reason: "root-unavailable",46 reason: "root-unavailable",
apps/server/src/os-jank.ts+9-4
...@@ -1,4 +1,8 @@...@@ -1,4 +1,8 @@
1import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";1import {
2 HostProcessEnvironment,
3 HostProcessHomeDirectory,
4 HostProcessPlatform,
5} from "@t3tools/shared/hostProcess";
2import {6import {
3 listLoginShellCandidates,7 listLoginShellCandidates,
4 mergePathEntries,8 mergePathEntries,
...@@ -93,11 +97,12 @@ export const fixPath = Effect.fn("fixPath")(function* (): Effect.fn.Return<...@@ -93,11 +97,12 @@ export const fixPath = Effect.fn("fixPath")(function* (): Effect.fn.Return<
9397
94export const expandHomePath = Effect.fn(function* (input: string) {98export const expandHomePath = Effect.fn(function* (input: string) {
95 const { join } = yield* Path.Path;99 const { join } = yield* Path.Path;
100 const home = yield* HostProcessHomeDirectory;
96 if (input === "~") {101 if (input === "~") {
97 return NodeOS.homedir();102 return home;
98 }103 }
99 if (input.startsWith("~/") || input.startsWith("~\\")) {104 if (input.startsWith("~/") || input.startsWith("~\\")) {
100 return join(NodeOS.homedir(), input.slice(2));105 return join(home, input.slice(2));
101 }106 }
102 return input;107 return input;
103});108});
...@@ -105,7 +110,7 @@ export const expandHomePath = Effect.fn(function* (input: string) {...@@ -105,7 +110,7 @@ export const expandHomePath = Effect.fn(function* (input: string) {
105export const resolveBaseDir = Effect.fn(function* (raw: string | undefined) {110export const resolveBaseDir = Effect.fn(function* (raw: string | undefined) {
106 const { join, resolve } = yield* Path.Path;111 const { join, resolve } = yield* Path.Path;
107 if (!raw || raw.trim().length === 0) {112 if (!raw || raw.trim().length === 0) {
108 return join(NodeOS.homedir(), ".t3");113 return join(yield* HostProcessHomeDirectory, ".t3");
109 }114 }
110 return resolve(yield* expandHomePath(raw.trim()));115 return resolve(yield* expandHomePath(raw.trim()));
111});116});
apps/server/src/project/AgentSessionScanner.ts+14-10
...@@ -44,7 +44,11 @@ import {...@@ -44,7 +44,11 @@ import {
44 parseGitHubRepositoryNameWithOwnerFromRemoteUrl,44 parseGitHubRepositoryNameWithOwnerFromRemoteUrl,
45 parseOriginUrlFromGitConfig,45 parseOriginUrlFromGitConfig,
46} from "@t3tools/shared/git";46} from "@t3tools/shared/git";
47import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";47import {
48 HostProcessEnvironment,
49 HostProcessHomeDirectory,
50 HostProcessPlatform,
51} from "@t3tools/shared/hostProcess";
48import { normalizeProjectPathForComparison } from "@t3tools/shared/path";52import { normalizeProjectPathForComparison } from "@t3tools/shared/path";
4953
50import * as ServerConfig from "../config.ts";54import * as ServerConfig from "../config.ts";
...@@ -631,7 +635,7 @@ export const make = Effect.gen(function* () {...@@ -631,7 +635,7 @@ export const make = Effect.gen(function* () {
631 // must case fold.635 // must case fold.
632 const foldWorktreeCase = (yield* HostProcessPlatform) === "win32";636 const foldWorktreeCase = (yield* HostProcessPlatform) === "win32";
633 const hostEnvironment = yield* HostProcessEnvironment;637 const hostEnvironment = yield* HostProcessEnvironment;
634 const homeDir = NodeOS.homedir();638 const homeDir = yield* HostProcessHomeDirectory;
635 // `/private/tmp` is what macOS reports for sessions started in `/tmp`.639 // `/private/tmp` is what macOS reports for sessions started in `/tmp`.
636 const excludedProjectRoots = new Set(640 const excludedProjectRoots = new Set(
637 [homeDir, NodeOS.tmpdir(), "/tmp", "/private/tmp"].map((directory) =>641 [homeDir, NodeOS.tmpdir(), "/tmp", "/private/tmp"].map((directory) =>
...@@ -912,13 +916,13 @@ export const make = Effect.gen(function* () {...@@ -912,13 +916,13 @@ export const make = Effect.gen(function* () {
912 const resolveClaudeConfigDir = (homePath: string, environmentHome?: string): string => {916 const resolveClaudeConfigDir = (homePath: string, environmentHome?: string): string => {
913 const configured = homePath.trim();917 const configured = homePath.trim();
914 if (configured.length > 0) {918 if (configured.length > 0) {
915 return path.resolve(expandHomePath(configured));919 return path.resolve(expandHomePath(configured, homeDir));
916 }920 }
917 const fromEnvironment = environmentHome?.trim() ?? "";921 const fromEnvironment = environmentHome?.trim() ?? "";
918 if (fromEnvironment.length > 0) {922 if (fromEnvironment.length > 0) {
919 return path.resolve(expandHomePath(fromEnvironment));923 return path.resolve(expandHomePath(fromEnvironment, homeDir));
920 }924 }
921 return path.join(NodeOS.homedir(), ".claude");925 return path.join(homeDir, ".claude");
922 };926 };
923927
924 const discoverClaudeTranscripts = Effect.fn("AgentSessionScanner.discoverClaudeTranscripts")(928 const discoverClaudeTranscripts = Effect.fn("AgentSessionScanner.discoverClaudeTranscripts")(
...@@ -1217,7 +1221,7 @@ export const make = Effect.gen(function* () {...@@ -1217,7 +1221,7 @@ export const make = Effect.gen(function* () {
1217 const gitIdentities = new Map<string, AgentSessionProjectGit | null>();1221 const gitIdentities = new Map<string, AgentSessionProjectGit | null>();
12181222
1219 for (const candidate of raw) {1223 for (const candidate of raw) {
1220 const expanded = expandHomePath(candidate.cwd.trim());1224 const expanded = expandHomePath(candidate.cwd.trim(), homeDir);
1221 if (!path.isAbsolute(expanded)) continue;1225 if (!path.isAbsolute(expanded)) continue;
1222 const resolved = path.resolve(expanded);1226 const resolved = path.resolve(expanded);
1223 if (isExcludedProjectPath(resolved)) continue;1227 if (isExcludedProjectPath(resolved)) continue;
...@@ -1281,7 +1285,7 @@ export const make = Effect.gen(function* () {...@@ -1281,7 +1285,7 @@ export const make = Effect.gen(function* () {
1281 );1285 );
1282 const importedProjectsByRoot = new Map<string, (typeof importedProjects)[number]>();1286 const importedProjectsByRoot = new Map<string, (typeof importedProjects)[number]>();
1283 for (const project of importedProjects) {1287 for (const project of importedProjects) {
1284 const projectRoot = path.resolve(expandHomePath(project.workspaceRoot));1288 const projectRoot = path.resolve(expandHomePath(project.workspaceRoot, homeDir));
1285 importedProjectsByRoot.set(normalizeProjectPathForComparison(projectRoot), project);1289 importedProjectsByRoot.set(normalizeProjectPathForComparison(projectRoot), project);
1286 importedProjectsByRoot.set(yield* directoryIdentity(projectRoot), project);1290 importedProjectsByRoot.set(yield* directoryIdentity(projectRoot), project);
1287 }1291 }
...@@ -1328,7 +1332,7 @@ export const make = Effect.gen(function* () {...@@ -1328,7 +1332,7 @@ export const make = Effect.gen(function* () {
1328 workspaceRoot: string,1332 workspaceRoot: string,
1329 completedSources: ReadonlyArray<AgentSessionImportSource>,1333 completedSources: ReadonlyArray<AgentSessionImportSource>,
1330 ) {1334 ) {
1331 const root = path.resolve(expandHomePath(workspaceRoot));1335 const root = path.resolve(expandHomePath(workspaceRoot, homeDir));
1332 const realRoot = yield* fileSystem.realPath(root).pipe(Effect.orElseSucceed(() => root));1336 const realRoot = yield* fileSystem.realPath(root).pipe(Effect.orElseSucceed(() => root));
1333 if (isExcludedProjectPath(root) || isExcludedProjectPath(realRoot)) return Stream.empty;1337 if (isExcludedProjectPath(root) || isExcludedProjectPath(realRoot)) return Stream.empty;
1334 const rootIdentity = yield* directoryIdentity(root);1338 const rootIdentity = yield* directoryIdentity(root);
...@@ -1343,7 +1347,7 @@ export const make = Effect.gen(function* () {...@@ -1343,7 +1347,7 @@ export const make = Effect.gen(function* () {
1343 readonly transcript: RawCandidate["transcripts"][number] & { readonly mtimeMs: number };1347 readonly transcript: RawCandidate["transcripts"][number] & { readonly mtimeMs: number };
1344 }> = [];1348 }> = [];
1345 for (const candidate of candidates) {1349 for (const candidate of candidates) {
1346 const expanded = expandHomePath(candidate.cwd.trim());1350 const expanded = expandHomePath(candidate.cwd.trim(), homeDir);
1347 if (!path.isAbsolute(expanded)) continue;1351 if (!path.isAbsolute(expanded)) continue;
1348 const resolved = path.resolve(expanded);1352 const resolved = path.resolve(expanded);
1349 if ((yield* directoryIdentity(resolved)) !== rootIdentity) continue;1353 if ((yield* directoryIdentity(resolved)) !== rootIdentity) continue;
...@@ -1439,7 +1443,7 @@ export const make = Effect.gen(function* () {...@@ -1439,7 +1443,7 @@ export const make = Effect.gen(function* () {
1439 if (snapshotCwd === null) {1443 if (snapshotCwd === null) {
1440 return Option.some<AgentSessionRecentThread>({ _tag: "Skipped" });1444 return Option.some<AgentSessionRecentThread>({ _tag: "Skipped" });
1441 }1445 }
1442 const expandedCwd = expandHomePath(snapshotCwd.trim());1446 const expandedCwd = expandHomePath(snapshotCwd.trim(), homeDir);
1443 if (1447 if (
1444 !path.isAbsolute(expandedCwd) ||1448 !path.isAbsolute(expandedCwd) ||
1445 (yield* directoryIdentity(path.resolve(expandedCwd))) !== rootIdentity1449 (yield* directoryIdentity(path.resolve(expandedCwd))) !== rootIdentity
apps/server/src/provider/CodexProvider.ts+4-1
...@@ -47,6 +47,7 @@ import {...@@ -47,6 +47,7 @@ import {
47 type CodexRateLimitSnapshot,47 type CodexRateLimitSnapshot,
48 type CodexResetCreditsSummary,48 type CodexResetCreditsSummary,
49} from "./codexUsageLimits.ts";49} from "./codexUsageLimits.ts";
50import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
50import packageJson from "../../package.json" with { type: "json" };51import packageJson from "../../package.json" with { type: "json" };
51const isCodexAppServerSpawnError = Schema.is(CodexErrors.CodexAppServerSpawnError);52const isCodexAppServerSpawnError = Schema.is(CodexErrors.CodexAppServerSpawnError);
52const RATE_LIMITS_PROBE_TIMEOUT_MS = 3_000;53const RATE_LIMITS_PROBE_TIMEOUT_MS = 3_000;
...@@ -374,7 +375,9 @@ export const withCodexAppServerClient = Effect.fn("withCodexAppServerClient")(fu...@@ -374,7 +375,9 @@ export const withCodexAppServerClient = Effect.fn("withCodexAppServerClient")(fu
374 // so `CODEX_HOME=~/.codex_work` would reach codex verbatim and trip375 // so `CODEX_HOME=~/.codex_work` would reach codex verbatim and trip
375 // "CODEX_HOME points to '~/.codex_work', but that path does not exist".376 // "CODEX_HOME points to '~/.codex_work', but that path does not exist".
376 // Expand here for parity with `CodexTextGeneration`.377 // 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;
378 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;381 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
379 const environment = {382 const environment = {
380 ...input.environment,383 ...input.environment,
apps/server/src/provider/Drivers/AntigravityDriver.ts+7-3
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { withAgentDeviceEnvironment } from "@t3tools/provider-core/server/mcpSession";1import { withAgentDeviceEnvironment } from "@t3tools/provider-core/server/mcpSession";
2import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";2import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
3import { AntigravitySettings, ProviderDriverKind, ProviderSetupError } from "@t3tools/contracts";3import { AntigravitySettings, ProviderDriverKind, ProviderSetupError } from "@t3tools/contracts";
4import { HostProcessPlatform } from "@t3tools/shared/hostProcess";4import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
5import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime";5import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime";
6import {6import {
7 NodeRuntimeUnavailableError,7 NodeRuntimeUnavailableError,
...@@ -113,8 +113,12 @@ export const AntigravityDriver: ProviderDriver<...@@ -113,8 +113,12 @@ export const AntigravityDriver: ProviderDriver<
113 gcpLocation: settings.gcpLocation,113 gcpLocation: settings.gcpLocation,
114 };114 };
115 const authConfigIssue = antigravityAuthConfigIssue(auth);115 const authConfigIssue = antigravityAuthConfigIssue(auth);
116 const processEnvironment = mergeProviderInstanceEnvironment(environment);116 const processEnvironment = yield* mergeProviderInstanceEnvironment(environment);
117 const userHome = resolveAntigravityUserHome(yield* HostProcessPlatform, processEnvironment);117 const userHome = resolveAntigravityUserHome(
118 yield* HostProcessPlatform,
119 processEnvironment,
120 yield* HostProcessHomeDirectory,
121 );
118 const directories = yield* resolveAntigravityInstanceDirectories(122 const directories = yield* resolveAntigravityInstanceDirectories(
119 host.paths.stateDir,123 host.paths.stateDir,
120 instanceId,124 instanceId,
apps/server/src/provider/Drivers/AntigravitySkills.test.ts+23-5
...@@ -344,17 +344,35 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => {...@@ -344,17 +344,35 @@ it.layer(NodeServices.layer)("discoverAntigravitySkills", (it) => {
344344
345it("resolves the home the agent expands ~ against", () => {345it("resolves the home the agent expands ~ against", () => {
346 assert.equal(346 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 ),
348 "/home/user",352 "/home/user",
349 );353 );
350 assert.equal(354 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 ),
352 "C:\\Users\\user",360 "C:\\Users\\user",
353 );361 );
354 assert.equal(362 assert.equal(
355 resolveAntigravityUserHome("win32", { HOMEDRIVE: "D:", HOMEPATH: "\\Users\\alice" }),363 resolveAntigravityUserHome(
364 "win32",
365 { HOMEDRIVE: "D:", HOMEPATH: "\\Users\\alice" },
366 "/home/fallback",
367 ),
356 "D:\\Users\\alice",368 "D:\\Users\\alice",
357 );369 );
358 assert.equal(resolveAntigravityUserHome("darwin", { HOME: "/Users/a b " }), "/Users/a b ");370 assert.equal(
359 assert.equal(resolveAntigravityUserHome("darwin", { HOME: "" }).length > 0, true);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 );
360});378});
apps/server/src/provider/Drivers/AntigravitySkills.ts+3-4
...@@ -1,5 +1,3 @@...@@ -1,5 +1,3 @@
1import * as NodeOS from "node:os";
2
3import type { ServerProviderSkill } from "@t3tools/contracts";1import type { ServerProviderSkill } from "@t3tools/contracts";
4import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";3import * as FileSystem from "effect/FileSystem";
...@@ -18,15 +16,16 @@ import { parse as parseYamlDocument } from "yaml";...@@ -18,15 +16,16 @@ import { parse as parseYamlDocument } from "yaml";
18export function resolveAntigravityUserHome(16export function resolveAntigravityUserHome(
19 platform: NodeJS.Platform,17 platform: NodeJS.Platform,
20 environment: NodeJS.ProcessEnv,18 environment: NodeJS.ProcessEnv,
19 home: string,
21): string {20): string {
22 if (platform === "win32") {21 if (platform === "win32") {
23 if (environment.USERPROFILE) return environment.USERPROFILE;22 if (environment.USERPROFILE) return environment.USERPROFILE;
24 if (environment.HOMEDRIVE && environment.HOMEPATH) {23 if (environment.HOMEDRIVE && environment.HOMEPATH) {
25 return `${environment.HOMEDRIVE}${environment.HOMEPATH}`;24 return `${environment.HOMEDRIVE}${environment.HOMEPATH}`;
26 }25 }
27 return NodeOS.homedir();26 return home;
28 }27 }
29 return environment.HOME || NodeOS.homedir();28 return environment.HOME || home;
30}29}
3130
32/**31/**
apps/server/src/provider/Drivers/AntigravityUsage.ts+3-4
...@@ -8,7 +8,6 @@...@@ -8,7 +8,6 @@
8 *8 *
9 * @module provider/Drivers/AntigravityUsage9 * @module provider/Drivers/AntigravityUsage
10 */10 */
11import * as NodeOS from "node:os";
1211
13import { type AntigravitySettings, UsageReadError } from "@t3tools/contracts";12import { type AntigravitySettings, UsageReadError } from "@t3tools/contracts";
14import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";13import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
...@@ -18,7 +17,7 @@ import type {...@@ -18,7 +17,7 @@ import type {
18 ProviderUsageReader,17 ProviderUsageReader,
19 ProviderUsageScan,18 ProviderUsageScan,
20} from "@t3tools/provider-core/server/usage";19} from "@t3tools/provider-core/server/usage";
21import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";20import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
22import * as Context from "effect/Context";21import * as Context from "effect/Context";
23import * as Crypto from "effect/Crypto";22import * as Crypto from "effect/Crypto";
24import * as Effect from "effect/Effect";23import * as Effect from "effect/Effect";
...@@ -50,7 +49,7 @@ const make = Effect.gen(function* () {...@@ -50,7 +49,7 @@ const make = Effect.gen(function* () {
5049
51 /** `ANTIGRAVITY_DATA_DIR` (comma-separated) or the defaults, canonicalized. */50 /** `ANTIGRAVITY_DATA_DIR` (comma-separated) or the defaults, canonicalized. */
52 const dataRoots = Effect.gen(function* () {51 const dataRoots = Effect.gen(function* () {
53 const home = NodeOS.homedir();52 const home = yield* HostProcessHomeDirectory;
54 const configured = hostEnvironment["ANTIGRAVITY_DATA_DIR"]53 const configured = hostEnvironment["ANTIGRAVITY_DATA_DIR"]
55 ?.split(",")54 ?.split(",")
56 .map((value) => value.trim())55 .map((value) => value.trim())
...@@ -63,7 +62,7 @@ const make = Effect.gen(function* () {...@@ -63,7 +62,7 @@ const make = Effect.gen(function* () {
63 ];62 ];
64 const canonical = new Set<string>();63 const canonical = new Set<string>();
65 for (const root of configured?.length ? configured : defaults) {64 for (const root of configured?.length ? configured : defaults) {
66 const resolved = path.resolve(expandHomePath(root));65 const resolved = path.resolve(expandHomePath(root, home));
67 canonical.add(66 canonical.add(
68 yield* fileSystem.realPath(resolved).pipe(Effect.orElseSucceed(() => resolved)),67 yield* fileSystem.realPath(resolved).pipe(Effect.orElseSucceed(() => resolved)),
69 );68 );
apps/server/src/provider/Drivers/ClaudeDriver.ts+3-2
...@@ -71,6 +71,7 @@ import {...@@ -71,6 +71,7 @@ import {
71 makeClaudeContinuationGroupKey,71 makeClaudeContinuationGroupKey,
72 resolveClaudeHomePath,72 resolveClaudeHomePath,
73} from "./ClaudeHome.ts";73} from "./ClaudeHome.ts";
74import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
74const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);75const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);
7576
76const DRIVER_KIND = ProviderDriverKind.make("claudeAgent");77const DRIVER_KIND = ProviderDriverKind.make("claudeAgent");
...@@ -130,7 +131,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path....@@ -130,7 +131,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
130 const modelCatalog = catalogService131 const modelCatalog = catalogService
131 .current(DRIVER_KIND)132 .current(DRIVER_KIND)
132 .pipe(Effect.map(resolveClaudeModelCatalog));133 .pipe(Effect.map(resolveClaudeModelCatalog));
133 const processEnv = mergeProviderInstanceEnvironment(environment);134 const processEnv = yield* mergeProviderInstanceEnvironment(environment);
134 const fallbackContinuationIdentity = defaultProviderContinuationIdentity({135 const fallbackContinuationIdentity = defaultProviderContinuationIdentity({
135 driverKind: DRIVER_KIND,136 driverKind: DRIVER_KIND,
136 instanceId,137 instanceId,
...@@ -138,7 +139,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path....@@ -138,7 +139,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
138 const effectiveConfig = {139 const effectiveConfig = {
139 ...config,140 ...config,
140 enabled,141 enabled,
141 binaryPath: expandHomePath(config.binaryPath),142 binaryPath: expandHomePath(config.binaryPath, yield* HostProcessHomeDirectory),
142 } satisfies ClaudeSettings;143 } satisfies ClaudeSettings;
143 const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution(144 const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution(
144 resolveProviderMaintenanceCapabilitiesEffect(UPDATE, {145 resolveProviderMaintenanceCapabilitiesEffect(UPDATE, {
apps/server/src/provider/Drivers/ClaudeHome.ts+4-4
...@@ -1,11 +1,10 @@...@@ -1,11 +1,10 @@
1import * as NodeOS from "node:os";
2
3import type { ClaudeSettings } from "@t3tools/contracts";1import type { ClaudeSettings } from "@t3tools/contracts";
4import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
5import * as Path from "effect/Path";3import * as Path from "effect/Path";
6import * as Schema from "effect/Schema";4import * as Schema from "effect/Schema";
75
8import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";6import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
7import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
98
10const quotePath = Schema.encodeSync(Schema.fromJsonString(Schema.String));9const quotePath = Schema.encodeSync(Schema.fromJsonString(Schema.String));
1110
...@@ -21,16 +20,17 @@ export const resolveClaudeHomePath = Effect.fn("resolveClaudeHomePath")(function...@@ -21,16 +20,17 @@ export const resolveClaudeHomePath = Effect.fn("resolveClaudeHomePath")(function
21 environment?: NodeJS.ProcessEnv,20 environment?: NodeJS.ProcessEnv,
22): Effect.fn.Return<string, never, Path.Path> {21): Effect.fn.Return<string, never, Path.Path> {
23 const path = yield* Path.Path;22 const path = yield* Path.Path;
23 const home = yield* HostProcessHomeDirectory;
24 const homePath = config.homePath.trim();24 const homePath = config.homePath.trim();
25 if (homePath.length > 0) {25 if (homePath.length > 0) {
26 return path.resolve(expandHomePath(homePath));26 return path.resolve(expandHomePath(homePath, home));
27 }27 }
28 // Inherited env vars are not shell-expanded, so a literal `~` stays literal.28 // Inherited env vars are not shell-expanded, so a literal `~` stays literal.
29 const inherited = environment?.CLAUDE_CONFIG_DIR?.trim() ?? "";29 const inherited = environment?.CLAUDE_CONFIG_DIR?.trim() ?? "";
30 if (inherited.length > 0) {30 if (inherited.length > 0) {
31 return path.resolve(inherited);31 return path.resolve(inherited);
32 }32 }
33 return path.resolve(path.join(NodeOS.homedir(), ".claude"));33 return path.resolve(path.join(home, ".claude"));
34});34});
3535
36export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function* (36export const makeClaudeEnvironment = Effect.fn("makeClaudeEnvironment")(function* (
apps/server/src/provider/Drivers/ClaudeSkills.ts+4-4
...@@ -13,14 +13,13 @@...@@ -13,14 +13,13 @@
13 *13 *
14 * @module provider/Drivers/ClaudeSkills14 * @module provider/Drivers/ClaudeSkills
15 */15 */
16import * as NodeOS from "node:os";
1716
18import type { ClaudeSettings, ServerProviderSkill } from "@t3tools/contracts";17import type { ClaudeSettings, ServerProviderSkill } from "@t3tools/contracts";
19import * as Effect from "effect/Effect";18import * as Effect from "effect/Effect";
20import * as FileSystem from "effect/FileSystem";19import * as FileSystem from "effect/FileSystem";
21import * as Path from "effect/Path";20import * as Path from "effect/Path";
22import * as Schema from "effect/Schema";21import * as Schema from "effect/Schema";
23import { HostProcessPlatform } from "@t3tools/shared/hostProcess";22import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
24import { fromLenientJson } from "@t3tools/shared/schemaJson";23import { fromLenientJson } from "@t3tools/shared/schemaJson";
25import { parse as parseYamlDocument } from "yaml";24import { parse as parseYamlDocument } from "yaml";
2625
...@@ -296,9 +295,10 @@ const resolveClaudeConfigDirPath = Effect.fn("resolveClaudeConfigDirPath")(funct...@@ -296,9 +295,10 @@ const resolveClaudeConfigDirPath = Effect.fn("resolveClaudeConfigDirPath")(funct
296 cwd?: string,295 cwd?: string,
297): Effect.fn.Return<string, never, Path.Path> {296): Effect.fn.Return<string, never, Path.Path> {
298 const path = yield* Path.Path;297 const path = yield* Path.Path;
298 const home = yield* HostProcessHomeDirectory;
299 const homePath = config.homePath.trim();299 const homePath = config.homePath.trim();
300 if (homePath.length > 0) {300 if (homePath.length > 0) {
301 return path.resolve(expandHomePath(homePath));301 return path.resolve(expandHomePath(homePath, home));
302 }302 }
303 // No tilde expansion here: the spawned CLI receives this env var verbatim303 // No tilde expansion here: the spawned CLI receives this env var verbatim
304 // (env vars are never shell-expanded), so a literal `~` must stay literal304 // (env vars are never shell-expanded), so a literal `~` must stay literal
...@@ -309,7 +309,7 @@ const resolveClaudeConfigDirPath = Effect.fn("resolveClaudeConfigDirPath")(funct...@@ -309,7 +309,7 @@ const resolveClaudeConfigDirPath = Effect.fn("resolveClaudeConfigDirPath")(funct
309 if (environmentConfigDir.length > 0) {309 if (environmentConfigDir.length > 0) {
310 return cwd ? path.resolve(cwd, environmentConfigDir) : path.resolve(environmentConfigDir);310 return cwd ? path.resolve(cwd, environmentConfigDir) : path.resolve(environmentConfigDir);
311 }311 }
312 return path.join(NodeOS.homedir(), ".claude");312 return path.join(home, ".claude");
313});313});
314314
315/**315/**
apps/server/src/provider/Drivers/CodexDriver.ts+3-2
...@@ -77,6 +77,7 @@ import { codexUsageReader } from "./codexUsage.ts";...@@ -77,6 +77,7 @@ import { codexUsageReader } from "./codexUsage.ts";
77import * as CodexInstallation from "../CodexInstallation.ts";77import * as CodexInstallation from "../CodexInstallation.ts";
78import * as ServerSecretStore from "../../auth/ServerSecretStore.ts";78import * as ServerSecretStore from "../../auth/ServerSecretStore.ts";
79import * as ServerEnvironment from "../../environment/ServerEnvironment.ts";79import * as ServerEnvironment from "../../environment/ServerEnvironment.ts";
80import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
80const decodeCodexSettings = Schema.decodeSync(CodexSettings);81const decodeCodexSettings = Schema.decodeSync(CodexSettings);
8182
82const DRIVER_KIND = ProviderDriverKind.make("codex");83const DRIVER_KIND = ProviderDriverKind.make("codex");
...@@ -154,7 +155,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv, Path.Pat...@@ -154,7 +155,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv, Path.Pat
154 const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions;155 const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions;
155 const modelCatalog = yield* ModelCatalog.ModelCatalog;156 const modelCatalog = yield* ModelCatalog.ModelCatalog;
156 const currentCatalog = modelCatalog.current(DRIVER_KIND);157 const currentCatalog = modelCatalog.current(DRIVER_KIND);
157 const processEnv = mergeProviderInstanceEnvironment(environment);158 const processEnv = yield* mergeProviderInstanceEnvironment(environment);
158 const homeLayout = yield* resolveCodexHomeLayout(config);159 const homeLayout = yield* resolveCodexHomeLayout(config);
159 const continuationIdentity = codexContinuationIdentity(homeLayout);160 const continuationIdentity = codexContinuationIdentity(homeLayout);
160 const stampIdentity = withInstanceIdentity({161 const stampIdentity = withInstanceIdentity({
...@@ -178,7 +179,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv, Path.Pat...@@ -178,7 +179,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv, Path.Pat
178 const effectiveConfig = {179 const effectiveConfig = {
179 ...config,180 ...config,
180 enabled,181 enabled,
181 binaryPath: expandHomePath(config.binaryPath),182 binaryPath: expandHomePath(config.binaryPath, yield* HostProcessHomeDirectory),
182 homePath: homeLayout.effectiveHomePath ?? "",183 homePath: homeLayout.effectiveHomePath ?? "",
183 } satisfies CodexSettings;184 } satisfies CodexSettings;
184 const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution(185 const resolveMaintenance = yield* makeCachedProviderMaintenanceResolution(
apps/server/src/provider/Drivers/CodexHomeLayout.ts+6-8
...@@ -1,5 +1,3 @@...@@ -1,5 +1,3 @@
1import * as NodeOS from "node:os";
2
3import { ProviderDriverKind, type CodexSettings } from "@t3tools/contracts";1import { ProviderDriverKind, type CodexSettings } from "@t3tools/contracts";
4import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";3import * as FileSystem from "effect/FileSystem";
...@@ -8,6 +6,7 @@ import * as Schema from "effect/Schema";...@@ -8,6 +6,7 @@ import * as Schema from "effect/Schema";
8import * as PlatformError from "effect/PlatformError";6import * as PlatformError from "effect/PlatformError";
97
10import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";8import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
9import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
1110
12export interface CodexHomeLayout {11export interface CodexHomeLayout {
13 readonly mode: "direct" | "authOverlay";12 readonly mode: "direct" | "authOverlay";
...@@ -33,11 +32,9 @@ const PRIVATE_ENTRY_NAMES = new Set(["auth.json", "models_cache.json"]);...@@ -33,11 +32,9 @@ const PRIVATE_ENTRY_NAMES = new Set(["auth.json", "models_cache.json"]);
33const SHADOW_LOCAL_ENTRY_NAMES = new Set(["log", "memories", "tmp"]);32const SHADOW_LOCAL_ENTRY_NAMES = new Set(["log", "memories", "tmp"]);
34const REPLACEABLE_SHARED_RUNTIME_DIRECTORIES = new Set(["mcp-oauth-locks"]);33const 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 {
37 const expanded =36 const expanded =
38 value && value.trim().length > 037 value && value.trim().length > 0 ? expandHomePath(value, home) : path.join(home, ".codex");
39 ? expandHomePath(value)
40 : path.join(NodeOS.homedir(), ".codex");
41 return path.resolve(expanded);38 return path.resolve(expanded);
42}39}
4340
...@@ -45,7 +42,8 @@ export const resolveCodexHomeLayout = Effect.fn("resolveCodexHomeLayout")(functi...@@ -45,7 +42,8 @@ export const resolveCodexHomeLayout = Effect.fn("resolveCodexHomeLayout")(functi
45 config: CodexSettings,42 config: CodexSettings,
46): Effect.fn.Return<CodexHomeLayout, never, Path.Path> {43): Effect.fn.Return<CodexHomeLayout, never, Path.Path> {
47 const path = yield* Path.Path;44 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);
49 const shadowHomePath = config.shadowHomePath.trim();47 const shadowHomePath = config.shadowHomePath.trim();
50 if (shadowHomePath.length === 0) {48 if (shadowHomePath.length === 0) {
51 return {49 return {
...@@ -56,7 +54,7 @@ export const resolveCodexHomeLayout = Effect.fn("resolveCodexHomeLayout")(functi...@@ -56,7 +54,7 @@ export const resolveCodexHomeLayout = Effect.fn("resolveCodexHomeLayout")(functi
56 };54 };
57 }55 }
5856
59 const effectiveHomePath = path.resolve(expandHomePath(shadowHomePath));57 const effectiveHomePath = path.resolve(expandHomePath(shadowHomePath, home));
60 return {58 return {
61 mode: "authOverlay",59 mode: "authOverlay",
62 sharedHomePath,60 sharedHomePath,
apps/server/src/provider/Drivers/CodexManagedProvider.ts+1-1
...@@ -36,7 +36,7 @@ export const makeManagedCodexProvider = Effect.fn("makeManagedCodexProvider")(fu...@@ -36,7 +36,7 @@ export const makeManagedCodexProvider = Effect.fn("makeManagedCodexProvider")(fu
36 instanceId,36 instanceId,
37 enabled,37 enabled,
38 config,38 config,
39 environment: mergeProviderInstanceEnvironment(input.environment),39 environment: yield* mergeProviderInstanceEnvironment(input.environment),
40 });40 });
41 const continuationIdentity = codexContinuationIdentity(runtime.homeLayout);41 const continuationIdentity = codexContinuationIdentity(runtime.homeLayout);
42 const stamp = withInstanceIdentity({42 const stamp = withInstanceIdentity({
apps/server/src/provider/Drivers/claudeUsage.ts+4-3
...@@ -4,7 +4,6 @@...@@ -4,7 +4,6 @@
4 *4 *
5 * @module provider/Drivers/claudeUsage5 * @module provider/Drivers/claudeUsage
6 */6 */
7import * as NodeOS from "node:os";
87
9import type { ClaudeSettings } from "@t3tools/contracts";8import type { ClaudeSettings } from "@t3tools/contracts";
10import * as Effect from "effect/Effect";9import * as Effect from "effect/Effect";
...@@ -18,6 +17,7 @@ import {...@@ -18,6 +17,7 @@ import {
18 type TranscriptUsageFormat,17 type TranscriptUsageFormat,
19 type UsageRecord,18 type UsageRecord,
20} from "@t3tools/provider-core/server/usage";19} from "@t3tools/provider-core/server/usage";
20import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
2121
22/**22/**
23 * Parses one line of a Claude Code transcript.23 * Parses one line of a Claude Code transcript.
...@@ -111,10 +111,11 @@ export const claudeUsageReader: ProviderUsageReader<ClaudeSettings, Path.Path> =...@@ -111,10 +111,11 @@ export const claudeUsageReader: ProviderUsageReader<ClaudeSettings, Path.Path> =
111 // An undecodable config has no trustworthy home to read.111 // An undecodable config has no trustworthy home to read.
112 if (config === undefined) return [];112 if (config === undefined) return [];
113 const path = yield* Path.Path;113 const path = yield* Path.Path;
114 const homeDirectory = yield* HostProcessHomeDirectory;
114 const configured = config.homePath.trim();115 const configured = config.homePath.trim();
115 const home = configured116 const home = configured
116 ? expandHomePath(configured)117 ? expandHomePath(configured, homeDirectory)
117 : environment.CLAUDE_CONFIG_DIR?.trim() || path.join(NodeOS.homedir(), ".claude");118 : environment.CLAUDE_CONFIG_DIR?.trim() || path.join(homeDirectory, ".claude");
118 return [{ dir: path.resolve(home, "projects") }];119 return [{ dir: path.resolve(home, "projects") }];
119 }),120 }),
120};121};
apps/server/src/provider/ProviderInstanceRegistry.test.ts+6-3
...@@ -39,7 +39,11 @@ import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderL...@@ -39,7 +39,11 @@ import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderL
39import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";39import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
40import type { GrokSettings } from "@t3tools/provider-grok/settings";40import type { GrokSettings } from "@t3tools/provider-grok/settings";
41import type { CursorSettings } from "@t3tools/provider-cursor/settings";41import 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";
43import * as DateTime from "effect/DateTime";47import * as DateTime from "effect/DateTime";
44import * as Effect from "effect/Effect";48import * as Effect from "effect/Effect";
45import * as FileSystem from "effect/FileSystem";49import * as FileSystem from "effect/FileSystem";
...@@ -51,7 +55,6 @@ import { HttpClient, HttpClientResponse } from "effect/http";...@@ -51,7 +55,6 @@ import { HttpClient, HttpClientResponse } from "effect/http";
51import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";55import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
52import * as AntigravityInstallation from "./AntigravityInstallation.ts";56import * as AntigravityInstallation from "./AntigravityInstallation.ts";
53import * as ServerConfig from "../config.ts";57import * as ServerConfig from "../config.ts";
54import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
55import * as ServerSettings from "../serverSettings.ts";58import * as ServerSettings from "../serverSettings.ts";
56import { ClaudeDriver, type ClaudeDriverEnv } from "./Drivers/ClaudeDriver.ts";59import { ClaudeDriver, type ClaudeDriverEnv } from "./Drivers/ClaudeDriver.ts";
57import { CodexDriver, type CodexDriverEnv } from "./Drivers/CodexDriver.ts";60import { CodexDriver, type CodexDriverEnv } from "./Drivers/CodexDriver.ts";
...@@ -153,7 +156,7 @@ const makeTildeProviderFixtures = Effect.fn(...@@ -153,7 +156,7 @@ const makeTildeProviderFixtures = Effect.fn(
153)(function* () {156)(function* () {
154 const fileSystem = yield* FileSystem.FileSystem;157 const fileSystem = yield* FileSystem.FileSystem;
155 const path = yield* Path.Path;158 const path = yield* Path.Path;
156 const homePath = expandHomePath("~");159 const homePath = yield* HostProcessHomeDirectory;
157 const fixtureDir = yield* fileSystem.makeTempDirectoryScoped({160 const fixtureDir = yield* fileSystem.makeTempDirectoryScoped({
158 directory: homePath,161 directory: homePath,
159 prefix: ".t3-provider-path-test-",162 prefix: ".t3-provider-path-test-",
apps/server/src/provider/antigravityAuthSupport.ts+7-2
...@@ -2,7 +2,7 @@...@@ -2,7 +2,7 @@
2import * as NodeFSP from "node:fs/promises";2import * as NodeFSP from "node:fs/promises";
33
4import type { AntigravityAuthMethod, ProviderInstanceId } from "@t3tools/contracts";4import type { AntigravityAuthMethod, ProviderInstanceId } from "@t3tools/contracts";
5import { HostProcessPlatform } from "@t3tools/shared/hostProcess";5import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
6import { resolveNodeExecutable, nodeRuntimeUnavailableMessage } from "@t3tools/shared/nodeRuntime";6import { resolveNodeExecutable, nodeRuntimeUnavailableMessage } from "@t3tools/shared/nodeRuntime";
7import * as Crypto from "effect/Crypto";7import * as Crypto from "effect/Crypto";
8import * as Effect from "effect/Effect";8import * as Effect from "effect/Effect";
...@@ -317,7 +317,12 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(...@@ -317,7 +317,12 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(
317 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;317 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
318 const platform = input.platform ?? (yield* HostProcessPlatform);318 const platform = input.platform ?? (yield* HostProcessPlatform);
319 const userHome =319 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 );
321 const runtimeExecutablePath =326 const runtimeExecutablePath =
322 input.runtimeExecutablePath ??327 input.runtimeExecutablePath ??
323 (yield* resolveNodeExecutable("Antigravity sign-in", input.baseEnv).pipe(328 (yield* resolveNodeExecutable("Antigravity sign-in", input.baseEnv).pipe(
apps/server/src/provider/claudeResetCredits.ts+9-6
...@@ -7,12 +7,11 @@...@@ -7,12 +7,11 @@
7 *7 *
8 * @module provider/claudeResetCredits8 * @module provider/claudeResetCredits
9 */9 */
10import * as NodeOS from "node:os";
11import type {10import type {
12 ProviderConsumeResetCreditOutcome,11 ProviderConsumeResetCreditOutcome,
13 ServerProviderResetCredits,12 ServerProviderResetCredits,
14} from "@t3tools/contracts";13} from "@t3tools/contracts";
15import { HostProcessPlatform } from "@t3tools/shared/hostProcess";14import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
16import * as DateTime from "effect/DateTime";15import * as DateTime from "effect/DateTime";
17import * as Effect from "effect/Effect";16import * as Effect from "effect/Effect";
18import * as FileSystem from "effect/FileSystem";17import * as FileSystem from "effect/FileSystem";
...@@ -190,10 +189,14 @@ export const readClaudeResetCredits = Effect.fn("readClaudeResetCredits")(...@@ -190,10 +189,14 @@ export const readClaudeResetCredits = Effect.fn("readClaudeResetCredits")(
190);189);
191190
192/** The CLI keeps the account record beside its settings, or in the home directory by default. */191/** The CLI keeps the account record beside its settings, or in the home directory by default. */
193export const claudeAccountConfigPath = (configDir: string | undefined) =>192export const claudeAccountConfigPath = Effect.fn("claudeAccountConfigPath")(function* (
194 Effect.map(Path.Path, (path) =>193 configDir: string | undefined,
195 configDir ? path.join(configDir, ".claude.json") : path.join(NodeOS.homedir(), ".claude.json"),194) {
196 );195 const path = yield* Path.Path;
196 return configDir
197 ? path.join(configDir, ".claude.json")
198 : path.join(yield* HostProcessHomeDirectory, ".claude.json");
199});
197200
198const CLAIM_OUTCOMES = {201const CLAIM_OUTCOMES = {
199 reset: "reset",202 reset: "reset",
apps/server/src/provider/providerInstallation.ts+15-12
...@@ -134,18 +134,21 @@ export const makeProviderInstallation = Effect.fn("makeProviderInstallation")(fu...@@ -134,18 +134,21 @@ export const makeProviderInstallation = Effect.fn("makeProviderInstallation")(fu
134 true,134 true,
135 );135 );
136 const entries = yield* readEntries(input.instanceId, "remove-install");136 const entries = yield* readEntries(input.instanceId, "remove-install");
137 const protectedPaths = yield* Effect.forEach(Object.values(entries), (entry) => {137 const protectedPaths = yield* Effect.forEach(
138 if (!hasBinaryPath(entry.config) || !entry.config.binaryPath.trim()) {138 Object.values(entries),
139 return Effect.succeed([]);139 Effect.fnUntraced(function* (entry) {
140 }140 if (!hasBinaryPath(entry.config) || !entry.config.binaryPath.trim()) {
141 const binaryPath = entry.config.binaryPath.trim();141 return [];
142 return resolveCommandPath(binaryPath, {142 }
143 env: mergeProviderInstanceEnvironment(entry.environment),143 const binaryPath = entry.config.binaryPath.trim();
144 }).pipe(144 return yield* resolveCommandPath(binaryPath, {
145 Effect.map((resolved) => [binaryPath, resolved]),145 env: yield* mergeProviderInstanceEnvironment(entry.environment),
146 Effect.orElseSucceed(() => [binaryPath]),146 }).pipe(
147 );147 Effect.map((resolved) => [binaryPath, resolved]),
148 });148 Effect.orElseSucceed(() => [binaryPath]),
149 );
150 }),
151 );
149 yield* installation152 yield* installation
150 .remove(protectedPaths.flat())153 .remove(protectedPaths.flat())
151 .pipe(Effect.mapError(failure(input.instanceId)));154 .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";...@@ -20,6 +20,7 @@ import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
20import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";20import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
21import * as ServerSettings from "../serverSettings.ts";21import * as ServerSettings from "../serverSettings.ts";
22import { isFilesystemRoot, managedWorktreesDirectories } from "../worktreesDirectory.ts";22import { isFilesystemRoot, managedWorktreesDirectories } from "../worktreesDirectory.ts";
23import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
2324
24export class ReviewService extends Context.Service<25export class ReviewService extends Context.Service<
25 ReviewService,26 ReviewService,
...@@ -73,13 +74,14 @@ export const make = Effect.gen(function* () {...@@ -73,13 +74,14 @@ export const make = Effect.gen(function* () {
73 const worktreesDirectories = yield* settings.getSettings.pipe(74 const worktreesDirectories = yield* settings.getSettings.pipe(
74 Effect.orElseSucceed(() => ({ worktreesDirectory: "", previousWorktreesDirectories: [] })),75 Effect.orElseSucceed(() => ({ worktreesDirectory: "", previousWorktreesDirectories: [] })),
75 );76 );
77 const home = yield* HostProcessHomeDirectory;
76 const [candidate, workspaceRoot, worktreesRoots] = yield* Effect.all([78 const [candidate, workspaceRoot, worktreesRoots] = yield* Effect.all([
77 canonicalizePath(cwd),79 canonicalizePath(cwd),
78 canonicalizePath(config.cwd),80 canonicalizePath(config.cwd),
79 // A managed root that cannot be resolved, or resolves to a filesystem81 // A managed root that cannot be resolved, or resolves to a filesystem
80 // root through a symlink, is skipped rather than failing every review.82 // root through a symlink, is skipped rather than failing every review.
81 Effect.forEach(83 Effect.forEach(
82 managedWorktreesDirectories(worktreesDirectories, config.worktreesDir, path),84 managedWorktreesDirectories(worktreesDirectories, config.worktreesDir, path, home),
83 (directory) => canonicalizePath(directory).pipe(Effect.orElseSucceed(() => null)),85 (directory) => canonicalizePath(directory).pipe(Effect.orElseSucceed(() => null)),
84 ).pipe(86 ).pipe(
85 Effect.map((roots) =>87 Effect.map((roots) =>
apps/server/src/sourceControl/SourceControlRepositoryService.ts+2-1
...@@ -29,6 +29,7 @@ import {...@@ -29,6 +29,7 @@ import {
29import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";29import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
30import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi";30import * as BitbucketApi from "@t3tools/source-control-bitbucket/server/BitbucketApi";
31import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";31import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";
32import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
32const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError);33const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError);
33const isSourceControlProviderError = Schema.is(SourceControlProviderError);34const isSourceControlProviderError = Schema.is(SourceControlProviderError);
34const isBitbucketRepositoryLocatorError = Schema.is(BitbucketApi.BitbucketRepositoryLocatorError);35const isBitbucketRepositoryLocatorError = Schema.is(BitbucketApi.BitbucketRepositoryLocatorError);
...@@ -213,7 +214,7 @@ export const make = Effect.gen(function* () {...@@ -213,7 +214,7 @@ export const make = Effect.gen(function* () {
213 });214 });
214 }215 }
215216
216 return path.resolve(expandHomePath(trimmed));217 return path.resolve(expandHomePath(trimmed, yield* HostProcessHomeDirectory));
217 },218 },
218 );219 );
219220
apps/server/src/storageCleanup.ts+2
...@@ -38,6 +38,7 @@ import * as TerminalManager from "./terminal/Manager.ts";...@@ -38,6 +38,7 @@ import * as TerminalManager from "./terminal/Manager.ts";
38import { isFilesystemRoot, managedWorktreesDirectories } from "./worktreesDirectory.ts";38import { isFilesystemRoot, managedWorktreesDirectories } from "./worktreesDirectory.ts";
39import * as GitVcsDriver from "./vcs/GitVcsDriver.ts";39import * as GitVcsDriver from "./vcs/GitVcsDriver.ts";
40import { withWorkspaceLease } from "./workspace/workspaceLease.ts";40import { withWorkspaceLease } from "./workspace/workspaceLease.ts";
41import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
4142
42const decodeCleanupThread = Schema.decodeUnknownEffect(43const decodeCleanupThread = Schema.decodeUnknownEffect(
43 Schema.fromJsonString(OrchestrationV2AppThreadJson),44 Schema.fromJsonString(OrchestrationV2AppThreadJson),
...@@ -213,6 +214,7 @@ export const make = Effect.gen(function* () {...@@ -213,6 +214,7 @@ export const make = Effect.gen(function* () {
213 serverSettings,214 serverSettings,
214 config.worktreesDir,215 config.worktreesDir,
215 path,216 path,
217 yield* HostProcessHomeDirectory,
216 )) {218 )) {
217 // An unmounted drive only skips its own worktrees.219 // An unmounted drive only skips its own worktrees.
218 const root = yield* fs.exists(directory).pipe(220 const root = yield* fs.exists(directory).pipe(
apps/server/src/telemetry/Identify.ts+4-3
...@@ -1,4 +1,3 @@...@@ -1,4 +1,3 @@
1import * as NodeOS from "node:os";
2import * as Crypto from "effect/Crypto";1import * as Crypto from "effect/Crypto";
3import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
4import * as Hex from "effect/encoding/Hex";3import * as Hex from "effect/encoding/Hex";
...@@ -10,6 +9,7 @@ import * as Schema from "effect/Schema";...@@ -10,6 +9,7 @@ import * as Schema from "effect/Schema";
109
11import { writeFileStringAtomically } from "@t3tools/shared/atomicWrite";10import { writeFileStringAtomically } from "@t3tools/shared/atomicWrite";
12import * as ServerConfig from "../config.ts";11import * as ServerConfig from "../config.ts";
12import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
1313
14/**14/**
15 * Codex writes `tokens` only for ChatGPT logins and omits the key for API-key,15 * Codex writes `tokens` only for ChatGPT logins and omits the key for API-key,
...@@ -311,6 +311,7 @@ export const getTelemetryIdentifierForHome = Effect.fn("getTelemetryIdentifierFo...@@ -311,6 +311,7 @@ export const getTelemetryIdentifierForHome = Effect.fn("getTelemetryIdentifierFo
311 Effect.orElseSucceed(() => null),311 Effect.orElseSucceed(() => null),
312);312);
313313
314export const getTelemetryIdentifier = Effect.suspend(() =>314export const getTelemetryIdentifier = Effect.flatMap(
315 getTelemetryIdentifierForHome(NodeOS.homedir()),315 HostProcessHomeDirectory,
316 getTelemetryIdentifierForHome,
316);317);
apps/server/src/terminal/Manager.ts+14-4
...@@ -42,7 +42,11 @@ import {...@@ -42,7 +42,11 @@ import {
42} from "@t3tools/contracts";42} from "@t3tools/contracts";
43import { makeKeyedCoalescingWorker } from "@t3tools/shared/KeyedCoalescingWorker";43import { makeKeyedCoalescingWorker } from "@t3tools/shared/KeyedCoalescingWorker";
44import * as KeyedLock from "@t3tools/shared/KeyedLock";44import * 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";
46import { mergePathEntries } from "@t3tools/shared/shell";50import { mergePathEntries } from "@t3tools/shared/shell";
4751
48import { acpRegistryManagedBinaryDirectories } from "@t3tools/provider-acp-registry/server";52import { acpRegistryManagedBinaryDirectories } from "@t3tools/provider-acp-registry/server";
...@@ -1317,6 +1321,7 @@ function createTerminalSpawnEnv(...@@ -1317,6 +1321,7 @@ function createTerminalSpawnEnv(
1317 baseEnv: NodeJS.ProcessEnv,1321 baseEnv: NodeJS.ProcessEnv,
1318 runtimeEnv: Record<string, string> | null | undefined,1322 runtimeEnv: Record<string, string> | null | undefined,
1319 platform: NodeJS.Platform,1323 platform: NodeJS.Platform,
1324 home: string,
1320): NodeJS.ProcessEnv {1325): NodeJS.ProcessEnv {
1321 const spawnEnv: NodeJS.ProcessEnv = {};1326 const spawnEnv: NodeJS.ProcessEnv = {};
1322 for (const [key, value] of Object.entries(baseEnv)) {1327 for (const [key, value] of Object.entries(baseEnv)) {
...@@ -1331,7 +1336,7 @@ function createTerminalSpawnEnv(...@@ -1331,7 +1336,7 @@ function createTerminalSpawnEnv(
1331 ? Object.keys(spawnEnv).find((candidate) => candidate.toLowerCase() === key.toLowerCase())1336 ? Object.keys(spawnEnv).find((candidate) => candidate.toLowerCase() === key.toLowerCase())
1332 : undefined;1337 : undefined;
1333 spawnEnv[existingKey ?? key] =1338 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;
1335 }1340 }
1336 }1341 }
1337 // An explicit empty override opts out for terminals started without a client.1342 // An explicit empty override opts out for terminals started without a client.
...@@ -1408,7 +1413,7 @@ export const resolveProviderInstanceTerminalEnvironment = Effect.fn(...@@ -1408,7 +1413,7 @@ export const resolveProviderInstanceTerminalEnvironment = Effect.fn(
1408 return yield* new TerminalProviderInstanceNotFoundError({ providerInstanceId });1413 return yield* new TerminalProviderInstanceNotFoundError({ providerInstanceId });
1409 }1414 }
14101415
1411 let resolved = mergeProviderInstanceEnvironment(instance.environment, input.env ?? {});1416 let resolved = yield* mergeProviderInstanceEnvironment(instance.environment, input.env ?? {});
1412 if (instance.driver === "codex") {1417 if (instance.driver === "codex") {
1413 const config = decodeCodexSettings(instance.config ?? {});1418 const config = decodeCodexSettings(instance.config ?? {});
1414 if (Option.isSome(config)) {1419 if (Option.isSome(config)) {
...@@ -2244,7 +2249,12 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func...@@ -2244,7 +2249,12 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func
2244 Effect.andThen(2249 Effect.andThen(
2245 Effect.gen(function* () {2250 Effect.gen(function* () {
2246 const shellCandidates = resolveShellCandidates(shellResolver, platform, baseEnv);2251 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 );
2248 // Append (never prepend) managed ACP agent install directories so2258 // Append (never prepend) managed ACP agent install directories so
2249 // `kimi login` and friends resolve by name without shadowing any2259 // `kimi login` and friends resolve by name without shadowing any
2250 // system or user tool of the same name.2260 // system or user tool of the same name.
apps/server/src/textGeneration/CodexTextGeneration.ts+7-1
...@@ -26,6 +26,7 @@ import {...@@ -26,6 +26,7 @@ import {
26} from "@t3tools/provider-core/server/textGenerationUtils";26} from "@t3tools/provider-core/server/textGenerationUtils";
27import { codexModelFamily, getModelSelectionStringOptionValue } from "@t3tools/shared/model";27import { codexModelFamily, getModelSelectionStringOptionValue } from "@t3tools/shared/model";
28import { getCodexServiceTierOptionValue } from "../codexModelOptions.ts";28import { getCodexServiceTierOptionValue } from "../codexModelOptions.ts";
29import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
2930
30const CODEX_TIMEOUT_MS = 180_000;31const CODEX_TIMEOUT_MS = 180_000;
31const encodeJsonString = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));32const encodeJsonString = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
...@@ -214,7 +215,12 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func...@@ -214,7 +215,12 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
214 env: {215 env: {
215 ...effectiveEnvironment,216 ...effectiveEnvironment,
216 ...(effectiveConfig.homePath217 ...(effectiveConfig.homePath
217 ? { CODEX_HOME: expandHomePath(effectiveConfig.homePath) }218 ? {
219 CODEX_HOME: expandHomePath(
220 effectiveConfig.homePath,
221 yield* HostProcessHomeDirectory,
222 ),
223 }
218 : {}),224 : {}),
219 },225 },
220 cwd,226 cwd,
apps/server/src/usage/UsageService.ts+27-19
...@@ -339,10 +339,10 @@ export const make = Effect.gen(function* () {...@@ -339,10 +339,10 @@ export const make = Effect.gen(function* () {
339 * not decode. Disabled accounts still have history. An unconfigured default339 * not decode. Disabled accounts still have history. An unconfigured default
340 * slot runs with default config, just as it does in the provider registry.340 * slot runs with default config, just as it does in the provider registry.
341 */341 */
342 const usageInstances = <Config>(342 const usageInstances = Effect.fn("UsageService.usageInstances")(function* <Config>(
343 driver: ProviderDriver<Config, unknown, unknown>,343 driver: ProviderDriver<Config, unknown, unknown>,
344 settings: ServerSettingsValue,344 settings: ServerSettingsValue,
345 ): Array<ProviderUsageInstance<Config>> => {345 ) {
346 const entries: Array<346 const entries: Array<
347 readonly [ProviderInstanceId, Pick<ProviderInstanceConfig, "config" | "environment">, boolean]347 readonly [ProviderInstanceId, Pick<ProviderInstanceConfig, "config" | "environment">, boolean]
348 > = Object.entries(settings.providerInstances)348 > = Object.entries(settings.providerInstances)
...@@ -352,13 +352,22 @@ export const make = Effect.gen(function* () {...@@ -352,13 +352,22 @@ export const make = Effect.gen(function* () {
352 entries.push([ProviderInstanceId.make(driver.driverKind), {}, false]);352 entries.push([ProviderInstanceId.make(driver.driverKind), {}, false]);
353 }353 }
354 const decodeConfig = Schema.decodeUnknownOption(driver.configSchema);354 const decodeConfig = Schema.decodeUnknownOption(driver.configSchema);
355 return entries.map(([instanceId, instance, configured]) => ({355 return yield* Effect.forEach(
356 instanceId,356 entries,
357 config: Option.getOrUndefined(decodeConfig(instance.config ?? {})),357 Effect.fnUntraced(function* ([instanceId, instance, configured]) {
358 environment: mergeProviderInstanceEnvironment(instance.environment, hostEnvironment),358 const instanceConfig: ProviderUsageInstance<Config> = {
359 configured,359 instanceId,
360 }));360 config: Option.getOrUndefined(decodeConfig(instance.config ?? {})),
361 };361 environment: yield* mergeProviderInstanceEnvironment(
362 instance.environment,
363 hostEnvironment,
364 ),
365 configured,
366 };
367 return instanceConfig;
368 }),
369 );
370 });
362371
363 /** Resolves every transcript directory the usage readers point at. */372 /** Resolves every transcript directory the usage readers point at. */
364 const resolveTranscriptDirs = Effect.fn("UsageService.resolveTranscriptDirs")(function* (373 const resolveTranscriptDirs = Effect.fn("UsageService.resolveTranscriptDirs")(function* (
...@@ -369,8 +378,9 @@ export const make = Effect.gen(function* () {...@@ -369,8 +378,9 @@ export const make = Effect.gen(function* () {
369 const seen = new Set<string>();378 const seen = new Set<string>();
370 for (const { driver, reader } of transcriptReaders) {379 for (const { driver, reader } of transcriptReaders) {
371 const { provider, format } = reader;380 const { provider, format } = reader;
372 const directories = yield* Effect.forEach(usageInstances(driver, settings), (instance) =>381 const directories = yield* Effect.forEach(
373 reader.directories(instance),382 yield* usageInstances(driver, settings),
383 (instance) => reader.directories(instance),
374 );384 );
375 for (const { dir: directory, fileName } of directories.flat()) {385 for (const { dir: directory, fileName } of directories.flat()) {
376 const sourceKey = provider + "\0" + directory;386 const sourceKey = provider + "\0" + directory;
...@@ -635,14 +645,12 @@ export const make = Effect.gen(function* () {...@@ -635,14 +645,12 @@ export const make = Effect.gen(function* () {
635 const scans = Effect.forEach(645 const scans = Effect.forEach(
636 scanReaders,646 scanReaders,
637 ({ driver, reader }) =>647 ({ driver, reader }) =>
638 reader648 usageInstances(driver, settings)
639 .scan({649 .pipe(
640 instances: usageInstances(driver, settings),650 Effect.flatMap((instances) =>
641 settings,651 reader.scan({ instances, settings, windowStartMs, retentionCutoffMs, awaitRefresh }),
642 windowStartMs,652 ),
643 retentionCutoffMs,653 )
644 awaitRefresh,
645 })
646 .pipe(654 .pipe(
647 Effect.flatMap((sources) =>655 Effect.flatMap((sources) =>
648 Effect.forEach(sources, ({ volumeId, ...source }) =>656 Effect.forEach(sources, ({ volumeId, ...source }) =>
apps/server/src/vcs/GitVcsDriverCore.ts+2-1
...@@ -29,7 +29,7 @@ import {...@@ -29,7 +29,7 @@ import {
29 type VcsRef,29 type VcsRef,
30} from "@t3tools/contracts";30} from "@t3tools/contracts";
31import { dedupeRemoteBranchesWithLocalMatches, normalizeGitRemoteUrl } from "@t3tools/shared/git";31import { dedupeRemoteBranchesWithLocalMatches, normalizeGitRemoteUrl } from "@t3tools/shared/git";
32import { HostProcessPlatform } from "@t3tools/shared/hostProcess";32import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
33import { compactTraceAttributes } from "@t3tools/shared/observability";33import { compactTraceAttributes } from "@t3tools/shared/observability";
34import { decodeJsonResult } from "@t3tools/shared/schemaJson";34import { decodeJsonResult } from "@t3tools/shared/schemaJson";
35import { parseT3ProjectFile } from "@t3tools/shared/t3ProjectFile";35import { parseT3ProjectFile } from "@t3tools/shared/t3ProjectFile";
...@@ -3403,6 +3403,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -3403,6 +3403,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
3403 options?.worktreesDirectory ?? "",3403 options?.worktreesDirectory ?? "",
3404 worktreesDir,3404 worktreesDir,
3405 path,3405 path,
3406 yield* HostProcessHomeDirectory,
3406 );3407 );
3407 if (parentDir === null) {3408 if (parentDir === null) {
3408 return yield* new GitCommandError({3409 return yield* new GitCommandError({
apps/server/src/workspace/WorkspaceEntries.ts+6-3
...@@ -19,7 +19,7 @@ import type {...@@ -19,7 +19,7 @@ import type {
19 ProjectSearchEntriesInput,19 ProjectSearchEntriesInput,
20 ProjectSearchEntriesResult,20 ProjectSearchEntriesResult,
21} from "@t3tools/contracts";21} from "@t3tools/contracts";
22import { HostProcessPlatform } from "@t3tools/shared/hostProcess";22import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
23import { isExplicitRelativePath, isWindowsAbsolutePath } from "@t3tools/shared/path";23import { isExplicitRelativePath, isWindowsAbsolutePath } from "@t3tools/shared/path";
24import { normalizeSearchQuery } from "@t3tools/shared/searchRanking";24import { normalizeSearchQuery } from "@t3tools/shared/searchRanking";
2525
...@@ -120,7 +120,7 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu...@@ -120,7 +120,7 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu
120 }120 }
121121
122 if (!isExplicitRelativePath(input.partialPath)) {122 if (!isExplicitRelativePath(input.partialPath)) {
123 return path.resolve(expandHomePath(input.partialPath));123 return path.resolve(expandHomePath(input.partialPath, yield* HostProcessHomeDirectory));
124 }124 }
125125
126 if (!input.cwd) {126 if (!input.cwd) {
...@@ -128,7 +128,10 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu...@@ -128,7 +128,10 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu
128 partialPath: input.partialPath,128 partialPath: input.partialPath,
129 });129 });
130 }130 }
131 return path.resolve(expandHomePath(input.cwd), input.partialPath);131 return path.resolve(
132 expandHomePath(input.cwd, yield* HostProcessHomeDirectory),
133 input.partialPath,
134 );
132});135});
133136
134/** @public Service construction is part of the canonical Effect module API. */137/** @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";...@@ -15,6 +15,7 @@ import * as Path from "effect/Path";
15import * as Schema from "effect/Schema";15import * as Schema from "effect/Schema";
1616
17import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";17import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
18import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
1819
19export class WorkspaceRootNotExistsError extends Schema.TaggedError<WorkspaceRootNotExistsError>()(20export class WorkspaceRootNotExistsError extends Schema.TaggedError<WorkspaceRootNotExistsError>()(
20 "WorkspaceRootNotExistsError",21 "WorkspaceRootNotExistsError",
...@@ -152,7 +153,9 @@ export const make = Effect.gen(function* () {...@@ -152,7 +153,9 @@ export const make = Effect.gen(function* () {
152 const normalizeWorkspaceRoot: WorkspacePaths["Service"]["normalizeWorkspaceRoot"] = Effect.fn(153 const normalizeWorkspaceRoot: WorkspacePaths["Service"]["normalizeWorkspaceRoot"] = Effect.fn(
153 "WorkspacePaths.normalizeWorkspaceRoot",154 "WorkspacePaths.normalizeWorkspaceRoot",
154 )(function* (workspaceRoot, options) {155 )(function* (workspaceRoot, options) {
155 const normalizedWorkspaceRoot = path.resolve(expandHomePath(workspaceRoot.trim()));156 const normalizedWorkspaceRoot = path.resolve(
157 expandHomePath(workspaceRoot.trim(), yield* HostProcessHomeDirectory),
158 );
156 let workspaceStat = yield* statWorkspaceRoot(159 let workspaceStat = yield* statWorkspaceRoot(
157 workspaceRoot,160 workspaceRoot,
158 normalizedWorkspaceRoot,161 normalizedWorkspaceRoot,
apps/server/src/worktreesDirectory.ts+6-3
...@@ -4,7 +4,8 @@ import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";...@@ -4,7 +4,8 @@ import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
44
5/**5/**
6 * Directory new worktrees are created under: the `worktreesDirectory`6 * Directory new worktrees are created under: the `worktreesDirectory`
7 * setting, or `defaultDir` (`<T3 home>/worktrees`) when it is empty. Null when7 * setting (`~` expands to `home`), or `defaultDir` (`<T3 home>/worktrees`)
8 * when it is empty. Null when
8 * the setting is not an absolute path on this machine, such as `D:\worktrees`9 * the setting is not an absolute path on this machine, such as `D:\worktrees`
9 * configured for a Windows server and synced to a Linux one, or when it is a10 * configured for a Windows server and synced to a Linux one, or when it is a
10 * filesystem root, which would make every path on that drive look managed.11 * filesystem root, which would make every path on that drive look managed.
...@@ -13,9 +14,10 @@ export function resolveWorktreesDirectory(...@@ -13,9 +14,10 @@ export function resolveWorktreesDirectory(
13 setting: string,14 setting: string,
14 defaultDir: string,15 defaultDir: string,
15 path: Path.Path,16 path: Path.Path,
17 home: string,
16): string | null {18): string | null {
17 if (setting === "") return defaultDir;19 if (setting === "") return defaultDir;
18 const expanded = expandHomePath(setting);20 const expanded = expandHomePath(setting, home);
19 if (!path.isAbsolute(expanded)) return null;21 if (!path.isAbsolute(expanded)) return null;
20 const resolved = path.resolve(expanded);22 const resolved = path.resolve(expanded);
21 return isFilesystemRoot(resolved, path) ? null : resolved;23 return isFilesystemRoot(resolved, path) ? null : resolved;
...@@ -34,10 +36,11 @@ export function managedWorktreesDirectories(...@@ -34,10 +36,11 @@ export function managedWorktreesDirectories(
34 },36 },
35 defaultDir: string,37 defaultDir: string,
36 path: Path.Path,38 path: Path.Path,
39 home: string,
37): ReadonlyArray<string> {40): ReadonlyArray<string> {
38 const directories = new Set([defaultDir]);41 const directories = new Set([defaultDir]);
39 for (const setting of [settings.worktreesDirectory, ...settings.previousWorktreesDirectories]) {42 for (const setting of [settings.worktreesDirectory, ...settings.previousWorktreesDirectories]) {
40 const directory = resolveWorktreesDirectory(setting, defaultDir, path);43 const directory = resolveWorktreesDirectory(setting, defaultDir, path, home);
41 if (directory !== null) directories.add(directory);44 if (directory !== null) directories.add(directory);
42 }45 }
43 return [...directories];46 return [...directories];
packages/provider-acp-registry/src/server/adapter.ts+1-1
...@@ -278,7 +278,7 @@ export const AcpRegistryAdapterV2Driver: ProviderAdapterDriver<...@@ -278,7 +278,7 @@ export const AcpRegistryAdapterV2Driver: ProviderAdapterDriver<
278 return yield* makeAcpRegistryAdapterV2({278 return yield* makeAcpRegistryAdapterV2({
279 instanceId: input.instanceId,279 instanceId: input.instanceId,
280 settings: { ...input.config, enabled: input.enabled },280 settings: { ...input.config, enabled: input.enabled },
281 environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment),281 environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
282 selfInvocation,282 selfInvocation,
283 nativeLogging: (threadId) =>283 nativeLogging: (threadId) =>
284 makeNativeLogger({284 makeNativeLogger({
packages/provider-acp-registry/src/server/driver.ts+4-1
...@@ -497,7 +497,10 @@ export const AcpRegistryDriver: ProviderDriver<AcpRegistrySettings, AcpRegistryD...@@ -497,7 +497,10 @@ export const AcpRegistryDriver: ProviderDriver<AcpRegistrySettings, AcpRegistryD
497 continuationKey: continuationIdentity.continuationKey,497 continuationKey: continuationIdentity.continuationKey,
498 };498 };
499 const effectiveConfig = { ...config, enabled } satisfies AcpRegistrySettings;499 const effectiveConfig = { ...config, enabled } satisfies AcpRegistrySettings;
500 const processEnvironment = mergeProviderInstanceEnvironment(environment, hostEnvironment);500 const processEnvironment = yield* mergeProviderInstanceEnvironment(
501 environment,
502 hostEnvironment,
503 );
501 const orchestrationAdapter = yield* AcpRegistryAdapterV2Driver.create({504 const orchestrationAdapter = yield* AcpRegistryAdapterV2Driver.create({
502 instanceId,505 instanceId,
503 displayName,506 displayName,
packages/provider-acp/src/server/AcpSessionRuntime.ts+3-2
...@@ -27,7 +27,7 @@ import * as EffectAcpErrors from "effect-acp/errors";...@@ -27,7 +27,7 @@ import * as EffectAcpErrors from "effect-acp/errors";
27import type * as EffectAcpSchema from "effect-acp/compat";27import type * as EffectAcpSchema from "effect-acp/compat";
28import type * as EffectAcpProtocol from "effect-acp/protocol";28import type * as EffectAcpProtocol from "effect-acp/protocol";
29import { resolveSpawnCommand } from "@t3tools/shared/shell";29import { resolveSpawnCommand } from "@t3tools/shared/shell";
30import { HostProcessPlatform } from "@t3tools/shared/hostProcess";30import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
3131
32import { signalProcessGroup } from "@t3tools/provider-core/server/processGroup";32import { signalProcessGroup } from "@t3tools/provider-core/server/processGroup";
33import { appendAcpStderrTail, sanitizeAcpStderrExcerpt } from "./stderr.ts";33import { appendAcpStderrTail, sanitizeAcpStderrExcerpt } from "./stderr.ts";
...@@ -1408,6 +1408,7 @@ export const make = (...@@ -1408,6 +1408,7 @@ export const make = (
1408 const stoppingRef = yield* Ref.make(false);1408 const stoppingRef = yield* Ref.make(false);
1409 const stderrFailure = yield* Deferred.make<never, EffectAcpErrors.AcpError>();1409 const stderrFailure = yield* Deferred.make<never, EffectAcpErrors.AcpError>();
1410 const stderrTailRef = yield* Ref.make("");1410 const stderrTailRef = yield* Ref.make("");
1411 const homeDirectory = yield* HostProcessHomeDirectory;
1411 const stderrDrained = yield* Deferred.make<void>();1412 const stderrDrained = yield* Deferred.make<void>();
1412 const runtimeClosed = yield* Deferred.make<void>();1413 const runtimeClosed = yield* Deferred.make<void>();
1413 const promptSerializationSemaphore = yield* Semaphore.make(1);1414 const promptSerializationSemaphore = yield* Semaphore.make(1);
...@@ -1440,7 +1441,7 @@ export const make = (...@@ -1440,7 +1441,7 @@ export const make = (
1440 Effect.ignore,1441 Effect.ignore,
1441 Effect.andThen(Ref.get(stderrTailRef)),1442 Effect.andThen(Ref.get(stderrTailRef)),
1442 Effect.map((tail) => {1443 Effect.map((tail) => {
1443 const stderr = sanitizeAcpStderrExcerpt(tail);1444 const stderr = sanitizeAcpStderrExcerpt(tail, homeDirectory);
1444 return stderr.length === 01445 return stderr.length === 0
1445 ? error1446 ? error
1446 : new EffectAcpErrors.AcpProcessExitedError({1447 : new EffectAcpErrors.AcpProcessExitedError({
packages/provider-acp/src/server/stderr.test.ts+3
...@@ -20,6 +20,7 @@ describe("AcpStderr", () => {...@@ -20,6 +20,7 @@ describe("AcpStderr", () => {
20 "Visit http://localhost:5733/pair#token=ABCDEF for pairing",20 "Visit http://localhost:5733/pair#token=ABCDEF for pairing",
21 "key=sk-abcdefghijklmnopqrstuv",21 "key=sk-abcdefghijklmnopqrstuv",
22 ].join("\n"),22 ].join("\n"),
23 "/home/server",
23 { HOME: "/Users/ada" },24 { HOME: "/Users/ada" },
24 );25 );
2526
...@@ -41,6 +42,8 @@ describe("AcpStderr", () => {...@@ -41,6 +42,8 @@ describe("AcpStderr", () => {
41 "Authorization: Basic dXNlcjpwYXNz",42 "Authorization: Basic dXNlcjpwYXNz",
42 "x-api-key: ant-api-key-value",43 "x-api-key: ant-api-key-value",
43 ].join("\n"),44 ].join("\n"),
45 "/Users/ada",
46 {},
44 );47 );
4548
46 expect(excerpt).toContain("[redacted]");49 expect(excerpt).toContain("[redacted]");
packages/provider-acp/src/server/stderr.ts+2-4
...@@ -1,6 +1,3 @@...@@ -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
4/** Last few KiB of ACP child stderr kept for startup / exit diagnostics. */1/** Last few KiB of ACP child stderr kept for startup / exit diagnostics. */
5export const ACP_STDERR_TAIL_MAX_CHARS = 4_096;2export const ACP_STDERR_TAIL_MAX_CHARS = 4_096;
63
...@@ -19,10 +16,11 @@ export function appendAcpStderrTail(current: string, chunk: string): string {...@@ -19,10 +16,11 @@ export function appendAcpStderrTail(current: string, chunk: string): string {
19/** Bounded, redacted excerpt safe to put on user-facing adapter errors. */16/** Bounded, redacted excerpt safe to put on user-facing adapter errors. */
20export function sanitizeAcpStderrExcerpt(17export function sanitizeAcpStderrExcerpt(
21 text: string,18 text: string,
19 homeDirectory: string,
22 environment: NodeJS.ProcessEnv = process.env,20 environment: NodeJS.ProcessEnv = process.env,
23): string {21): string {
24 let result = text.replaceAll("\0", "");22 let result = text.replaceAll("\0", "");
25 const homes = [environment.HOME, environment.USERPROFILE, NodeOS.homedir()].filter(23 const homes = [environment.HOME, environment.USERPROFILE, homeDirectory].filter(
26 (value): value is string => typeof value === "string" && value.length > 1,24 (value): value is string => typeof value === "string" && value.length > 1,
27 );25 );
28 for (const home of new Set(homes)) {26 for (const home of new Set(homes)) {
packages/provider-core/src/server/instanceEnvironment.test.ts+36-30
...@@ -1,9 +1,8 @@...@@ -1,9 +1,8 @@
1import * as NodeOS from "node:os";
2
3import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
4import { describe, expect, it } from "@effect/vitest";2import { describe, expect, it } from "@effect/vitest";
5import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
6import * as Path from "effect/Path";4import * as Path from "effect/Path";
5import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
76
8import { mergeProviderInstanceEnvironment } from "./instanceEnvironment.ts";7import { mergeProviderInstanceEnvironment } from "./instanceEnvironment.ts";
98
...@@ -18,7 +17,7 @@ describe("mergeProviderInstanceEnvironment", () => {...@@ -18,7 +17,7 @@ describe("mergeProviderInstanceEnvironment", () => {
18 CODEX_HOME: "~/.inherited-codex",17 CODEX_HOME: "~/.inherited-codex",
19 CLAUDE_CONFIG_DIR: "~/.inherited-claude",18 CLAUDE_CONFIG_DIR: "~/.inherited-claude",
20 };19 };
21 const environment = mergeProviderInstanceEnvironment(20 const environment = yield* mergeProviderInstanceEnvironment(
22 [21 [
23 { name: "CODEX_HOME", value, sensitive: false },22 { name: "CODEX_HOME", value, sensitive: false },
24 { name: "CLAUDE_CONFIG_DIR", value, sensitive: false },23 { name: "CLAUDE_CONFIG_DIR", value, sensitive: false },
...@@ -28,41 +27,48 @@ describe("mergeProviderInstanceEnvironment", () => {...@@ -28,41 +27,48 @@ describe("mergeProviderInstanceEnvironment", () => {
28 );27 );
2928
30 expect(environment).toEqual({29 expect(environment).toEqual({
31 CODEX_HOME: path.join(NodeOS.homedir(), tail),30 CODEX_HOME: path.join("/home/ada", tail),
32 CLAUDE_CONFIG_DIR: path.join(NodeOS.homedir(), tail),31 CLAUDE_CONFIG_DIR: path.join("/home/ada", tail),
33 CUSTOM_VALUE: value,32 CUSTOM_VALUE: value,
34 });33 });
35 expect(baseEnv).toEqual({34 expect(baseEnv).toEqual({
36 CODEX_HOME: "~/.inherited-codex",35 CODEX_HOME: "~/.inherited-codex",
37 CLAUDE_CONFIG_DIR: "~/.inherited-claude",36 CLAUDE_CONFIG_DIR: "~/.inherited-claude",
38 });37 });
39 }).pipe(Effect.provide(NodeServices.layer)),38 }).pipe(
39 Effect.provideService(HostProcessHomeDirectory, "/home/ada"),
40 Effect.provide(NodeServices.layer),
41 ),
40 );42 );
4143
42 it("leaves inherited provider homes unchanged", () => {44 it.effect("leaves inherited provider homes unchanged", () =>
43 const baseEnv = { CODEX_HOME: "~/.codex", CLAUDE_CONFIG_DIR: "~\\.claude" };45 Effect.gen(function* () {
46 const baseEnv = { CODEX_HOME: "~/.codex", CLAUDE_CONFIG_DIR: "~\\.claude" };
4447
45 expect(48 expect(
46 mergeProviderInstanceEnvironment(49 yield* mergeProviderInstanceEnvironment(
47 [{ name: "CUSTOM_VALUE", value: "~/.custom", sensitive: false }],50 [{ name: "CUSTOM_VALUE", value: "~/.custom", sensitive: false }],
48 baseEnv,51 baseEnv,
49 ),52 ),
50 ).toEqual({ ...baseEnv, CUSTOM_VALUE: "~/.custom" });53 ).toEqual({ ...baseEnv, CUSTOM_VALUE: "~/.custom" });
51 });54 }),
55 );
5256
53 it("overrides inherited environment values and preserves empty strings", () => {57 it.effect("overrides inherited environment values and preserves empty strings", () =>
54 expect(58 Effect.gen(function* () {
55 mergeProviderInstanceEnvironment(59 expect(
56 [60 yield* mergeProviderInstanceEnvironment(
57 { name: "OPENROUTER_API_KEY", value: "sk-or-test", sensitive: true },61 [
58 { name: "ANTHROPIC_API_KEY", value: "", sensitive: false },62 { name: "OPENROUTER_API_KEY", value: "sk-or-test", sensitive: true },
59 ],63 { name: "ANTHROPIC_API_KEY", value: "", sensitive: false },
60 { ANTHROPIC_API_KEY: "inherited", PATH: "/bin" },64 ],
61 ),65 { ANTHROPIC_API_KEY: "inherited", PATH: "/bin" },
62 ).toMatchObject({66 ),
63 OPENROUTER_API_KEY: "sk-or-test",67 ).toMatchObject({
64 ANTHROPIC_API_KEY: "",68 OPENROUTER_API_KEY: "sk-or-test",
65 PATH: "/bin",69 ANTHROPIC_API_KEY: "",
66 });70 PATH: "/bin",
67 });71 });
72 }),
73 );
68});74});
packages/provider-core/src/server/instanceEnvironment.ts+7-4
...@@ -1,22 +1,25 @@...@@ -1,22 +1,25 @@
1import type { ProviderInstanceEnvironment } from "@t3tools/contracts";1import type { ProviderInstanceEnvironment } from "@t3tools/contracts";
2import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
3import * as Effect from "effect/Effect";
24
3import { expandHomePath } from "./pathExpansion.ts";5import { expandHomePath } from "./pathExpansion.ts";
46
5export function mergeProviderInstanceEnvironment(7export const mergeProviderInstanceEnvironment = Effect.fn(function* (
6 environment: ProviderInstanceEnvironment | undefined,8 environment: ProviderInstanceEnvironment | undefined,
7 baseEnv: NodeJS.ProcessEnv = process.env,9 baseEnv: NodeJS.ProcessEnv = process.env,
8): NodeJS.ProcessEnv {10) {
9 if (!environment || environment.length === 0) {11 if (!environment || environment.length === 0) {
10 return baseEnv;12 return baseEnv;
11 }13 }
1214
15 const home = yield* HostProcessHomeDirectory;
13 const next: NodeJS.ProcessEnv = { ...baseEnv };16 const next: NodeJS.ProcessEnv = { ...baseEnv };
14 for (const variable of environment) {17 for (const variable of environment) {
15 // Child processes do not apply shell expansion to environment values.18 // Child processes do not apply shell expansion to environment values.
16 next[variable.name] =19 next[variable.name] =
17 variable.name === "CODEX_HOME" || variable.name === "CLAUDE_CONFIG_DIR"20 variable.name === "CODEX_HOME" || variable.name === "CLAUDE_CONFIG_DIR"
18 ? expandHomePath(variable.value)21 ? expandHomePath(variable.value, home)
19 : variable.value;22 : variable.value;
20 }23 }
21 return next;24 return next;
22}25});
packages/provider-core/src/server/pathExpansion.test.ts+10-9
...@@ -1,34 +1,35 @@...@@ -1,34 +1,35 @@
1// @effect-diagnostics nodeBuiltinImport:off1// @effect-diagnostics nodeBuiltinImport:off
2import * as NodeOS from "node:os";
3import * as NodePath from "node:path";2import * as NodePath from "node:path";
4import { describe, expect, it } from "vite-plus/test";3import { describe, expect, it } from "vite-plus/test";
54
6import { expandHomePath } from "./pathExpansion.ts";5import { expandHomePath } from "./pathExpansion.ts";
76
7const home = "/home/ada";
8
8describe("expandHomePath", () => {9describe("expandHomePath", () => {
9 it("returns an empty string unchanged", () => {10 it("returns an empty string unchanged", () => {
10 expect(expandHomePath("")).toBe("");11 expect(expandHomePath("", home)).toBe("");
11 });12 });
1213
13 it("returns paths without a leading tilde unchanged", () => {14 it("returns paths without a leading tilde unchanged", () => {
14 expect(expandHomePath("/absolute/path")).toBe("/absolute/path");15 expect(expandHomePath("/absolute/path", home)).toBe("/absolute/path");
15 expect(expandHomePath("relative/path")).toBe("relative/path");16 expect(expandHomePath("relative/path", home)).toBe("relative/path");
16 expect(expandHomePath("some~weird~path")).toBe("some~weird~path");17 expect(expandHomePath("some~weird~path", home)).toBe("some~weird~path");
17 });18 });
1819
19 it("expands a lone tilde to the home directory", () => {20 it("expands a lone tilde to the home directory", () => {
20 expect(expandHomePath("~")).toBe(NodeOS.homedir());21 expect(expandHomePath("~", home)).toBe(home);
21 });22 });
2223
23 it("expands ~/ to a subpath of the home directory", () => {24 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"));
25 });26 });
2627
27 it("expands a Windows-style ~\\ prefix", () => {28 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"));
29 });30 });
3031
31 it("does not expand ~user paths", () => {32 it("does not expand ~user paths", () => {
32 expect(expandHomePath("~alice/foo")).toBe("~alice/foo");33 expect(expandHomePath("~alice/foo", home)).toBe("~alice/foo");
33 });34 });
34});35});
packages/provider-core/src/server/pathExpansion.ts+8-8
...@@ -1,12 +1,12 @@...@@ -1,12 +1,12 @@
1// @effect-diagnostics nodeBuiltinImport:off1// @effect-diagnostics nodeBuiltinImport:off
2import * as NodeOS from "node:os";
3import * as NodePath from "node:path";2import * as NodePath from "node:path";
43
5/**4/**
6 * Expand a leading `~` (or `~/…`, `~\…`) in a user-supplied path to the5 * Expand a leading `~` (or `~/…`, `~\…`) in a user-supplied path to `home`,
7 * current user's home directory. Spawned processes don't get shell6 * the current user's home directory (`HostProcessHomeDirectory`). Spawned
8 * expansion, so env vars like `CODEX_HOME=~/.codex-work` would be passed7 * processes don't get shell expansion, so env vars like
9 * verbatim and treated as relative paths by the receiver.8 * `CODEX_HOME=~/.codex-work` would be passed verbatim and treated as relative
9 * paths by the receiver.
10 *10 *
11 * Matches the behavior of the other `expandHomePath` helpers in the11 * Matches the behavior of the other `expandHomePath` helpers in the
12 * workspace layers and CLI bootstrap: `~` alone and both `~/` and `~\`12 * workspace layers and CLI bootstrap: `~` alone and both `~/` and `~\`
...@@ -14,11 +14,11 @@ import * as NodePath from "node:path";...@@ -14,11 +14,11 @@ import * as NodePath from "node:path";
14 * start with `~` or is empty. Does not handle `~user` (other-user)14 * start with `~` or is empty. Does not handle `~user` (other-user)
15 * expansion.15 * expansion.
16 */16 */
17export function expandHomePath(value: string): string {17export function expandHomePath(value: string, home: string): string {
18 if (!value) return value;18 if (!value) return value;
19 if (value === "~") return NodeOS.homedir();19 if (value === "~") return home;
20 if (value.startsWith("~/") || value.startsWith("~\\")) {20 if (value.startsWith("~/") || value.startsWith("~\\")) {
21 return NodePath.join(NodeOS.homedir(), value.slice(2));21 return NodePath.join(home, value.slice(2));
22 }22 }
23 return value;23 return value;
24}24}
packages/provider-cursor/src/server/CursorUsageAccounts.ts+6-3
...@@ -10,12 +10,15 @@...@@ -10,12 +10,15 @@
10 *10 *
11 * @module provider-cursor/server/CursorUsageAccounts11 * @module provider-cursor/server/CursorUsageAccounts
12 */12 */
13import * as NodeOS from "node:os";
1413
15import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";14import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
16import type { ProviderUsageScan } from "@t3tools/provider-core/server/usage";15import type { ProviderUsageScan } from "@t3tools/provider-core/server/usage";
17import { writeFileStringAtomically } from "@t3tools/shared/atomicWrite";16import { writeFileStringAtomically } from "@t3tools/shared/atomicWrite";
18import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";17import {
18 HostProcessEnvironment,
19 HostProcessHomeDirectory,
20 HostProcessPlatform,
21} from "@t3tools/shared/hostProcess";
19import * as Clock from "effect/Clock";22import * as Clock from "effect/Clock";
20import * as Context from "effect/Context";23import * as Context from "effect/Context";
21import * as Deferred from "effect/Deferred";24import * as Deferred from "effect/Deferred";
...@@ -283,7 +286,7 @@ const make = Effect.gen(function* () {...@@ -283,7 +286,7 @@ const make = Effect.gen(function* () {
283286
284 const scan: CursorUsageAccounts["Service"]["scan"] = Effect.fn("CursorUsageAccounts.scan")(287 const scan: CursorUsageAccounts["Service"]["scan"] = Effect.fn("CursorUsageAccounts.scan")(
285 function* ({ keychainUsageEnabled, windowStartMs, retentionCutoffMs, awaitRefresh }) {288 function* ({ keychainUsageEnabled, windowStartMs, retentionCutoffMs, awaitRefresh }) {
286 const home = NodeOS.homedir();289 const home = yield* HostProcessHomeDirectory;
287 const userHome =290 const userHome =
288 (platform === "win32" ? hostEnvironment["USERPROFILE"] : hostEnvironment["HOME"]) || home;291 (platform === "win32" ? hostEnvironment["USERPROFILE"] : hostEnvironment["HOME"]) || home;
289 const configHome = hostEnvironment["XDG_CONFIG_HOME"]?.trim();292 const configHome = hostEnvironment["XDG_CONFIG_HOME"]?.trim();
packages/provider-cursor/src/server/adapter.ts+1-1
...@@ -2627,7 +2627,7 @@ export const CursorAdapterV2Driver: ProviderAdapterDriver<...@@ -2627,7 +2627,7 @@ export const CursorAdapterV2Driver: ProviderAdapterDriver<
2627 ...input.config,2627 ...input.config,
2628 enabled: input.enabled,2628 enabled: input.enabled,
2629 },2629 },
2630 environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment),2630 environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
2631 });2631 });
2632 },2632 },
2633 (effect, input) =>2633 (effect, input) =>
packages/provider-cursor/src/server/driver.ts+1-1
...@@ -99,7 +99,7 @@ export const CursorDriver: ProviderDriver<...@@ -99,7 +99,7 @@ export const CursorDriver: ProviderDriver<
99 const httpClient = yield* HttpClient.HttpClient;99 const httpClient = yield* HttpClient.HttpClient;
100 const keychain = yield* CursorKeychain.CursorKeychain;100 const keychain = yield* CursorKeychain.CursorKeychain;
101 const sdkRunner = yield* CursorAgentSdk.CursorAgentSdkRunner;101 const sdkRunner = yield* CursorAgentSdk.CursorAgentSdkRunner;
102 const processEnv = mergeProviderInstanceEnvironment(environment);102 const processEnv = yield* mergeProviderInstanceEnvironment(environment);
103 const continuationIdentity = defaultProviderContinuationIdentity({103 const continuationIdentity = defaultProviderContinuationIdentity({
104 driverKind: DRIVER_KIND,104 driverKind: DRIVER_KIND,
105 instanceId,105 instanceId,
packages/provider-cursor/src/server/skills.ts+5-2
...@@ -8,7 +8,6 @@...@@ -8,7 +8,6 @@
8 *8 *
9 * @module provider/Drivers/CursorSkills9 * @module provider/Drivers/CursorSkills
10 */10 */
11import * as NodeOS from "node:os";
1211
13import type { ServerProviderSkill } from "@t3tools/contracts";12import type { ServerProviderSkill } from "@t3tools/contracts";
14import * as ByteSize from "effect/ByteSize";13import * as ByteSize from "effect/ByteSize";
...@@ -18,6 +17,7 @@ import * as Path from "effect/Path";...@@ -18,6 +17,7 @@ import * as Path from "effect/Path";
18import * as PlatformError from "effect/PlatformError";17import * as PlatformError from "effect/PlatformError";
19import * as Schema from "effect/Schema";18import * as Schema from "effect/Schema";
20import { parse as parseYamlDocument } from "yaml";19import { parse as parseYamlDocument } from "yaml";
20import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
2121
22const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;22const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
23const SKILL_MENTION_PATTERN =23const SKILL_MENTION_PATTERN =
...@@ -221,7 +221,10 @@ const inspectCursorSkills = Effect.fn("inspectCursorSkills")(function* (...@@ -221,7 +221,10 @@ const inspectCursorSkills = Effect.fn("inspectCursorSkills")(function* (
221 environment: NodeJS.ProcessEnv = process.env,221 environment: NodeJS.ProcessEnv = process.env,
222) {222) {
223 const path = yield* Path.Path;223 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);
225 const rootsBelow = (base: string, scope: "user" | "project") => [228 const rootsBelow = (base: string, scope: "user" | "project") => [
226 { directory: path.join(base, ".cursor", "skills"), scope },229 { directory: path.join(base, ".cursor", "skills"), scope },
227 { directory: path.join(base, ".agents", "skills"), scope },230 { directory: path.join(base, ".agents", "skills"), scope },
packages/provider-cursor/src/server/textGeneration.ts+2-2
...@@ -1,4 +1,3 @@...@@ -1,4 +1,3 @@
1import * as NodeOS from "node:os";
2import * as FileSystem from "effect/FileSystem";1import * as FileSystem from "effect/FileSystem";
32
4import type { AgentOptions, RunResult } from "@cursor/sdk";3import type { AgentOptions, RunResult } from "@cursor/sdk";
...@@ -13,6 +12,7 @@ import * as TextGenerationOperations from "@t3tools/provider-core/server/textGen...@@ -13,6 +12,7 @@ import * as TextGenerationOperations from "@t3tools/provider-core/server/textGen
13import { cursorSdkModelSelection } from "./sdkModel.ts";12import { cursorSdkModelSelection } from "./sdkModel.ts";
14import type { CursorAuth } from "./auth.ts";13import type { CursorAuth } from "./auth.ts";
15import * as CursorSdk from "./CursorSdk.ts";14import * as CursorSdk from "./CursorSdk.ts";
15import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
1616
17const CURSOR_TIMEOUT_MS = 180_000;17const CURSOR_TIMEOUT_MS = 180_000;
1818
...@@ -90,7 +90,7 @@ export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(fu...@@ -90,7 +90,7 @@ export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(fu
90 const apiKey = yield* resolveCursorApiKey(operation);90 const apiKey = yield* resolveCursorApiKey(operation);
91 // The SDK loads sandbox.json independently of settingSources and lets it91 // The SDK loads sandbox.json independently of settingSources and lets it
92 // expand the writable paths. Its public API cannot override that policy.92 // 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`)) {
94 return yield* new TextGenerationError({94 return yield* new TextGenerationError({
95 operation,95 operation,
96 detail:96 detail:
packages/provider-cursor/src/server/usageLimits.ts+3-3
...@@ -1,7 +1,6 @@...@@ -1,7 +1,6 @@
1import * as NodeOS from "node:os";
2import type { ServerProviderUsageWindow } from "@t3tools/contracts";1import type { ServerProviderUsageWindow } from "@t3tools/contracts";
3import type { CursorSettings } from "../settings.ts";2import type { CursorSettings } from "../settings.ts";
4import { HostProcessPlatform } from "@t3tools/shared/hostProcess";3import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
5import { CURSOR_USAGE_WINDOWS } from "@t3tools/shared/usageLimits";4import { CURSOR_USAGE_WINDOWS } from "@t3tools/shared/usageLimits";
6import * as DateTime from "effect/DateTime";5import * as DateTime from "effect/DateTime";
7import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
...@@ -109,7 +108,8 @@ export const readCursorUsageLimits = Effect.fn("readCursorUsageLimits")(function...@@ -109,7 +108,8 @@ export const readCursorUsageLimits = Effect.fn("readCursorUsageLimits")(function
109 token = (yield* keychain.accessToken)?.trim();108 token = (yield* keychain.accessToken)?.trim();
110 } else if (!token) {109 } else if (!token) {
111 const home =110 const home =
112 (platform === "win32" ? environment.USERPROFILE : environment.HOME) || NodeOS.homedir();111 (platform === "win32" ? environment.USERPROFILE : environment.HOME) ||
112 (yield* HostProcessHomeDirectory);
113 const directory =113 const directory =
114 platform === "win32"114 platform === "win32"
115 ? path.join(environment.APPDATA || path.join(home, "AppData", "Roaming"), "Cursor")115 ? path.join(environment.APPDATA || path.join(home, "AppData", "Roaming"), "Cursor")
packages/provider-grok/src/server/adapter.ts+12-4
...@@ -4,7 +4,11 @@ import {...@@ -4,7 +4,11 @@ import {
4 isXAiTaskCompletedWakeNotification,4 isXAiTaskCompletedWakeNotification,
5 xAiRateLimitedErrorCode,5 xAiRateLimitedErrorCode,
6} from "./xaiAcpExtension.ts";6} from "./xaiAcpExtension.ts";
7import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";7import {
8 HostProcessEnvironment,
9 HostProcessHomeDirectory,
10 HostProcessPlatform,
11} from "@t3tools/shared/hostProcess";
8import { resolveSelfInvocation, type SelfInvocation } from "@t3tools/shared/nodeRuntime";12import { resolveSelfInvocation, type SelfInvocation } from "@t3tools/shared/nodeRuntime";
9import {13import {
10 defaultInstanceIdForDriver,14 defaultInstanceIdForDriver,
...@@ -227,7 +231,10 @@ export function grokLaunchRuntimeMode(...@@ -227,7 +231,10 @@ export function grokLaunchRuntimeMode(
227}231}
228232
229/** The flavor runs Grok's launcher through the adapter's spawner. */233/** 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 {
231 return {238 return {
232 driver: GROK_PROVIDER,239 driver: GROK_PROVIDER,
233 runtimeHarness: "Grok",240 runtimeHarness: "Grok",
...@@ -311,6 +318,7 @@ export function makeGrokAcpAdapterFlavor(options: GrokAdapterV2Options): AcpAdap...@@ -311,6 +318,7 @@ export function makeGrokAcpAdapterFlavor(options: GrokAdapterV2Options): AcpAdap
311 extractGrokPlanMarkdownFromToolCallData(toolCall.data, {318 extractGrokPlanMarkdownFromToolCallData(toolCall.data, {
312 platform: options.hostPlatform,319 platform: options.hostPlatform,
313 environment: options.environment,320 environment: options.environment,
321 homeDirectory,
314 }),322 }),
315 extractBackgroundTaskId: extractXAiMonitorTaskId,323 extractBackgroundTaskId: extractXAiMonitorTaskId,
316 extractBackgroundToolMutation: extractXAiAcpBackgroundToolMutation,324 extractBackgroundToolMutation: extractXAiAcpBackgroundToolMutation,
...@@ -331,7 +339,7 @@ export const makeGrokAdapterV2 = Effect.fn("makeGrokAdapterV2")(function* (...@@ -331,7 +339,7 @@ export const makeGrokAdapterV2 = Effect.fn("makeGrokAdapterV2")(function* (
331) {339) {
332 return yield* makeAcpAdapterV2({340 return yield* makeAcpAdapterV2({
333 instanceId: options.instanceId,341 instanceId: options.instanceId,
334 flavor: makeGrokAcpAdapterFlavor(options),342 flavor: makeGrokAcpAdapterFlavor(options, yield* HostProcessHomeDirectory),
335 selfInvocation: options.selfInvocation,343 selfInvocation: options.selfInvocation,
336 ...(options.nativeLogging === undefined ? {} : { nativeLogging: options.nativeLogging }),344 ...(options.nativeLogging === undefined ? {} : { nativeLogging: options.nativeLogging }),
337 ...(options.continuationRequests === undefined345 ...(options.continuationRequests === undefined
...@@ -366,7 +374,7 @@ export const GrokAdapterV2Driver: ProviderAdapterDriver<GrokSettings, GrokAdapte...@@ -366,7 +374,7 @@ export const GrokAdapterV2Driver: ProviderAdapterDriver<GrokSettings, GrokAdapte
366 return yield* makeGrokAdapterV2({374 return yield* makeGrokAdapterV2({
367 instanceId: input.instanceId,375 instanceId: input.instanceId,
368 settings: { ...input.config, enabled: input.enabled },376 settings: { ...input.config, enabled: input.enabled },
369 environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment),377 environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
370 hostPlatform,378 hostPlatform,
371 selfInvocation,379 selfInvocation,
372 continuationRequests,380 continuationRequests,
packages/provider-grok/src/server/driver.ts+1-1
...@@ -102,7 +102,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv, Path.Path>...@@ -102,7 +102,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv, Path.Path>
102 const path = yield* Path.Path;102 const path = yield* Path.Path;
103 const host = yield* ProviderHost.ProviderHost;103 const host = yield* ProviderHost.ProviderHost;
104 const { cwd } = host.paths;104 const { cwd } = host.paths;
105 const processEnv = mergeProviderInstanceEnvironment(environment);105 const processEnv = yield* mergeProviderInstanceEnvironment(environment);
106 const continuationIdentity = defaultProviderContinuationIdentity({106 const continuationIdentity = defaultProviderContinuationIdentity({
107 driverKind: DRIVER_KIND,107 driverKind: DRIVER_KIND,
108 instanceId,108 instanceId,
packages/provider-grok/src/server/usage.ts+4-2
...@@ -4,7 +4,6 @@...@@ -4,7 +4,6 @@
4 *4 *
5 * @module provider-grok/server/usage5 * @module provider-grok/server/usage
6 */6 */
7import * as NodeOS from "node:os";
87
9import type { UsageTokenTotals } from "@t3tools/contracts";8import type { UsageTokenTotals } from "@t3tools/contracts";
10import * as Effect from "effect/Effect";9import * as Effect from "effect/Effect";
...@@ -20,6 +19,7 @@ import {...@@ -20,6 +19,7 @@ import {
20} from "@t3tools/provider-core/server/usage";19} from "@t3tools/provider-core/server/usage";
2120
22import type { GrokSettings } from "../settings.ts";21import type { GrokSettings } from "../settings.ts";
22import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
2323
24/**24/**
25 * Grok reports cost in integer ticks where `1 USD = 10^10` ticks. See Grok25 * 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> = {...@@ -229,8 +229,10 @@ export const grokUsageReader: ProviderUsageReader<GrokSettings, Path.Path> = {
229 format: grokUsageFormat,229 format: grokUsageFormat,
230 directories: Effect.fn("grokUsageReader.directories")(function* ({ environment }) {230 directories: Effect.fn("grokUsageReader.directories")(function* ({ environment }) {
231 const path = yield* Path.Path;231 const path = yield* Path.Path;
232 const homeDirectory = yield* HostProcessHomeDirectory;
232 const home = expandHomePath(233 const home = expandHomePath(
233 environment.GROK_HOME?.trim() || path.join(NodeOS.homedir(), ".grok"),234 environment.GROK_HOME?.trim() || path.join(homeDirectory, ".grok"),
235 homeDirectory,
234 );236 );
235 // Sessions also ship multi-megabyte `chat_history` and `events` logs that237 // Sessions also ship multi-megabyte `chat_history` and `events` logs that
236 // never carry usage; only `updates.jsonl` does.238 // never carry usage; only `updates.jsonl` does.
packages/provider-grok/src/server/usageLimits.ts+5-2
...@@ -1,4 +1,3 @@...@@ -1,4 +1,3 @@
1import * as NodeOS from "node:os";
2import type { ServerProviderUsageWindow } from "@t3tools/contracts";1import type { ServerProviderUsageWindow } from "@t3tools/contracts";
3import * as DateTime from "effect/DateTime";2import * as DateTime from "effect/DateTime";
4import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
...@@ -14,6 +13,7 @@ import {...@@ -14,6 +13,7 @@ import {
14 makeUnavailableUsageLimits,13 makeUnavailableUsageLimits,
15 makeUsageLimits,14 makeUsageLimits,
16} from "@t3tools/provider-core/server/usageLimits";15} from "@t3tools/provider-core/server/usageLimits";
16import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
1717
18const GrokCredentials = Schema.Record(18const GrokCredentials = Schema.Record(
19 Schema.String,19 Schema.String,
...@@ -100,7 +100,10 @@ const readGrokCredential = Effect.fn("readGrokCredential")(function* (...@@ -100,7 +100,10 @@ const readGrokCredential = Effect.fn("readGrokCredential")(function* (
100 const path = yield* Path.Path;100 const path = yield* Path.Path;
101 const home =101 const home =
102 environment.GROK_HOME?.trim() ||102 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 );
104 for (const configPath of [107 for (const configPath of [
105 path.join(home, "config.toml"),108 path.join(home, "config.toml"),
106 path.join(home, "managed_config.toml"),109 path.join(home, "managed_config.toml"),
packages/provider-grok/src/server/xaiAcpExtension.test.ts+5-5
...@@ -1,5 +1,3 @@...@@ -1,5 +1,3 @@
1// @effect-diagnostics nodeBuiltinImport:off -- plan-path tests resolve the OS home dir.
2import * as NodeOS from "node:os";
3import { it } from "@effect/vitest";1import { it } from "@effect/vitest";
4import * as Deferred from "effect/Deferred";2import * as Deferred from "effect/Deferred";
5import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
...@@ -1902,13 +1900,14 @@ describe("Grok exit_plan_mode capture (#8358)", () => {...@@ -1902,13 +1900,14 @@ describe("Grok exit_plan_mode capture (#8358)", () => {
1902 expect(extractXAiExitPlanMarkdown(wrapped)).toBe(XAI_EMPTY_PLAN_MARKDOWN);1900 expect(extractXAiExitPlanMarkdown(wrapped)).toBe(XAI_EMPTY_PLAN_MARKDOWN);
1903 });1901 });
1904 it("identifies Grok plan.md paths and extracts markdown from tool call data", () => {1902 it("identifies Grok plan.md paths and extracts markdown from tool call data", () => {
1905 const linuxHost = { platform: "linux" as const, environment: {} };1903 const home = "/home/ada";
1906 const windowsHost = { platform: "win32" as const, environment: {} };1904 const linuxHost = { platform: "linux" as const, environment: {}, homeDirectory: home };
1905 const windowsHost = { platform: "win32" as const, environment: {}, homeDirectory: home };
1907 const grokHomeHost = {1906 const grokHomeHost = {
1908 platform: "linux" as const,1907 platform: "linux" as const,
1909 environment: { GROK_HOME: "/opt/grok-data" },1908 environment: { GROK_HOME: "/opt/grok-data" },
1909 homeDirectory: home,
1910 };1910 };
1911 const home = NodeOS.homedir().replace(/\\/g, "/");
1912 const sessionPlan = `${home}/.grok/sessions/abc/plan.md`;1911 const sessionPlan = `${home}/.grok/sessions/abc/plan.md`;
1913 const nestedSessionPlan = `${home}/.grok/sessions/%2Fhome%2Fproj/019fd20e-c563-70a0-b801-a6bc51815a9b/plan.md`;1912 const nestedSessionPlan = `${home}/.grok/sessions/%2Fhome%2Fproj/019fd20e-c563-70a0-b801-a6bc51815a9b/plan.md`;
1914 expect(isGrokPlanMarkdownPath(sessionPlan, linuxHost)).toBe(true);1913 expect(isGrokPlanMarkdownPath(sessionPlan, linuxHost)).toBe(true);
...@@ -1935,6 +1934,7 @@ describe("Grok exit_plan_mode capture (#8358)", () => {...@@ -1935,6 +1934,7 @@ describe("Grok exit_plan_mode capture (#8358)", () => {
1935 isGrokPlanMarkdownPath("/OPT/GROK-DATA/sessions/sess/plan.md", {1934 isGrokPlanMarkdownPath("/OPT/GROK-DATA/sessions/sess/plan.md", {
1936 platform: "win32",1935 platform: "win32",
1937 environment: { GROK_HOME: "/opt/grok-data" },1936 environment: { GROK_HOME: "/opt/grok-data" },
1937 homeDirectory: home,
1938 }),1938 }),
1939 ).toBe(true);1939 ).toBe(true);
1940 expect(isGrokPlanMarkdownPath("/OPT/GROK-DATA/sessions/sess/plan.md", grokHomeHost)).toBe(1940 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,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
4import type { ProviderUserInputAnswers, UserInputQuestion } from "@t3tools/contracts";1import type { ProviderUserInputAnswers, UserInputQuestion } from "@t3tools/contracts";
5import * as Cause from "effect/Cause";2import * as Cause from "effect/Cause";
6import * as Deferred from "effect/Deferred";3import * as Deferred from "effect/Deferred";
...@@ -1088,11 +1085,13 @@ function addGrokSessionPrefix(...@@ -1088,11 +1085,13 @@ function addGrokSessionPrefix(
1088export interface GrokPlanPathHost {1085export interface GrokPlanPathHost {
1089 readonly platform: NodeJS.Platform;1086 readonly platform: NodeJS.Platform;
1090 readonly environment: NodeJS.ProcessEnv;1087 readonly environment: NodeJS.ProcessEnv;
1088 readonly homeDirectory: string;
1091}1089}
10921090
1093function grokPlanSessionPrefixes(environment: NodeJS.ProcessEnv): ReadonlySet<string> {1091function grokPlanSessionPrefixes(host: GrokPlanPathHost): ReadonlySet<string> {
1092 const { environment } = host;
1094 const prefixes = new Set<string>();1093 const prefixes = new Set<string>();
1095 addGrokSessionPrefix(prefixes, NodeOS.homedir(), true);1094 addGrokSessionPrefix(prefixes, host.homeDirectory, true);
1096 addGrokSessionPrefix(prefixes, "~", true);1095 addGrokSessionPrefix(prefixes, "~", true);
1097 addGrokSessionPrefix(prefixes, environment.HOME ?? "", true);1096 addGrokSessionPrefix(prefixes, environment.HOME ?? "", true);
1098 addGrokSessionPrefix(prefixes, environment.USERPROFILE ?? "", true);1097 addGrokSessionPrefix(prefixes, environment.USERPROFILE ?? "", true);
...@@ -1134,7 +1133,7 @@ export function isGrokPlanMarkdownPath(...@@ -1134,7 +1133,7 @@ export function isGrokPlanMarkdownPath(
1134 ) {1133 ) {
1135 return false;1134 return false;
1136 }1135 }
1137 for (const prefix of grokPlanSessionPrefixes(host.environment)) {1136 for (const prefix of grokPlanSessionPrefixes(host)) {
1138 const needle = win32 ? prefix.toLowerCase() : prefix;1137 const needle = win32 ? prefix.toLowerCase() : prefix;
1139 if (!haystack.startsWith(needle)) {1138 if (!haystack.startsWith(needle)) {
1140 continue;1139 continue;
packages/provider-muse/src/server/driver.ts+3-3
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { ProviderDriverKind } from "@t3tools/contracts";1import { ProviderDriverKind } from "@t3tools/contracts";
2import { MuseSettings } from "../settings.ts";2import { MuseSettings } from "../settings.ts";
3import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";3import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";6import * as Path from "effect/Path";
...@@ -71,14 +71,14 @@ export const MuseDriver: ProviderDriver<MuseSettings, MuseDriverEnv> = {...@@ -71,14 +71,14 @@ export const MuseDriver: ProviderDriver<MuseSettings, MuseDriverEnv> = {
71 const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;71 const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
72 const hostEnvironment = yield* HostProcessEnvironment;72 const hostEnvironment = yield* HostProcessEnvironment;
73 // Drop an inherited META_API_KEY so Muse uses its login; an instance value still wins.73 // 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(
75 environment,75 environment,
76 makeMuseEnvironment(hostEnvironment),76 makeMuseEnvironment(hostEnvironment),
77 );77 );
78 const effectiveConfig = {78 const effectiveConfig = {
79 ...config,79 ...config,
80 enabled,80 enabled,
81 binaryPath: expandHomePath(config.binaryPath),81 binaryPath: expandHomePath(config.binaryPath, yield* HostProcessHomeDirectory),
82 } satisfies MuseSettings;82 } satisfies MuseSettings;
83 const continuationIdentity = defaultProviderContinuationIdentity({83 const continuationIdentity = defaultProviderContinuationIdentity({
84 driverKind: DRIVER_KIND,84 driverKind: DRIVER_KIND,
packages/provider-opencode/src/server/adapter.ts+1-1
...@@ -3744,7 +3744,7 @@ export const OpenCodeAdapterV2Driver: ProviderAdapterDriver<...@@ -3744,7 +3744,7 @@ export const OpenCodeAdapterV2Driver: ProviderAdapterDriver<
3744 return yield* makeOpenCodeAdapterV2({3744 return yield* makeOpenCodeAdapterV2({
3745 instanceId: input.instanceId,3745 instanceId: input.instanceId,
3746 settings: { ...input.config, enabled: input.enabled },3746 settings: { ...input.config, enabled: input.enabled },
3747 environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment),3747 environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
3748 ...(providerEventLoggers.native === undefined3748 ...(providerEventLoggers.native === undefined
3749 ? {}3749 ? {}
3750 : { nativeEventLogger: providerEventLoggers.native }),3750 : { nativeEventLogger: providerEventLoggers.native }),
packages/provider-opencode/src/server/driver.ts+1-1
...@@ -206,7 +206,7 @@ export const OpenCodeDriver: ProviderDriver<...@@ -206,7 +206,7 @@ export const OpenCodeDriver: ProviderDriver<
206 const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions;206 const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions;
207 const crypto = yield* Crypto.Crypto;207 const crypto = yield* Crypto.Crypto;
208 const host = yield* ProviderHost.ProviderHost;208 const host = yield* ProviderHost.ProviderHost;
209 const processEnv = mergeProviderInstanceEnvironment(environment);209 const processEnv = yield* mergeProviderInstanceEnvironment(environment);
210 const continuationIdentity = defaultProviderContinuationIdentity({210 const continuationIdentity = defaultProviderContinuationIdentity({
211 driverKind: DRIVER_KIND,211 driverKind: DRIVER_KIND,
212 instanceId,212 instanceId,
packages/provider-opencode/src/server/usage.ts+4-4
...@@ -4,7 +4,6 @@...@@ -4,7 +4,6 @@
4 *4 *
5 * @module provider-opencode/server/usage5 * @module provider-opencode/server/usage
6 */6 */
7import * as NodeOS from "node:os";
87
9import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";8import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
10import {9import {
...@@ -12,7 +11,7 @@ import {...@@ -12,7 +11,7 @@ import {
12 type ProviderUsageReader,11 type ProviderUsageReader,
13 type UsageRecord,12 type UsageRecord,
14} from "@t3tools/provider-core/server/usage";13} from "@t3tools/provider-core/server/usage";
15import { HostProcessEnvironment } from "@t3tools/shared/hostProcess";14import { HostProcessEnvironment, HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
16import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";15import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
17import * as Effect from "effect/Effect";16import * as Effect from "effect/Effect";
18import * as Exit from "effect/Exit";17import * as Exit from "effect/Exit";
...@@ -259,6 +258,7 @@ const resolveOpenCodeDataDirs = Effect.fn("resolveOpenCodeDataDirs")(function* (...@@ -259,6 +258,7 @@ const resolveOpenCodeDataDirs = Effect.fn("resolveOpenCodeDataDirs")(function* (
259 const fileSystem = yield* FileSystem.FileSystem;258 const fileSystem = yield* FileSystem.FileSystem;
260 const path = yield* Path.Path;259 const path = yield* Path.Path;
261 const environment = yield* HostProcessEnvironment;260 const environment = yield* HostProcessEnvironment;
261 const homeDirectory = yield* HostProcessHomeDirectory;
262 const roots = environment["OPENCODE_DATA_DIR"]262 const roots = environment["OPENCODE_DATA_DIR"]
263 ?.split(",")263 ?.split(",")
264 .map((value) => value.trim())264 .map((value) => value.trim())
...@@ -268,13 +268,13 @@ const resolveOpenCodeDataDirs = Effect.fn("resolveOpenCodeDataDirs")(function* (...@@ -268,13 +268,13 @@ const resolveOpenCodeDataDirs = Effect.fn("resolveOpenCodeDataDirs")(function* (
268 path.join(268 path.join(
269 dataHome && path.isAbsolute(dataHome)269 dataHome && path.isAbsolute(dataHome)
270 ? dataHome270 ? dataHome
271 : path.join(NodeOS.homedir(), ".local", "share"),271 : path.join(homeDirectory, ".local", "share"),
272 "opencode",272 "opencode",
273 ),273 ),
274 ];274 ];
275 const canonical = new Set<string>();275 const canonical = new Set<string>();
276 for (const root of roots?.length ? roots : defaults) {276 for (const root of roots?.length ? roots : defaults) {
277 const resolved = path.resolve(expandHomePath(root));277 const resolved = path.resolve(expandHomePath(root, homeDirectory));
278 canonical.add(yield* fileSystem.realPath(resolved).pipe(Effect.orElseSucceed(() => resolved)));278 canonical.add(yield* fileSystem.realPath(resolved).pipe(Effect.orElseSucceed(() => resolved)));
279 }279 }
280 return [...canonical];280 return [...canonical];
packages/provider-opencode/src/server/usageLimits.ts+6-3
...@@ -1,5 +1,3 @@...@@ -1,5 +1,3 @@
1import * as NodeOS from "node:os";
2
3import type { ServerProviderUsageWindow } from "@t3tools/contracts";1import type { ServerProviderUsageWindow } from "@t3tools/contracts";
4import * as Crypto from "effect/Crypto";2import * as Crypto from "effect/Crypto";
5import * as DateTime from "effect/DateTime";3import * as DateTime from "effect/DateTime";
...@@ -18,6 +16,7 @@ import {...@@ -18,6 +16,7 @@ import {
18 makeUnavailableUsageLimits,16 makeUnavailableUsageLimits,
19 makeUsageLimits,17 makeUsageLimits,
20} from "@t3tools/provider-core/server/usageLimits";18} from "@t3tools/provider-core/server/usageLimits";
19import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
2120
22const AuthFile = Schema.Struct({ "opencode-go": Schema.optionalKey(Schema.Unknown) });21const AuthFile = Schema.Struct({ "opencode-go": Schema.optionalKey(Schema.Unknown) });
23const ApiAuth = Schema.Struct({ type: Schema.Literal("api"), key: Schema.String });22const ApiAuth = Schema.Struct({ type: Schema.Literal("api"), key: Schema.String });
...@@ -47,7 +46,11 @@ export const readOpenCodeGoUsageLimits = Effect.fn("readOpenCodeGoUsageLimits")(...@@ -47,7 +46,11 @@ export const readOpenCodeGoUsageLimits = Effect.fn("readOpenCodeGoUsageLimits")(
47 const env = input.environment;46 const env = input.environment;
48 const dataHome =47 const dataHome =
49 env.XDG_DATA_HOME ||48 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 );
51 const authPath = path.join(dataHome, "opencode", "auth.json");54 const authPath = path.join(dataHome, "opencode", "auth.json");
52 const contents =55 const contents =
53 env.OPENCODE_AUTH_CONTENT ||56 env.OPENCODE_AUTH_CONTENT ||
packages/provider-pi/src/server/adapter.ts+1-1
...@@ -3276,7 +3276,7 @@ export const PiAdapterV2Driver: ProviderAdapterDriver<PiSettings, PiAdapterV2Dri...@@ -3276,7 +3276,7 @@ export const PiAdapterV2Driver: ProviderAdapterDriver<PiSettings, PiAdapterV2Dri
3276 return yield* makePiAdapterV2({3276 return yield* makePiAdapterV2({
3277 instanceId: input.instanceId,3277 instanceId: input.instanceId,
3278 settings: { ...input.config, enabled: input.enabled },3278 settings: { ...input.config, enabled: input.enabled },
3279 environment: mergeProviderInstanceEnvironment(input.environment, hostEnvironment),3279 environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment),
3280 continuationRequests,3280 continuationRequests,
3281 });3281 });
3282 },3282 },
packages/provider-pi/src/server/driver.ts+1-1
...@@ -97,7 +97,7 @@ export const PiDriver: ProviderDriver<PiSettings, PiDriverEnv> = {...@@ -97,7 +97,7 @@ export const PiDriver: ProviderDriver<PiSettings, PiDriverEnv> = {
97 const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions;97 const latestVersions = yield* ProviderLatestVersions.ProviderLatestVersions;
98 const host = yield* ProviderHost.ProviderHost;98 const host = yield* ProviderHost.ProviderHost;
99 const { cwd } = host.paths;99 const { cwd } = host.paths;
100 const processEnv = mergeProviderInstanceEnvironment(environment);100 const processEnv = yield* mergeProviderInstanceEnvironment(environment);
101 const continuationIdentity = defaultProviderContinuationIdentity({101 const continuationIdentity = defaultProviderContinuationIdentity({
102 driverKind: DRIVER_KIND,102 driverKind: DRIVER_KIND,
103 instanceId,103 instanceId,
packages/shared/src/hostProcess.ts+7
...@@ -25,6 +25,13 @@ export const HostProcessHostname = Context.Reference<string>(...@@ -25,6 +25,13 @@ export const HostProcessHostname = Context.Reference<string>(
25 },25 },
26);26);
2727
28export const HostProcessHomeDirectory = Context.Reference<string>(
29 "@t3tools/shared/hostProcess/HostProcessHomeDirectory",
30 {
31 defaultValue: () => NodeOS.homedir(),
32 },
33);
34
28export const HostProcessEnvironment = Context.Reference<NodeJS.ProcessEnv>(35export const HostProcessEnvironment = Context.Reference<NodeJS.ProcessEnv>(
29 "@t3tools/shared/hostProcess/HostProcessEnvironment",36 "@t3tools/shared/hostProcess/HostProcessEnvironment",
30 {37 {
packages/source-control-forgejo/src/server/ForgejoCli.ts+2-3
...@@ -6,13 +6,12 @@ import * as Result from "effect/Result";...@@ -6,13 +6,12 @@ import * as Result from "effect/Result";
6import * as Clock from "effect/Clock";6import * as Clock from "effect/Clock";
7import * as FileSystem from "effect/FileSystem";7import * as FileSystem from "effect/FileSystem";
8import * as Semaphore from "effect/Semaphore";8import * as Semaphore from "effect/Semaphore";
9import * as NodeOS from "node:os";
10// @effect-diagnostics-next-line nodeBuiltinImport:off - fj storage paths use explicit Windows and POSIX layouts, independently of this process's platform.9// @effect-diagnostics-next-line nodeBuiltinImport:off - fj storage paths use explicit Windows and POSIX layouts, independently of this process's platform.
11import * as NodePath from "node:path";10import * as NodePath from "node:path";
12import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";11import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";
13import { ChildProcessSpawner } from "effect/process";12import { ChildProcessSpawner } from "effect/process";
14import { decodeJsonResult } from "@t3tools/shared/schemaJson";13import { decodeJsonResult } from "@t3tools/shared/schemaJson";
15import { HostProcessPlatform } from "@t3tools/shared/hostProcess";14import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
16import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost";15import * as SourceControlHost from "@t3tools/source-control-core/server/SourceControlHost";
17import { collectUint8StreamText } from "@t3tools/provider-core/server/collectStreamText";16import { collectUint8StreamText } from "@t3tools/provider-core/server/collectStreamText";
18import type { SourceControlProviderContext } from "@t3tools/source-control-core/server/SourceControlProvider";17import type { SourceControlProviderContext } from "@t3tools/source-control-core/server/SourceControlProvider";
...@@ -250,7 +249,7 @@ export const make = Effect.gen(function* () {...@@ -250,7 +249,7 @@ export const make = Effect.gen(function* () {
250 const readKeys = Effect.fn("ForgejoCli.readKeys")(function* (cwd: string) {249 const readKeys = Effect.fn("ForgejoCli.readKeys")(function* (cwd: string) {
251 for (const path of forgejoKeysPaths({250 for (const path of forgejoKeysPaths({
252 platform: yield* HostProcessPlatform,251 platform: yield* HostProcessPlatform,
253 home: NodeOS.homedir(),252 home: yield* HostProcessHomeDirectory,
254 ...(globalThis.process.env.XDG_DATA_HOME253 ...(globalThis.process.env.XDG_DATA_HOME
255 ? { dataHome: globalThis.process.env.XDG_DATA_HOME }254 ? { dataHome: globalThis.process.env.XDG_DATA_HOME }
256 : {}),255 : {}),