Skip to content

Commit bad2dd3

Browse files
refactor: home directory comes from a HostProcessHomeDirectory reference
Add HostProcessHomeDirectory to @t3tools/shared/hostProcess (default NodeOS.homedir()) and yield it wherever Effect code used to call NodeOS.homedir() directly, so the dependency is visible and swappable in tests. - expandHomePath(value, home) takes the home directory explicitly. - mergeProviderInstanceEnvironment is an Effect.fn that yields the reference; callers yield* it. - Plain helpers reached from Effect code (worktree directory resolution, terminal spawn env, Antigravity user home, ACP stderr sanitizing, Grok plan path host, desktop snapshot config paths) take the home from their Effect caller. - Tests that mocked os.homedir provide the reference instead. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent a0a0e94 commit bad2dd3

74 files changed

Lines changed: 442 additions & 308 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/desktop/src/app/DesktopApp.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import * as DesktopUpdates from "../updates/DesktopUpdates.ts";
3535
import * as DesktopSnapShot from "../snapShot/DesktopSnapShot.ts";
3636
import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts";
3737
import * as DesktopRendererHistory from "../telemetry/DesktopRendererHistory.ts";
38+
import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
3839

3940
const DEFAULT_DESKTOP_BACKEND_PORT = 3773;
4041
const MAX_TCP_PORT = 65_535;
@@ -284,7 +285,9 @@ const startup = Effect.gen(function* () {
284285
preReadyElectronOptions.linuxPasswordStoreCommandLine !== null;
285286
const linuxElectronOptions =
286287
environment.platform === "linux" && !hasCommandLinePasswordStore
287-
? DesktopPreReadyPlatform.resolveEarlyLinuxElectronOptionsFromProcess()
288+
? DesktopPreReadyPlatform.resolveEarlyLinuxElectronOptionsFromProcess(
289+
yield* HostProcessHomeDirectory,
290+
)
288291
: preReadyElectronOptions.linux;
289292
if (linuxElectronOptions !== null && !hasCommandLinePasswordStore) {
290293
if (

‎apps/desktop/src/app/DesktopPreReadyPlatform.ts‎

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
// @effect-diagnostics nodeBuiltinImport:off - pre-ready Electron setup reads settings and prepares the Linux desktop entry synchronously before app services are available.
22
import * as NodeFS from "node:fs";
3-
import * as NodeOS from "node:os";
43
import * as NodePath from "node:path";
54
import * as Context from "effect/Context";
65
import * as Effect from "effect/Effect";
76
import * as Layer from "effect/Layer";
87

98
import * as Electron from "electron";
10-
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
9+
import { HostProcessHomeDirectory, HostProcessPlatform } from "@t3tools/shared/hostProcess";
1110

1211
import * as DesktopEarlyElectronStartup from "./DesktopEarlyElectronStartup.ts";
1312
import { resolveDesktopAppBranding } from "./DesktopEnvironment.ts";
@@ -31,14 +30,15 @@ function readCommandLineSwitchValue(
3130
return value.length > 0 ? value : null;
3231
}
3332

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+
});
4242

4343
export class DesktopPreReadyElectronOptions extends Context.Service<
4444
DesktopPreReadyElectronOptions,
@@ -51,20 +51,22 @@ export class DesktopPreReadyElectronOptions extends Context.Service<
5151
/** @public Service construction is part of the canonical Effect module API. */
5252
export const make = Effect.gen(function* () {
5353
const platform = yield* HostProcessPlatform;
54+
const homeDirectory = yield* HostProcessHomeDirectory;
5455
return yield* Effect.sync((): DesktopPreReadyElectronOptions["Service"] => {
5556
const linuxPasswordStoreCommandLine =
5657
platform === "linux"
5758
? readCommandLineSwitchValue(Electron.app.commandLine, "password-store")
5859
: null;
59-
const linux = platform === "linux" ? resolveEarlyLinuxElectronOptionsFromProcess() : null;
60+
const linux =
61+
platform === "linux" ? resolveEarlyLinuxElectronOptionsFromProcess(homeDirectory) : null;
6062

6163
if (linux !== null) {
6264
// The portal also requires a valid desktop entry. An AppImage update may
6365
// have removed the executable referenced by the previous launch's entry.
6466
try {
6567
const applicationsDir = NodePath.posix.join(
6668
process.env.XDG_DATA_HOME?.trim() ||
67-
NodePath.posix.join(NodeOS.homedir(), ".local", "share"),
69+
NodePath.posix.join(homeDirectory, ".local", "share"),
6870
"applications",
6971
);
7072
NodeFS.mkdirSync(applicationsDir, { recursive: true });

‎apps/desktop/src/ipc/methods/window.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
import { WORKSPACE_IMAGE_PREVIEW_EXTENSIONS } from "@t3tools/shared/filePreview";
1717
import { resolveEditorCommand } from "@t3tools/shared/editor";
1818
import * as HostProcess from "@t3tools/shared/hostProcess";
19-
import * as NodeOS from "node:os";
2019
import * as FileSystem from "effect/FileSystem";
2120
import * as Path from "effect/Path";
2221
import * as Effect from "effect/Effect";
@@ -46,6 +45,7 @@ import {
4645
resolveWslPickFolderDefaultPath,
4746
wslUncPathToLinuxPath,
4847
} from "../../wsl/wslPathParsing.ts";
48+
import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
4949

5050
const ContextMenuPosition = Schema.Struct({
5151
x: Schema.Number,
@@ -398,7 +398,7 @@ export const pickThemeFiles = DesktopIpc.makeIpcMethod({
398398
// The VS Code extensions directory is the same dotfolder on Windows,
399399
// macOS, and Linux; when it is missing the picker opens wherever the
400400
// platform would by default.
401-
const extensionsDir = path.join(NodeOS.homedir(), ".vscode", "extensions");
401+
const extensionsDir = path.join(yield* HostProcessHomeDirectory, ".vscode", "extensions");
402402
const defaultPath = yield* fileSystem
403403
.exists(extensionsDir)
404404
.pipe(Effect.orElseSucceed(() => false));

‎apps/desktop/src/snapShot/CaptureShortcutConfig.test.ts‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ beforeEach(async () => {
2929
tools.validateNiri.mockReset().mockResolvedValue(undefined);
3030
tools.hyprlandBindings.mockReset().mockResolvedValue([]);
3131
tools.reloadHyprland.mockReset().mockResolvedValue(undefined);
32-
setup = new CaptureShortcutConfig(tools);
32+
setup = new CaptureShortcutConfig(NodeOS.homedir(), tools);
3333
});
3434
afterEach(async () => {
3535
await NodeFSP.rm(directory, { recursive: true, force: true });
@@ -219,15 +219,15 @@ it("does not edit Omarchy's shipped defaults", async () => {
219219
});
220220
it("honors NIRI_CONFIG and XDG_CONFIG_HOME", () => {
221221
expect(
222-
niriCaptureConfigPath(
223-
{ NIRI_CONFIG: "/custom/test.kdl", XDG_CONFIG_HOME: "/config" },
224-
"/home/test",
225-
),
222+
niriCaptureConfigPath("/home/test", {
223+
NIRI_CONFIG: "/custom/test.kdl",
224+
XDG_CONFIG_HOME: "/config",
225+
}),
226226
).toBe("/custom/test.kdl");
227-
expect(niriCaptureConfigPath({ XDG_CONFIG_HOME: "/config" }, "/home/test")).toBe(
227+
expect(niriCaptureConfigPath("/home/test", { XDG_CONFIG_HOME: "/config" })).toBe(
228228
"/config/niri/config.kdl",
229229
);
230-
expect(niriCaptureConfigPath({}, "/home/test")).toBe("/home/test/.config/niri/config.kdl");
230+
expect(niriCaptureConfigPath("/home/test", {})).toBe("/home/test/.config/niri/config.kdl");
231231
});
232232
it("preserves a UTF-8 BOM through preview and apply", async () => {
233233
await NodeFSP.writeFile(path, "\uFEFFbinds {}\r\n");
@@ -251,7 +251,7 @@ it("the installed Niri parser accepts the generated edit and its relative includ
251251
NodePath.join(directory, "keys.kdl"),
252252
"binds {\n Ctrl+Alt+Q { quit; }\n}\n",
253253
);
254-
const checked = new CaptureShortcutConfig({
254+
const checked = new CaptureShortcutConfig(NodeOS.homedir(), {
255255
...tools,
256256
validateNiri: async (file) => {
257257
await NodeUtil.promisify(NodeChildProcess.execFile)("niri", ["validate", "--config", file]);

‎apps/desktop/src/snapShot/CaptureShortcutConfig.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import * as NodeFSP from "node:fs/promises";
33
import * as NodeFS from "node:fs";
44
import * as NodePath from "node:path";
5-
import * as NodeOS from "node:os";
65
import * as NodeCrypto from "node:crypto";
76
import * as NodeChildProcess from "node:child_process";
87
import * as NodeUtil from "node:util";
@@ -39,7 +38,7 @@ const decodeHyprlandBindings = Schema.decodeUnknownSync(
3938
),
4039
);
4140

42-
export function niriCaptureConfigPath(env = process.env, home = NodeOS.homedir()) {
41+
export function niriCaptureConfigPath(home: string, env = process.env) {
4342
return (
4443
env.NIRI_CONFIG ||
4544
NodePath.join(env.XDG_CONFIG_HOME || NodePath.join(home, ".config"), "niri", "config.kdl")
@@ -99,8 +98,10 @@ export class CaptureShortcutConfig {
9998
| { preview: DesktopCaptureConfigPreview; target: Target; files: Snapshot[]; missing: string[] }
10099
| undefined;
101100
private applying = false;
101+
private readonly home: string;
102102
private readonly tools: typeof defaultTools;
103-
constructor(tools = defaultTools) {
103+
constructor(home: string, tools = defaultTools) {
104+
this.home = home;
104105
this.tools = tools;
105106
}
106107

@@ -166,7 +167,7 @@ export class CaptureShortcutConfig {
166167
if (include.path.includes("$") || /[*?[\]]/.test(include.path))
167168
throw new Error("This config uses a dynamic include. Use manual setup in Advanced.");
168169
const path = include.path.startsWith("~/")
169-
? NodePath.join(NodeOS.homedir(), include.path.slice(2))
170+
? NodePath.join(this.home, include.path.slice(2))
170171
: NodePath.resolve(NodePath.dirname(file.path), include.path);
171172
let child: Snapshot;
172173
try {

‎apps/desktop/src/snapShot/DesktopSnapShot.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ import {
8888
snapShotShortcutRegistrationFailureMessage,
8989
snapShotShortcutSystemConflict,
9090
} from "./snapShot.ts";
91+
import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
9192

9293
const MAX_CAPTURE_WIDTH = 2_560;
9394
const MAX_CAPTURE_HEIGHT = 1_600;
@@ -727,6 +728,7 @@ export const make = Effect.gen(function* () {
727728
const runPromise = Effect.runPromiseWith(context);
728729
const captureDirectory = path.join(environment.stateDir, "snap-shots");
729730
const linuxAppId = environment.linuxDesktopEntryName.replace(/\.desktop$/, "");
731+
const home = yield* HostProcessHomeDirectory;
730732
let shortcutVerified = false;
731733
const gnomeSetupPaths = {
732734
bundle: environment.isPackaged
@@ -757,7 +759,7 @@ export const make = Effect.gen(function* () {
757759
dataHome: path.dirname(environment.linuxApplicationsDir),
758760
};
759761
const shiftShortcutWorkerPath = path.join(__dirname, "snapShot", "GlobalShiftShortcutWorker.cjs");
760-
const shortcutConfig = new CaptureShortcutConfig();
762+
const shortcutConfig = new CaptureShortcutConfig(home);
761763
const accessibilityWorkerPath = path.join(
762764
__dirname,
763765
"snapShot",
@@ -1184,7 +1186,7 @@ export const make = Effect.gen(function* () {
11841186
shortcut,
11851187
shortcutRegistered: false,
11861188
shortcutBinding: niriCaptureBinding(linuxAppId),
1187-
shortcutConfigPath: niriCaptureConfigPath(),
1189+
shortcutConfigPath: niriCaptureConfigPath(home),
11881190
shortcutActionRegistered: registered,
11891191
shortcutMessage: registered
11901192
? "Set up the shortcut to add it to your Niri config."
@@ -1474,8 +1476,8 @@ export const make = Effect.gen(function* () {
14741476
const configPath =
14751477
selectedPath ??
14761478
(desktop === "niri"
1477-
? niriCaptureConfigPath()
1478-
: (await hyprlandCaptureShortcut(linuxAppId)).shortcutConfigPath);
1479+
? niriCaptureConfigPath(home)
1480+
: (await hyprlandCaptureShortcut(linuxAppId, home)).shortcutConfigPath);
14791481
return shortcutConfig.preview({ desktop, path: configPath, appId: linuxAppId }, request);
14801482
},
14811483
catch: (cause) =>
@@ -1593,7 +1595,7 @@ export const make = Effect.gen(function* () {
15931595
: undefined;
15941596
const hyprlandShortcut =
15951597
state.linuxBackend === "hyprland"
1596-
? yield* Effect.promise(() => hyprlandCaptureShortcut(linuxAppId))
1598+
? yield* Effect.promise(() => hyprlandCaptureShortcut(linuxAppId, home))
15971599
: undefined;
15981600
return {
15991601
...state,
@@ -1609,7 +1611,7 @@ export const make = Effect.gen(function* () {
16091611
: {}),
16101612
...hyprlandShortcut,
16111613
...(state.linuxBackend === "niri"
1612-
? { shortcutConfigPath: niriCaptureConfigPath() }
1614+
? { shortcutConfigPath: niriCaptureConfigPath(home) }
16131615
: {}),
16141616
...(kdeHelper
16151617
? {

‎apps/desktop/src/snapShot/HyprlandSnapShot.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ it.each([true, false])("uses the active config syntax and user bindings (Lua: %s
121121
NodePath.join(config, lua ? "bindings.lua" : "bindings.conf"),
122122
"custom bindings",
123123
);
124-
const result = await hyprlandCaptureShortcut("com.t3tools.T3Code", directory);
124+
const result = await hyprlandCaptureShortcut("com.t3tools.T3Code", "/home/test", directory);
125125
expect(result.shortcutConfigPath).toBe(
126126
NodePath.join(config, lua ? "bindings.lua" : "bindings.conf"),
127127
);

‎apps/desktop/src/snapShot/HyprlandSnapShot.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ function hyprlandCaptureBinding(appId: string, lua: boolean): string {
2929
/** Omarchy owns its defaults; instructions always point at a user-owned config. */
3030
export async function hyprlandCaptureShortcut(
3131
appId: string,
32-
configHome = process.env.XDG_CONFIG_HOME || NodePath.join(NodeOS.homedir(), ".config"),
32+
home: string,
33+
configHome = process.env.XDG_CONFIG_HOME || NodePath.join(home, ".config"),
3334
) {
3435
const directory = NodePath.join(configHome, "hypr");
3536
const exists = async (name: string) =>

‎apps/server/src/assets/AssetAccess.test.ts‎

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import * as NodeServices from "@effect/platform-node/NodeServices";
33
import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform";
44
import * as NodeFSP from "node:fs/promises";
5-
import * as NodeOS from "node:os";
65
import {
76
AssetAccessError,
87
AssetPreviewTypeValidationError,
@@ -37,17 +36,13 @@ import { openMediaFile } from "./MediaFile.ts";
3736
import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
3837
import * as GitHubCredentials from "@t3tools/source-control-github/server/GitHubCredentials";
3938
import { githubMediaResponse } from "./GitHubMediaFetch.ts";
39+
import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
4040

4141
vi.mock("node:fs/promises", async (importOriginal) => {
4242
const actual = await importOriginal<typeof NodeFSP>();
4343
return { ...actual, open: vi.fn(actual.open), realpath: vi.fn(actual.realpath) };
4444
});
4545

46-
vi.mock("node:os", async (importOriginal) => {
47-
const actual = await importOriginal<typeof NodeOS>();
48-
return { ...actual, homedir: vi.fn(actual.homedir) };
49-
});
50-
5146
const layerConfig = ServerConfig.ServerConfig.layerTest(process.cwd(), {
5247
prefix: "t3-asset-access-test-",
5348
});
@@ -299,8 +294,7 @@ describe("AssetAccess", () => {
299294
yield* fs.makeDirectory(path.dirname(filePath), { recursive: true });
300295
yield* fs.writeFileString(filePath, "recording bytes");
301296
const canonicalFile = yield* fs.realPath(filePath);
302-
const homeSpy = vi.mocked(NodeOS.homedir).mockReturnValue(home);
303-
try {
297+
yield* Effect.gen(function* () {
304298
for (const workspaceRoot of [
305299
path.join(home, "project"),
306300
path.join(directory, "srv", "project"),
@@ -332,9 +326,7 @@ describe("AssetAccess", () => {
332326
expect(yield* Effect.promise(() => response.text())).toBe("recording bytes");
333327
}
334328
}
335-
} finally {
336-
homeSpy.mockRestore();
337-
}
329+
}).pipe(Effect.provideService(HostProcessHomeDirectory, home));
338330
}).pipe(Effect.provide(layerTest)),
339331
);
340332

‎apps/server/src/assets/AssetAccess.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts";
5858
import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";
5959
import * as NativeAppIconResolver from "./NativeAppIconResolver.ts";
6060
import { openMediaFile, readMediaFileHeader, type OpenMediaFile } from "./MediaFile.ts";
61+
import { HostProcessHomeDirectory } from "@t3tools/shared/hostProcess";
6162

6263
export const ASSET_ROUTE_PREFIX = "/api/assets";
6364

@@ -469,7 +470,7 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i
469470

470471
switch (input.resource._tag) {
471472
case "media-file": {
472-
let requestedPath = expandHomePath(input.resource.path);
473+
let requestedPath = expandHomePath(input.resource.path, yield* HostProcessHomeDirectory);
473474
if (!path.isAbsolute(requestedPath)) {
474475
if (!input.workspaceRoot) {
475476
return yield* new AssetWorkspaceContextNotFoundError({ resource: input.resource });

0 commit comments

Comments
 (0)