#!/usr/bin/env node import { homedir } from "node:os"; import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { NetService } from "@t3tools/shared/Net"; import { Config, Data, Effect, Hash, Layer, Logger, Option, Path, Schema } from "effect"; import { Argument, Command, Flag } from "effect/unstable/cli"; import { ChildProcess } from "effect/unstable/process"; const BASE_SERVER_PORT = 3773; const BASE_WEB_PORT = 5733; const MAX_HASH_OFFSET = 3000; const MAX_PORT = 65535; export const DEFAULT_T3_HOME = Effect.map(Effect.service(Path.Path), (path) => path.join(homedir(), ".t3"), ); const MODE_ARGS = { dev: [ "run", "dev", "--ui=tui", "--filter=@t3tools/contracts", "--filter=@t3tools/web", "--filter=t3", "--parallel", ], "dev:server": ["run", "dev", "--filter=t3"], "dev:web": ["run", "dev", "--filter=@t3tools/web"], "dev:desktop": ["run", "dev", "--filter=@t3tools/desktop", "--filter=@t3tools/web", "--parallel"], } as const satisfies Record>; type DevMode = keyof typeof MODE_ARGS; type PortAvailabilityCheck = (port: number) => Effect.Effect; const DEV_RUNNER_MODES = Object.keys(MODE_ARGS) as Array; class DevRunnerError extends Data.TaggedError("DevRunnerError")<{ readonly message: string; readonly cause?: unknown; }> {} const optionalStringConfig = (name: string): Config.Config => Config.string(name).pipe( Config.option, Config.map((value) => Option.getOrUndefined(value)), ); const optionalBooleanConfig = (name: string): Config.Config => Config.boolean(name).pipe( Config.option, Config.map((value) => Option.getOrUndefined(value)), ); const optionalPortConfig = (name: string): Config.Config => Config.port(name).pipe( Config.option, Config.map((value) => Option.getOrUndefined(value)), ); const optionalIntegerConfig = (name: string): Config.Config => Config.int(name).pipe( Config.option, Config.map((value) => Option.getOrUndefined(value)), ); const optionalUrlConfig = (name: string): Config.Config => Config.url(name).pipe( Config.option, Config.map((value) => Option.getOrUndefined(value)), ); const OffsetConfig = Config.all({ portOffset: optionalIntegerConfig("T3CODE_PORT_OFFSET"), devInstance: optionalStringConfig("T3CODE_DEV_INSTANCE"), }); export function resolveOffset(config: { readonly portOffset: number | undefined; readonly devInstance: string | undefined; }): { readonly offset: number; readonly source: string } { if (config.portOffset !== undefined) { if (config.portOffset < 0) { throw new Error(`Invalid T3CODE_PORT_OFFSET: ${config.portOffset}`); } return { offset: config.portOffset, source: `T3CODE_PORT_OFFSET=${config.portOffset}`, }; } const seed = config.devInstance?.trim(); if (!seed) { return { offset: 0, source: "default ports" }; } if (/^\d+$/.test(seed)) { return { offset: Number(seed), source: `numeric T3CODE_DEV_INSTANCE=${seed}` }; } const offset = ((Hash.string(seed) >>> 0) % MAX_HASH_OFFSET) + 1; return { offset, source: `hashed T3CODE_DEV_INSTANCE=${seed}` }; } function resolveBaseDir(baseDir: string | undefined): Effect.Effect { return Effect.gen(function* () { const path = yield* Path.Path; const configured = baseDir?.trim(); if (configured) { return path.resolve(configured); } return yield* DEFAULT_T3_HOME; }); } interface CreateDevRunnerEnvInput { readonly mode: DevMode; readonly baseEnv: NodeJS.ProcessEnv; readonly serverOffset: number; readonly webOffset: number; readonly t3Home: string | undefined; readonly authToken: string | undefined; readonly noBrowser: boolean | undefined; readonly autoBootstrapProjectFromCwd: boolean | undefined; readonly logWebSocketEvents: boolean | undefined; readonly host: string | undefined; readonly port: number | undefined; readonly devUrl: URL | undefined; } export function createDevRunnerEnv({ mode, baseEnv, serverOffset, webOffset, t3Home, authToken, noBrowser, autoBootstrapProjectFromCwd, logWebSocketEvents, host, port, devUrl, }: CreateDevRunnerEnvInput): Effect.Effect { return Effect.gen(function* () { const serverPort = port ?? BASE_SERVER_PORT + serverOffset; const webPort = BASE_WEB_PORT + webOffset; const resolvedBaseDir = yield* resolveBaseDir(t3Home); const output: NodeJS.ProcessEnv = { ...baseEnv, T3CODE_PORT: String(serverPort), PORT: String(webPort), ELECTRON_RENDERER_PORT: String(webPort), VITE_WS_URL: `ws://localhost:${serverPort}`, VITE_DEV_SERVER_URL: devUrl?.toString() ?? `http://localhost:${webPort}`, T3CODE_HOME: resolvedBaseDir, }; if (host !== undefined) { output.T3CODE_HOST = host; } if (authToken !== undefined) { output.T3CODE_AUTH_TOKEN = authToken; } else { delete output.T3CODE_AUTH_TOKEN; } if (noBrowser !== undefined) { output.T3CODE_NO_BROWSER = noBrowser ? "1" : "0"; } else { delete output.T3CODE_NO_BROWSER; } if (autoBootstrapProjectFromCwd !== undefined) { output.T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD = autoBootstrapProjectFromCwd ? "1" : "0"; } else { delete output.T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD; } if (logWebSocketEvents !== undefined) { output.T3CODE_LOG_WS_EVENTS = logWebSocketEvents ? "1" : "0"; } else { delete output.T3CODE_LOG_WS_EVENTS; } if (mode === "dev") { output.T3CODE_MODE = "web"; delete output.T3CODE_DESKTOP_WS_URL; } if (mode === "dev:server" || mode === "dev:web") { output.T3CODE_MODE = "web"; delete output.T3CODE_DESKTOP_WS_URL; } return output; }); } function portPairForOffset(offset: number): { readonly serverPort: number; readonly webPort: number; } { return { serverPort: BASE_SERVER_PORT + offset, webPort: BASE_WEB_PORT + offset, }; } const defaultCheckPortAvailability: PortAvailabilityCheck = (port) => Effect.gen(function* () { const net = yield* NetService; return yield* net.isPortAvailableOnLoopback(port); }); interface FindFirstAvailableOffsetInput { readonly startOffset: number; readonly requireServerPort: boolean; readonly requireWebPort: boolean; readonly checkPortAvailability?: PortAvailabilityCheck; } export function findFirstAvailableOffset({ startOffset, requireServerPort, requireWebPort, checkPortAvailability, }: FindFirstAvailableOffsetInput): Effect.Effect { return Effect.gen(function* () { const checkPort = (checkPortAvailability ?? defaultCheckPortAvailability) as PortAvailabilityCheck; for (let candidate = startOffset; ; candidate += 1) { const { serverPort, webPort } = portPairForOffset(candidate); const serverPortOutOfRange = serverPort > MAX_PORT; const webPortOutOfRange = webPort > MAX_PORT; if ( (requireServerPort && serverPortOutOfRange) || (requireWebPort && webPortOutOfRange) || (!requireServerPort && !requireWebPort && (serverPortOutOfRange || webPortOutOfRange)) ) { break; } const checks: Array> = []; if (requireServerPort) { checks.push(checkPort(serverPort)); } if (requireWebPort) { checks.push(checkPort(webPort)); } if (checks.length === 0) { return candidate; } const availability = yield* Effect.all(checks); if (availability.every(Boolean)) { return candidate; } } return yield* new DevRunnerError({ message: `No available dev ports found from offset ${startOffset}. Tried server=${BASE_SERVER_PORT}+n web=${BASE_WEB_PORT}+n up to port ${MAX_PORT}.`, }); }); } interface ResolveModePortOffsetsInput { readonly mode: DevMode; readonly startOffset: number; readonly hasExplicitServerPort: boolean; readonly hasExplicitDevUrl: boolean; readonly checkPortAvailability?: PortAvailabilityCheck; } export function resolveModePortOffsets({ mode, startOffset, hasExplicitServerPort, hasExplicitDevUrl, checkPortAvailability, }: ResolveModePortOffsetsInput): Effect.Effect< { readonly serverOffset: number; readonly webOffset: number }, DevRunnerError, R > { return Effect.gen(function* () { const checkPort = (checkPortAvailability ?? defaultCheckPortAvailability) as PortAvailabilityCheck; if (mode === "dev:web") { if (hasExplicitDevUrl) { return { serverOffset: startOffset, webOffset: startOffset }; } const webOffset = yield* findFirstAvailableOffset({ startOffset, requireServerPort: false, requireWebPort: true, checkPortAvailability: checkPort, }); return { serverOffset: startOffset, webOffset }; } if (mode === "dev:server") { if (hasExplicitServerPort) { return { serverOffset: startOffset, webOffset: startOffset }; } const serverOffset = yield* findFirstAvailableOffset({ startOffset, requireServerPort: true, requireWebPort: false, checkPortAvailability: checkPort, }); return { serverOffset, webOffset: serverOffset }; } const sharedOffset = yield* findFirstAvailableOffset({ startOffset, requireServerPort: !hasExplicitServerPort, requireWebPort: !hasExplicitDevUrl, checkPortAvailability: checkPort, }); return { serverOffset: sharedOffset, webOffset: sharedOffset }; }); } interface DevRunnerCliInput { readonly mode: DevMode; readonly t3Home: string | undefined; readonly authToken: string | undefined; readonly noBrowser: boolean | undefined; readonly autoBootstrapProjectFromCwd: boolean | undefined; readonly logWebSocketEvents: boolean | undefined; readonly host: string | undefined; readonly port: number | undefined; readonly devUrl: URL | undefined; readonly dryRun: boolean; readonly turboArgs: ReadonlyArray; } const readOptionalBooleanEnv = (name: string): boolean | undefined => { const value = process.env[name]; if (value === undefined) { return undefined; } if (value === "1" || value.toLowerCase() === "true") { return true; } if (value === "0" || value.toLowerCase() === "false") { return false; } return undefined; }; const resolveOptionalBooleanOverride = ( explicitValue: boolean | undefined, envValue: boolean | undefined, ): boolean | undefined => { if (explicitValue === true) { return true; } if (explicitValue === false) { return envValue; } return envValue; }; export function runDevRunnerWithInput(input: DevRunnerCliInput) { return Effect.gen(function* () { const { portOffset, devInstance } = yield* OffsetConfig.asEffect().pipe( Effect.mapError( (cause) => new DevRunnerError({ message: "Failed to read T3CODE_PORT_OFFSET/T3CODE_DEV_INSTANCE configuration.", cause, }), ), ); const { offset, source } = yield* Effect.try({ try: () => resolveOffset({ portOffset, devInstance }), catch: (cause) => new DevRunnerError({ message: cause instanceof Error ? cause.message : String(cause), cause, }), }); const envOverrides = { noBrowser: readOptionalBooleanEnv("T3CODE_NO_BROWSER"), autoBootstrapProjectFromCwd: readOptionalBooleanEnv("T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD"), logWebSocketEvents: readOptionalBooleanEnv("T3CODE_LOG_WS_EVENTS"), }; const { serverOffset, webOffset } = yield* resolveModePortOffsets({ mode: input.mode, startOffset: offset, hasExplicitServerPort: input.port !== undefined, hasExplicitDevUrl: input.devUrl !== undefined, }); const env = yield* createDevRunnerEnv({ mode: input.mode, baseEnv: process.env, serverOffset, webOffset, t3Home: input.t3Home, authToken: input.authToken, noBrowser: resolveOptionalBooleanOverride(input.noBrowser, envOverrides.noBrowser), autoBootstrapProjectFromCwd: resolveOptionalBooleanOverride( input.autoBootstrapProjectFromCwd, envOverrides.autoBootstrapProjectFromCwd, ), logWebSocketEvents: resolveOptionalBooleanOverride( input.logWebSocketEvents, envOverrides.logWebSocketEvents, ), host: input.host, port: input.port, devUrl: input.devUrl, }); const selectionSuffix = serverOffset !== offset || webOffset !== offset ? ` selectedOffset(server=${serverOffset},web=${webOffset})` : ""; yield* Effect.logInfo( `[dev-runner] mode=${input.mode} source=${source}${selectionSuffix} serverPort=${String(env.T3CODE_PORT)} webPort=${String(env.PORT)} baseDir=${String(env.T3CODE_HOME)}`, ); if (input.dryRun) { return; } const child = yield* ChildProcess.make( "turbo", [...MODE_ARGS[input.mode], ...input.turboArgs], { stdin: "inherit", stdout: "inherit", stderr: "inherit", env, extendEnv: false, // Windows needs shell mode to resolve .cmd shims (e.g. bun.cmd). shell: process.platform === "win32", // Keep turbo in the same process group so terminal signals (Ctrl+C) // reach it directly. Effect defaults to detached: true on non-Windows, // which would put turbo in a new group and require manual forwarding. detached: false, forceKillAfter: "1500 millis", }, ); const exitCode = yield* child.exitCode; if (exitCode !== 0) { return yield* new DevRunnerError({ message: `turbo exited with code ${exitCode}`, }); } }).pipe( Effect.mapError((cause) => cause instanceof DevRunnerError ? cause : new DevRunnerError({ message: cause instanceof Error ? cause.message : "dev-runner failed", cause, }), ), ); } const devRunnerCli = Command.make("dev-runner", { mode: Argument.choice("mode", DEV_RUNNER_MODES).pipe( Argument.withDescription("Development mode to run."), ), t3Home: Flag.string("home-dir").pipe( Flag.withDescription("Base directory for all T3 Code data (equivalent to T3CODE_HOME)."), Flag.withFallbackConfig(optionalStringConfig("T3CODE_HOME")), ), authToken: Flag.string("auth-token").pipe( Flag.withDescription("Auth token (forwards to T3CODE_AUTH_TOKEN)."), Flag.withAlias("token"), Flag.withFallbackConfig(optionalStringConfig("T3CODE_AUTH_TOKEN")), ), noBrowser: Flag.boolean("no-browser").pipe( Flag.withDescription("Browser auto-open toggle (equivalent to T3CODE_NO_BROWSER)."), Flag.withFallbackConfig(optionalBooleanConfig("T3CODE_NO_BROWSER")), ), autoBootstrapProjectFromCwd: Flag.boolean("auto-bootstrap-project-from-cwd").pipe( Flag.withDescription( "Auto-bootstrap toggle (equivalent to T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD).", ), Flag.withFallbackConfig(optionalBooleanConfig("T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD")), ), logWebSocketEvents: Flag.boolean("log-websocket-events").pipe( Flag.withDescription("WebSocket event logging toggle (equivalent to T3CODE_LOG_WS_EVENTS)."), Flag.withAlias("log-ws-events"), Flag.withFallbackConfig(optionalBooleanConfig("T3CODE_LOG_WS_EVENTS")), ), host: Flag.string("host").pipe( Flag.withDescription("Server host/interface override (forwards to T3CODE_HOST)."), Flag.withFallbackConfig(optionalStringConfig("T3CODE_HOST")), ), port: Flag.integer("port").pipe( Flag.withSchema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }))), Flag.withDescription("Server port override (forwards to T3CODE_PORT)."), Flag.withFallbackConfig(optionalPortConfig("T3CODE_PORT")), ), devUrl: Flag.string("dev-url").pipe( Flag.withSchema(Schema.URLFromString), Flag.withDescription("Web dev URL override (forwards to VITE_DEV_SERVER_URL)."), Flag.withFallbackConfig(optionalUrlConfig("VITE_DEV_SERVER_URL")), ), dryRun: Flag.boolean("dry-run").pipe( Flag.withDescription("Resolve mode/ports/env and print, but do not spawn turbo."), Flag.withDefault(false), ), turboArgs: Argument.string("turbo-arg").pipe( Argument.withDescription("Additional turbo args (pass after `--`)."), Argument.variadic(), ), }).pipe( Command.withDescription("Run monorepo development modes with deterministic port/env wiring."), Command.withHandler((input) => runDevRunnerWithInput(input)), ); const cliRuntimeLayer = Layer.mergeAll( Logger.layer([Logger.consolePretty()]), NodeServices.layer, NetService.layer, ); const runtimeProgram = Command.run(devRunnerCli, { version: "0.0.0" }).pipe( Effect.scoped, Effect.provide(cliRuntimeLayer), ); if (import.meta.main) { NodeRuntime.runMain(runtimeProgram); }