1/**
2 * ServerConfig - Runtime configuration services.
3 *
4 * Defines process-level server configuration and networking helpers used by
5 * startup and runtime layers.
6 *
7 * @module ServerConfig
8 */
9import * as Context from "effect/Context";
10import * as Clock from "effect/Clock";
11import * as Effect from "effect/Effect";
12import * as FileSystem from "effect/FileSystem";
13import * as Layer from "effect/Layer";
14import * as LogLevel from "effect/LogLevel";
15import * as Path from "effect/Path";
16import type * as Redacted from "effect/Redacted";
17import * as Schema from "effect/Schema";
18
19import { sweepStalePendingAttachments } from "./attachmentStore.ts";
20import { DEFAULT_SIGNAL_EXPORT, type SignalExport } from "@t3tools/shared/observability";
21import * as OtelEnvironment from "@t3tools/shared/otelEnvironment";
22
23export const DEFAULT_PORT = 3773;
24
25export const RuntimeMode = Schema.Literals(["web", "desktop"]);
26export type RuntimeMode = typeof RuntimeMode.Type;
27
28export const StartupPresentation = Schema.Literals(["browser", "headless"]);
29export type StartupPresentation = typeof StartupPresentation.Type;
30
31/**
32 * ServerDerivedPaths - Derived paths from the base directory.
33 */
34export interface ServerDerivedPaths {
35 readonly stateDir: string;
36 readonly dbPath: string;
37 readonly keybindingsConfigPath: string;
38 readonly settingsPath: string;
39 /** Palettes this machine publishes for clients to follow, one file per theme. */
40 readonly environmentThemesDir: string;
41 readonly providerStatusCacheDir: string;
42 readonly worktreesDir: string;
43 readonly attachmentsDir: string;
44 /** Screenshots the agent asks the collaborative browser to keep for the user. */
45 readonly browserArtifactsDir: string;
46 readonly logsDir: string;
47 readonly serverTracePath: string;
48 readonly providerLogsDir: string;
49 readonly providerEventLogPath: string;
50 readonly terminalLogsDir: string;
51 readonly environmentIdPath: string;
52 readonly serverRuntimeStatePath: string;
53 readonly secretsDir: string;
54}
55
56export interface DeriveServerPathsOptions {
57 readonly baseDirIsExplicit?: boolean;
58}
59
60/**
61 * ServerConfig - Service tag for server runtime configuration.
62 */
63export class ServerConfig extends Context.Service<
64 ServerConfig,
65 ServerDerivedPaths & {
66 readonly logLevel: LogLevel.LogLevel;
67 readonly traceMinLevel: LogLevel.LogLevel;
68 readonly traceTimingEnabled: boolean;
69 readonly traceBatchWindowMs: number;
70 readonly traceMaxBytes: number;
71 readonly traceMaxFiles: number;
72 readonly otlpTracesUrl: string | undefined;
73 readonly otlpMetricsUrl: string | undefined;
74 readonly otlpLogsUrl: string | undefined;
75 /**
76 * How each signal is exported. Read instead of a process-wide setting so
77 * the wire format, credential, and schedule travel with the endpoint they
78 * were configured beside.
79 */
80 readonly otlpTracesExport: SignalExport;
81 readonly otlpMetricsExport: SignalExport;
82 readonly otlpLogsExport: SignalExport;
83 readonly otelEnvironment: OtelEnvironment.OtelEnvironment;
84 readonly mode: RuntimeMode;
85 readonly port: number;
86 readonly host: string | undefined;
87 readonly cwd: string;
88 readonly baseDir: string;
89 readonly staticDir: string | undefined;
90 readonly devUrl: URL | undefined;
91 readonly devAuthToken?: Redacted.Redacted<string> | undefined;
92 readonly devAllowedOrigins: ReadonlyArray<string>;
93 readonly noBrowser: boolean;
94 readonly startupPresentation: StartupPresentation;
95 readonly desktopBootstrapToken: string | undefined;
96 readonly desktopBootstrapSecret?: string | undefined;
97 readonly shellEnvironmentPrepared?: boolean | undefined;
98 readonly desktopTelemetryFd?: number | undefined;
99 readonly desktopTelemetryControlFd?: number | undefined;
100 readonly desktopTerminalFd?: number | undefined;
101 readonly desktopTerminalControlFd?: number | undefined;
102 readonly desktopBrowserFd?: number | undefined;
103 readonly desktopBrowserControlFd?: number | undefined;
104 readonly resourceMonitorPath?: string | undefined;
105 readonly autoBootstrapProjectFromCwd: boolean;
106 readonly logWebSocketEvents: boolean;
107 readonly tailscaleServeEnabled: boolean;
108 readonly tailscaleServePort: number;
109 }
110>()("t3/config/ServerConfig") {
111 /** @deprecated Import and use `layerTest` from this module. */
112 static readonly layerTest = (
113 cwd: string,
114 baseDirOrPrefix: string | { readonly prefix: string },
115 ) => layerTest(cwd, baseDirOrPrefix);
116}
117
118export const make = (config: ServerConfig["Service"]) => ServerConfig.of(config);
119
120/**
121 * Resource attributes shared by every OTLP exporter, so traces, metrics, and
122 * logs report the same service identity to the collector.
123 */
124export const otlpResource = (config: ServerConfig["Service"]) => ({
125 serviceName: "t3code-server",
126 attributes: {
127 "service.namespace": "t3code",
128 "service.runtime": "t3-server",
129 "service.mode": config.mode,
130 },
131});
132
133export const layer = (config: ServerConfig["Service"]) => Layer.succeed(ServerConfig, make(config));
134
135export const deriveServerPaths = Effect.fn(function* (
136 baseDir: ServerConfig["Service"]["baseDir"],
137 devUrl: ServerConfig["Service"]["devUrl"],
138 options: DeriveServerPathsOptions = {},
139): Effect.fn.Return<ServerDerivedPaths, never, Path.Path> {
140 const { join } = yield* Path.Path;
141 const stateDir = join(
142 baseDir,
143 devUrl !== undefined && !options.baseDirIsExplicit ? "dev" : "userdata",
144 );
145 const dbPath = join(stateDir, "statev2.sqlite");
146 const attachmentsDir = join(stateDir, "attachments");
147 const logsDir = join(stateDir, "logs");
148 const providerLogsDir = join(logsDir, "provider");
149 const providerStatusCacheDir = join(baseDir, "caches");
150 return {
151 stateDir,
152 dbPath,
153 keybindingsConfigPath: join(stateDir, "keybindings.json"),
154 settingsPath: join(stateDir, "settings.json"),
155 environmentThemesDir: join(stateDir, "themes"),
156 providerStatusCacheDir,
157 worktreesDir: join(baseDir, "worktrees"),
158 attachmentsDir,
159 browserArtifactsDir: join(stateDir, "browser-artifacts"),
160 logsDir,
161 serverTracePath: join(logsDir, "server.trace.ndjson"),
162 providerLogsDir,
163 providerEventLogPath: join(providerLogsDir, "events.log"),
164 terminalLogsDir: join(logsDir, "terminals"),
165 environmentIdPath: join(stateDir, "environment-id"),
166 serverRuntimeStatePath: join(stateDir, "server-runtime.json"),
167 secretsDir: join(stateDir, "secrets"),
168 };
169});
170
171export const ensureServerDirectories = Effect.fn(function* (derivedPaths: ServerDerivedPaths) {
172 const fs = yield* FileSystem.FileSystem;
173 const path = yield* Path.Path;
174
175 yield* Effect.all(
176 [
177 fs.makeDirectory(derivedPaths.stateDir, { recursive: true }),
178 fs.makeDirectory(derivedPaths.logsDir, { recursive: true }),
179 fs.makeDirectory(derivedPaths.providerLogsDir, { recursive: true }),
180 fs.makeDirectory(derivedPaths.terminalLogsDir, { recursive: true }),
181 fs.makeDirectory(derivedPaths.attachmentsDir, { recursive: true }),
182 fs.makeDirectory(derivedPaths.worktreesDir, { recursive: true }),
183 fs.makeDirectory(path.dirname(derivedPaths.keybindingsConfigPath), { recursive: true }),
184 fs.makeDirectory(path.dirname(derivedPaths.settingsPath), { recursive: true }),
185 fs.makeDirectory(derivedPaths.providerStatusCacheDir, { recursive: true }),
186 fs.makeDirectory(path.dirname(derivedPaths.serverRuntimeStatePath), { recursive: true }),
187 ],
188 { concurrency: "unbounded" },
189 );
190
191 const swept = sweepStalePendingAttachments({
192 attachmentsDir: derivedPaths.attachmentsDir,
193 nowMs: yield* Clock.currentTimeMillis,
194 });
195 if (swept.deleted > 0) {
196 yield* Effect.logInfo("Removed expired attachment uploads.", { deleted: swept.deleted });
197 }
198});
199
200const makeTest = Effect.fn("ServerConfig.makeTest")(function* (
201 cwd: string,
202 baseDirOrPrefix: string | { readonly prefix: string },
203) {
204 const devUrl = undefined;
205 const fs = yield* FileSystem.FileSystem;
206 const baseDir =
207 typeof baseDirOrPrefix === "string"
208 ? baseDirOrPrefix
209 : yield* fs.makeTempDirectoryScoped({ prefix: baseDirOrPrefix.prefix });
210 const derivedPaths = yield* deriveServerPaths(baseDir, devUrl);
211 yield* ensureServerDirectories(derivedPaths);
212
213 return ServerConfig.of({
214 logLevel: "Error",
215 traceMinLevel: "Info",
216 traceTimingEnabled: true,
217 traceBatchWindowMs: 200,
218 traceMaxBytes: 10 * 1024 * 1024,
219 traceMaxFiles: 10,
220 otlpTracesUrl: undefined,
221 otlpMetricsUrl: undefined,
222 otlpLogsUrl: undefined,
223 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
224 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
225 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
226 otelEnvironment: OtelEnvironment.none,
227 cwd,
228 baseDir,
229 ...derivedPaths,
230 mode: "web",
231 autoBootstrapProjectFromCwd: false,
232 logWebSocketEvents: false,
233 tailscaleServeEnabled: false,
234 tailscaleServePort: 443,
235 port: 0,
236 host: undefined,
237 desktopBootstrapToken: undefined,
238 desktopTelemetryFd: undefined,
239 desktopTelemetryControlFd: undefined,
240 desktopBrowserFd: undefined,
241 desktopBrowserControlFd: undefined,
242 resourceMonitorPath: undefined,
243 staticDir: undefined,
244 devUrl,
245 devAllowedOrigins: [],
246 noBrowser: false,
247 startupPresentation: "browser",
248 });
249});
250
251export const layerTest = (cwd: string, baseDirOrPrefix: string | { readonly prefix: string }) =>
252 Layer.effect(ServerConfig, makeTest(cwd, baseDirOrPrefix));
253
254export const resolveStaticDir = Effect.fn(function* () {
255 const { join, resolve } = yield* Path.Path;
256 const { exists } = yield* FileSystem.FileSystem;
257 const bundledClient = resolve(join(import.meta.dirname, "client"));
258 const bundledStat = yield* exists(join(bundledClient, "index.html")).pipe(
259 Effect.orElseSucceed(() => false),
260 );
261 if (bundledStat) {
262 return bundledClient;
263 }
264
265 const monorepoClient = resolve(join(import.meta.dirname, "../../web/dist"));
266 const monorepoStat = yield* exists(join(monorepoClient, "index.html")).pipe(
267 Effect.orElseSucceed(() => false),
268 );
269 if (monorepoStat) {
270 return monorepoClient;
271 }
272 return undefined;
273});