import { createApp, type Config } from "./app.js"; const config: Config = { host: process.env.TRAPOS_SERVER_HOST ?? "0.0.0.0", port: readPort(process.env.TRAPOS_SERVER_PORT, 4444), serverPassword: requireSecret("TRAPOS_SERVER_PASSWORD", process.env.TRAPOS_SERVER_PASSWORD), requestTimeoutMs: readPositiveInt(process.env.TRAPOS_SERVER_REQUEST_TIMEOUT_MS, 600_000), opencodeUrl: process.env.OPENCODE_URL ?? "http://127.0.0.1:4242", opencodeUsername: process.env.OPENCODE_USERNAME ?? "opencode", opencodeServerPassword: requireSecret( "OPENCODE_SERVER_PASSWORD", process.env.OPENCODE_SERVER_PASSWORD, ), // MCP endpoint: on by default, bound to localhost. exec-lua / write-file are as // powerful as the linked computer, so the listener stays off the public gateway port. mcpEnabled: process.env.MCP_ENABLED !== "0", mcpHost: process.env.MCP_HOST ?? "127.0.0.1", mcpPort: readPort(process.env.MCP_PORT, 4445), mcpDefaultTimeoutMs: readPositiveInt(process.env.MCP_DEFAULT_TIMEOUT_MS, 30_000), mcpMaxTimeoutMs: readPositiveInt(process.env.MCP_MAX_TIMEOUT_MS, 30_000), mcpProbeTimeoutMs: readPositiveInt(process.env.MCP_PROBE_TIMEOUT_MS, 5_000), logger: buildLogger(), }; const { app, mcpApp } = await createApp(config); try { await app.listen({ host: config.host, port: config.port }); } catch (err) { reportListenError(err, config.host, config.port, "TRAPOS_SERVER_PORT"); app.log.error(err); process.exit(1); } if (mcpApp) { try { await mcpApp.listen({ host: config.mcpHost, port: config.mcpPort }); } catch (err) { reportListenError(err, config.mcpHost, config.mcpPort, "MCP_PORT"); mcpApp.log.error(err); process.exit(1); } } function buildLogger(): Config["logger"] { const level = process.env.LOG_LEVEL ?? "info"; if (process.env.LOG_PRETTY) { return { level, transport: { target: "pino-pretty", options: { translateTime: "HH:MM:ss", ignore: "pid,hostname" }, }, }; } return { level }; } // A non-empty password is mandatory: an empty TRAPOS_SERVER_PASSWORD disables auth // on the public gateway (resolveNetwork maps any secret to "global"), and an empty // OPENCODE_SERVER_PASSWORD drops the outbound Basic-auth header. Refuse to start // rather than run silently unauthenticated. function requireSecret(name: string, value: string | undefined): string { const secret = value?.trim(); if (!secret) { process.stderr.write( `\n[trapos-server] ${name} is empty or unset. Set it in .env ` + `(run \`just install\` to generate secrets). Refusing to start.\n\n`, ); process.exit(1); } return secret; } function readPort(value: string | undefined, fallback: number): number { return readPositiveInt(value, fallback); } function readPositiveInt(value: string | undefined, fallback: number): number { if (!value) { return fallback; } const parsed = Number(value); return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback; } function isErrnoException(err: unknown): err is NodeJS.ErrnoException { return err instanceof Error && "code" in err; } function reportListenError(err: unknown, host: string, port: number, portEnv: string): void { if (isErrnoException(err) && err.code === "EADDRINUSE") { process.stderr.write( `\n[trapos-server] Port ${port} is already in use on ${host}.\n` + `Another instance may be running, or set ${portEnv} to a free port.\n\n`, ); process.exit(1); } }