| 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 | */ |
| 9 | import * as Context from "effect/Context"; |
| 10 | import * as Clock from "effect/Clock"; |
| 11 | import * as Effect from "effect/Effect"; |
| 12 | import * as FileSystem from "effect/FileSystem"; |
| 13 | import * as Layer from "effect/Layer"; |
| 14 | import * as LogLevel from "effect/LogLevel"; |
| 15 | import * as Path from "effect/Path"; |
| 16 | import type * as Redacted from "effect/Redacted"; |
| 17 | import * as Schema from "effect/Schema"; |
| 18 | |
| 19 | import { sweepStalePendingAttachments } from "./attachmentStore.ts"; |
| 20 | import { DEFAULT_SIGNAL_EXPORT, type SignalExport } from "@t3tools/shared/observability"; |
| 21 | import * as OtelEnvironment from "@t3tools/shared/otelEnvironment"; |
| 22 | |
| 23 | export const DEFAULT_PORT = 3773; |
| 24 | |
| 25 | export const RuntimeMode = Schema.Literals(["web", "desktop"]); |
| 26 | export type RuntimeMode = typeof RuntimeMode.Type; |
| 27 | |
| 28 | export const StartupPresentation = Schema.Literals(["browser", "headless"]); |
| 29 | export type StartupPresentation = typeof StartupPresentation.Type; |
| 30 | |
| 31 | /** |
| 32 | * ServerDerivedPaths - Derived paths from the base directory. |
| 33 | */ |
| 34 | export 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 | |
| 56 | export interface DeriveServerPathsOptions { |
| 57 | readonly baseDirIsExplicit?: boolean; |
| 58 | } |
| 59 | |
| 60 | /** |
| 61 | * ServerConfig - Service tag for server runtime configuration. |
| 62 | */ |
| 63 | export 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 | |
| 118 | export 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 | */ |
| 124 | export 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 | |
| 133 | export const layer = (config: ServerConfig["Service"]) => Layer.succeed(ServerConfig, make(config)); |
| 134 | |
| 135 | export 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 | |
| 171 | export 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 | |
| 200 | const 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 | |
| 251 | export const layerTest = (cwd: string, baseDirOrPrefix: string | { readonly prefix: string }) => |
| 252 | Layer.effect(ServerConfig, makeTest(cwd, baseDirOrPrefix)); |
| 253 | |
| 254 | export 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 | }); |