552 lines · 17.1 KB · TypeScript
RawDownload
#!/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_DEV_STATE_DIR = Effect.map(Effect.service(Path.Path), (path) =>  path.join(homedir(), ".t3", "dev"),); 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<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>; 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 resolveStateDir(stateDir: string | undefined): Effect.Effect<string, never, Path.Path> {  return Effect.gen(function* () {    const path = yield* Path.Path;    const configured = stateDir?.trim();     if (configured) {      // Resolve relative paths against cwd (monorepo root) before turbo changes directories.      return path.resolve(configured);    }     return yield* DEFAULT_DEV_STATE_DIR;  });} interface CreateDevRunnerEnvInput {  readonly mode: DevMode;  readonly baseEnv: NodeJS.ProcessEnv;  readonly serverOffset: number;  readonly webOffset: number;  readonly stateDir: 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,  stateDir,  authToken,  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 resolvedStateDir = yield* resolveStateDir(stateDir);     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_STATE_DIR: resolvedStateDir,    };     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<NetService> = (port) =>  Effect.gen(function* () {    const net = yield* NetService;    return yield* net.isPortAvailableOnLoopback(port);  }); interface FindFirstAvailableOffsetInput<R = NetService> {  readonly startOffset: number;  readonly requireServerPort: boolean;  readonly requireWebPort: boolean;  readonly checkPortAvailability?: PortAvailabilityCheck<R>;} export function findFirstAvailableOffset<R = 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> {  readonly mode: DevMode;  readonly startOffset: number;  readonly hasExplicitServerPort: boolean;  readonly hasExplicitDevUrl: boolean;  readonly checkPortAvailability?: PortAvailabilityCheck<R>;} export function resolveModePortOffsets<R = 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 stateDir: 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<string>;} 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,      stateDir: input.stateDir,      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)} stateDir=${String(env.T3CODE_STATE_DIR)}`,    );     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,        // 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."),  ),  stateDir: Flag.string("state-dir").pipe(    Flag.withDescription("State directory path (forwards to T3CODE_STATE_DIR)."),    Flag.withFallbackConfig(optionalStringConfig("T3CODE_STATE_DIR")),  ),  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")),  ),

Lines 1–500 of 552.