| 1 | import * as NetService from "@t3tools/shared/Net"; |
| 2 | import { |
| 3 | OtlpHeadersFromString, |
| 4 | OtlpProtocol, |
| 5 | type SignalExport, |
| 6 | } from "@t3tools/shared/observability"; |
| 7 | import * as OtelEnvironment from "@t3tools/shared/otelEnvironment"; |
| 8 | import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings"; |
| 9 | import { DesktopBackendBootstrap, PortSchema } from "@t3tools/contracts"; |
| 10 | import * as Config from "effect/Config"; |
| 11 | import * as Duration from "effect/Duration"; |
| 12 | import * as Effect from "effect/Effect"; |
| 13 | import * as FileSystem from "effect/FileSystem"; |
| 14 | import * as LogLevel from "effect/LogLevel"; |
| 15 | import * as Option from "effect/Option"; |
| 16 | import * as Path from "effect/Path"; |
| 17 | import * as Redacted from "effect/Redacted"; |
| 18 | import * as Schema from "effect/Schema"; |
| 19 | import * as SchemaIssue from "effect/SchemaIssue"; |
| 20 | import * as SchemaTransformation from "effect/SchemaTransformation"; |
| 21 | import { Argument, Flag } from "effect/cli"; |
| 22 | import * as CliError from "effect/cli/CliError"; |
| 23 | |
| 24 | import { readBootstrapEnvelope } from "../bootstrap.ts"; |
| 25 | import * as ServerConfig from "../config.ts"; |
| 26 | import { expandHomePath, resolveBaseDir } from "../os-jank.ts"; |
| 27 | import { isProcessAlive, readPersistedServerRuntimeState } from "../serverRuntimeState.ts"; |
| 28 | |
| 29 | const modeFlag = Flag.Literals("mode", ServerConfig.RuntimeMode.literals).pipe( |
| 30 | Flag.withDescription("Runtime mode. `desktop` keeps loopback defaults unless overridden."), |
| 31 | Flag.optional, |
| 32 | ); |
| 33 | const portFlag = Flag.Int("port").pipe( |
| 34 | Flag.withSchema(PortSchema), |
| 35 | Flag.withDescription("Port for the HTTP/WebSocket server."), |
| 36 | Flag.optional, |
| 37 | ); |
| 38 | const hostFlag = Flag.String("host").pipe( |
| 39 | Flag.withDescription("Host/interface to bind (for example 127.0.0.1, 0.0.0.0, or a Tailnet IP)."), |
| 40 | Flag.optional, |
| 41 | ); |
| 42 | export const baseDirFlag = Flag.String("base-dir").pipe( |
| 43 | Flag.withDescription( |
| 44 | "Explicit T3 Code data directory; runtime state is stored under userdata (equivalent to T3CODE_HOME).", |
| 45 | ), |
| 46 | Flag.optional, |
| 47 | ); |
| 48 | const devUrlFlag = Flag.String("dev-url").pipe( |
| 49 | Flag.withSchema(Schema.URLFromString), |
| 50 | Flag.withDescription("Dev web URL to proxy/redirect to (equivalent to VITE_DEV_SERVER_URL)."), |
| 51 | Flag.optional, |
| 52 | ); |
| 53 | const noBrowserFlag = Flag.Boolean("no-browser").pipe( |
| 54 | Flag.withDescription("Disable automatic browser opening."), |
| 55 | Flag.optional, |
| 56 | ); |
| 57 | const bootstrapFdFlag = Flag.Int("bootstrap-fd").pipe( |
| 58 | Flag.withSchema(Schema.Int), |
| 59 | Flag.withDescription("Read one-time bootstrap secrets from the given file descriptor."), |
| 60 | Flag.optional, |
| 61 | ); |
| 62 | const autoBootstrapProjectFromCwdFlag = Flag.Boolean("auto-bootstrap-project-from-cwd").pipe( |
| 63 | Flag.withDescription( |
| 64 | "Create a project for the current working directory on startup when missing.", |
| 65 | ), |
| 66 | Flag.optional, |
| 67 | ); |
| 68 | const logWebSocketEventsFlag = Flag.Boolean("log-websocket-events").pipe( |
| 69 | Flag.withDescription( |
| 70 | "Emit server-side logs for outbound WebSocket push traffic (equivalent to T3CODE_LOG_WS_EVENTS).", |
| 71 | ), |
| 72 | Flag.withAlias("log-ws-events"), |
| 73 | Flag.optional, |
| 74 | ); |
| 75 | const tailscaleServeFlag = Flag.Boolean("tailscale-serve").pipe( |
| 76 | Flag.withDescription( |
| 77 | "Configure Tailscale Serve to expose this backend over HTTPS on the Tailnet.", |
| 78 | ), |
| 79 | Flag.optional, |
| 80 | ); |
| 81 | const tailscaleServePortFlag = Flag.Int("tailscale-serve-port").pipe( |
| 82 | Flag.withSchema(PortSchema), |
| 83 | Flag.withDescription("HTTPS port for Tailscale Serve when --tailscale-serve is enabled."), |
| 84 | Flag.optional, |
| 85 | ); |
| 86 | |
| 87 | // Trace file location, shared by the server and `t3 trace summary`. |
| 88 | export const traceFileConfig = Config.String("T3CODE_TRACE_FILE").pipe( |
| 89 | Config.option, |
| 90 | Config.map(Option.getOrUndefined), |
| 91 | ); |
| 92 | export const traceMaxFilesConfig = Config.Int("T3CODE_TRACE_MAX_FILES").pipe( |
| 93 | Config.withDefault(10), |
| 94 | ); |
| 95 | |
| 96 | const EnvServerConfig = Config.all({ |
| 97 | logLevel: Config.LogLevel("T3CODE_LOG_LEVEL").pipe(Config.withDefault("Info")), |
| 98 | traceMinLevel: Config.LogLevel("T3CODE_TRACE_MIN_LEVEL").pipe(Config.withDefault("Info")), |
| 99 | traceTimingEnabled: Config.Boolean("T3CODE_TRACE_TIMING_ENABLED").pipe(Config.withDefault(true)), |
| 100 | traceFile: traceFileConfig, |
| 101 | traceMaxBytes: Config.Int("T3CODE_TRACE_MAX_BYTES").pipe(Config.withDefault(10 * 1024 * 1024)), |
| 102 | traceMaxFiles: traceMaxFilesConfig, |
| 103 | traceBatchWindowMs: Config.Int("T3CODE_TRACE_BATCH_WINDOW_MS").pipe(Config.withDefault(1_000)), |
| 104 | otlpTracesUrl: Config.String("T3CODE_OTLP_TRACES_URL").pipe( |
| 105 | Config.option, |
| 106 | Config.map(Option.getOrUndefined), |
| 107 | ), |
| 108 | otlpMetricsUrl: Config.String("T3CODE_OTLP_METRICS_URL").pipe( |
| 109 | Config.option, |
| 110 | Config.map(Option.getOrUndefined), |
| 111 | ), |
| 112 | otlpLogsUrl: Config.String("T3CODE_OTLP_LOGS_URL").pipe( |
| 113 | Config.option, |
| 114 | Config.map(Option.getOrUndefined), |
| 115 | ), |
| 116 | otlpExportIntervalMs: Config.Int("T3CODE_OTLP_EXPORT_INTERVAL_MS").pipe( |
| 117 | Config.withDefault(10_000), |
| 118 | ), |
| 119 | otlpHeaders: Config.schema(OtlpHeadersFromString, "T3CODE_OTLP_HEADERS").pipe( |
| 120 | Config.option, |
| 121 | Config.map(Option.getOrUndefined), |
| 122 | ), |
| 123 | otlpProtocol: Config.schema(OtlpProtocol, "T3CODE_OTLP_PROTOCOL").pipe( |
| 124 | Config.withDefault("http/json"), |
| 125 | ), |
| 126 | mode: Config.schema(ServerConfig.RuntimeMode, "T3CODE_MODE").pipe( |
| 127 | Config.option, |
| 128 | Config.map(Option.getOrUndefined), |
| 129 | ), |
| 130 | port: Config.Port("T3CODE_PORT").pipe(Config.option, Config.map(Option.getOrUndefined)), |
| 131 | host: Config.String("T3CODE_HOST").pipe(Config.option, Config.map(Option.getOrUndefined)), |
| 132 | t3Home: Config.String("T3CODE_HOME").pipe(Config.option, Config.map(Option.getOrUndefined)), |
| 133 | devUrl: Config.URL("VITE_DEV_SERVER_URL").pipe(Config.option, Config.map(Option.getOrUndefined)), |
| 134 | devAllowedOrigins: Config.String("T3CODE_DEV_ALLOWED_ORIGINS").pipe( |
| 135 | Config.withDefault(""), |
| 136 | Config.map((value) => |
| 137 | value |
| 138 | .split(",") |
| 139 | .map((entry) => entry.trim()) |
| 140 | .filter((entry) => entry.length > 0), |
| 141 | ), |
| 142 | ), |
| 143 | noBrowser: Config.Boolean("T3CODE_NO_BROWSER").pipe( |
| 144 | Config.option, |
| 145 | Config.map(Option.getOrUndefined), |
| 146 | ), |
| 147 | bootstrapFd: Config.Int("T3CODE_BOOTSTRAP_FD").pipe( |
| 148 | Config.option, |
| 149 | Config.map(Option.getOrUndefined), |
| 150 | ), |
| 151 | autoBootstrapProjectFromCwd: Config.Boolean("T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD").pipe( |
| 152 | Config.option, |
| 153 | Config.map(Option.getOrUndefined), |
| 154 | ), |
| 155 | logWebSocketEvents: Config.Boolean("T3CODE_LOG_WS_EVENTS").pipe( |
| 156 | Config.option, |
| 157 | Config.map(Option.getOrUndefined), |
| 158 | ), |
| 159 | tailscaleServeEnabled: Config.Boolean("T3CODE_TAILSCALE_SERVE").pipe( |
| 160 | Config.option, |
| 161 | Config.map(Option.getOrUndefined), |
| 162 | ), |
| 163 | tailscaleServePort: Config.Port("T3CODE_TAILSCALE_SERVE_PORT").pipe( |
| 164 | Config.option, |
| 165 | Config.map(Option.getOrUndefined), |
| 166 | ), |
| 167 | }); |
| 168 | |
| 169 | const DevAuthTokenConfig = Config.Redacted("T3CODE_DEV_AUTH_TOKEN").pipe( |
| 170 | Config.map((token) => Redacted.make(Redacted.value(token).trim())), |
| 171 | Config.mapEffect((token) => |
| 172 | Redacted.value(token).length === 0 || Redacted.value(token).length >= 32 |
| 173 | ? Effect.succeed(token) |
| 174 | : Effect.fail( |
| 175 | new Config.ConfigError( |
| 176 | new Schema.SchemaError( |
| 177 | new SchemaIssue.InvalidValue({ |
| 178 | message: "T3CODE_DEV_AUTH_TOKEN must contain at least 32 characters.", |
| 179 | }), |
| 180 | ), |
| 181 | ), |
| 182 | ), |
| 183 | ), |
| 184 | Config.option, |
| 185 | Config.map(Option.filter((token) => Redacted.value(token).length > 0)), |
| 186 | Config.map(Option.getOrUndefined), |
| 187 | ); |
| 188 | |
| 189 | export interface CliServerFlags { |
| 190 | readonly mode: Option.Option<ServerConfig.RuntimeMode>; |
| 191 | readonly port: Option.Option<number>; |
| 192 | readonly host: Option.Option<string>; |
| 193 | readonly baseDir: Option.Option<string>; |
| 194 | readonly cwd: Option.Option<string>; |
| 195 | readonly devUrl: Option.Option<URL>; |
| 196 | readonly noBrowser: Option.Option<boolean>; |
| 197 | readonly bootstrapFd: Option.Option<number>; |
| 198 | readonly autoBootstrapProjectFromCwd: Option.Option<boolean>; |
| 199 | readonly logWebSocketEvents: Option.Option<boolean>; |
| 200 | readonly tailscaleServeEnabled: Option.Option<boolean>; |
| 201 | readonly tailscaleServePort: Option.Option<number>; |
| 202 | } |
| 203 | |
| 204 | export interface CliAuthLocationFlags { |
| 205 | readonly baseDir: Option.Option<string>; |
| 206 | readonly devUrl?: Option.Option<URL>; |
| 207 | } |
| 208 | |
| 209 | export const authLocationFlags = { |
| 210 | baseDir: baseDirFlag, |
| 211 | devUrl: devUrlFlag, |
| 212 | } as const; |
| 213 | |
| 214 | export const projectLocationFlags = { |
| 215 | baseDir: baseDirFlag, |
| 216 | } as const; |
| 217 | |
| 218 | export const sharedServerCommandFlags = { |
| 219 | mode: modeFlag, |
| 220 | port: portFlag, |
| 221 | host: hostFlag, |
| 222 | baseDir: baseDirFlag, |
| 223 | cwd: Argument.String("cwd").pipe( |
| 224 | Argument.withDescription( |
| 225 | "Working directory for provider sessions (defaults to the current directory).", |
| 226 | ), |
| 227 | Argument.optional, |
| 228 | ), |
| 229 | devUrl: devUrlFlag, |
| 230 | noBrowser: noBrowserFlag, |
| 231 | bootstrapFd: bootstrapFdFlag, |
| 232 | autoBootstrapProjectFromCwd: autoBootstrapProjectFromCwdFlag, |
| 233 | logWebSocketEvents: logWebSocketEventsFlag, |
| 234 | tailscaleServeEnabled: tailscaleServeFlag, |
| 235 | tailscaleServePort: tailscaleServePortFlag, |
| 236 | } as const; |
| 237 | |
| 238 | const resolveOptionPrecedence = <Value>( |
| 239 | ...values: ReadonlyArray<Option.Option<Value>> |
| 240 | ): Option.Option<Value> => Option.firstSomeOf(values); |
| 241 | |
| 242 | const loadPersistedObservabilitySettings = Effect.fn(function* (settingsPath: string) { |
| 243 | const fs = yield* FileSystem.FileSystem; |
| 244 | const exists = yield* fs.exists(settingsPath).pipe(Effect.orElseSucceed(() => false)); |
| 245 | if (!exists) { |
| 246 | return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined, otlpLogsUrl: undefined }; |
| 247 | } |
| 248 | |
| 249 | const raw = yield* fs.readFileString(settingsPath).pipe(Effect.orElseSucceed(() => "")); |
| 250 | return parsePersistedServerObservabilitySettings(raw); |
| 251 | }); |
| 252 | |
| 253 | export const resolveServerConfig = ( |
| 254 | flags: CliServerFlags, |
| 255 | cliLogLevel: Option.Option<LogLevel.LogLevel>, |
| 256 | options?: { |
| 257 | readonly startupPresentation?: ServerConfig.StartupPresentation; |
| 258 | readonly forceAutoBootstrapProjectFromCwd?: boolean; |
| 259 | readonly rejectRunningServer?: boolean; |
| 260 | }, |
| 261 | ) => |
| 262 | Effect.gen(function* () { |
| 263 | const { findAvailablePort } = yield* NetService.NetService; |
| 264 | const path = yield* Path.Path; |
| 265 | const fs = yield* FileSystem.FileSystem; |
| 266 | const env = yield* EnvServerConfig; |
| 267 | const normalizedFlags = { |
| 268 | mode: flags.mode ?? Option.none(), |
| 269 | port: flags.port ?? Option.none(), |
| 270 | host: flags.host ?? Option.none(), |
| 271 | baseDir: flags.baseDir ?? Option.none(), |
| 272 | cwd: flags.cwd ?? Option.none(), |
| 273 | devUrl: flags.devUrl ?? Option.none(), |
| 274 | noBrowser: flags.noBrowser ?? Option.none(), |
| 275 | bootstrapFd: flags.bootstrapFd ?? Option.none(), |
| 276 | autoBootstrapProjectFromCwd: flags.autoBootstrapProjectFromCwd ?? Option.none(), |
| 277 | logWebSocketEvents: flags.logWebSocketEvents ?? Option.none(), |
| 278 | tailscaleServeEnabled: flags.tailscaleServeEnabled ?? Option.none(), |
| 279 | tailscaleServePort: flags.tailscaleServePort ?? Option.none(), |
| 280 | } satisfies CliServerFlags; |
| 281 | const bootstrapFd = Option.getOrUndefined(normalizedFlags.bootstrapFd) ?? env.bootstrapFd; |
| 282 | const bootstrapEnvelope = |
| 283 | bootstrapFd !== undefined |
| 284 | ? yield* readBootstrapEnvelope(DesktopBackendBootstrap, bootstrapFd) |
| 285 | : Option.none(); |
| 286 | const bootstrap = Option.getOrUndefined(bootstrapEnvelope); |
| 287 | |
| 288 | const mode: ServerConfig.RuntimeMode = Option.getOrElse( |
| 289 | resolveOptionPrecedence( |
| 290 | normalizedFlags.mode, |
| 291 | Option.fromUndefinedOr(env.mode), |
| 292 | Option.fromUndefinedOr(bootstrap?.mode), |
| 293 | ), |
| 294 | () => "web", |
| 295 | ); |
| 296 | |
| 297 | const port = yield* Option.match( |
| 298 | resolveOptionPrecedence( |
| 299 | normalizedFlags.port, |
| 300 | Option.fromUndefinedOr(env.port), |
| 301 | Option.fromUndefinedOr(bootstrap?.port), |
| 302 | ), |
| 303 | { |
| 304 | onSome: (value) => Effect.succeed(value), |
| 305 | onNone: () => { |
| 306 | if (mode === "desktop") { |
| 307 | return Effect.succeed(ServerConfig.DEFAULT_PORT); |
| 308 | } |
| 309 | return findAvailablePort(ServerConfig.DEFAULT_PORT); |
| 310 | }, |
| 311 | }, |
| 312 | ); |
| 313 | const devUrl = Option.getOrElse( |
| 314 | resolveOptionPrecedence(normalizedFlags.devUrl, Option.fromUndefinedOr(env.devUrl)), |
| 315 | () => undefined, |
| 316 | ); |
| 317 | const devAuthToken = |
| 318 | mode === "web" && devUrl !== undefined ? yield* DevAuthTokenConfig : undefined; |
| 319 | const explicitBaseDir = resolveOptionPrecedence( |
| 320 | normalizedFlags.baseDir, |
| 321 | Option.fromUndefinedOr(env.t3Home), |
| 322 | ).pipe(Option.filter((value) => value.trim().length > 0)); |
| 323 | const baseDir = yield* resolveBaseDir( |
| 324 | Option.getOrUndefined( |
| 325 | resolveOptionPrecedence(explicitBaseDir, Option.fromUndefinedOr(bootstrap?.t3Home)), |
| 326 | ), |
| 327 | ); |
| 328 | const rawCwd = Option.getOrElse(normalizedFlags.cwd, () => process.cwd()); |
| 329 | const cwd = path.resolve(yield* expandHomePath(rawCwd.trim())); |
| 330 | const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, devUrl, { |
| 331 | baseDirIsExplicit: Option.isSome(explicitBaseDir), |
| 332 | }); |
| 333 | // An interactive CLI must not start over a discovered server. Lifetime locking |
| 334 | // and supervisor handoff are separate; this preflight cannot arbitrate two starts. |
| 335 | if (options?.rejectRunningServer && mode === "web") { |
| 336 | const runtime = yield* readPersistedServerRuntimeState(derivedPaths.serverRuntimeStatePath); |
| 337 | if (Option.isSome(runtime) && runtime.value.pid > 0 && isProcessAlive(runtime.value.pid)) { |
| 338 | return yield* new CliError.UserError({ |
| 339 | cause: `A T3 Code server is already running for ${baseDir} (pid ${runtime.value.pid}, ${runtime.value.origin}). Connect to that server, stop it before starting another, or use a different --base-dir.`, |
| 340 | }); |
| 341 | } |
| 342 | } |
| 343 | yield* fs.makeDirectory(cwd, { recursive: true }); |
| 344 | yield* ServerConfig.ensureServerDirectories(derivedPaths); |
| 345 | const persistedObservabilitySettings = yield* loadPersistedObservabilitySettings( |
| 346 | derivedPaths.settingsPath, |
| 347 | ); |
| 348 | const serverTracePath = env.traceFile ?? derivedPaths.serverTracePath; |
| 349 | yield* fs.makeDirectory(path.dirname(serverTracePath), { recursive: true }); |
| 350 | const startupPresentation = options?.startupPresentation ?? "browser"; |
| 351 | const isHeadlessStartup = startupPresentation === "headless"; |
| 352 | const noBrowser = Option.getOrElse( |
| 353 | resolveOptionPrecedence( |
| 354 | isHeadlessStartup ? Option.some(true) : Option.none(), |
| 355 | normalizedFlags.noBrowser, |
| 356 | Option.fromUndefinedOr(env.noBrowser), |
| 357 | Option.fromUndefinedOr(bootstrap?.noBrowser), |
| 358 | ), |
| 359 | () => mode === "desktop", |
| 360 | ); |
| 361 | const desktopBootstrapToken = bootstrap?.desktopBootstrapToken; |
| 362 | const desktopBootstrapSecret = bootstrap?.desktopBootstrapSecret; |
| 363 | const shellEnvironmentPrepared = bootstrap?.shellEnvironmentPrepared; |
| 364 | const desktopTelemetryFd = bootstrap?.desktopTelemetryFd; |
| 365 | const desktopTelemetryControlFd = bootstrap?.desktopTelemetryControlFd; |
| 366 | const desktopBrowserFd = bootstrap?.desktopBrowserFd; |
| 367 | const desktopBrowserControlFd = bootstrap?.desktopBrowserControlFd; |
| 368 | const resourceMonitorPath = bootstrap?.resourceMonitorPath; |
| 369 | const autoBootstrapProjectFromCwd = Option.getOrElse( |
| 370 | resolveOptionPrecedence( |
| 371 | Option.fromUndefinedOr(options?.forceAutoBootstrapProjectFromCwd), |
| 372 | isHeadlessStartup ? Option.some(false) : Option.none(), |
| 373 | normalizedFlags.autoBootstrapProjectFromCwd, |
| 374 | Option.fromUndefinedOr(env.autoBootstrapProjectFromCwd), |
| 375 | ), |
| 376 | () => mode === "web", |
| 377 | ); |
| 378 | const logWebSocketEvents = Option.getOrElse( |
| 379 | resolveOptionPrecedence( |
| 380 | normalizedFlags.logWebSocketEvents, |
| 381 | Option.fromUndefinedOr(env.logWebSocketEvents), |
| 382 | ), |
| 383 | () => Boolean(devUrl), |
| 384 | ); |
| 385 | const tailscaleServeEnabled = Option.getOrElse( |
| 386 | resolveOptionPrecedence( |
| 387 | normalizedFlags.tailscaleServeEnabled, |
| 388 | Option.fromUndefinedOr(env.tailscaleServeEnabled), |
| 389 | Option.fromUndefinedOr(bootstrap?.tailscaleServeEnabled), |
| 390 | ), |
| 391 | () => false, |
| 392 | ); |
| 393 | const tailscaleServePort = Option.getOrElse( |
| 394 | resolveOptionPrecedence( |
| 395 | normalizedFlags.tailscaleServePort, |
| 396 | Option.fromUndefinedOr(env.tailscaleServePort), |
| 397 | Option.fromUndefinedOr(bootstrap?.tailscaleServePort), |
| 398 | ), |
| 399 | () => 443, |
| 400 | ); |
| 401 | const staticDir = devUrl ? undefined : yield* ServerConfig.resolveStaticDir(); |
| 402 | const host = Option.getOrElse( |
| 403 | resolveOptionPrecedence( |
| 404 | normalizedFlags.host, |
| 405 | Option.fromUndefinedOr(env.host), |
| 406 | Option.fromUndefinedOr(bootstrap?.host), |
| 407 | ), |
| 408 | () => (mode === "desktop" ? "127.0.0.1" : undefined), |
| 409 | ); |
| 410 | const logLevel = Option.getOrElse(cliLogLevel, () => env.logLevel); |
| 411 | |
| 412 | const otel = yield* OtelEnvironment.load; |
| 413 | |
| 414 | // T3 Code's own OTLP variables name no signal, so the one answer they give |
| 415 | // is the answer for all three. |
| 416 | const signalExport: SignalExport = { |
| 417 | protocol: env.otlpProtocol, |
| 418 | headers: env.otlpHeaders, |
| 419 | exportIntervalMs: env.otlpExportIntervalMs, |
| 420 | }; |
| 421 | const traces = OtelEnvironment.resolveSignalEndpoint( |
| 422 | otel, |
| 423 | "traces", |
| 424 | { url: env.otlpTracesUrl, export: signalExport }, |
| 425 | bootstrap?.otlpTracesUrl, |
| 426 | persistedObservabilitySettings.otlpTracesUrl, |
| 427 | ); |
| 428 | const metrics = OtelEnvironment.resolveSignalEndpoint( |
| 429 | otel, |
| 430 | "metrics", |
| 431 | { url: env.otlpMetricsUrl, export: signalExport }, |
| 432 | bootstrap?.otlpMetricsUrl, |
| 433 | persistedObservabilitySettings.otlpMetricsUrl, |
| 434 | ); |
| 435 | const logs = OtelEnvironment.resolveSignalEndpoint( |
| 436 | otel, |
| 437 | "logs", |
| 438 | { url: env.otlpLogsUrl, export: signalExport }, |
| 439 | bootstrap?.otlpLogsUrl, |
| 440 | persistedObservabilitySettings.otlpLogsUrl, |
| 441 | ); |
| 442 | |
| 443 | const config: ServerConfig.ServerConfig["Service"] = { |
| 444 | logLevel, |
| 445 | traceMinLevel: env.traceMinLevel, |
| 446 | traceTimingEnabled: env.traceTimingEnabled, |
| 447 | traceBatchWindowMs: env.traceBatchWindowMs, |
| 448 | traceMaxBytes: env.traceMaxBytes, |
| 449 | traceMaxFiles: env.traceMaxFiles, |
| 450 | otlpTracesUrl: traces?.url, |
| 451 | otlpMetricsUrl: metrics?.url, |
| 452 | otlpLogsUrl: logs?.url, |
| 453 | otlpTracesExport: traces?.export ?? signalExport, |
| 454 | otlpMetricsExport: metrics?.export ?? signalExport, |
| 455 | otlpLogsExport: logs?.export ?? signalExport, |
| 456 | otelEnvironment: otel, |
| 457 | mode, |
| 458 | port, |
| 459 | cwd, |
| 460 | baseDir, |
| 461 | ...derivedPaths, |
| 462 | serverTracePath, |
| 463 | host, |
| 464 | staticDir, |
| 465 | devUrl, |
| 466 | ...(devAuthToken === undefined ? {} : { devAuthToken }), |
| 467 | devAllowedOrigins: env.devAllowedOrigins, |
| 468 | noBrowser, |
| 469 | startupPresentation, |
| 470 | desktopBootstrapToken, |
| 471 | ...(desktopBootstrapSecret === undefined ? {} : { desktopBootstrapSecret }), |
| 472 | ...(shellEnvironmentPrepared === undefined ? {} : { shellEnvironmentPrepared }), |
| 473 | desktopTelemetryFd, |
| 474 | desktopTelemetryControlFd, |
| 475 | desktopTerminalFd: bootstrap?.desktopTerminalFd, |
| 476 | desktopTerminalControlFd: bootstrap?.desktopTerminalControlFd, |
| 477 | desktopBrowserFd, |
| 478 | desktopBrowserControlFd, |
| 479 | resourceMonitorPath, |
| 480 | autoBootstrapProjectFromCwd, |
| 481 | logWebSocketEvents, |
| 482 | tailscaleServeEnabled, |
| 483 | tailscaleServePort, |
| 484 | }; |
| 485 | |
| 486 | return config; |
| 487 | }); |
| 488 | |
| 489 | export const resolveCliAuthConfig = ( |
| 490 | flags: CliAuthLocationFlags, |
| 491 | cliLogLevel: Option.Option<LogLevel.LogLevel>, |
| 492 | ) => |
| 493 | resolveServerConfig( |
| 494 | { |
| 495 | mode: Option.none(), |
| 496 | port: Option.none(), |
| 497 | host: Option.none(), |
| 498 | baseDir: flags.baseDir, |
| 499 | cwd: Option.none(), |
| 500 | devUrl: flags.devUrl ?? Option.none(), |
| 501 | noBrowser: Option.none(), |
| 502 | bootstrapFd: Option.none(), |
| 503 | autoBootstrapProjectFromCwd: Option.none(), |
| 504 | logWebSocketEvents: Option.none(), |
| 505 | tailscaleServeEnabled: Option.none(), |
| 506 | tailscaleServePort: Option.none(), |
| 507 | }, |
| 508 | cliLogLevel, |
| 509 | ); |
| 510 | |
| 511 | const DurationShorthandPattern = /^(?<value>\d+)(?<unit>ms|s|m|h|d|w)$/i; |
| 512 | |
| 513 | const parseDurationInput = (value: string): Duration.Duration | null => { |
| 514 | const trimmed = value.trim(); |
| 515 | if (trimmed.length === 0) return null; |
| 516 | |
| 517 | const shorthand = DurationShorthandPattern.exec(trimmed); |
| 518 | const normalizedInput = shorthand?.groups |
| 519 | ? (() => { |
| 520 | const amountText = shorthand.groups.value; |
| 521 | const unitText = shorthand.groups.unit; |
| 522 | if (typeof amountText !== "string" || typeof unitText !== "string") { |
| 523 | return null; |
| 524 | } |
| 525 | |
| 526 | const amount = Number.parseInt(amountText, 10); |
| 527 | if (!Number.isFinite(amount)) return null; |
| 528 | |
| 529 | switch (unitText.toLowerCase()) { |
| 530 | case "ms": |
| 531 | return `${amount} millis`; |
| 532 | case "s": |
| 533 | return `${amount} seconds`; |
| 534 | case "m": |
| 535 | return `${amount} minutes`; |
| 536 | case "h": |
| 537 | return `${amount} hours`; |
| 538 | case "d": |
| 539 | return `${amount} days`; |
| 540 | case "w": |
| 541 | return `${amount} weeks`; |
| 542 | default: |
| 543 | return null; |
| 544 | } |
| 545 | })() |
| 546 | : (trimmed as Duration.Input); |
| 547 | |
| 548 | if (normalizedInput === null) return null; |
| 549 | |
| 550 | const decoded = Duration.fromInput(normalizedInput as Duration.Input); |
| 551 | return Option.isSome(decoded) ? decoded.value : null; |
| 552 | }; |
| 553 | |
| 554 | export const DurationFromString = Schema.String.pipe( |
| 555 | Schema.decodeTo( |
| 556 | Schema.Duration, |
| 557 | SchemaTransformation.transformEffect({ |
| 558 | decode: (value) => { |
| 559 | const duration = parseDurationInput(value); |
| 560 | if (duration !== null) { |
| 561 | return Effect.succeed(duration); |
| 562 | } |
| 563 | return Effect.fail( |
| 564 | new SchemaIssue.InvalidValue({ |
| 565 | message: "Invalid duration. Use values like 5m, 1h, 30d, or 15 minutes.", |
| 566 | }), |
| 567 | ); |
| 568 | }, |
| 569 | encode: (duration) => Effect.succeed(Duration.format(duration)), |
| 570 | }), |
| 571 | ), |
| 572 | ); |