543 lines · 17.1 KB · TypeScript
RawDownload
#!/usr/bin/env node import * as NodeOS from "node:os"; import * as NodeRuntime from "@effect/platform-node/NodeRuntime";import * as NodeServices from "@effect/platform-node/NodeServices";import * as NetService from "@t3tools/shared/Net";import * as Config from "effect/Config";import * as Data from "effect/Data";import * as Effect from "effect/Effect";import * as Hash from "effect/Hash";import * as Layer from "effect/Layer";import * as Logger from "effect/Logger";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as Schema from "effect/Schema";import { Argument, Command, Flag } from "effect/unstable/cli";import { ChildProcess } from "effect/unstable/process"; import { loadRepoEnv } from "./lib/public-config.ts"; Object.assign(process.env, loadRepoEnv()); const BASE_SERVER_PORT = 13773;const BASE_WEB_PORT = 5733;const MAX_HASH_OFFSET = 3000;const MAX_PORT = 65535;const DESKTOP_DEV_LOOPBACK_HOST = "127.0.0.1";const DEV_PORT_PROBE_HOSTS = ["127.0.0.1", "0.0.0.0", "::1", "::"] as const; export const DEFAULT_T3_HOME = Effect.map(Effect.service(Path.Path), (path) =>  path.join(NodeOS.homedir(), ".t3"),); const MODE_ARGS = {  dev: [    "run",    "--filter=@t3tools/contracts",    "--filter=@t3tools/web",    "--filter=t3",    "--parallel",    "dev",  ],  "dev:server": ["run", "--filter=t3", "dev"],  "dev:web": ["run", "--filter=@t3tools/web", "dev"],  "dev:desktop": ["run", "--filter=@t3tools/desktop", "--filter=@t3tools/web", "dev"],} as const satisfies Record<string, ReadonlyArray<string>>; type DevMode = keyof typeof MODE_ARGS;type PortAvailabilityCheck<R = never> = (port: number) => Effect.Effect<boolean, never, R>; const DEV_RUNNER_MODES = Object.keys(MODE_ARGS) as Array<DevMode>; export function getDevRunnerModeArgs(mode: DevMode): ReadonlyArray<string> {  return MODE_ARGS[mode];} class DevRunnerError extends Data.TaggedError("DevRunnerError")<{  readonly message: string;  readonly cause?: unknown;}> {} const optionalStringConfig = (name: string): Config.Config<string | undefined> =>  Config.string(name).pipe(    Config.option,    Config.map((value) => Option.getOrUndefined(value)),  );const optionalBooleanConfig = (name: string): Config.Config<boolean | undefined> =>  Config.boolean(name).pipe(    Config.option,    Config.map((value) => Option.getOrUndefined(value)),  );const optionalPortConfig = (name: string): Config.Config<number | undefined> =>  Config.port(name).pipe(    Config.option,    Config.map((value) => Option.getOrUndefined(value)),  );const optionalIntegerConfig = (name: string): Config.Config<number | undefined> =>  Config.int(name).pipe(    Config.option,    Config.map((value) => Option.getOrUndefined(value)),  );const optionalUrlConfig = (name: string): Config.Config<URL | undefined> =>  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<string, never, Path.Path> {  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 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,  noBrowser,  autoBootstrapProjectFromCwd,  logWebSocketEvents,  host,  port,  devUrl,}: CreateDevRunnerEnvInput): Effect.Effect<NodeJS.ProcessEnv, never, Path.Path> {  return Effect.gen(function* () {    const serverPort = port ?? BASE_SERVER_PORT + serverOffset;    const webPort = BASE_WEB_PORT + webOffset;    const resolvedBaseDir = yield* resolveBaseDir(t3Home);    const isDesktopMode = mode === "dev:desktop";     const output: NodeJS.ProcessEnv = {      ...baseEnv,      PORT: String(webPort),      VITE_DEV_SERVER_URL:        devUrl?.toString() ??        `http://${isDesktopMode ? DESKTOP_DEV_LOOPBACK_HOST : "localhost"}:${webPort}`,      T3CODE_HOME: resolvedBaseDir,    };     if (!isDesktopMode) {      output.T3CODE_PORT = String(serverPort);      output.VITE_HTTP_URL = `http://localhost:${serverPort}`;      output.VITE_WS_URL = `ws://localhost:${serverPort}`;    } else {      output.T3CODE_PORT = String(serverPort);      output.VITE_HTTP_URL = `http://${DESKTOP_DEV_LOOPBACK_HOST}:${serverPort}`;      output.VITE_WS_URL = `ws://${DESKTOP_DEV_LOOPBACK_HOST}:${serverPort}`;      delete output.T3CODE_MODE;      delete output.T3CODE_NO_BROWSER;      delete output.T3CODE_HOST;    }     if (!isDesktopMode && host !== undefined) {      output.T3CODE_HOST = host;    }     if (!isDesktopMode && noBrowser !== undefined) {      output.T3CODE_NO_BROWSER = noBrowser ? "1" : "0";    } else if (!isDesktopMode) {      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;    }     if (isDesktopMode) {      output.HOST = DESKTOP_DEV_LOOPBACK_HOST;      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,  };} export function checkPortAvailabilityOnHosts<R>(  port: number,  hosts: ReadonlyArray<string>,  canListenOnHost: (port: number, host: string) => Effect.Effect<boolean, never, R>,): Effect.Effect<boolean, never, R> {  return Effect.gen(function* () {    for (const host of hosts) {      if (!(yield* canListenOnHost(port, host))) {        return false;      }    }     return true;  });} const defaultCheckPortAvailability: PortAvailabilityCheck<NetService.NetService> = (port) =>  Effect.gen(function* () {    const net = yield* NetService.NetService;    return yield* checkPortAvailabilityOnHosts(port, DEV_PORT_PROBE_HOSTS, (candidatePort, host) =>      net.canListenOnHost(candidatePort, host),    );  }); interface FindFirstAvailableOffsetInput<R = NetService.NetService> {  readonly startOffset: number;  readonly requireServerPort: boolean;  readonly requireWebPort: boolean;  readonly checkPortAvailability?: PortAvailabilityCheck<R>;} export function findFirstAvailableOffset<R = NetService.NetService>({  startOffset,  requireServerPort,  requireWebPort,  checkPortAvailability,}: FindFirstAvailableOffsetInput<R>): Effect.Effect<number, DevRunnerError, R> {  return Effect.gen(function* () {    const checkPort = (checkPortAvailability ??      defaultCheckPortAvailability) as PortAvailabilityCheck<R>;     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<Effect.Effect<boolean, never, R>> = [];      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<R = NetService.NetService> {  readonly mode: DevMode;  readonly startOffset: number;  readonly hasExplicitServerPort: boolean;  readonly hasExplicitDevUrl: boolean;  readonly checkPortAvailability?: PortAvailabilityCheck<R>;} export function resolveModePortOffsets<R = NetService.NetService>({  mode,  startOffset,  hasExplicitServerPort,  hasExplicitDevUrl,  checkPortAvailability,}: ResolveModePortOffsetsInput<R>): Effect.Effect<  { readonly serverOffset: number; readonly webOffset: number },  DevRunnerError,  R> {  return Effect.gen(function* () {    const checkPort = (checkPortAvailability ??      defaultCheckPortAvailability) as PortAvailabilityCheck<R>;     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 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 runArgs: ReadonlyArray<string>;} export function runDevRunnerWithInput(input: DevRunnerCliInput) {  return Effect.gen(function* () {    const { portOffset, devInstance } = yield* OffsetConfig.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 { 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,      noBrowser: input.noBrowser,      autoBootstrapProjectFromCwd: input.autoBootstrapProjectFromCwd,      logWebSocketEvents: input.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("vp", [...MODE_ARGS[input.mode], ...input.runArgs], {      stdin: "inherit",      stdout: "inherit",      stderr: "inherit",      env,      extendEnv: false,      // Windows needs shell mode to resolve .cmd shims (e.g. vp.cmd).      shell: process.platform === "win32",      // Keep Vite+ in the same process group so terminal signals (Ctrl+C)      // reach it directly. Effect defaults to detached: true on non-Windows,      // which would put the runner 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: `vp run 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")),  ),  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)."),

Lines 1–500 of 543.