| author | |
| committer | |
| log | 448c48cd109bee06abecffddb3fc7130a50e3666 |
| tree | 5289bbe4b091069e827a0949efeed18b401b19e6 |
| parent | 470f282819079be867cdc1eaa52b0eeb632c14e7 |
4 files changed, 260 insertions(+), 38 deletions(-)
REMOTE.md created+65| ... | ... | @@ -0,0 +1,65 @@ |
| 1 | # Remote Access Setup | |
| 2 | ||
| 3 | Use this when you want to open T3 Code from another device (phone, tablet, another laptop). | |
| 4 | ||
| 5 | ## CLI ↔ Env option map | |
| 6 | ||
| 7 | The T3 Code CLI accepts the following configuration options, available either as CLI flags or environment variables: | |
| 8 | ||
| 9 | | CLI flag | Env var | Notes | | |
| 10 | | ----------------------- | --------------------- | ---------------------------------- | | |
| 11 | | `--mode <web\|desktop>` | `T3CODE_MODE` | Runtime mode. | | |
| 12 | | `--port <number>` | `T3CODE_PORT` | HTTP/WebSocket port. | | |
| 13 | | `--host <address>` | `T3CODE_HOST` | Bind interface/address. | | |
| 14 | | `--state-dir <path>` | `T3CODE_STATE_DIR` | State directory. | | |
| 15 | | `--dev-url <url>` | `VITE_DEV_SERVER_URL` | Dev web URL redirect/proxy target. | | |
| 16 | | `--no-browser` | `T3CODE_NO_BROWSER` | Disable auto-open browser. | | |
| 17 | | `--auth-token <token>` | `T3CODE_AUTH_TOKEN` | WebSocket auth token. | | |
| 18 | ||
| 19 | > TIP: Use the `--help` flag to see all available options and their descriptions. | |
| 20 | ||
| 21 | ## Security First | |
| 22 | ||
| 23 | - Always set `--auth-token` before exposing the server outside localhost. | |
| 24 | - Treat the token like a password. | |
| 25 | - Prefer binding to trusted interfaces (LAN IP or Tailnet IP) instead of opening all interfaces unless needed. | |
| 26 | ||
| 27 | ## 1) Build + run server for remote access | |
| 28 | ||
| 29 | Remote access should use the built web app (not local Vite redirect mode). | |
| 30 | ||
| 31 | ```bash | |
| 32 | bun run build | |
| 33 | TOKEN="$(openssl rand -hex 24)" | |
| 34 | bun run --cwd apps/server start -- --host 0.0.0.0 --port 3773 --auth-token "$TOKEN" --no-browser | |
| 35 | ``` | |
| 36 | ||
| 37 | Then open on your phone: | |
| 38 | ||
| 39 | `http://<your-machine-ip>:3773` | |
| 40 | ||
| 41 | Example: | |
| 42 | ||
| 43 | `http://192.168.1.42:3773` | |
| 44 | ||
| 45 | Notes: | |
| 46 | ||
| 47 | - `--host 0.0.0.0` listens on all IPv4 interfaces. | |
| 48 | - `--no-browser` prevents local auto-open, which is usually better for headless/remote sessions. | |
| 49 | - Ensure your OS firewall allows inbound TCP on the selected port. | |
| 50 | ||
| 51 | ## 2) Tailnet / Tailscale access | |
| 52 | ||
| 53 | If you use Tailscale, you can bind directly to your Tailnet address. | |
| 54 | ||
| 55 | ```bash | |
| 56 | TAILNET_IP="$(tailscale ip -4)" | |
| 57 | TOKEN="$(openssl rand -hex 24)" | |
| 58 | bun run --cwd apps/server start -- --host "$(tailscale ip -4)" --port 3773 --auth-token "$TOKEN" --no-browser | |
| 59 | ``` | |
| 60 | ||
| 61 | Open from any device in your tailnet: | |
| 62 | ||
| 63 | `http://<tailnet-ip>:3773` | |
| 64 | ||
| 65 | You can also bind `--host 0.0.0.0` and connect through the Tailnet IP, but binding directly to the Tailnet IP limits exposure. |
apps/server/src/index.ts+2-2| ... | ... | @@ -3,7 +3,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | 3 | import * as Effect from "effect/Effect"; |
| 4 | 4 | import * as Layer from "effect/Layer"; |
| 5 | 5 | |
| 6 | import { CliConfig, makeCliCommand } from "./main"; | |
| 6 | import { CliConfig, t3Cli } from "./main"; | |
| 7 | 7 | import { OpenLive } from "./open"; |
| 8 | 8 | import { Command } from "effect/unstable/cli"; |
| 9 | 9 | import { version } from "../package.json" with { type: "json" }; |
| ... | ... | @@ -18,4 +18,4 @@ const RuntimeLayer = Layer.empty.pipe( |
| 18 | 18 | Layer.provideMerge(NodeServices.layer), |
| 19 | 19 | ); |
| 20 | 20 | |
| 21 | Command.run(makeCliCommand(), { version }).pipe(Effect.provide(RuntimeLayer), NodeRuntime.runMain); | |
| 21 | Command.run(t3Cli, { version }).pipe(Effect.provide(RuntimeLayer), NodeRuntime.runMain); |
apps/server/src/main.test.ts+104-6| ... | ... | @@ -7,7 +7,7 @@ import * as Layer from "effect/Layer"; |
| 7 | 7 | import * as Command from "effect/unstable/cli/Command"; |
| 8 | 8 | import { beforeEach } from "vitest"; |
| 9 | 9 | |
| 10 | import { CliConfig, makeCliCommand, type CliConfigShape } from "./main"; | |
| 10 | import { CliConfig, t3Cli, type CliConfigShape } from "./main"; | |
| 11 | 11 | import { NetService, ServerConfig, type ServerConfigShape } from "./config"; |
| 12 | 12 | import { Open, type OpenShape } from "./open"; |
| 13 | 13 | import { Server, type ServerShape, type ServerOptions } from "./wsServer"; |
| ... | ... | @@ -51,7 +51,7 @@ const runCli = ( |
| 51 | 51 | args: ReadonlyArray<string>, |
| 52 | 52 | env: Record<string, string> = { T3CODE_NO_BROWSER: "true" }, |
| 53 | 53 | ) => |
| 54 | Command.runWith(makeCliCommand(), { version: "0.0.0-test" })(args).pipe( | |
| 54 | Command.runWith(t3Cli, { version: "0.0.0-test" })(args).pipe( | |
| 55 | 55 | Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env }))), |
| 56 | 56 | ); |
| 57 | 57 | |
| ... | ... | @@ -74,33 +74,97 @@ beforeEach(() => { |
| 74 | 74 | }); |
| 75 | 75 | |
| 76 | 76 | it.layer(testLayer)("server cli", (it) => { |
| 77 | it.effect("parses --port and --token and wires scoped start/stop", () => | |
| 77 | it.effect("parses all CLI flags and wires scoped start/stop", () => | |
| 78 | 78 | Effect.gen(function* () { |
| 79 | yield* runCli(["--port", "4010", "--token", "secret"]); | |
| 79 | yield* runCli([ | |
| 80 | "--mode", | |
| 81 | "desktop", | |
| 82 | "--port", | |
| 83 | "4010", | |
| 84 | "--host", | |
| 85 | "0.0.0.0", | |
| 86 | "--state-dir", | |
| 87 | "/tmp/t3-cli-state", | |
| 88 | "--dev-url", | |
| 89 | "http://127.0.0.1:5173", | |
| 90 | "--no-browser", | |
| 91 | "--auth-token", | |
| 92 | "auth-secret", | |
| 93 | ]); | |
| 80 | 94 | |
| 81 | 95 | assert.equal(createServer.mock.calls.length, 1); |
| 96 | assert.equal(resolvedConfig?.mode, "desktop"); | |
| 82 | 97 | assert.equal(resolvedConfig?.port, 4010); |
| 83 | assert.equal(resolvedConfig?.authToken, "secret"); | |
| 98 | assert.equal(resolvedConfig?.host, "0.0.0.0"); | |
| 99 | assert.equal(resolvedConfig?.stateDir, "/tmp/t3-cli-state"); | |
| 100 | assert.equal(resolvedConfig?.devUrl?.toString(), "http://127.0.0.1:5173/"); | |
| 101 | assert.equal(resolvedConfig?.noBrowser, true); | |
| 102 | assert.equal(resolvedConfig?.authToken, "auth-secret"); | |
| 84 | 103 | assert.equal(start.mock.calls.length, 1); |
| 85 | 104 | assert.equal(stop.mock.calls.length, 1); |
| 86 | 105 | }), |
| 87 | 106 | ); |
| 88 | 107 | |
| 108 | it.effect("supports --token as an alias for --auth-token", () => | |
| 109 | Effect.gen(function* () { | |
| 110 | yield* runCli(["--token", "token-secret"]); | |
| 111 | ||
| 112 | assert.equal(createServer.mock.calls.length, 1); | |
| 113 | assert.equal(resolvedConfig?.authToken, "token-secret"); | |
| 114 | }), | |
| 115 | ); | |
| 116 | ||
| 89 | 117 | it.effect("uses env fallbacks when flags are not provided", () => |
| 90 | 118 | Effect.gen(function* () { |
| 91 | 119 | yield* runCli([], { |
| 92 | T3CODE_NO_BROWSER: "true", | |
| 120 | T3CODE_MODE: "desktop", | |
| 93 | 121 | T3CODE_PORT: "4999", |
| 122 | T3CODE_HOST: "100.88.10.4", | |
| 123 | T3CODE_STATE_DIR: "/tmp/t3-env-state", | |
| 124 | VITE_DEV_SERVER_URL: "http://localhost:5173", | |
| 125 | T3CODE_NO_BROWSER: "true", | |
| 94 | 126 | T3CODE_AUTH_TOKEN: "env-token", |
| 95 | 127 | }); |
| 96 | 128 | |
| 97 | 129 | assert.equal(createServer.mock.calls.length, 1); |
| 130 | assert.equal(resolvedConfig?.mode, "desktop"); | |
| 98 | 131 | assert.equal(resolvedConfig?.port, 4999); |
| 132 | assert.equal(resolvedConfig?.host, "100.88.10.4"); | |
| 133 | assert.equal(resolvedConfig?.stateDir, "/tmp/t3-env-state"); | |
| 134 | assert.equal(resolvedConfig?.devUrl?.toString(), "http://localhost:5173/"); | |
| 135 | assert.equal(resolvedConfig?.noBrowser, true); | |
| 99 | 136 | assert.equal(resolvedConfig?.authToken, "env-token"); |
| 100 | 137 | assert.equal(findAvailablePort.mock.calls.length, 0); |
| 101 | 138 | }), |
| 102 | 139 | ); |
| 103 | 140 | |
| 141 | it.effect("prefers --mode over T3CODE_MODE", () => | |
| 142 | Effect.gen(function* () { | |
| 143 | findAvailablePort.mockImplementation((_preferred: number) => Effect.succeed(4666)); | |
| 144 | yield* runCli(["--mode", "web"], { | |
| 145 | T3CODE_MODE: "desktop", | |
| 146 | T3CODE_NO_BROWSER: "true", | |
| 147 | }); | |
| 148 | ||
| 149 | assert.deepStrictEqual(findAvailablePort.mock.calls, [[3773]]); | |
| 150 | assert.equal(createServer.mock.calls.length, 1); | |
| 151 | assert.equal(resolvedConfig?.mode, "web"); | |
| 152 | assert.equal(resolvedConfig?.port, 4666); | |
| 153 | assert.equal(resolvedConfig?.host, undefined); | |
| 154 | }), | |
| 155 | ); | |
| 156 | ||
| 157 | it.effect("prefers --no-browser over T3CODE_NO_BROWSER", () => | |
| 158 | Effect.gen(function* () { | |
| 159 | yield* runCli(["--no-browser"], { | |
| 160 | T3CODE_NO_BROWSER: "false", | |
| 161 | }); | |
| 162 | ||
| 163 | assert.equal(createServer.mock.calls.length, 1); | |
| 164 | assert.equal(resolvedConfig?.noBrowser, true); | |
| 165 | }), | |
| 166 | ); | |
| 167 | ||
| 104 | 168 | it.effect("uses dynamic port discovery in web mode when port is omitted", () => |
| 105 | 169 | Effect.gen(function* () { |
| 106 | 170 | findAvailablePort.mockImplementation((_preferred: number) => Effect.succeed(5444)); |
| ... | ... | @@ -123,7 +187,41 @@ it.layer(testLayer)("server cli", (it) => { |
| 123 | 187 | assert.equal(findAvailablePort.mock.calls.length, 0); |
| 124 | 188 | assert.equal(createServer.mock.calls.length, 1); |
| 125 | 189 | assert.equal(resolvedConfig?.port, 3773); |
| 190 | assert.equal(resolvedConfig?.host, "127.0.0.1"); | |
| 191 | assert.equal(resolvedConfig?.mode, "desktop"); | |
| 192 | }), | |
| 193 | ); | |
| 194 | ||
| 195 | it.effect("allows overriding desktop host with --host", () => | |
| 196 | Effect.gen(function* () { | |
| 197 | yield* runCli(["--host", "0.0.0.0"], { | |
| 198 | T3CODE_MODE: "desktop", | |
| 199 | T3CODE_NO_BROWSER: "true", | |
| 200 | }); | |
| 201 | ||
| 202 | assert.equal(createServer.mock.calls.length, 1); | |
| 126 | 203 | assert.equal(resolvedConfig?.mode, "desktop"); |
| 204 | assert.equal(resolvedConfig?.host, "0.0.0.0"); | |
| 205 | }), | |
| 206 | ); | |
| 207 | ||
| 208 | it.effect("does not start server for invalid --mode values", () => | |
| 209 | Effect.gen(function* () { | |
| 210 | yield* runCli(["--mode", "invalid"]); | |
| 211 | ||
| 212 | assert.equal(createServer.mock.calls.length, 0); | |
| 213 | assert.equal(start.mock.calls.length, 0); | |
| 214 | assert.equal(stop.mock.calls.length, 0); | |
| 215 | }), | |
| 216 | ); | |
| 217 | ||
| 218 | it.effect("does not start server for invalid --dev-url values", () => | |
| 219 | Effect.gen(function* () { | |
| 220 | yield* runCli(["--dev-url", "not-a-url"]).pipe(Effect.catch(() => Effect.void)); | |
| 221 | ||
| 222 | assert.equal(createServer.mock.calls.length, 0); | |
| 223 | assert.equal(start.mock.calls.length, 0); | |
| 224 | assert.equal(stop.mock.calls.length, 0); | |
| 127 | 225 | }), |
| 128 | 226 | ); |
| 129 | 227 |
apps/server/src/main.ts+89-30| ... | ... | @@ -21,8 +21,13 @@ export class StartupError extends Data.TaggedError("StartupError")<{ |
| 21 | 21 | }> {} |
| 22 | 22 | |
| 23 | 23 | interface CliInput { |
| 24 | readonly mode: Option.Option<RuntimeMode>; | |
| 24 | 25 | readonly port: Option.Option<number>; |
| 25 | readonly token: Option.Option<string>; | |
| 26 | readonly host: Option.Option<string>; | |
| 27 | readonly stateDir: Option.Option<string>; | |
| 28 | readonly devUrl: Option.Option<URL>; | |
| 29 | readonly noBrowser: Option.Option<boolean>; | |
| 30 | readonly authToken: Option.Option<string>; | |
| 26 | 31 | } |
| 27 | 32 | |
| 28 | 33 | export interface CliConfigShape { |
| ... | ... | @@ -60,6 +65,7 @@ const CliEnvConfig = Config.all({ |
| 60 | 65 | ), |
| 61 | 66 | ), |
| 62 | 67 | port: Config.port("T3CODE_PORT").pipe(Config.option, Config.map(Option.getOrUndefined)), |
| 68 | host: Config.string("T3CODE_HOST").pipe(Config.option, Config.map(Option.getOrUndefined)), | |
| 63 | 69 | stateDir: Config.string("T3CODE_STATE_DIR").pipe( |
| 64 | 70 | Config.option, |
| 65 | 71 | Config.map(Option.getOrUndefined), |
| ... | ... | @@ -88,27 +94,39 @@ const ServerConfigLive = (input: CliInput) => |
| 88 | 94 | ), |
| 89 | 95 | ); |
| 90 | 96 | |
| 97 | const mode = Option.getOrElse(input.mode, () => env.mode); | |
| 98 | ||
| 91 | 99 | const port = yield* Option.match(input.port, { |
| 92 | 100 | onSome: (value) => Effect.succeed(value), |
| 93 | 101 | onNone: () => { |
| 94 | 102 | if (env.port) { |
| 95 | 103 | return Effect.succeed(env.port); |
| 96 | 104 | } |
| 97 | if (env.mode === "desktop") { | |
| 105 | if (mode === "desktop") { | |
| 98 | 106 | return Effect.succeed(DEFAULT_PORT); |
| 99 | 107 | } |
| 100 | 108 | return findAvailablePort(DEFAULT_PORT); |
| 101 | 109 | }, |
| 102 | 110 | }); |
| 103 | const stateDir = yield* resolveStateDir(env.stateDir); | |
| 104 | const devUrl = env.devUrl; | |
| 105 | const noBrowser = env.noBrowser ?? env.mode === "desktop"; | |
| 106 | const authToken = Option.getOrUndefined(input.token) ?? env.authToken; | |
| 111 | const stateDir = yield* resolveStateDir( | |
| 112 | Option.getOrUndefined(input.stateDir) ?? env.stateDir, | |
| 113 | ); | |
| 114 | const devUrl = Option.getOrElse(input.devUrl, () => env.devUrl); | |
| 115 | const noBrowser = Option.match(input.noBrowser, { | |
| 116 | // effect/cli boolean flags parse to `false` when absent; in that case | |
| 117 | // we still want env/mode fallbacks to apply. | |
| 118 | onSome: (value) => (value ? true : (env.noBrowser ?? mode === "desktop")), | |
| 119 | onNone: () => env.noBrowser ?? mode === "desktop", | |
| 120 | }); | |
| 121 | const authToken = Option.getOrUndefined(input.authToken) ?? env.authToken; | |
| 107 | 122 | const staticDir = devUrl ? undefined : yield* cliConfig.resolveStaticDir; |
| 108 | const host = env.mode === "desktop" ? "127.0.0.1" : undefined; | |
| 123 | const host = | |
| 124 | Option.getOrUndefined(input.host) ?? | |
| 125 | env.host ?? | |
| 126 | (mode === "desktop" ? "127.0.0.1" : undefined); | |
| 109 | 127 | |
| 110 | 128 | return { |
| 111 | mode: env.mode, | |
| 129 | mode, | |
| 112 | 130 | port, |
| 113 | 131 | cwd: cliConfig.cwd, |
| 114 | 132 | host, |
| ... | ... | @@ -129,6 +147,12 @@ const LayerLive = (input: CliInput) => |
| 129 | 147 | Layer.provideMerge(ServerConfigLive(input)), |
| 130 | 148 | ); |
| 131 | 149 | |
| 150 | const isWildcardHost = (host: string | undefined): boolean => | |
| 151 | host === "0.0.0.0" || host === "::" || host === "[::]"; | |
| 152 | ||
| 153 | const formatHostForUrl = (host: string): string => | |
| 154 | host.includes(":") && !host.startsWith("[") ? `[${host}]` : host; | |
| 155 | ||
| 132 | 156 | const makeServerProgram = (input: CliInput) => |
| 133 | 157 | Effect.gen(function* () { |
| 134 | 158 | const cliConfig = yield* CliConfig; |
| ... | ... | @@ -149,9 +173,15 @@ const makeServerProgram = (input: CliInput) => |
| 149 | 173 | |
| 150 | 174 | yield* createServer(); |
| 151 | 175 | |
| 152 | const url = `http://localhost:${config.port}`; | |
| 176 | const localUrl = `http://localhost:${config.port}`; | |
| 177 | const bindUrl = | |
| 178 | config.host && !isWildcardHost(config.host) | |
| 179 | ? `http://${formatHostForUrl(config.host)}:${config.port}` | |
| 180 | : localUrl; | |
| 153 | 181 | yield* Effect.logInfo("T3 Code running", { |
| 154 | url, | |
| 182 | url: bindUrl, | |
| 183 | localUrl, | |
| 184 | bindHost: config.host ?? "default", | |
| 155 | 185 | cwd: config.cwd, |
| 156 | 186 | mode: config.mode, |
| 157 | 187 | stateDir: config.stateDir, |
| ... | ... | @@ -159,7 +189,7 @@ const makeServerProgram = (input: CliInput) => |
| 159 | 189 | }); |
| 160 | 190 | |
| 161 | 191 | if (!config.noBrowser) { |
| 162 | const target = config.devUrl?.toString() ?? url; | |
| 192 | const target = config.devUrl?.toString() ?? bindUrl; | |
| 163 | 193 | yield* openDeps.openBrowser(target).pipe( |
| 164 | 194 | Effect.catch(() => |
| 165 | 195 | Effect.logInfo("browser auto-open unavailable", { |
| ... | ... | @@ -172,22 +202,51 @@ const makeServerProgram = (input: CliInput) => |
| 172 | 202 | return yield* stopSignal; |
| 173 | 203 | }).pipe(Effect.provide(LayerLive(input))); |
| 174 | 204 | |
| 175 | export function makeCliCommand() { | |
| 176 | const portFlag = Flag.integer("port").pipe( | |
| 177 | Flag.withSchema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }))), | |
| 178 | Flag.withDescription("Port for the HTTP/WebSocket server."), | |
| 179 | Flag.optional, | |
| 180 | ); | |
| 181 | const tokenFlag = Flag.string("token").pipe( | |
| 182 | Flag.withDescription("Auth token required for WebSocket connections."), | |
| 183 | Flag.optional, | |
| 184 | ); | |
| 185 | ||
| 186 | return Command.make("t3", { | |
| 187 | port: portFlag, | |
| 188 | token: tokenFlag, | |
| 189 | }).pipe( | |
| 190 | Command.withDescription("Run the T3 Code server."), | |
| 191 | Command.withHandler((input) => Effect.scoped(makeServerProgram(input))), | |
| 192 | ); | |
| 193 | } | |
| 205 | /** | |
| 206 | * These flags mirrors the environment variables and the config shape. | |
| 207 | */ | |
| 208 | ||
| 209 | const modeFlag = Flag.choice("mode", ["web", "desktop"]).pipe( | |
| 210 | Flag.withDescription("Runtime mode. `desktop` keeps loopback defaults unless overridden."), | |
| 211 | Flag.optional, | |
| 212 | ); | |
| 213 | const portFlag = Flag.integer("port").pipe( | |
| 214 | Flag.withSchema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }))), | |
| 215 | Flag.withDescription("Port for the HTTP/WebSocket server."), | |
| 216 | Flag.optional, | |
| 217 | ); | |
| 218 | const hostFlag = Flag.string("host").pipe( | |
| 219 | Flag.withDescription("Host/interface to bind (for example 127.0.0.1, 0.0.0.0, or a Tailnet IP)."), | |
| 220 | Flag.optional, | |
| 221 | ); | |
| 222 | const stateDirFlag = Flag.string("state-dir").pipe( | |
| 223 | Flag.withDescription("State directory path (equivalent to T3CODE_STATE_DIR)."), | |
| 224 | Flag.optional, | |
| 225 | ); | |
| 226 | const devUrlFlag = Flag.string("dev-url").pipe( | |
| 227 | Flag.withSchema(Schema.URLFromString), | |
| 228 | Flag.withDescription("Dev web URL to proxy/redirect to (equivalent to VITE_DEV_SERVER_URL)."), | |
| 229 | Flag.optional, | |
| 230 | ); | |
| 231 | const noBrowserFlag = Flag.boolean("no-browser").pipe( | |
| 232 | Flag.withDescription("Disable automatic browser opening."), | |
| 233 | Flag.optional, | |
| 234 | ); | |
| 235 | const authTokenFlag = Flag.string("auth-token").pipe( | |
| 236 | Flag.withDescription("Auth token required for WebSocket connections."), | |
| 237 | Flag.withAlias("token"), | |
| 238 | Flag.optional, | |
| 239 | ); | |
| 240 | ||
| 241 | export const t3Cli = Command.make("t3", { | |
| 242 | mode: modeFlag, | |
| 243 | port: portFlag, | |
| 244 | host: hostFlag, | |
| 245 | stateDir: stateDirFlag, | |
| 246 | devUrl: devUrlFlag, | |
| 247 | noBrowser: noBrowserFlag, | |
| 248 | authToken: authTokenFlag, | |
| 249 | }).pipe( | |
| 250 | Command.withDescription("Run the T3 Code server."), | |
| 251 | Command.withHandler((input) => Effect.scoped(makeServerProgram(input))), | |
| 252 | ); |