Unified · Split
| @@ -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. | |
| @@ -3,7 +3,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; · 2 unmodified lines | ||
| 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( · 8 unmodified lines | ||
| 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); | |
| @@ -7,7 +7,7 @@ import * as Layer from "effect/Layer"; · 6 unmodified lines | ||
| 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 = ( · 37 unmodified lines | ||
| 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(() => { · 16 unmodified lines | ||
| 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) => { · 16 unmodified lines | ||
| 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 | |
| @@ -21,8 +21,13 @@ export class StartupError extends Data.TaggedError("StartupError")<{ · 20 unmodified lines | ||
| 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({ · 31 unmodified lines | ||
| 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) => · 22 unmodified lines | ||
| 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) => · 14 unmodified lines | ||
| 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) => · 14 unmodified lines | ||
| 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) => · 1 unmodified line | ||
| 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) => · 6 unmodified lines | ||
| 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 | +); | |