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