From 0c4c027e92bae27716d94d14e3531c390d9ecef1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 25 Feb 2026 20:21:20 -0800 Subject: [PATCH] small config change --- apps/server/src/config.ts | 2 + apps/server/src/main.test.ts | 69 +++++++++++++++++--------------- apps/server/src/main.ts | 43 +++++++++++++++++++- apps/server/src/wsServer.test.ts | 7 ++-- apps/server/src/wsServer.ts | 38 ++++++++---------- 5 files changed, 99 insertions(+), 60 deletions(-) diff --git a/apps/server/src/config.ts b/apps/server/src/config.ts index 1d817b2a4ded0ca3aefa0e1d73b88788011ce868..ba523e3b52d141e090a1aca3c4dec70e3a2f9e65 100644 --- a/apps/server/src/config.ts +++ b/apps/server/src/config.ts @@ -16,6 +16,8 @@ export interface ServerConfigShape { readonly devUrl: URL | undefined; readonly noBrowser: boolean; readonly authToken: string | undefined; + readonly autoBootstrapProjectFromCwd: boolean; + readonly logWebSocketEvents: boolean; } export class ServerConfig extends ServiceMap.Service()( diff --git a/apps/server/src/main.test.ts b/apps/server/src/main.test.ts index e14141fb7dead5f5e6cb9baddbc1e24e7e9ee7c5..5ddde930b2c7c21fe04fc49c1037361d5814dc99 100644 --- a/apps/server/src/main.test.ts +++ b/apps/server/src/main.test.ts @@ -10,20 +10,18 @@ import { beforeEach } from "vitest"; import { CliConfig, t3Cli, type CliConfigShape } from "./main"; import { NetService, ServerConfig, type ServerConfigShape } from "./config"; import { Open, type OpenShape } from "./open"; -import { Server, type ServerShape, type ServerOptions } from "./wsServer"; +import { Server, type ServerShape } from "./wsServer"; const start = vi.fn(() => undefined); const stop = vi.fn(() => undefined); let resolvedConfig: ServerConfigShape | null = null; -const createServer = vi.fn((_: ServerOptions | undefined) => - Effect.acquireRelease( - Effect.gen(function* () { - resolvedConfig = yield* ServerConfig; - start(); - return {} as unknown as Http.Server; - }), - () => Effect.sync(() => stop()), - ), +const serverStart = Effect.acquireRelease( + Effect.gen(function* () { + resolvedConfig = yield* ServerConfig; + start(); + return {} as unknown as Http.Server; + }), + () => Effect.sync(() => stop()), ); const findAvailablePort = vi.fn((preferred: number) => Effect.succeed(preferred)); @@ -37,7 +35,7 @@ const testLayer = Layer.mergeAll( findAvailablePort, }), Layer.succeed(Server, { - createServer, + start: serverStart, stopSignal: Effect.void, } satisfies ServerShape), Layer.succeed(Open, { @@ -60,16 +58,6 @@ beforeEach(() => { resolvedConfig = null; start.mockImplementation(() => undefined); stop.mockImplementation(() => undefined); - createServer.mockImplementation((_: ServerOptions | undefined) => - Effect.acquireRelease( - Effect.gen(function* () { - resolvedConfig = yield* ServerConfig; - start(); - return {} as unknown as Http.Server; - }), - () => Effect.sync(() => stop()), - ), - ); findAvailablePort.mockImplementation((preferred: number) => Effect.succeed(preferred)); }); @@ -92,7 +80,7 @@ it.layer(testLayer)("server cli", (it) => { "auth-secret", ]); - assert.equal(createServer.mock.calls.length, 1); + assert.equal(start.mock.calls.length, 1); assert.equal(resolvedConfig?.mode, "desktop"); assert.equal(resolvedConfig?.port, 4010); assert.equal(resolvedConfig?.host, "0.0.0.0"); @@ -100,7 +88,8 @@ it.layer(testLayer)("server cli", (it) => { assert.equal(resolvedConfig?.devUrl?.toString(), "http://127.0.0.1:5173/"); assert.equal(resolvedConfig?.noBrowser, true); assert.equal(resolvedConfig?.authToken, "auth-secret"); - assert.equal(start.mock.calls.length, 1); + assert.equal(resolvedConfig?.autoBootstrapProjectFromCwd, false); + assert.equal(resolvedConfig?.logWebSocketEvents, true); assert.equal(stop.mock.calls.length, 1); }), ); @@ -109,7 +98,7 @@ it.layer(testLayer)("server cli", (it) => { Effect.gen(function* () { yield* runCli(["--token", "token-secret"]); - assert.equal(createServer.mock.calls.length, 1); + assert.equal(start.mock.calls.length, 1); assert.equal(resolvedConfig?.authToken, "token-secret"); }), ); @@ -126,7 +115,7 @@ it.layer(testLayer)("server cli", (it) => { T3CODE_AUTH_TOKEN: "env-token", }); - assert.equal(createServer.mock.calls.length, 1); + assert.equal(start.mock.calls.length, 1); assert.equal(resolvedConfig?.mode, "desktop"); assert.equal(resolvedConfig?.port, 4999); assert.equal(resolvedConfig?.host, "100.88.10.4"); @@ -134,6 +123,8 @@ it.layer(testLayer)("server cli", (it) => { assert.equal(resolvedConfig?.devUrl?.toString(), "http://localhost:5173/"); assert.equal(resolvedConfig?.noBrowser, true); assert.equal(resolvedConfig?.authToken, "env-token"); + assert.equal(resolvedConfig?.autoBootstrapProjectFromCwd, false); + assert.equal(resolvedConfig?.logWebSocketEvents, true); assert.equal(findAvailablePort.mock.calls.length, 0); }), ); @@ -147,7 +138,7 @@ it.layer(testLayer)("server cli", (it) => { }); assert.deepStrictEqual(findAvailablePort.mock.calls, [[3773]]); - assert.equal(createServer.mock.calls.length, 1); + assert.equal(start.mock.calls.length, 1); assert.equal(resolvedConfig?.mode, "web"); assert.equal(resolvedConfig?.port, 4666); assert.equal(resolvedConfig?.host, undefined); @@ -160,7 +151,7 @@ it.layer(testLayer)("server cli", (it) => { T3CODE_NO_BROWSER: "false", }); - assert.equal(createServer.mock.calls.length, 1); + assert.equal(start.mock.calls.length, 1); assert.equal(resolvedConfig?.noBrowser, true); }), ); @@ -171,7 +162,7 @@ it.layer(testLayer)("server cli", (it) => { yield* runCli([]); assert.deepStrictEqual(findAvailablePort.mock.calls, [[3773]]); - assert.equal(createServer.mock.calls.length, 1); + assert.equal(start.mock.calls.length, 1); assert.equal(resolvedConfig?.port, 5444); assert.equal(resolvedConfig?.mode, "web"); }), @@ -185,7 +176,7 @@ it.layer(testLayer)("server cli", (it) => { }); assert.equal(findAvailablePort.mock.calls.length, 0); - assert.equal(createServer.mock.calls.length, 1); + assert.equal(start.mock.calls.length, 1); assert.equal(resolvedConfig?.port, 3773); assert.equal(resolvedConfig?.host, "127.0.0.1"); assert.equal(resolvedConfig?.mode, "desktop"); @@ -199,17 +190,31 @@ it.layer(testLayer)("server cli", (it) => { T3CODE_NO_BROWSER: "true", }); - assert.equal(createServer.mock.calls.length, 1); + assert.equal(start.mock.calls.length, 1); assert.equal(resolvedConfig?.mode, "desktop"); assert.equal(resolvedConfig?.host, "0.0.0.0"); }), ); + it.effect("supports CLI and env for bootstrap/log websocket toggles", () => + Effect.gen(function* () { + yield* runCli(["--auto-bootstrap-project-from-cwd"], { + T3CODE_MODE: "desktop", + T3CODE_LOG_WS_EVENTS: "false", + T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD: "false", + T3CODE_NO_BROWSER: "true", + }); + + assert.equal(start.mock.calls.length, 1); + assert.equal(resolvedConfig?.autoBootstrapProjectFromCwd, true); + assert.equal(resolvedConfig?.logWebSocketEvents, false); + }), + ); + it.effect("does not start server for invalid --mode values", () => Effect.gen(function* () { yield* runCli(["--mode", "invalid"]); - assert.equal(createServer.mock.calls.length, 0); assert.equal(start.mock.calls.length, 0); assert.equal(stop.mock.calls.length, 0); }), @@ -219,7 +224,6 @@ it.layer(testLayer)("server cli", (it) => { Effect.gen(function* () { yield* runCli(["--dev-url", "not-a-url"]).pipe(Effect.catch(() => Effect.void)); - assert.equal(createServer.mock.calls.length, 0); assert.equal(start.mock.calls.length, 0); assert.equal(stop.mock.calls.length, 0); }), @@ -230,7 +234,6 @@ it.layer(testLayer)("server cli", (it) => { yield* runCli(["--port", "70000"]); // effect/unstable/cli renders help/errors for parse failures and returns success. - assert.equal(createServer.mock.calls.length, 0); assert.equal(start.mock.calls.length, 0); assert.equal(stop.mock.calls.length, 0); }), diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 0dd164cb99478c4eb8bfa3fe93ce55f8b770e774..0358959352b70c6055b90f14ffc69282dfeb06a2 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -28,6 +28,8 @@ interface CliInput { readonly devUrl: Option.Option; readonly noBrowser: Option.Option; readonly authToken: Option.Option; + readonly autoBootstrapProjectFromCwd: Option.Option; + readonly logWebSocketEvents: Option.Option; } export interface CliConfigShape { @@ -79,6 +81,14 @@ const CliEnvConfig = Config.all({ 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), + ), }); const ServerConfigLive = (input: CliInput) => @@ -119,6 +129,18 @@ const ServerConfigLive = (input: CliInput) => onNone: () => env.noBrowser ?? mode === "desktop", }); const authToken = Option.getOrUndefined(input.authToken) ?? env.authToken; + const autoBootstrapProjectFromCwd = Option.match(input.autoBootstrapProjectFromCwd, { + // effect/cli boolean flags parse to `false` when absent; in that case + // we still want env/mode fallbacks to apply. + onSome: (value) => (value ? true : (env.autoBootstrapProjectFromCwd ?? mode === "web")), + onNone: () => env.autoBootstrapProjectFromCwd ?? mode === "web", + }); + const logWebSocketEvents = Option.match(input.logWebSocketEvents, { + // effect/cli boolean flags parse to `false` when absent; in that case + // we still want env/dev fallbacks to apply. + onSome: (value) => (value ? true : (env.logWebSocketEvents ?? Boolean(devUrl))), + onNone: () => env.logWebSocketEvents ?? Boolean(devUrl), + }); const staticDir = devUrl ? undefined : yield* cliConfig.resolveStaticDir; const { join } = yield* Path.Path; const keybindingsConfigPath = join(stateDir, "keybindings.json"); @@ -138,6 +160,8 @@ const ServerConfigLive = (input: CliInput) => devUrl, noBrowser, authToken, + autoBootstrapProjectFromCwd, + logWebSocketEvents, } satisfies ServerConfigShape; }), ); @@ -159,7 +183,7 @@ const formatHostForUrl = (host: string): string => const makeServerProgram = (input: CliInput) => Effect.gen(function* () { const cliConfig = yield* CliConfig; - const { createServer, stopSignal } = yield* Server; + const { start, stopSignal } = yield* Server; const openDeps = yield* Open; yield* cliConfig.fixPath; @@ -174,7 +198,7 @@ const makeServerProgram = (input: CliInput) => ); } - yield* createServer(); + yield* start; const localUrl = `http://localhost:${config.port}`; const bindUrl = @@ -240,6 +264,19 @@ const authTokenFlag = Flag.string("auth-token").pipe( Flag.withAlias("token"), 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, +); export const t3Cli = Command.make("t3", { mode: modeFlag, @@ -249,6 +286,8 @@ export const t3Cli = Command.make("t3", { devUrl: devUrlFlag, noBrowser: noBrowserFlag, authToken: authTokenFlag, + autoBootstrapProjectFromCwd: autoBootstrapProjectFromCwdFlag, + logWebSocketEvents: logWebSocketEventsFlag, }).pipe( Command.withDescription("Run the T3 Code server."), Command.withHandler((input) => Effect.scoped(makeServerProgram(input))), diff --git a/apps/server/src/wsServer.test.ts b/apps/server/src/wsServer.test.ts index b0057acd69b00436fd921dca23d98f79489c2264..ab67bdbd7c0f3a899623760fceeb6ba276ce76a0 100644 --- a/apps/server/src/wsServer.test.ts +++ b/apps/server/src/wsServer.test.ts @@ -338,6 +338,7 @@ describe("WebSocket Server", () => { persistenceLayer?: Layer.Layer; cwd?: string; autoBootstrapProjectFromCwd?: boolean; + logWebSocketEvents?: boolean; devUrl?: string; authToken?: string; stateDir?: string; @@ -368,6 +369,8 @@ describe("WebSocket Server", () => { devUrl: options.devUrl ? new URL(options.devUrl) : undefined, noBrowser: true, authToken: options.authToken, + autoBootstrapProjectFromCwd: options.autoBootstrapProjectFromCwd ?? false, + logWebSocketEvents: options.logWebSocketEvents ?? Boolean(options.devUrl), } satisfies ServerConfigShape); const infrastructureLayer = providerLayer.pipe(Layer.provideMerge(persistenceLayer)); const runtimeOverrides = Layer.mergeAll( @@ -397,9 +400,7 @@ describe("WebSocket Server", () => { try { const runtime = await Effect.runPromise( - createServer({ - autoBootstrapProjectFromCwd: options.autoBootstrapProjectFromCwd ?? false, - }).pipe(Effect.provide(runtimeServices), Scope.provide(scope)), + createServer().pipe(Effect.provide(runtimeServices), Scope.provide(scope)), ); serverScope = scope; return runtime; diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts index b79f1bd65a0ce12e565fc9e502917a69870837ed..ed61fe39e81af94ad57b569dcdb6abb640284554 100644 --- a/apps/server/src/wsServer.ts +++ b/apps/server/src/wsServer.ts @@ -49,15 +49,12 @@ const MIME_TYPES: Record = { ".map": "application/json", }; -export interface ServerOptions { - autoBootstrapProjectFromCwd?: boolean | undefined; - logWebSocketEvents?: boolean | undefined; -} - export interface ServerShape { - readonly createServer: ( - options?: ServerOptions, - ) => Effect.Effect; + readonly start: Effect.Effect< + http.Server, + unknown, + Scope.Scope | ServerRuntimeServices | ServerConfig + >; readonly stopSignal: Effect.Effect; } @@ -82,14 +79,6 @@ function rejectUpgrade(socket: Duplex, statusCode: number, message: string): voi ); } -function parseBooleanEnv(value: string | undefined): boolean | undefined { - if (value === undefined) return undefined; - const normalized = value.trim().toLowerCase(); - if (["1", "true", "yes", "on"].includes(normalized)) return true; - if (["0", "false", "no", "off"].includes(normalized)) return false; - return undefined; -} - function websocketRawToString(raw: unknown): string | null { if (typeof raw === "string") { return raw; @@ -153,17 +142,22 @@ class RouteRequestError extends Schema.TaggedErrorClass()("Ro message: Schema.String, }) {} -export const createServer = Effect.fn(function* (options: ServerOptions = {}): Effect.fn.Return< +export const createServer = Effect.fn(function* (): Effect.fn.Return< http.Server, unknown, // TODO: This should not be unknown Scope.Scope | ServerRuntimeServices | ServerConfig > { const serverConfig = yield* ServerConfig; - const { port, cwd, staticDir, devUrl, authToken, mode, host } = serverConfig; const { - logWebSocketEvents = parseBooleanEnv(process.env.T3CODE_LOG_WS_EVENTS) ?? Boolean(devUrl), - autoBootstrapProjectFromCwd = mode === "web", - } = options; + port, + cwd, + staticDir, + devUrl, + authToken, + host, + logWebSocketEvents, + autoBootstrapProjectFromCwd, + } = serverConfig; const gitManager = yield* GitManager; const terminalManager = yield* TerminalManager; @@ -599,6 +593,6 @@ export const createServer = Effect.fn(function* (options: ServerOptions = {}): E }); export const ServerLive = Layer.succeed(Server, { - createServer, + start: createServer(), stopSignal: Effect.never, } satisfies ServerShape); -- 2.54.0