1import * as HostProcess from "@t3tools/shared/HostProcess";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Option from "effect/Option";
5import * as Schema from "effect/Schema";
6
7import * as ProcessRunner from "../processRunner.ts";
8
9interface ResolveServerEnvironmentLabelInput {
10 readonly cwdBaseName: string;
11}
12
13const ServerEnvironmentLabelCommandProbe = Schema.Literals([
14 "macos-computer-name",
15 "linux-pretty-hostname",
16]);
17type ServerEnvironmentLabelCommandProbe = typeof ServerEnvironmentLabelCommandProbe.Type;
18
19export class ServerEnvironmentLabelFileError extends Schema.TaggedError<ServerEnvironmentLabelFileError>()(
20 "ServerEnvironmentLabelFileError",
21 {
22 operation: Schema.Literals(["inspect", "read"]),
23 path: Schema.String,
24 cause: Schema.Defect(),
25 },
26) {
27 override get message(): string {
28 return `Failed to ${this.operation} environment-label file at ${this.path}.`;
29 }
30}
31
32export class ServerEnvironmentLabelCommandError extends Schema.TaggedError<ServerEnvironmentLabelCommandError>()(
33 "ServerEnvironmentLabelCommandError",
34 {
35 probe: ServerEnvironmentLabelCommandProbe,
36 executable: Schema.String,
37 argumentCount: Schema.Number,
38 cause: Schema.Defect(),
39 },
40) {
41 override get message(): string {
42 return `Failed to run environment-label probe '${this.probe}' with ${this.executable}.`;
43 }
44}
45
46function normalizeLabel(value: string | null | undefined): string | null {
47 const trimmed = value?.trim();
48 return trimmed && trimmed.length > 0 ? trimmed : null;
49}
50
51function parseMachineInfoValue(raw: string, key: string): string | null {
52 for (const line of raw.split(/\r?\n/g)) {
53 const trimmed = line.trim();
54 if (trimmed.length === 0 || trimmed.startsWith("#") || !trimmed.startsWith(`${key}=`)) {
55 continue;
56 }
57 const value = trimmed.slice(key.length + 1).trim();
58 if (
59 (value.startsWith('"') && value.endsWith('"')) ||
60 (value.startsWith("'") && value.endsWith("'"))
61 ) {
62 return normalizeLabel(value.slice(1, -1));
63 }
64 return normalizeLabel(value);
65 }
66 return null;
67}
68
69const readLinuxMachineInfo = Effect.fn("readLinuxMachineInfo")(function* () {
70 const fileSystem = yield* FileSystem.FileSystem;
71 const machineInfoPath = "/etc/machine-info";
72 return yield* fileSystem.exists(machineInfoPath).pipe(
73 Effect.mapError(
74 (cause) =>
75 new ServerEnvironmentLabelFileError({
76 operation: "inspect",
77 path: machineInfoPath,
78 cause,
79 }),
80 ),
81 Effect.flatMap((exists) =>
82 exists
83 ? fileSystem.readFileString(machineInfoPath).pipe(
84 Effect.mapError(
85 (cause) =>
86 new ServerEnvironmentLabelFileError({
87 operation: "read",
88 path: machineInfoPath,
89 cause,
90 }),
91 ),
92 )
93 : Effect.succeed(null),
94 ),
95 Effect.catchTags({
96 ServerEnvironmentLabelFileError: (error) =>
97 Effect.logDebug(error.message).pipe(
98 Effect.annotateLogs({
99 operation: error.operation,
100 path: error.path,
101 cause: error,
102 }),
103 Effect.as(null),
104 ),
105 }),
106 );
107});
108
109const runFriendlyLabelCommand = Effect.fn("runFriendlyLabelCommand")(function* (input: {
110 readonly probe: ServerEnvironmentLabelCommandProbe;
111 readonly command: string;
112 readonly args: readonly string[];
113}) {
114 const processRunner = yield* ProcessRunner.ProcessRunner;
115 const result = yield* processRunner
116 .run({
117 command: input.command,
118 args: input.args,
119 timeoutBehavior: "timedOutResult",
120 })
121 .pipe(
122 Effect.mapError(
123 (cause) =>
124 new ServerEnvironmentLabelCommandError({
125 probe: input.probe,
126 executable: input.command,
127 argumentCount: input.args.length,
128 cause,
129 }),
130 ),
131 Effect.asSome,
132 Effect.catchTags({
133 ServerEnvironmentLabelCommandError: (error) =>
134 Effect.logDebug(error.message).pipe(
135 Effect.annotateLogs({
136 probe: error.probe,
137 executable: error.executable,
138 argumentCount: error.argumentCount,
139 cause: error,
140 }),
141 Effect.as(Option.none()),
142 ),
143 }),
144 );
145
146 if (Option.isNone(result) || result.value.code !== 0) {
147 return null;
148 }
149
150 return normalizeLabel(result.value.stdout);
151});
152
153const resolveFriendlyHostLabel = Effect.fn("resolveFriendlyHostLabel")(function* () {
154 const platform = yield* HostProcess.Platform;
155 if (platform === "darwin") {
156 return yield* runFriendlyLabelCommand({
157 probe: "macos-computer-name",
158 command: "scutil",
159 args: ["--get", "ComputerName"],
160 });
161 }
162
163 if (platform === "linux") {
164 const machineInfo = normalizeLabel(yield* readLinuxMachineInfo());
165 if (machineInfo) {
166 const prettyHostname = parseMachineInfoValue(machineInfo, "PRETTY_HOSTNAME");
167 if (prettyHostname) {
168 return prettyHostname;
169 }
170 }
171
172 return yield* runFriendlyLabelCommand({
173 probe: "linux-pretty-hostname",
174 command: "hostnamectl",
175 args: ["--pretty"],
176 });
177 }
178
179 return null;
180});
181
182export const resolveServerEnvironmentLabel = Effect.fn("resolveServerEnvironmentLabel")(function* (
183 input: ResolveServerEnvironmentLabelInput,
184) {
185 const friendlyHostLabel = yield* resolveFriendlyHostLabel();
186 if (friendlyHostLabel) {
187 return friendlyHostLabel;
188 }
189
190 const hostname = normalizeLabel(yield* HostProcess.Hostname);
191 if (hostname) {
192 return hostname;
193 }
194
195 return normalizeLabel(input.cwdBaseName) ?? "T3 environment";
196});