import { NetService } from "@t3tools/shared/Net"; import { Config, Effect, LogLevel, Option, Schema } from "effect"; import { Command, Flag, GlobalFlag } from "effect/unstable/cli"; import { DEFAULT_PORT, deriveServerPaths, ensureServerDirectories, resolveStaticDir, ServerConfig, RuntimeMode, type ServerConfigShape, } from "./config"; import { readBootstrapEnvelope } from "./bootstrap"; import { resolveBaseDir } from "./os-jank"; import { runServer } from "./server"; const PortSchema = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 })); const BootstrapEnvelopeSchema = Schema.Struct({ mode: Schema.optional(RuntimeMode), port: Schema.optional(PortSchema), host: Schema.optional(Schema.String), t3Home: Schema.optional(Schema.String), devUrl: Schema.optional(Schema.URLFromString), noBrowser: Schema.optional(Schema.Boolean), authToken: Schema.optional(Schema.String), autoBootstrapProjectFromCwd: Schema.optional(Schema.Boolean), logWebSocketEvents: Schema.optional(Schema.Boolean), }); const modeFlag = Flag.choice("mode", RuntimeMode.literals).pipe( Flag.withDescription("Runtime mode. `desktop` keeps loopback defaults unless overridden."), Flag.optional, ); const portFlag = Flag.integer("port").pipe( Flag.withSchema(PortSchema), Flag.withDescription("Port for the HTTP/WebSocket server."), Flag.optional, ); const hostFlag = Flag.string("host").pipe( Flag.withDescription("Host/interface to bind (for example 127.0.0.1, 0.0.0.0, or a Tailnet IP)."), Flag.optional, ); const baseDirFlag = Flag.string("base-dir").pipe( Flag.withDescription("Base directory path (equivalent to T3CODE_HOME)."), Flag.optional, ); const devUrlFlag = Flag.string("dev-url").pipe( Flag.withSchema(Schema.URLFromString), Flag.withDescription("Dev web URL to proxy/redirect to (equivalent to VITE_DEV_SERVER_URL)."), Flag.optional, ); const noBrowserFlag = Flag.boolean("no-browser").pipe( Flag.withDescription("Disable automatic browser opening."), Flag.optional, ); const authTokenFlag = Flag.string("auth-token").pipe( Flag.withDescription("Auth token required for WebSocket connections."), Flag.withAlias("token"), Flag.optional, ); const bootstrapFdFlag = Flag.integer("bootstrap-fd").pipe( Flag.withSchema(Schema.Int), Flag.withDescription("Read one-time bootstrap secrets from the given file descriptor."), Flag.optional, ); const autoBootstrapProjectFromCwdFlag = Flag.boolean("auto-bootstrap-project-from-cwd").pipe( Flag.withDescription( "Create a project for the current working directory on startup when missing.", ), Flag.optional, ); const logWebSocketEventsFlag = Flag.boolean("log-websocket-events").pipe( Flag.withDescription( "Emit server-side logs for outbound WebSocket push traffic (equivalent to T3CODE_LOG_WS_EVENTS).", ), Flag.withAlias("log-ws-events"), Flag.optional, ); const EnvServerConfig = Config.all({ logLevel: Config.logLevel("T3CODE_LOG_LEVEL").pipe(Config.withDefault("Info")), mode: Config.schema(RuntimeMode, "T3CODE_MODE").pipe( Config.option, Config.map(Option.getOrUndefined), ), port: Config.port("T3CODE_PORT").pipe(Config.option, Config.map(Option.getOrUndefined)), host: Config.string("T3CODE_HOST").pipe(Config.option, Config.map(Option.getOrUndefined)), t3Home: Config.string("T3CODE_HOME").pipe(Config.option, Config.map(Option.getOrUndefined)), devUrl: Config.url("VITE_DEV_SERVER_URL").pipe(Config.option, Config.map(Option.getOrUndefined)), noBrowser: Config.boolean("T3CODE_NO_BROWSER").pipe( Config.option, Config.map(Option.getOrUndefined), ), authToken: Config.string("T3CODE_AUTH_TOKEN").pipe( Config.option, Config.map(Option.getOrUndefined), ), bootstrapFd: Config.int("T3CODE_BOOTSTRAP_FD").pipe( Config.option, Config.map(Option.getOrUndefined), ), autoBootstrapProjectFromCwd: Config.boolean("T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD").pipe( Config.option, Config.map(Option.getOrUndefined), ), logWebSocketEvents: Config.boolean("T3CODE_LOG_WS_EVENTS").pipe( Config.option, Config.map(Option.getOrUndefined), ), }); interface CliServerFlags { readonly mode: Option.Option; readonly port: Option.Option; readonly host: Option.Option; readonly baseDir: Option.Option; readonly devUrl: Option.Option; readonly noBrowser: Option.Option; readonly authToken: Option.Option; readonly bootstrapFd: Option.Option; readonly autoBootstrapProjectFromCwd: Option.Option; readonly logWebSocketEvents: Option.Option; } const resolveBooleanFlag = (flag: Option.Option, envValue: boolean) => Option.getOrElse(Option.filter(flag, Boolean), () => envValue); const resolveOptionPrecedence = ( ...values: ReadonlyArray> ): Option.Option => Option.firstSomeOf(values); export const resolveServerConfig = ( flags: CliServerFlags, cliLogLevel: Option.Option, ) => Effect.gen(function* () { const { findAvailablePort } = yield* NetService; const env = yield* EnvServerConfig; const bootstrapFd = Option.getOrUndefined(flags.bootstrapFd) ?? env.bootstrapFd; const bootstrapEnvelope = bootstrapFd !== undefined ? yield* readBootstrapEnvelope(BootstrapEnvelopeSchema, bootstrapFd) : Option.none(); const mode: RuntimeMode = Option.getOrElse( resolveOptionPrecedence( flags.mode, Option.fromUndefinedOr(env.mode), Option.flatMap(bootstrapEnvelope, (bootstrap) => Option.fromUndefinedOr(bootstrap.mode)), ), () => "web", ); const port = yield* Option.match( resolveOptionPrecedence( flags.port, Option.fromUndefinedOr(env.port), Option.flatMap(bootstrapEnvelope, (bootstrap) => Option.fromUndefinedOr(bootstrap.port)), ), { onSome: (value) => Effect.succeed(value), onNone: () => { if (mode === "desktop") { return Effect.succeed(DEFAULT_PORT); } return findAvailablePort(DEFAULT_PORT); }, }, ); const devUrl = Option.getOrElse( resolveOptionPrecedence( flags.devUrl, Option.fromUndefinedOr(env.devUrl), Option.flatMap(bootstrapEnvelope, (bootstrap) => Option.fromUndefinedOr(bootstrap.devUrl)), ), () => undefined, ); const baseDir = yield* resolveBaseDir( Option.getOrUndefined( resolveOptionPrecedence( flags.baseDir, Option.fromUndefinedOr(env.t3Home), Option.flatMap(bootstrapEnvelope, (bootstrap) => Option.fromUndefinedOr(bootstrap.t3Home), ), ), ), ); const derivedPaths = yield* deriveServerPaths(baseDir, devUrl); yield* ensureServerDirectories(derivedPaths); const noBrowser = resolveBooleanFlag( flags.noBrowser, Option.getOrElse( resolveOptionPrecedence( Option.fromUndefinedOr(env.noBrowser), Option.flatMap(bootstrapEnvelope, (bootstrap) => Option.fromUndefinedOr(bootstrap.noBrowser), ), ), () => mode === "desktop", ), ); const authToken = Option.getOrUndefined( resolveOptionPrecedence( flags.authToken, Option.fromUndefinedOr(env.authToken), Option.flatMap(bootstrapEnvelope, (bootstrap) => Option.fromUndefinedOr(bootstrap.authToken), ), ), ); const autoBootstrapProjectFromCwd = resolveBooleanFlag( flags.autoBootstrapProjectFromCwd, Option.getOrElse( resolveOptionPrecedence( Option.fromUndefinedOr(env.autoBootstrapProjectFromCwd), Option.flatMap(bootstrapEnvelope, (bootstrap) => Option.fromUndefinedOr(bootstrap.autoBootstrapProjectFromCwd), ), ), () => mode === "web", ), ); const logWebSocketEvents = resolveBooleanFlag( flags.logWebSocketEvents, Option.getOrElse( resolveOptionPrecedence( Option.fromUndefinedOr(env.logWebSocketEvents), Option.flatMap(bootstrapEnvelope, (bootstrap) => Option.fromUndefinedOr(bootstrap.logWebSocketEvents), ), ), () => Boolean(devUrl), ), ); const staticDir = devUrl ? undefined : yield* resolveStaticDir(); const host = Option.getOrElse( resolveOptionPrecedence( flags.host, Option.fromUndefinedOr(env.host), Option.flatMap(bootstrapEnvelope, (bootstrap) => Option.fromUndefinedOr(bootstrap.host)), ), () => (mode === "desktop" ? "127.0.0.1" : undefined), ); const logLevel = Option.getOrElse(cliLogLevel, () => env.logLevel); const config: ServerConfigShape = { logLevel, mode, port, cwd: process.cwd(), baseDir, ...derivedPaths, host, staticDir, devUrl, noBrowser, authToken, autoBootstrapProjectFromCwd, logWebSocketEvents, }; return config; }); const commandFlags = { mode: modeFlag, port: portFlag, host: hostFlag, baseDir: baseDirFlag, devUrl: devUrlFlag, noBrowser: noBrowserFlag, authToken: authTokenFlag, bootstrapFd: bootstrapFdFlag, autoBootstrapProjectFromCwd: autoBootstrapProjectFromCwdFlag, logWebSocketEvents: logWebSocketEventsFlag, } as const; const rootCommand = Command.make("t3", commandFlags).pipe( Command.withDescription("Run the T3 Code server."), Command.withHandler((flags) => Effect.gen(function* () { const logLevel = yield* GlobalFlag.LogLevel; const config = yield* resolveServerConfig(flags, logLevel); return yield* runServer.pipe(Effect.provideService(ServerConfig, config)); }), ), ); export const cli = rootCommand;