1import * as HostProcess from "@t3tools/shared/HostProcess";
2import {
3 listLoginShellCandidates,
4 mergePathEntries,
5 readEnvironmentFromLoginShell,
6 readPathFromLaunchctl,
7 resolveWindowsEnvironment,
8} from "@t3tools/shared/shell";
9import * as Effect from "effect/Effect";
10import * as FileSystem from "effect/FileSystem";
11import * as Path from "effect/Path";
12import * as NodeOS from "node:os";
13
14function logPathHydrationWarning(message: string, error?: unknown): void {
15 process.stderr.write(
16 `[server] ${message} ${error instanceof Error ? error.message : (error ?? "")}\n`,
17 );
18}
19
20function hydratePosixPath(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): void {
21 let shellEnvironment: Partial<Record<string, string>> = {};
22 for (const shell of listLoginShellCandidates(platform, env.SHELL)) {
23 try {
24 // mise's record of the PATH entries it added. It must travel with the
25 // shell's PATH, or `mise x` can rebuild PATH behind a same-named wrapper
26 // (`exec mise x claude -- claude`) that then re-runs itself forever.
27 shellEnvironment = readEnvironmentFromLoginShell(shell, ["PATH", "__MISE_DIFF"]);
28 } catch (error) {
29 logPathHydrationWarning(`Failed to read PATH from login shell ${shell}.`, error);
30 }
31
32 if (shellEnvironment.PATH) break;
33 }
34
35 const shellPath = shellEnvironment.PATH;
36 const launchctlPath = platform === "darwin" && !shellPath ? readPathFromLaunchctl() : undefined;
37 const mergedPath = mergePathEntries(shellPath ?? launchctlPath, env.PATH, platform);
38 if (mergedPath) {
39 env.PATH = mergedPath;
40 }
41 if (shellPath && shellEnvironment.__MISE_DIFF) {
42 env.__MISE_DIFF = shellEnvironment.__MISE_DIFF;
43 }
44}
45
46export function hydratePosixHome(
47 env: NodeJS.ProcessEnv,
48 resolveHomeDir = () => NodeOS.userInfo().homedir,
49): void {
50 if ((env.HOME?.trim() ?? "").length > 0) return;
51
52 const homeDir = resolveHomeDir();
53 if (homeDir.length > 0) {
54 env.HOME = homeDir;
55 }
56}
57
58export const fixPath = Effect.fn("fixPath")(function* (options?: {
59 readonly shellEnvironmentPrepared?: boolean | undefined;
60}): Effect.fn.Return<void, never, FileSystem.FileSystem | Path.Path> {
61 const platform = yield* HostProcess.Platform;
62 const env = yield* HostProcess.Environment;
63
64 if (platform === "win32") {
65 const repairedEnvironment = yield* resolveWindowsEnvironment(env).pipe(
66 Effect.catchDefect((defect) =>
67 Effect.sync(() => {
68 logPathHydrationWarning("Failed to hydrate PATH from the user environment.", defect);
69 return {} as Partial<NodeJS.ProcessEnv>;
70 }),
71 ),
72 );
73 for (const [key, value] of Object.entries(repairedEnvironment)) {
74 if (value !== undefined) {
75 env[key] = value;
76 }
77 }
78 return;
79 }
80
81 if (platform !== "darwin" && platform !== "linux") return;
82
83 yield* Effect.sync(() => hydratePosixHome(env)).pipe(
84 Effect.catchDefect((defect) =>
85 Effect.sync(() => {
86 logPathHydrationWarning("Failed to hydrate HOME from the user account.", defect);
87 }),
88 ),
89 );
90 if (options?.shellEnvironmentPrepared === true) return;
91 yield* Effect.sync(() => hydratePosixPath(env, platform)).pipe(
92 Effect.catchDefect((defect) =>
93 Effect.sync(() => {
94 logPathHydrationWarning("Failed to hydrate PATH from the user environment.", defect);
95 }),
96 ),
97 );
98});
99
100export const expandHomePath = Effect.fn(function* (input: string) {
101 const { join } = yield* Path.Path;
102 const home = yield* HostProcess.HomeDirectory;
103 if (input === "~") {
104 return home;
105 }
106 if (input.startsWith("~/") || input.startsWith("~\\")) {
107 return join(home, input.slice(2));
108 }
109 return input;
110});
111
112export const resolveBaseDir = Effect.fn(function* (raw: string | undefined) {
113 const { join, resolve } = yield* Path.Path;
114 if (!raw || raw.trim().length === 0) {
115 return join(yield* HostProcess.HomeDirectory, ".t3");
116 }
117 return resolve(yield* expandHomePath(raw.trim()));
118});