/** * ServerConfig - Runtime configuration services. * * Defines process-level server configuration and networking helpers used by * startup and runtime layers. * * @module ServerConfig */ import * as Context from "effect/Context"; import * as Clock from "effect/Clock"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as LogLevel from "effect/LogLevel"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import { sweepStalePendingAttachments } from "./attachmentStore.ts"; export const DEFAULT_PORT = 3773; export const RuntimeMode = Schema.Literals(["web", "desktop"]); export type RuntimeMode = typeof RuntimeMode.Type; export const StartupPresentation = Schema.Literals(["browser", "headless"]); export type StartupPresentation = typeof StartupPresentation.Type; /** * ServerDerivedPaths - Derived paths from the base directory. */ export interface ServerDerivedPaths { readonly stateDir: string; readonly dbPath: string; readonly keybindingsConfigPath: string; readonly settingsPath: string; /** Palettes this machine publishes for clients to follow, one file per theme. */ readonly environmentThemesDir: string; readonly providerStatusCacheDir: string; readonly worktreesDir: string; readonly attachmentsDir: string; /** Screenshots the agent asks the collaborative browser to keep for the user. */ readonly browserArtifactsDir: string; readonly logsDir: string; readonly serverLogPath: string; readonly serverTracePath: string; readonly providerLogsDir: string; readonly providerEventLogPath: string; readonly terminalLogsDir: string; readonly anonymousIdPath: string; readonly environmentIdPath: string; readonly serverRuntimeStatePath: string; readonly secretsDir: string; } export interface DeriveServerPathsOptions { readonly baseDirIsExplicit?: boolean; } /** * ServerConfig - Service tag for server runtime configuration. */ export class ServerConfig extends Context.Service< ServerConfig, ServerDerivedPaths & { readonly logLevel: LogLevel.LogLevel; readonly traceMinLevel: LogLevel.LogLevel; readonly traceTimingEnabled: boolean; readonly traceBatchWindowMs: number; readonly traceMaxBytes: number; readonly traceMaxFiles: number; readonly otlpTracesUrl: string | undefined; readonly otlpMetricsUrl: string | undefined; readonly otlpExportIntervalMs: number; readonly otlpServiceName: string; readonly mode: RuntimeMode; readonly port: number; readonly host: string | undefined; readonly cwd: string; readonly baseDir: string; readonly staticDir: string | undefined; readonly devUrl: URL | undefined; readonly devAllowedOrigins: ReadonlyArray; readonly noBrowser: boolean; readonly startupPresentation: StartupPresentation; readonly desktopBootstrapToken: string | undefined; readonly desktopTelemetryFd?: number | undefined; readonly desktopTelemetryControlFd?: number | undefined; readonly resourceMonitorPath?: string | undefined; readonly autoBootstrapProjectFromCwd: boolean; readonly logWebSocketEvents: boolean; readonly tailscaleServeEnabled: boolean; readonly tailscaleServePort: number; } >()("t3/config/ServerConfig") { /** @deprecated Import and use `layerTest` from this module. */ static readonly layerTest = ( cwd: string, baseDirOrPrefix: string | { readonly prefix: string }, ) => layerTest(cwd, baseDirOrPrefix); } export const make = (config: ServerConfig["Service"]) => ServerConfig.of(config); export const layer = (config: ServerConfig["Service"]) => Layer.succeed(ServerConfig, make(config)); export const deriveServerPaths = Effect.fn(function* ( baseDir: ServerConfig["Service"]["baseDir"], devUrl: ServerConfig["Service"]["devUrl"], options: DeriveServerPathsOptions = {}, ): Effect.fn.Return { const { join } = yield* Path.Path; const stateDir = join( baseDir, devUrl !== undefined && !options.baseDirIsExplicit ? "dev" : "userdata", ); const dbPath = join(stateDir, "state.sqlite"); const attachmentsDir = join(stateDir, "attachments"); const logsDir = join(stateDir, "logs"); const providerLogsDir = join(logsDir, "provider"); const providerStatusCacheDir = join(baseDir, "caches"); return { stateDir, dbPath, keybindingsConfigPath: join(stateDir, "keybindings.json"), settingsPath: join(stateDir, "settings.json"), environmentThemesDir: join(stateDir, "themes"), providerStatusCacheDir, worktreesDir: join(baseDir, "worktrees"), attachmentsDir, browserArtifactsDir: join(stateDir, "browser-artifacts"), logsDir, serverLogPath: join(logsDir, "server.log"), serverTracePath: join(logsDir, "server.trace.ndjson"), providerLogsDir, providerEventLogPath: join(providerLogsDir, "events.log"), terminalLogsDir: join(logsDir, "terminals"), anonymousIdPath: join(stateDir, "anonymous-id"), environmentIdPath: join(stateDir, "environment-id"), serverRuntimeStatePath: join(stateDir, "server-runtime.json"), secretsDir: join(stateDir, "secrets"), }; }); export const ensureServerDirectories = Effect.fn(function* (derivedPaths: ServerDerivedPaths) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; yield* Effect.all( [ fs.makeDirectory(derivedPaths.stateDir, { recursive: true }), fs.makeDirectory(derivedPaths.logsDir, { recursive: true }), fs.makeDirectory(derivedPaths.providerLogsDir, { recursive: true }), fs.makeDirectory(derivedPaths.terminalLogsDir, { recursive: true }), fs.makeDirectory(derivedPaths.attachmentsDir, { recursive: true }), fs.makeDirectory(derivedPaths.worktreesDir, { recursive: true }), fs.makeDirectory(path.dirname(derivedPaths.keybindingsConfigPath), { recursive: true }), fs.makeDirectory(path.dirname(derivedPaths.settingsPath), { recursive: true }), fs.makeDirectory(derivedPaths.providerStatusCacheDir, { recursive: true }), fs.makeDirectory(path.dirname(derivedPaths.anonymousIdPath), { recursive: true }), fs.makeDirectory(path.dirname(derivedPaths.serverRuntimeStatePath), { recursive: true }), ], { concurrency: "unbounded" }, ); const swept = sweepStalePendingAttachments({ attachmentsDir: derivedPaths.attachmentsDir, nowMs: yield* Clock.currentTimeMillis, }); if (swept.deleted > 0) { yield* Effect.logInfo("Removed expired attachment uploads.", { deleted: swept.deleted }); } }); const makeTest = Effect.fn("ServerConfig.makeTest")(function* ( cwd: string, baseDirOrPrefix: string | { readonly prefix: string }, ) { const devUrl = undefined; const fs = yield* FileSystem.FileSystem; const baseDir = typeof baseDirOrPrefix === "string" ? baseDirOrPrefix : yield* fs.makeTempDirectoryScoped({ prefix: baseDirOrPrefix.prefix }); const derivedPaths = yield* deriveServerPaths(baseDir, devUrl); yield* ensureServerDirectories(derivedPaths); return ServerConfig.of({ logLevel: "Error", traceMinLevel: "Info", traceTimingEnabled: true, traceBatchWindowMs: 200, traceMaxBytes: 10 * 1024 * 1024, traceMaxFiles: 10, otlpTracesUrl: undefined, otlpMetricsUrl: undefined, otlpExportIntervalMs: 10_000, otlpServiceName: "t3-server", cwd, baseDir, ...derivedPaths, mode: "web", autoBootstrapProjectFromCwd: false, logWebSocketEvents: false, tailscaleServeEnabled: false, tailscaleServePort: 443, port: 0, host: undefined, desktopBootstrapToken: undefined, desktopTelemetryFd: undefined, desktopTelemetryControlFd: undefined, resourceMonitorPath: undefined, staticDir: undefined, devUrl, devAllowedOrigins: [], noBrowser: false, startupPresentation: "browser", }); }); export const layerTest = (cwd: string, baseDirOrPrefix: string | { readonly prefix: string }) => Layer.effect(ServerConfig, makeTest(cwd, baseDirOrPrefix)); export const resolveStaticDir = Effect.fn(function* () { const { join, resolve } = yield* Path.Path; const { exists } = yield* FileSystem.FileSystem; const bundledClient = resolve(join(import.meta.dirname, "client")); const bundledStat = yield* exists(join(bundledClient, "index.html")).pipe( Effect.orElseSucceed(() => false), ); if (bundledStat) { return bundledClient; } const monorepoClient = resolve(join(import.meta.dirname, "../../web/dist")); const monorepoStat = yield* exists(join(monorepoClient, "index.html")).pipe( Effect.orElseSucceed(() => false), ); if (monorepoStat) { return monorepoClient; } return undefined; });