| ... | ... | @@ -5,7 +5,7 @@ import { homedir } from "node:os"; |
| 5 | 5 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; |
| 6 | 6 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 7 | 7 | import { NetService } from "@t3tools/shared/Net"; |
| 8 | | import { Data, Effect, Layer, Logger, Option, Path } from "effect"; |
| 8 | import { Config, Data, Effect, Hash, Layer, Logger, Option, Path, Schema } from "effect"; |
| 9 | 9 | import { Argument, Command, Flag } from "effect/unstable/cli"; |
| 10 | 10 | import { ChildProcess } from "effect/unstable/process"; |
| 11 | 11 | |
| ... | ... | @@ -14,11 +14,9 @@ const BASE_WEB_PORT = 5733; |
| 14 | 14 | const MAX_HASH_OFFSET = 3000; |
| 15 | 15 | const MAX_PORT = 65535; |
| 16 | 16 | |
| 17 | | export const DEFAULT_DEV_STATE_DIR: Effect.Effect<string, never, Path.Path> = Effect.gen( |
| 18 | | function* () { |
| 19 | | const path = yield* Path.Path; |
| 20 | | return path.join(homedir(), ".t3", "dev"); |
| 21 | | }); |
| 17 | export const DEFAULT_DEV_STATE_DIR = Effect.map(Effect.service(Path.Path), (path) => |
| 18 | path.join(homedir(), ".t3", "dev"), |
| 19 | ); |
| 22 | 20 | |
| 23 | 21 | const MODE_ARGS = { |
| 24 | 22 | dev: [ |
| ... | ... | @@ -36,7 +34,6 @@ const MODE_ARGS = { |
| 36 | 34 | } as const satisfies Record<string, ReadonlyArray<string>>; |
| 37 | 35 | |
| 38 | 36 | type DevMode = keyof typeof MODE_ARGS; |
| 39 | | type EnvOverrides = Record<string, string>; |
| 40 | 37 | type PortAvailabilityCheck<R = never> = (port: number) => Effect.Effect<boolean, never, R>; |
| 41 | 38 | |
| 42 | 39 | const DEV_RUNNER_MODES = Object.keys(MODE_ARGS) as Array<DevMode>; |
| ... | ... | @@ -46,37 +43,52 @@ class DevRunnerError extends Data.TaggedError("DevRunnerError")<{ |
| 46 | 43 | readonly cause?: unknown; |
| 47 | 44 | }> {} |
| 48 | 45 | |
| 49 | | function parseInteger(value: string, envName: string): number { |
| 50 | | const parsed = Number(value); |
| 51 | | if (!Number.isInteger(parsed)) { |
| 52 | | throw new Error(`Invalid ${envName}: ${value}`); |
| 53 | | } |
| 54 | | return parsed; |
| 55 | | } |
| 46 | const optionalStringConfig = (name: string): Config.Config<string | undefined> => |
| 47 | Config.string(name).pipe( |
| 48 | Config.option, |
| 49 | Config.map((value) => Option.getOrUndefined(value)), |
| 50 | ); |
| 51 | const optionalBooleanConfig = (name: string): Config.Config<boolean | undefined> => |
| 52 | Config.boolean(name).pipe( |
| 53 | Config.option, |
| 54 | Config.map((value) => Option.getOrUndefined(value)), |
| 55 | ); |
| 56 | const optionalPortConfig = (name: string): Config.Config<number | undefined> => |
| 57 | Config.port(name).pipe( |
| 58 | Config.option, |
| 59 | Config.map((value) => Option.getOrUndefined(value)), |
| 60 | ); |
| 61 | const optionalIntegerConfig = (name: string): Config.Config<number | undefined> => |
| 62 | Config.int(name).pipe( |
| 63 | Config.option, |
| 64 | Config.map((value) => Option.getOrUndefined(value)), |
| 65 | ); |
| 66 | const optionalUrlConfig = (name: string): Config.Config<URL | undefined> => |
| 67 | Config.url(name).pipe( |
| 68 | Config.option, |
| 69 | Config.map((value) => Option.getOrUndefined(value)), |
| 70 | ); |
| 56 | 71 | |
| 57 | | function hashSeed(seed: string): number { |
| 58 | | let hash = 0x811c9dc5; |
| 59 | | for (let i = 0; i < seed.length; i += 1) { |
| 60 | | hash ^= seed.charCodeAt(i); |
| 61 | | hash = Math.imul(hash, 0x01000193); |
| 62 | | } |
| 63 | | return hash >>> 0; |
| 64 | | } |
| 72 | const OffsetConfig = Config.all({ |
| 73 | portOffset: optionalIntegerConfig("T3CODE_PORT_OFFSET"), |
| 74 | devInstance: optionalStringConfig("T3CODE_DEV_INSTANCE"), |
| 75 | }); |
| 65 | 76 | |
| 66 | | export function resolveOffset(env: NodeJS.ProcessEnv = process.env): { |
| 67 | | readonly offset: number; |
| 68 | | readonly source: string; |
| 69 | | } { |
| 70 | | const explicitOffset = env.T3CODE_PORT_OFFSET?.trim(); |
| 71 | | if (explicitOffset) { |
| 72 | | const parsed = parseInteger(explicitOffset, "T3CODE_PORT_OFFSET"); |
| 73 | | if (parsed < 0) { |
| 74 | | throw new Error(`Invalid T3CODE_PORT_OFFSET: ${explicitOffset}`); |
| 77 | export function resolveOffset(config: { |
| 78 | readonly portOffset: number | undefined; |
| 79 | readonly devInstance: string | undefined; |
| 80 | }): { readonly offset: number; readonly source: string } { |
| 81 | if (config.portOffset !== undefined) { |
| 82 | if (config.portOffset < 0) { |
| 83 | throw new Error(`Invalid T3CODE_PORT_OFFSET: ${config.portOffset}`); |
| 75 | 84 | } |
| 76 | | return { offset: parsed, source: `T3CODE_PORT_OFFSET=${explicitOffset}` }; |
| 85 | return { |
| 86 | offset: config.portOffset, |
| 87 | source: `T3CODE_PORT_OFFSET=${config.portOffset}`, |
| 88 | }; |
| 77 | 89 | } |
| 78 | 90 | |
| 79 | | const seed = env.T3CODE_DEV_INSTANCE?.trim(); |
| 91 | const seed = config.devInstance?.trim(); |
| 80 | 92 | if (!seed) { |
| 81 | 93 | return { offset: 0, source: "default ports" }; |
| 82 | 94 | } |
| ... | ... | @@ -85,32 +97,18 @@ export function resolveOffset(env: NodeJS.ProcessEnv = process.env): { |
| 85 | 97 | return { offset: Number(seed), source: `numeric T3CODE_DEV_INSTANCE=${seed}` }; |
| 86 | 98 | } |
| 87 | 99 | |
| 88 | | const offset = (hashSeed(seed) % MAX_HASH_OFFSET) + 1; |
| 100 | const offset = ((Hash.string(seed) >>> 0) % MAX_HASH_OFFSET) + 1; |
| 89 | 101 | return { offset, source: `hashed T3CODE_DEV_INSTANCE=${seed}` }; |
| 90 | 102 | } |
| 91 | 103 | |
| 92 | | function toBooleanEnvValue(value: string | undefined): string { |
| 93 | | if (value === undefined) return "1"; |
| 94 | | const normalized = value.trim().toLowerCase(); |
| 95 | | if (normalized === "false" || normalized === "0" || normalized === "no") return "0"; |
| 96 | | return "1"; |
| 97 | | } |
| 98 | | |
| 99 | | function resolveStateDir( |
| 100 | | env: NodeJS.ProcessEnv, |
| 101 | | envOverrides: EnvOverrides, |
| 102 | | ): Effect.Effect<string, never, Path.Path> { |
| 104 | function resolveStateDir(stateDir: string | undefined): Effect.Effect<string, never, Path.Path> { |
| 103 | 105 | return Effect.gen(function* () { |
| 104 | 106 | const path = yield* Path.Path; |
| 105 | | const overrideValue = envOverrides?.T3CODE_STATE_DIR?.trim(); |
| 106 | | if (overrideValue) { |
| 107 | | // Resolve relative paths against cwd (monorepo root) before turbo changes directories. |
| 108 | | return path.resolve(overrideValue); |
| 109 | | } |
| 107 | const configured = stateDir?.trim(); |
| 110 | 108 | |
| 111 | | const envValue = env?.T3CODE_STATE_DIR?.trim(); |
| 112 | | if (envValue) { |
| 113 | | return path.resolve(envValue); |
| 109 | if (configured) { |
| 110 | // Resolve relative paths against cwd (monorepo root) before turbo changes directories. |
| 111 | return path.resolve(configured); |
| 114 | 112 | } |
| 115 | 113 | |
| 116 | 114 | return yield* DEFAULT_DEV_STATE_DIR; |
| ... | ... | @@ -119,72 +117,97 @@ function resolveStateDir( |
| 119 | 117 | |
| 120 | 118 | interface CreateDevRunnerEnvInput { |
| 121 | 119 | readonly mode: DevMode; |
| 122 | | readonly env: NodeJS.ProcessEnv; |
| 123 | | readonly offset: number; |
| 124 | | readonly envOverrides: EnvOverrides; |
| 120 | readonly baseEnv: NodeJS.ProcessEnv; |
| 121 | readonly serverOffset: number; |
| 122 | readonly webOffset: number; |
| 123 | readonly stateDir: string | undefined; |
| 124 | readonly authToken: string | undefined; |
| 125 | readonly noBrowser: boolean | undefined; |
| 126 | readonly autoBootstrapProjectFromCwd: boolean | undefined; |
| 127 | readonly logWebSocketEvents: boolean | undefined; |
| 128 | readonly host: string | undefined; |
| 129 | readonly port: number | undefined; |
| 130 | readonly devUrl: URL | undefined; |
| 125 | 131 | } |
| 126 | 132 | |
| 127 | 133 | export function createDevRunnerEnv({ |
| 128 | 134 | mode, |
| 129 | | env, |
| 130 | | offset, |
| 131 | | envOverrides, |
| 132 | | }: CreateDevRunnerEnvInput): Effect.Effect<NodeJS.ProcessEnv, DevRunnerError, Path.Path> { |
| 135 | baseEnv, |
| 136 | serverOffset, |
| 137 | webOffset, |
| 138 | stateDir, |
| 139 | authToken, |
| 140 | noBrowser, |
| 141 | autoBootstrapProjectFromCwd, |
| 142 | logWebSocketEvents, |
| 143 | host, |
| 144 | port, |
| 145 | devUrl, |
| 146 | }: CreateDevRunnerEnvInput): Effect.Effect<NodeJS.ProcessEnv, never, Path.Path> { |
| 133 | 147 | return Effect.gen(function* () { |
| 134 | | const serverPort = BASE_SERVER_PORT + offset; |
| 135 | | const webPort = BASE_WEB_PORT + offset; |
| 148 | const serverPort = port ?? BASE_SERVER_PORT + serverOffset; |
| 149 | const webPort = BASE_WEB_PORT + webOffset; |
| 150 | const resolvedStateDir = yield* resolveStateDir(stateDir); |
| 136 | 151 | |
| 137 | 152 | const output: NodeJS.ProcessEnv = { |
| 138 | | ...env, |
| 153 | ...baseEnv, |
| 139 | 154 | T3CODE_PORT: String(serverPort), |
| 140 | 155 | PORT: String(webPort), |
| 141 | 156 | ELECTRON_RENDERER_PORT: String(webPort), |
| 142 | 157 | VITE_WS_URL: `ws://localhost:${serverPort}`, |
| 143 | | VITE_DEV_SERVER_URL: `http://localhost:${webPort}`, |
| 144 | | ...envOverrides, |
| 158 | VITE_DEV_SERVER_URL: devUrl?.toString() ?? `http://localhost:${webPort}`, |
| 159 | T3CODE_STATE_DIR: resolvedStateDir, |
| 145 | 160 | }; |
| 146 | 161 | |
| 147 | | output.T3CODE_STATE_DIR = yield* resolveStateDir(env, envOverrides); |
| 148 | | |
| 149 | | const parsedServerPort = yield* Effect.try({ |
| 150 | | try: () => { |
| 151 | | const parsed = Number(output.T3CODE_PORT); |
| 152 | | if (!Number.isInteger(parsed) || parsed < 1 || parsed > MAX_PORT) { |
| 153 | | throw new Error( |
| 154 | | `Invalid T3CODE_PORT override: '${String(output.T3CODE_PORT)}'. Expected an integer between 1 and 65535.`, |
| 155 | | ); |
| 156 | | } |
| 157 | | return parsed; |
| 158 | | }, |
| 159 | | catch: (cause) => |
| 160 | | new DevRunnerError({ |
| 161 | | message: cause instanceof Error ? cause.message : String(cause), |
| 162 | | cause, |
| 163 | | }), |
| 164 | | }); |
| 165 | | output.VITE_WS_URL = `ws://localhost:${parsedServerPort}`; |
| 162 | if (host !== undefined) { |
| 163 | output.T3CODE_HOST = host; |
| 164 | } |
| 165 | |
| 166 | if (authToken !== undefined) { |
| 167 | output.T3CODE_AUTH_TOKEN = authToken; |
| 168 | } else { |
| 169 | delete output.T3CODE_AUTH_TOKEN; |
| 170 | } |
| 171 | |
| 172 | if (noBrowser !== undefined) { |
| 173 | output.T3CODE_NO_BROWSER = noBrowser ? "1" : "0"; |
| 174 | } else { |
| 175 | delete output.T3CODE_NO_BROWSER; |
| 176 | } |
| 177 | |
| 178 | if (autoBootstrapProjectFromCwd !== undefined) { |
| 179 | output.T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD = autoBootstrapProjectFromCwd ? "1" : "0"; |
| 180 | } else { |
| 181 | delete output.T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD; |
| 182 | } |
| 183 | |
| 184 | if (logWebSocketEvents !== undefined) { |
| 185 | output.T3CODE_LOG_WS_EVENTS = logWebSocketEvents ? "1" : "0"; |
| 186 | } else { |
| 187 | delete output.T3CODE_LOG_WS_EVENTS; |
| 188 | } |
| 166 | 189 | |
| 167 | | if (mode === "dev" || mode === "dev:server" || mode === "dev:web") { |
| 168 | | // Running server/web in browser mode should not inherit desktop launcher state. |
| 190 | if (mode === "dev") { |
| 169 | 191 | output.T3CODE_MODE = "web"; |
| 170 | | if (!("T3CODE_NO_BROWSER" in envOverrides)) { |
| 171 | | delete output.T3CODE_NO_BROWSER; |
| 172 | | } |
| 173 | | if (!("T3CODE_AUTH_TOKEN" in envOverrides)) { |
| 174 | | delete output.T3CODE_AUTH_TOKEN; |
| 192 | if (logWebSocketEvents === undefined) { |
| 193 | output.T3CODE_LOG_WS_EVENTS = "1"; |
| 175 | 194 | } |
| 176 | 195 | delete output.T3CODE_DESKTOP_WS_URL; |
| 177 | 196 | } |
| 178 | 197 | |
| 179 | | if (mode === "dev" && !output.T3CODE_LOG_WS_EVENTS) { |
| 180 | | output.T3CODE_LOG_WS_EVENTS = "1"; |
| 198 | if (mode === "dev:server" || mode === "dev:web") { |
| 199 | output.T3CODE_MODE = "web"; |
| 200 | delete output.T3CODE_DESKTOP_WS_URL; |
| 181 | 201 | } |
| 182 | 202 | |
| 183 | 203 | return output; |
| 184 | 204 | }); |
| 185 | 205 | } |
| 186 | 206 | |
| 187 | | function portPairForOffset(offset: number): { readonly serverPort: number; readonly webPort: number } { |
| 207 | function portPairForOffset(offset: number): { |
| 208 | readonly serverPort: number; |
| 209 | readonly webPort: number; |
| 210 | } { |
| 188 | 211 | return { |
| 189 | 212 | serverPort: BASE_SERVER_PORT + offset, |
| 190 | 213 | webPort: BASE_WEB_PORT + offset, |
| ... | ... | @@ -211,11 +234,14 @@ export function findFirstAvailableOffset<R = NetService>({ |
| 211 | 234 | checkPortAvailability, |
| 212 | 235 | }: FindFirstAvailableOffsetInput<R>): Effect.Effect<number, DevRunnerError, R> { |
| 213 | 236 | return Effect.gen(function* () { |
| 214 | | const checkPort = (checkPortAvailability ?? defaultCheckPortAvailability) as PortAvailabilityCheck<R>; |
| 237 | const checkPort = (checkPortAvailability ?? |
| 238 | defaultCheckPortAvailability) as PortAvailabilityCheck<R>; |
| 239 | |
| 215 | 240 | for (let candidate = startOffset; ; candidate += 1) { |
| 216 | 241 | const { serverPort, webPort } = portPairForOffset(candidate); |
| 217 | 242 | const serverPortOutOfRange = serverPort > MAX_PORT; |
| 218 | 243 | const webPortOutOfRange = webPort > MAX_PORT; |
| 244 | |
| 219 | 245 | if ( |
| 220 | 246 | (requireServerPort && serverPortOutOfRange) || |
| 221 | 247 | (requireWebPort && webPortOutOfRange) || |
| ... | ... | @@ -251,14 +277,16 @@ export function findFirstAvailableOffset<R = NetService>({ |
| 251 | 277 | interface ResolveModePortOffsetsInput<R = NetService> { |
| 252 | 278 | readonly mode: DevMode; |
| 253 | 279 | readonly startOffset: number; |
| 254 | | readonly envOverrides: EnvOverrides; |
| 280 | readonly hasExplicitServerPort: boolean; |
| 281 | readonly hasExplicitDevUrl: boolean; |
| 255 | 282 | readonly checkPortAvailability?: PortAvailabilityCheck<R>; |
| 256 | 283 | } |
| 257 | 284 | |
| 258 | 285 | export function resolveModePortOffsets<R = NetService>({ |
| 259 | 286 | mode, |
| 260 | 287 | startOffset, |
| 261 | | envOverrides, |
| 288 | hasExplicitServerPort, |
| 289 | hasExplicitDevUrl, |
| 262 | 290 | checkPortAvailability, |
| 263 | 291 | }: ResolveModePortOffsetsInput<R>): Effect.Effect< |
| 264 | 292 | { readonly serverOffset: number; readonly webOffset: number }, |
| ... | ... | @@ -266,14 +294,14 @@ export function resolveModePortOffsets<R = NetService>({ |
| 266 | 294 | R |
| 267 | 295 | > { |
| 268 | 296 | return Effect.gen(function* () { |
| 269 | | const checkPort = (checkPortAvailability ?? defaultCheckPortAvailability) as PortAvailabilityCheck<R>; |
| 270 | | const hasExplicitServerPort = typeof envOverrides.T3CODE_PORT === "string"; |
| 271 | | const hasExplicitDevUrl = typeof envOverrides.VITE_DEV_SERVER_URL === "string"; |
| 297 | const checkPort = (checkPortAvailability ?? |
| 298 | defaultCheckPortAvailability) as PortAvailabilityCheck<R>; |
| 272 | 299 | |
| 273 | 300 | if (mode === "dev:web") { |
| 274 | 301 | if (hasExplicitDevUrl) { |
| 275 | 302 | return { serverOffset: startOffset, webOffset: startOffset }; |
| 276 | 303 | } |
| 304 | |
| 277 | 305 | const webOffset = yield* findFirstAvailableOffset({ |
| 278 | 306 | startOffset, |
| 279 | 307 | requireServerPort: false, |
| ... | ... | @@ -287,6 +315,7 @@ export function resolveModePortOffsets<R = NetService>({ |
| 287 | 315 | if (hasExplicitServerPort) { |
| 288 | 316 | return { serverOffset: startOffset, webOffset: startOffset }; |
| 289 | 317 | } |
| 318 | |
| 290 | 319 | const serverOffset = yield* findFirstAvailableOffset({ |
| 291 | 320 | startOffset, |
| 292 | 321 | requireServerPort: true, |
| ... | ... | @@ -302,71 +331,39 @@ export function resolveModePortOffsets<R = NetService>({ |
| 302 | 331 | requireWebPort: !hasExplicitDevUrl, |
| 303 | 332 | checkPortAvailability: checkPort, |
| 304 | 333 | }); |
| 334 | |
| 305 | 335 | return { serverOffset: sharedOffset, webOffset: sharedOffset }; |
| 306 | 336 | }); |
| 307 | 337 | } |
| 308 | 338 | |
| 309 | | const optionValue = <A>(option: Option.Option<A>): A | undefined => |
| 310 | | Option.isSome(option) ? option.value : undefined; |
| 311 | | |
| 312 | 339 | interface DevRunnerCliInput { |
| 313 | 340 | readonly mode: DevMode; |
| 314 | | readonly stateDir: Option.Option<string>; |
| 315 | | readonly authToken: Option.Option<string>; |
| 316 | | readonly noBrowser: Option.Option<string>; |
| 317 | | readonly autoBootstrapProjectFromCwd: Option.Option<string>; |
| 318 | | readonly logWebSocketEvents: Option.Option<string>; |
| 319 | | readonly host: Option.Option<string>; |
| 320 | | readonly port: Option.Option<string>; |
| 321 | | readonly devUrl: Option.Option<string>; |
| 341 | readonly stateDir: string | undefined; |
| 342 | readonly authToken: string | undefined; |
| 343 | readonly noBrowser: boolean | undefined; |
| 344 | readonly autoBootstrapProjectFromCwd: boolean | undefined; |
| 345 | readonly logWebSocketEvents: boolean | undefined; |
| 346 | readonly host: string | undefined; |
| 347 | readonly port: number | undefined; |
| 348 | readonly devUrl: URL | undefined; |
| 322 | 349 | readonly dryRun: boolean; |
| 323 | 350 | readonly turboArgs: ReadonlyArray<string>; |
| 324 | 351 | } |
| 325 | 352 | |
| 326 | | function envOverridesFromCliInput(input: DevRunnerCliInput): EnvOverrides { |
| 327 | | const envOverrides: EnvOverrides = {}; |
| 328 | | |
| 329 | | const stateDir = optionValue(input.stateDir); |
| 330 | | if (stateDir !== undefined) envOverrides.T3CODE_STATE_DIR = stateDir; |
| 331 | | |
| 332 | | const authToken = optionValue(input.authToken); |
| 333 | | if (authToken !== undefined) envOverrides.T3CODE_AUTH_TOKEN = authToken; |
| 334 | | |
| 335 | | const host = optionValue(input.host); |
| 336 | | if (host !== undefined) envOverrides.T3CODE_HOST = host; |
| 337 | | |
| 338 | | const port = optionValue(input.port); |
| 339 | | if (port !== undefined) envOverrides.T3CODE_PORT = port; |
| 340 | | |
| 341 | | const devUrl = optionValue(input.devUrl); |
| 342 | | if (devUrl !== undefined) envOverrides.VITE_DEV_SERVER_URL = devUrl; |
| 343 | | |
| 344 | | const noBrowser = optionValue(input.noBrowser); |
| 345 | | if (noBrowser !== undefined) { |
| 346 | | envOverrides.T3CODE_NO_BROWSER = toBooleanEnvValue(noBrowser); |
| 347 | | } |
| 348 | | |
| 349 | | const autoBootstrapProjectFromCwd = optionValue(input.autoBootstrapProjectFromCwd); |
| 350 | | if (autoBootstrapProjectFromCwd !== undefined) { |
| 351 | | envOverrides.T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD = toBooleanEnvValue( |
| 352 | | autoBootstrapProjectFromCwd, |
| 353 | | ); |
| 354 | | } |
| 355 | | |
| 356 | | const logWebSocketEvents = optionValue(input.logWebSocketEvents); |
| 357 | | if (logWebSocketEvents !== undefined) { |
| 358 | | envOverrides.T3CODE_LOG_WS_EVENTS = toBooleanEnvValue(logWebSocketEvents); |
| 359 | | } |
| 360 | | |
| 361 | | return envOverrides; |
| 362 | | } |
| 363 | | |
| 364 | 353 | export function runDevRunnerWithInput(input: DevRunnerCliInput) { |
| 365 | 354 | return Effect.gen(function* () { |
| 366 | | const envOverrides = envOverridesFromCliInput(input); |
| 367 | | const turboArgs = Array.from(input.turboArgs); |
| 355 | const { portOffset, devInstance } = yield* OffsetConfig.asEffect().pipe( |
| 356 | Effect.mapError( |
| 357 | (cause) => |
| 358 | new DevRunnerError({ |
| 359 | message: "Failed to read T3CODE_PORT_OFFSET/T3CODE_DEV_INSTANCE configuration.", |
| 360 | cause, |
| 361 | }), |
| 362 | ), |
| 363 | ); |
| 364 | |
| 368 | 365 | const { offset, source } = yield* Effect.try({ |
| 369 | | try: () => resolveOffset(), |
| 366 | try: () => resolveOffset({ portOffset, devInstance }), |
| 370 | 367 | catch: (cause) => |
| 371 | 368 | new DevRunnerError({ |
| 372 | 369 | message: cause instanceof Error ? cause.message : String(cause), |
| ... | ... | @@ -377,46 +374,32 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) { |
| 377 | 374 | const { serverOffset, webOffset } = yield* resolveModePortOffsets({ |
| 378 | 375 | mode: input.mode, |
| 379 | 376 | startOffset: offset, |
| 380 | | envOverrides, |
| 377 | hasExplicitServerPort: input.port !== undefined, |
| 378 | hasExplicitDevUrl: input.devUrl !== undefined, |
| 381 | 379 | }); |
| 382 | 380 | |
| 383 | | const mergedEnvOverrides = { ...envOverrides }; |
| 384 | | if (!("VITE_DEV_SERVER_URL" in mergedEnvOverrides)) { |
| 385 | | const { webPort } = portPairForOffset(webOffset); |
| 386 | | mergedEnvOverrides.PORT = String(webPort); |
| 387 | | mergedEnvOverrides.ELECTRON_RENDERER_PORT = String(webPort); |
| 388 | | mergedEnvOverrides.VITE_DEV_SERVER_URL = `http://localhost:${webPort}`; |
| 389 | | } |
| 390 | | |
| 391 | 381 | const env = yield* createDevRunnerEnv({ |
| 392 | 382 | mode: input.mode, |
| 393 | | env: process.env, |
| 394 | | offset: serverOffset, |
| 395 | | envOverrides: mergedEnvOverrides, |
| 383 | baseEnv: process.env, |
| 384 | serverOffset, |
| 385 | webOffset, |
| 386 | stateDir: input.stateDir, |
| 387 | authToken: input.authToken, |
| 388 | noBrowser: input.noBrowser, |
| 389 | autoBootstrapProjectFromCwd: input.autoBootstrapProjectFromCwd, |
| 390 | logWebSocketEvents: input.logWebSocketEvents, |
| 391 | host: input.host, |
| 392 | port: input.port, |
| 393 | devUrl: input.devUrl, |
| 396 | 394 | }); |
| 397 | 395 | |
| 398 | | const parsedServerPort = Number(env.T3CODE_PORT); |
| 399 | | const parsedWebPort = Number(env.PORT); |
| 400 | | if ( |
| 401 | | !Number.isInteger(parsedServerPort) || |
| 402 | | parsedServerPort < 1 || |
| 403 | | parsedServerPort > MAX_PORT || |
| 404 | | !Number.isInteger(parsedWebPort) || |
| 405 | | parsedWebPort < 1 || |
| 406 | | parsedWebPort > MAX_PORT |
| 407 | | ) { |
| 408 | | return yield* new DevRunnerError({ |
| 409 | | message: `Invalid computed dev ports: server='${String(env.T3CODE_PORT)}' web='${String(env.PORT)}'.`, |
| 410 | | }); |
| 411 | | } |
| 412 | | |
| 413 | 396 | const selectionSuffix = |
| 414 | 397 | serverOffset !== offset || webOffset !== offset |
| 415 | 398 | ? ` selectedOffset(server=${serverOffset},web=${webOffset})` |
| 416 | 399 | : ""; |
| 417 | 400 | |
| 418 | 401 | yield* Effect.logInfo( |
| 419 | | `[dev-runner] mode=${input.mode} source=${source}${selectionSuffix} serverPort=${parsedServerPort} webPort=${parsedWebPort} stateDir=${String(env.T3CODE_STATE_DIR)}`, |
| 402 | `[dev-runner] mode=${input.mode} source=${source}${selectionSuffix} serverPort=${String(env.T3CODE_PORT)} webPort=${String(env.PORT)} stateDir=${String(env.T3CODE_STATE_DIR)}`, |
| 420 | 403 | ); |
| 421 | 404 | |
| 422 | 405 | if (input.dryRun) { |
| ... | ... | @@ -424,7 +407,7 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) { |
| 424 | 407 | } |
| 425 | 408 | |
| 426 | 409 | const command = process.platform === "win32" ? "turbo.cmd" : "turbo"; |
| 427 | | const child = yield* ChildProcess.make(command, [...MODE_ARGS[input.mode], ...turboArgs], { |
| 410 | const child = yield* ChildProcess.make(command, [...MODE_ARGS[input.mode], ...input.turboArgs], { |
| 428 | 411 | stdin: "inherit", |
| 429 | 412 | stdout: "inherit", |
| 430 | 413 | stderr: "inherit", |
| ... | ... | @@ -432,7 +415,7 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) { |
| 432 | 415 | extendEnv: false, |
| 433 | 416 | }); |
| 434 | 417 | |
| 435 | | const exitCode = Number(yield* child.exitCode); |
| 418 | const exitCode = yield* child.exitCode; |
| 436 | 419 | if (exitCode !== 0) { |
| 437 | 420 | return yield* new DevRunnerError({ |
| 438 | 421 | message: `turbo exited with code ${exitCode}`, |
| ... | ... | @@ -456,37 +439,41 @@ const devRunnerCli = Command.make("dev-runner", { |
| 456 | 439 | ), |
| 457 | 440 | stateDir: Flag.string("state-dir").pipe( |
| 458 | 441 | Flag.withDescription("State directory path (forwards to T3CODE_STATE_DIR)."), |
| 459 | | Flag.optional, |
| 442 | Flag.withFallbackConfig(optionalStringConfig("T3CODE_STATE_DIR")), |
| 460 | 443 | ), |
| 461 | 444 | authToken: Flag.string("auth-token").pipe( |
| 462 | 445 | Flag.withDescription("Auth token (forwards to T3CODE_AUTH_TOKEN)."), |
| 463 | 446 | Flag.withAlias("token"), |
| 464 | | Flag.optional, |
| 447 | Flag.withFallbackConfig(optionalStringConfig("T3CODE_AUTH_TOKEN")), |
| 465 | 448 | ), |
| 466 | | noBrowser: Flag.string("no-browser").pipe( |
| 467 | | Flag.withDescription("Browser auto-open toggle. Accepts true/false/1/0/no."), |
| 468 | | Flag.optional, |
| 449 | noBrowser: Flag.boolean("no-browser").pipe( |
| 450 | Flag.withDescription("Browser auto-open toggle (equivalent to T3CODE_NO_BROWSER)."), |
| 451 | Flag.withFallbackConfig(optionalBooleanConfig("T3CODE_NO_BROWSER")), |
| 469 | 452 | ), |
| 470 | | autoBootstrapProjectFromCwd: Flag.string("auto-bootstrap-project-from-cwd").pipe( |
| 471 | | Flag.withDescription("Auto-bootstrap toggle. Accepts true/false/1/0/no."), |
| 472 | | Flag.optional, |
| 453 | autoBootstrapProjectFromCwd: Flag.boolean("auto-bootstrap-project-from-cwd").pipe( |
| 454 | Flag.withDescription( |
| 455 | "Auto-bootstrap toggle (equivalent to T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD).", |
| 456 | ), |
| 457 | Flag.withFallbackConfig(optionalBooleanConfig("T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD")), |
| 473 | 458 | ), |
| 474 | | logWebSocketEvents: Flag.string("log-websocket-events").pipe( |
| 475 | | Flag.withDescription("WebSocket event logging toggle. Accepts true/false/1/0/no."), |
| 459 | logWebSocketEvents: Flag.boolean("log-websocket-events").pipe( |
| 460 | Flag.withDescription("WebSocket event logging toggle (equivalent to T3CODE_LOG_WS_EVENTS)."), |
| 476 | 461 | Flag.withAlias("log-ws-events"), |
| 477 | | Flag.optional, |
| 462 | Flag.withFallbackConfig(optionalBooleanConfig("T3CODE_LOG_WS_EVENTS")), |
| 478 | 463 | ), |
| 479 | 464 | host: Flag.string("host").pipe( |
| 480 | 465 | Flag.withDescription("Server host/interface override (forwards to T3CODE_HOST)."), |
| 481 | | Flag.optional, |
| 466 | Flag.withFallbackConfig(optionalStringConfig("T3CODE_HOST")), |
| 482 | 467 | ), |
| 483 | | port: Flag.string("port").pipe( |
| 468 | port: Flag.integer("port").pipe( |
| 469 | Flag.withSchema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }))), |
| 484 | 470 | Flag.withDescription("Server port override (forwards to T3CODE_PORT)."), |
| 485 | | Flag.optional, |
| 471 | Flag.withFallbackConfig(optionalPortConfig("T3CODE_PORT")), |
| 486 | 472 | ), |
| 487 | 473 | devUrl: Flag.string("dev-url").pipe( |
| 474 | Flag.withSchema(Schema.URLFromString), |
| 488 | 475 | Flag.withDescription("Web dev URL override (forwards to VITE_DEV_SERVER_URL)."), |
| 489 | | Flag.optional, |
| 476 | Flag.withFallbackConfig(optionalUrlConfig("VITE_DEV_SERVER_URL")), |
| 490 | 477 | ), |
| 491 | 478 | dryRun: Flag.boolean("dry-run").pipe( |
| 492 | 479 | Flag.withDescription("Resolve mode/ports/env and print, but do not spawn turbo."), |
| ... | ... | @@ -501,26 +488,16 @@ const devRunnerCli = Command.make("dev-runner", { |
| 501 | 488 | Command.withHandler((input) => runDevRunnerWithInput(input)), |
| 502 | 489 | ); |
| 503 | 490 | |
| 504 | | export function runDevRunner(argv = process.argv) { |
| 505 | | return Command.runWith(devRunnerCli, { version: "0.0.0" })(argv.slice(2)).pipe( |
| 506 | | Effect.mapError((cause) => |
| 507 | | cause instanceof DevRunnerError |
| 508 | | ? cause |
| 509 | | : new DevRunnerError({ |
| 510 | | message: cause instanceof Error ? cause.message : "dev-runner argument parsing failed", |
| 511 | | cause, |
| 512 | | }), |
| 513 | | ), |
| 514 | | ); |
| 515 | | } |
| 516 | | |
| 517 | | const runtimeLayer = Layer.mergeAll( |
| 491 | const cliRuntimeLayer = Layer.mergeAll( |
| 518 | 492 | Logger.layer([Logger.consolePretty()]), |
| 519 | 493 | NodeServices.layer, |
| 520 | 494 | NetService.layer, |
| 521 | 495 | ); |
| 522 | 496 | |
| 523 | | const runtimeProgram = runDevRunner().pipe(Effect.scoped, Effect.provide(runtimeLayer)); |
| 497 | const runtimeProgram = Command.run(devRunnerCli, { version: "0.0.0" }).pipe( |
| 498 | Effect.scoped, |
| 499 | Effect.provide(cliRuntimeLayer), |
| 500 | ); |
| 524 | 501 | |
| 525 | 502 | if (import.meta.main) { |
| 526 | 503 | NodeRuntime.runMain(runtimeProgram); |