authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-03 12:42:07-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-03 18:22:29-08:00
loge6ac39de75de4abc5bf13a06a3e5c618a9759c51
treee3e676c031c220bb4c84b92e05a563b5a93f2926
parentb76a0a6c913f680ad5be15bd50b890249aeae24b

Migrate dev-runner to typed Effect CLI config and env fallbacks

- Replace string-based env override plumbing with typed config/flag parsing - Split server/web offset resolution from explicit port/dev-url overrides - Refactor and expand dev-runner tests around offset/env behavior - Add `vitest` as a contracts dev dependency for test execution

4 files changed, 396 insertions(+), 425 deletions(-)

bun.lock+1
...@@ -107,6 +107,7 @@...@@ -107,6 +107,7 @@
107 "@effect/vitest": "catalog:",107 "@effect/vitest": "catalog:",
108 "tsdown": "catalog:",108 "tsdown": "catalog:",
109 "typescript": "catalog:",109 "typescript": "catalog:",
110 "vitest": "catalog:",
110 },111 },
111 },112 },
112 "packages/shared": {113 "packages/shared": {
packages/contracts/package.json+2-1
...@@ -30,6 +30,7 @@...@@ -30,6 +30,7 @@
30 "@effect/language-service": "catalog:",30 "@effect/language-service": "catalog:",
31 "@effect/vitest": "catalog:",31 "@effect/vitest": "catalog:",
32 "tsdown": "catalog:",32 "tsdown": "catalog:",
33 "typescript": "catalog:"33 "typescript": "catalog:",
34 "vitest": "catalog:"
34 }35 }
35}36}
scripts/dev-runner.test.ts+204-212
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, it } from "@effect/vitest";2import { assert, describe, it } from "@effect/vitest";
3import { Effect } from "effect";3import { Effect } from "effect";
44
5import {5import {
...@@ -7,219 +7,211 @@ import {...@@ -7,219 +7,211 @@ import {
7 createDevRunnerEnv,7 createDevRunnerEnv,
8 findFirstAvailableOffset,8 findFirstAvailableOffset,
9 resolveModePortOffsets,9 resolveModePortOffsets,
10 resolveOffset,
10} from "./dev-runner.ts";11} from "./dev-runner.ts";
1112
12it.layer(NodeServices.layer)("dev-runner", (it) => {13it.layer(NodeServices.layer)("dev-runner", (it) => {
13 it.effect("defaults state dir to ~/.t3/dev when not provided", () =>14 describe("resolveOffset", () => {
14 Effect.gen(function* () {15 it.effect("uses explicit T3CODE_PORT_OFFSET when provided", () =>
15 const env = yield* createDevRunnerEnv({16 Effect.sync(() => {
16 mode: "dev",17 const result = resolveOffset({ portOffset: 12, devInstance: undefined });
17 env: {},18 assert.deepStrictEqual(result, {
18 offset: 0,19 offset: 12,
19 envOverrides: {},20 source: "T3CODE_PORT_OFFSET=12",
20 });21 });
21 const defaultStateDir = yield* DEFAULT_DEV_STATE_DIR;22 }),
2223 );
23 assert.equal(env.T3CODE_STATE_DIR, defaultStateDir);24
24 }),25 it.effect("hashes non-numeric instance values", () =>
25 );26 Effect.sync(() => {
2627 const result = resolveOffset({ portOffset: undefined, devInstance: "feature-branch" });
27 it.effect("uses existing env state dir when --state-dir is not provided", () =>28 assert.ok(result.offset >= 1);
28 Effect.gen(function* () {29 assert.ok(result.offset <= 3000);
29 const env = yield* createDevRunnerEnv({30 }),
30 mode: "dev:server",31 );
31 env: { T3CODE_STATE_DIR: "/tmp/existing-state" },32
32 offset: 0,33 it.effect("throws for negative port offset", () =>
33 envOverrides: {},34 Effect.gen(function* () {
34 });35 const error = yield* Effect.flip(
3536 Effect.try({
36 assert.equal(env.T3CODE_STATE_DIR, "/tmp/existing-state");37 try: () => resolveOffset({ portOffset: -1, devInstance: undefined }),
37 }),38 catch: (cause) => String(cause),
38 );39 }),
3940 );
40 it.effect("lets --state-dir override existing env state dir", () =>41
41 Effect.gen(function* () {42 assert.ok(error.includes("Invalid T3CODE_PORT_OFFSET"));
42 const env = yield* createDevRunnerEnv({43 }),
43 mode: "dev:server",44 );
44 env: { T3CODE_STATE_DIR: "/tmp/existing-state" },45 });
45 offset: 0,46
46 envOverrides: { T3CODE_STATE_DIR: "/tmp/override-state" },47 describe("createDevRunnerEnv", () => {
47 });48 it.effect("defaults state dir to ~/.t3/dev when not provided", () =>
4849 Effect.gen(function* () {
49 assert.equal(env.T3CODE_STATE_DIR, "/tmp/override-state");50 const [env, defaultStateDir] = yield* Effect.all([
50 }),51 createDevRunnerEnv({
51 );52 mode: "dev",
5253 baseEnv: {},
53 it.effect("treats whitespace-only --state-dir as missing and falls back to env/default", () =>54 serverOffset: 0,
54 Effect.gen(function* () {55 webOffset: 0,
55 const env = yield* createDevRunnerEnv({56 stateDir: undefined,
56 mode: "dev:server",57 authToken: undefined,
57 env: { T3CODE_STATE_DIR: "/tmp/existing-state" },58 noBrowser: undefined,
58 offset: 0,59 autoBootstrapProjectFromCwd: undefined,
59 envOverrides: { T3CODE_STATE_DIR: " " },60 logWebSocketEvents: undefined,
60 });61 host: undefined,
6162 port: undefined,
62 assert.equal(env.T3CODE_STATE_DIR, "/tmp/existing-state");63 devUrl: undefined,
63 }),64 }),
64 );65 DEFAULT_DEV_STATE_DIR,
6566 ]);
66 it.effect("recomputes websocket url when port is overridden", () =>67
67 Effect.gen(function* () {68 assert.equal(env.T3CODE_STATE_DIR, defaultStateDir);
68 const env = yield* createDevRunnerEnv({69 }),
69 mode: "dev",70 );
70 env: {},71
71 offset: 0,72 it.effect("supports explicit typed overrides", () =>
72 envOverrides: { T3CODE_PORT: "4222" },73 Effect.gen(function* () {
73 });74 const env = yield* createDevRunnerEnv({
7475 mode: "dev:server",
75 assert.equal(env.T3CODE_PORT, "4222");76 baseEnv: {},
76 assert.equal(env.VITE_WS_URL, "ws://localhost:4222");77 serverOffset: 0,
77 }),78 webOffset: 0,
78 );79 stateDir: "/tmp/override-state",
7980 authToken: "secret",
80 it.effect("keeps explicitly forwarded web-mode flags", () =>81 noBrowser: true,
81 Effect.gen(function* () {82 autoBootstrapProjectFromCwd: false,
82 const env = yield* createDevRunnerEnv({83 logWebSocketEvents: true,
83 mode: "dev",84 host: "0.0.0.0",
84 env: {85 port: 4222,
85 T3CODE_AUTH_TOKEN: "desktop-token",86 devUrl: new URL("http://localhost:7331"),
86 },87 });
87 offset: 0,88
88 envOverrides: {89 assert.equal(env.T3CODE_STATE_DIR, "/tmp/override-state");
89 T3CODE_NO_BROWSER: "1",90 assert.equal(env.T3CODE_PORT, "4222");
90 T3CODE_AUTH_TOKEN: "cli-token",91 assert.equal(env.VITE_WS_URL, "ws://localhost:4222");
91 },92 assert.equal(env.T3CODE_NO_BROWSER, "1");
92 });93 assert.equal(env.T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD, "0");
9394 assert.equal(env.T3CODE_LOG_WS_EVENTS, "1");
94 assert.equal(env.T3CODE_NO_BROWSER, "1");95 assert.equal(env.T3CODE_HOST, "0.0.0.0");
95 assert.equal(env.T3CODE_AUTH_TOKEN, "cli-token");96 assert.equal(env.VITE_DEV_SERVER_URL, "http://localhost:7331/");
96 }),97 }),
97 );98 );
9899 });
99 it.effect("fails fast for invalid port overrides", () =>100
100 Effect.gen(function* () {101 describe("findFirstAvailableOffset", () => {
101 const error = yield* Effect.flip(102 it.effect("returns the starting offset when required ports are available", () =>
102 createDevRunnerEnv({103 Effect.gen(function* () {
104 const offset = yield* findFirstAvailableOffset({
105 startOffset: 0,
106 requireServerPort: true,
107 requireWebPort: true,
108 checkPortAvailability: () => Effect.succeed(true),
109 });
110
111 assert.equal(offset, 0);
112 }),
113 );
114
115 it.effect("advances until all required ports are available", () =>
116 Effect.gen(function* () {
117 const taken = new Set([3773, 5733, 3774, 5734]);
118 const offset = yield* findFirstAvailableOffset({
119 startOffset: 0,
120 requireServerPort: true,
121 requireWebPort: true,
122 checkPortAvailability: (port) => Effect.succeed(!taken.has(port)),
123 });
124
125 assert.equal(offset, 2);
126 }),
127 );
128
129 it.effect("allows offsets where only non-required ports exceed max", () =>
130 Effect.gen(function* () {
131 const offset = yield* findFirstAvailableOffset({
132 startOffset: 59_803,
133 requireServerPort: true,
134 requireWebPort: false,
135 checkPortAvailability: () => Effect.succeed(true),
136 });
137
138 assert.equal(offset, 59_803);
139 }),
140 );
141 });
142
143 describe("resolveModePortOffsets", () => {
144 it.effect("uses a shared fallback offset for dev mode", () =>
145 Effect.gen(function* () {
146 const taken = new Set([3773, 5733]);
147 const offsets = yield* resolveModePortOffsets({
103 mode: "dev",148 mode: "dev",
104 env: {},149 startOffset: 0,
105 offset: 0,150 hasExplicitServerPort: false,
106 envOverrides: { T3CODE_PORT: "not-a-port" },151 hasExplicitDevUrl: false,
107 }),152 checkPortAvailability: (port) => Effect.succeed(!taken.has(port)),
108 );153 });
109154
110 assert.ok(error.message.includes("Invalid T3CODE_PORT override"));155 assert.deepStrictEqual(offsets, { serverOffset: 1, webOffset: 1 });
111 }),156 }),
112 );157 );
113});158
114159 it.effect("keeps server offset stable for dev:web and only shifts web offset", () =>
115it.layer(NodeServices.layer)("findFirstAvailableOffset", (it) => {160 Effect.gen(function* () {
116 it.effect("returns the starting offset when required ports are available", () =>161 const taken = new Set([5733]);
117 Effect.gen(function* () {162 const offsets = yield* resolveModePortOffsets({
118 const offset = yield* findFirstAvailableOffset({163 mode: "dev:web",
119 startOffset: 0,164 startOffset: 0,
120 requireServerPort: true,165 hasExplicitServerPort: false,
121 requireWebPort: true,166 hasExplicitDevUrl: false,
122 checkPortAvailability: () => Effect.succeed(true),167 checkPortAvailability: (port) => Effect.succeed(!taken.has(port)),
123 });168 });
124169
125 assert.equal(offset, 0);170 assert.deepStrictEqual(offsets, { serverOffset: 0, webOffset: 1 });
126 }),171 }),
127 );172 );
128173
129 it.effect("advances until all required ports are available", () =>174 it.effect("shifts only server offset for dev:server", () =>
130 Effect.gen(function* () {175 Effect.gen(function* () {
131 const taken = new Set([3773, 5733, 3774, 5734]);176 const taken = new Set([3773]);
132 const offset = yield* findFirstAvailableOffset({177 const offsets = yield* resolveModePortOffsets({
133 startOffset: 0,178 mode: "dev:server",
134 requireServerPort: true,179 startOffset: 0,
135 requireWebPort: true,180 hasExplicitServerPort: false,
136 checkPortAvailability: (port) => Effect.succeed(!taken.has(port)),181 hasExplicitDevUrl: false,
137 });182 checkPortAvailability: (port) => Effect.succeed(!taken.has(port)),
138183 });
139 assert.equal(offset, 2);184
140 }),185 assert.deepStrictEqual(offsets, { serverOffset: 1, webOffset: 1 });
141 );186 }),
142187 );
143 it.effect("allows offsets where only non-required ports exceed max", () =>188
144 Effect.gen(function* () {189 it.effect("respects explicit dev-url override for dev:web", () =>
145 const offset = yield* findFirstAvailableOffset({190 Effect.gen(function* () {
146 startOffset: 59_803,191 const offsets = yield* resolveModePortOffsets({
147 requireServerPort: true,192 mode: "dev:web",
148 requireWebPort: false,193 startOffset: 0,
149 checkPortAvailability: () => Effect.succeed(true),194 hasExplicitServerPort: false,
150 });195 hasExplicitDevUrl: true,
151196 checkPortAvailability: () => Effect.succeed(false),
152 assert.equal(offset, 59_803);197 });
153 }),198
154 );199 assert.deepStrictEqual(offsets, { serverOffset: 0, webOffset: 0 });
155});200 }),
156201 );
157it.layer(NodeServices.layer)("resolveModePortOffsets", (it) => {202
158 it.effect("uses a shared fallback offset for dev mode", () =>203 it.effect("respects explicit server port override for dev:server", () =>
159 Effect.gen(function* () {204 Effect.gen(function* () {
160 const taken = new Set([3773, 5733]);205 const offsets = yield* resolveModePortOffsets({
161 const offsets = yield* resolveModePortOffsets({206 mode: "dev:server",
162 mode: "dev",207 startOffset: 0,
163 startOffset: 0,208 hasExplicitServerPort: true,
164 envOverrides: {},209 hasExplicitDevUrl: false,
165 checkPortAvailability: (port) => Effect.succeed(!taken.has(port)),210 checkPortAvailability: () => Effect.succeed(false),
166 });211 });
167212
168 assert.deepStrictEqual(offsets, { serverOffset: 1, webOffset: 1 });213 assert.deepStrictEqual(offsets, { serverOffset: 0, webOffset: 0 });
169 }),214 }),
170 );215 );
171216 });
172 it.effect("keeps server offset stable for dev:web and only shifts web offset", () =>
173 Effect.gen(function* () {
174 const taken = new Set([5733]);
175 const offsets = yield* resolveModePortOffsets({
176 mode: "dev:web",
177 startOffset: 0,
178 envOverrides: {},
179 checkPortAvailability: (port) => Effect.succeed(!taken.has(port)),
180 });
181
182 assert.deepStrictEqual(offsets, { serverOffset: 0, webOffset: 1 });
183 }),
184 );
185
186 it.effect("shifts only server offset for dev:server", () =>
187 Effect.gen(function* () {
188 const taken = new Set([3773]);
189 const offsets = yield* resolveModePortOffsets({
190 mode: "dev:server",
191 startOffset: 0,
192 envOverrides: {},
193 checkPortAvailability: (port) => Effect.succeed(!taken.has(port)),
194 });
195
196 assert.deepStrictEqual(offsets, { serverOffset: 1, webOffset: 1 });
197 }),
198 );
199
200 it.effect("respects explicit dev-url override for dev:web", () =>
201 Effect.gen(function* () {
202 const offsets = yield* resolveModePortOffsets({
203 mode: "dev:web",
204 startOffset: 0,
205 envOverrides: { VITE_DEV_SERVER_URL: "http://localhost:9999" },
206 checkPortAvailability: () => Effect.succeed(false),
207 });
208
209 assert.deepStrictEqual(offsets, { serverOffset: 0, webOffset: 0 });
210 }),
211 );
212
213 it.effect("respects explicit server port override for dev:server", () =>
214 Effect.gen(function* () {
215 const offsets = yield* resolveModePortOffsets({
216 mode: "dev:server",
217 startOffset: 0,
218 envOverrides: { T3CODE_PORT: "4888" },
219 checkPortAvailability: () => Effect.succeed(false),
220 });
221
222 assert.deepStrictEqual(offsets, { serverOffset: 0, webOffset: 0 });
223 }),
224 );
225});217});
scripts/dev-runner.ts+189-212
...@@ -5,7 +5,7 @@ import { homedir } from "node:os";...@@ -5,7 +5,7 @@ import { homedir } from "node:os";
5import * as NodeRuntime from "@effect/platform-node/NodeRuntime";5import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
6import * as NodeServices from "@effect/platform-node/NodeServices";6import * as NodeServices from "@effect/platform-node/NodeServices";
7import { NetService } from "@t3tools/shared/Net";7import { NetService } from "@t3tools/shared/Net";
8import { Data, Effect, Layer, Logger, Option, Path } from "effect";8import { Config, Data, Effect, Hash, Layer, Logger, Option, Path, Schema } from "effect";
9import { Argument, Command, Flag } from "effect/unstable/cli";9import { Argument, Command, Flag } from "effect/unstable/cli";
10import { ChildProcess } from "effect/unstable/process";10import { ChildProcess } from "effect/unstable/process";
1111
...@@ -14,11 +14,9 @@ const BASE_WEB_PORT = 5733;...@@ -14,11 +14,9 @@ const BASE_WEB_PORT = 5733;
14const MAX_HASH_OFFSET = 3000;14const MAX_HASH_OFFSET = 3000;
15const MAX_PORT = 65535;15const MAX_PORT = 65535;
1616
17export const DEFAULT_DEV_STATE_DIR: Effect.Effect<string, never, Path.Path> = Effect.gen(17export const DEFAULT_DEV_STATE_DIR = Effect.map(Effect.service(Path.Path), (path) =>
18 function* () {18 path.join(homedir(), ".t3", "dev"),
19 const path = yield* Path.Path;19);
20 return path.join(homedir(), ".t3", "dev");
21});
2220
23const MODE_ARGS = {21const MODE_ARGS = {
24 dev: [22 dev: [
...@@ -36,7 +34,6 @@ const MODE_ARGS = {...@@ -36,7 +34,6 @@ const MODE_ARGS = {
36} as const satisfies Record<string, ReadonlyArray<string>>;34} as const satisfies Record<string, ReadonlyArray<string>>;
3735
38type DevMode = keyof typeof MODE_ARGS;36type DevMode = keyof typeof MODE_ARGS;
39type EnvOverrides = Record<string, string>;
40type PortAvailabilityCheck<R = never> = (port: number) => Effect.Effect<boolean, never, R>;37type PortAvailabilityCheck<R = never> = (port: number) => Effect.Effect<boolean, never, R>;
4138
42const DEV_RUNNER_MODES = Object.keys(MODE_ARGS) as Array<DevMode>;39const DEV_RUNNER_MODES = Object.keys(MODE_ARGS) as Array<DevMode>;
...@@ -46,37 +43,52 @@ class DevRunnerError extends Data.TaggedError("DevRunnerError")<{...@@ -46,37 +43,52 @@ class DevRunnerError extends Data.TaggedError("DevRunnerError")<{
46 readonly cause?: unknown;43 readonly cause?: unknown;
47}> {}44}> {}
4845
49function parseInteger(value: string, envName: string): number {46const optionalStringConfig = (name: string): Config.Config<string | undefined> =>
50 const parsed = Number(value);47 Config.string(name).pipe(
51 if (!Number.isInteger(parsed)) {48 Config.option,
52 throw new Error(`Invalid ${envName}: ${value}`);49 Config.map((value) => Option.getOrUndefined(value)),
53 }50 );
54 return parsed;51const optionalBooleanConfig = (name: string): Config.Config<boolean | undefined> =>
55}52 Config.boolean(name).pipe(
53 Config.option,
54 Config.map((value) => Option.getOrUndefined(value)),
55 );
56const optionalPortConfig = (name: string): Config.Config<number | undefined> =>
57 Config.port(name).pipe(
58 Config.option,
59 Config.map((value) => Option.getOrUndefined(value)),
60 );
61const optionalIntegerConfig = (name: string): Config.Config<number | undefined> =>
62 Config.int(name).pipe(
63 Config.option,
64 Config.map((value) => Option.getOrUndefined(value)),
65 );
66const optionalUrlConfig = (name: string): Config.Config<URL | undefined> =>
67 Config.url(name).pipe(
68 Config.option,
69 Config.map((value) => Option.getOrUndefined(value)),
70 );
5671
57function hashSeed(seed: string): number {72const OffsetConfig = Config.all({
58 let hash = 0x811c9dc5;73 portOffset: optionalIntegerConfig("T3CODE_PORT_OFFSET"),
59 for (let i = 0; i < seed.length; i += 1) {74 devInstance: optionalStringConfig("T3CODE_DEV_INSTANCE"),
60 hash ^= seed.charCodeAt(i);75});
61 hash = Math.imul(hash, 0x01000193);
62 }
63 return hash >>> 0;
64}
6576
66export function resolveOffset(env: NodeJS.ProcessEnv = process.env): {77export function resolveOffset(config: {
67 readonly offset: number;78 readonly portOffset: number | undefined;
68 readonly source: string;79 readonly devInstance: string | undefined;
69} {80}): { readonly offset: number; readonly source: string } {
70 const explicitOffset = env.T3CODE_PORT_OFFSET?.trim();81 if (config.portOffset !== undefined) {
71 if (explicitOffset) {82 if (config.portOffset < 0) {
72 const parsed = parseInteger(explicitOffset, "T3CODE_PORT_OFFSET");83 throw new Error(`Invalid T3CODE_PORT_OFFSET: ${config.portOffset}`);
73 if (parsed < 0) {
74 throw new Error(`Invalid T3CODE_PORT_OFFSET: ${explicitOffset}`);
75 }84 }
76 return { offset: parsed, source: `T3CODE_PORT_OFFSET=${explicitOffset}` };85 return {
86 offset: config.portOffset,
87 source: `T3CODE_PORT_OFFSET=${config.portOffset}`,
88 };
77 }89 }
7890
79 const seed = env.T3CODE_DEV_INSTANCE?.trim();91 const seed = config.devInstance?.trim();
80 if (!seed) {92 if (!seed) {
81 return { offset: 0, source: "default ports" };93 return { offset: 0, source: "default ports" };
82 }94 }
...@@ -85,32 +97,18 @@ export function resolveOffset(env: NodeJS.ProcessEnv = process.env): {...@@ -85,32 +97,18 @@ export function resolveOffset(env: NodeJS.ProcessEnv = process.env): {
85 return { offset: Number(seed), source: `numeric T3CODE_DEV_INSTANCE=${seed}` };97 return { offset: Number(seed), source: `numeric T3CODE_DEV_INSTANCE=${seed}` };
86 }98 }
8799
88 const offset = (hashSeed(seed) % MAX_HASH_OFFSET) + 1;100 const offset = ((Hash.string(seed) >>> 0) % MAX_HASH_OFFSET) + 1;
89 return { offset, source: `hashed T3CODE_DEV_INSTANCE=${seed}` };101 return { offset, source: `hashed T3CODE_DEV_INSTANCE=${seed}` };
90}102}
91103
92function toBooleanEnvValue(value: string | undefined): string {104function resolveStateDir(stateDir: string | undefined): Effect.Effect<string, never, Path.Path> {
93 if (value === undefined) return "1";
94 const normalized = value.trim().toLowerCase();
95 if (normalized === "false" || normalized === "0" || normalized === "no") return "0";
96 return "1";
97}
98
99function resolveStateDir(
100 env: NodeJS.ProcessEnv,
101 envOverrides: EnvOverrides,
102): Effect.Effect<string, never, Path.Path> {
103 return Effect.gen(function* () {105 return Effect.gen(function* () {
104 const path = yield* Path.Path;106 const path = yield* Path.Path;
105 const overrideValue = envOverrides?.T3CODE_STATE_DIR?.trim();107 const configured = stateDir?.trim();
106 if (overrideValue) {
107 // Resolve relative paths against cwd (monorepo root) before turbo changes directories.
108 return path.resolve(overrideValue);
109 }
110108
111 const envValue = env?.T3CODE_STATE_DIR?.trim();109 if (configured) {
112 if (envValue) {110 // Resolve relative paths against cwd (monorepo root) before turbo changes directories.
113 return path.resolve(envValue);111 return path.resolve(configured);
114 }112 }
115113
116 return yield* DEFAULT_DEV_STATE_DIR;114 return yield* DEFAULT_DEV_STATE_DIR;
...@@ -119,72 +117,97 @@ function resolveStateDir(...@@ -119,72 +117,97 @@ function resolveStateDir(
119117
120interface CreateDevRunnerEnvInput {118interface CreateDevRunnerEnvInput {
121 readonly mode: DevMode;119 readonly mode: DevMode;
122 readonly env: NodeJS.ProcessEnv;120 readonly baseEnv: NodeJS.ProcessEnv;
123 readonly offset: number;121 readonly serverOffset: number;
124 readonly envOverrides: EnvOverrides;122 readonly webOffset: number;
123 readonly stateDir: string | undefined;
124 readonly authToken: string | undefined;
125 readonly noBrowser: boolean | undefined;
126 readonly autoBootstrapProjectFromCwd: boolean | undefined;
127 readonly logWebSocketEvents: boolean | undefined;
128 readonly host: string | undefined;
129 readonly port: number | undefined;
130 readonly devUrl: URL | undefined;
125}131}
126132
127export function createDevRunnerEnv({133export function createDevRunnerEnv({
128 mode,134 mode,
129 env,135 baseEnv,
130 offset,136 serverOffset,
131 envOverrides,137 webOffset,
132}: CreateDevRunnerEnvInput): Effect.Effect<NodeJS.ProcessEnv, DevRunnerError, Path.Path> {138 stateDir,
139 authToken,
140 noBrowser,
141 autoBootstrapProjectFromCwd,
142 logWebSocketEvents,
143 host,
144 port,
145 devUrl,
146}: CreateDevRunnerEnvInput): Effect.Effect<NodeJS.ProcessEnv, never, Path.Path> {
133 return Effect.gen(function* () {147 return Effect.gen(function* () {
134 const serverPort = BASE_SERVER_PORT + offset;148 const serverPort = port ?? BASE_SERVER_PORT + serverOffset;
135 const webPort = BASE_WEB_PORT + offset;149 const webPort = BASE_WEB_PORT + webOffset;
150 const resolvedStateDir = yield* resolveStateDir(stateDir);
136151
137 const output: NodeJS.ProcessEnv = {152 const output: NodeJS.ProcessEnv = {
138 ...env,153 ...baseEnv,
139 T3CODE_PORT: String(serverPort),154 T3CODE_PORT: String(serverPort),
140 PORT: String(webPort),155 PORT: String(webPort),
141 ELECTRON_RENDERER_PORT: String(webPort),156 ELECTRON_RENDERER_PORT: String(webPort),
142 VITE_WS_URL: `ws://localhost:${serverPort}`,157 VITE_WS_URL: `ws://localhost:${serverPort}`,
143 VITE_DEV_SERVER_URL: `http://localhost:${webPort}`,158 VITE_DEV_SERVER_URL: devUrl?.toString() ?? `http://localhost:${webPort}`,
144 ...envOverrides,159 T3CODE_STATE_DIR: resolvedStateDir,
145 };160 };
146161
147 output.T3CODE_STATE_DIR = yield* resolveStateDir(env, envOverrides);162 if (host !== undefined) {
148163 output.T3CODE_HOST = host;
149 const parsedServerPort = yield* Effect.try({164 }
150 try: () => {165
151 const parsed = Number(output.T3CODE_PORT);166 if (authToken !== undefined) {
152 if (!Number.isInteger(parsed) || parsed < 1 || parsed > MAX_PORT) {167 output.T3CODE_AUTH_TOKEN = authToken;
153 throw new Error(168 } else {
154 `Invalid T3CODE_PORT override: '${String(output.T3CODE_PORT)}'. Expected an integer between 1 and 65535.`,169 delete output.T3CODE_AUTH_TOKEN;
155 );170 }
156 }171
157 return parsed;172 if (noBrowser !== undefined) {
158 },173 output.T3CODE_NO_BROWSER = noBrowser ? "1" : "0";
159 catch: (cause) =>174 } else {
160 new DevRunnerError({175 delete output.T3CODE_NO_BROWSER;
161 message: cause instanceof Error ? cause.message : String(cause),176 }
162 cause,177
163 }),178 if (autoBootstrapProjectFromCwd !== undefined) {
164 });179 output.T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD = autoBootstrapProjectFromCwd ? "1" : "0";
165 output.VITE_WS_URL = `ws://localhost:${parsedServerPort}`;180 } else {
181 delete output.T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD;
182 }
183
184 if (logWebSocketEvents !== undefined) {
185 output.T3CODE_LOG_WS_EVENTS = logWebSocketEvents ? "1" : "0";
186 } else {
187 delete output.T3CODE_LOG_WS_EVENTS;
188 }
166189
167 if (mode === "dev" || mode === "dev:server" || mode === "dev:web") {190 if (mode === "dev") {
168 // Running server/web in browser mode should not inherit desktop launcher state.
169 output.T3CODE_MODE = "web";191 output.T3CODE_MODE = "web";
170 if (!("T3CODE_NO_BROWSER" in envOverrides)) {192 if (logWebSocketEvents === undefined) {
171 delete output.T3CODE_NO_BROWSER;193 output.T3CODE_LOG_WS_EVENTS = "1";
172 }
173 if (!("T3CODE_AUTH_TOKEN" in envOverrides)) {
174 delete output.T3CODE_AUTH_TOKEN;
175 }194 }
176 delete output.T3CODE_DESKTOP_WS_URL;195 delete output.T3CODE_DESKTOP_WS_URL;
177 }196 }
178197
179 if (mode === "dev" && !output.T3CODE_LOG_WS_EVENTS) {198 if (mode === "dev:server" || mode === "dev:web") {
180 output.T3CODE_LOG_WS_EVENTS = "1";199 output.T3CODE_MODE = "web";
200 delete output.T3CODE_DESKTOP_WS_URL;
181 }201 }
182202
183 return output;203 return output;
184 });204 });
185}205}
186206
187function portPairForOffset(offset: number): { readonly serverPort: number; readonly webPort: number } {207function portPairForOffset(offset: number): {
208 readonly serverPort: number;
209 readonly webPort: number;
210} {
188 return {211 return {
189 serverPort: BASE_SERVER_PORT + offset,212 serverPort: BASE_SERVER_PORT + offset,
190 webPort: BASE_WEB_PORT + offset,213 webPort: BASE_WEB_PORT + offset,
...@@ -211,11 +234,14 @@ export function findFirstAvailableOffset<R = NetService>({...@@ -211,11 +234,14 @@ export function findFirstAvailableOffset<R = NetService>({
211 checkPortAvailability,234 checkPortAvailability,
212}: FindFirstAvailableOffsetInput<R>): Effect.Effect<number, DevRunnerError, R> {235}: FindFirstAvailableOffsetInput<R>): Effect.Effect<number, DevRunnerError, R> {
213 return Effect.gen(function* () {236 return Effect.gen(function* () {
214 const checkPort = (checkPortAvailability ?? defaultCheckPortAvailability) as PortAvailabilityCheck<R>;237 const checkPort = (checkPortAvailability ??
238 defaultCheckPortAvailability) as PortAvailabilityCheck<R>;
239
215 for (let candidate = startOffset; ; candidate += 1) {240 for (let candidate = startOffset; ; candidate += 1) {
216 const { serverPort, webPort } = portPairForOffset(candidate);241 const { serverPort, webPort } = portPairForOffset(candidate);
217 const serverPortOutOfRange = serverPort > MAX_PORT;242 const serverPortOutOfRange = serverPort > MAX_PORT;
218 const webPortOutOfRange = webPort > MAX_PORT;243 const webPortOutOfRange = webPort > MAX_PORT;
244
219 if (245 if (
220 (requireServerPort && serverPortOutOfRange) ||246 (requireServerPort && serverPortOutOfRange) ||
221 (requireWebPort && webPortOutOfRange) ||247 (requireWebPort && webPortOutOfRange) ||
...@@ -251,14 +277,16 @@ export function findFirstAvailableOffset<R = NetService>({...@@ -251,14 +277,16 @@ export function findFirstAvailableOffset<R = NetService>({
251interface ResolveModePortOffsetsInput<R = NetService> {277interface ResolveModePortOffsetsInput<R = NetService> {
252 readonly mode: DevMode;278 readonly mode: DevMode;
253 readonly startOffset: number;279 readonly startOffset: number;
254 readonly envOverrides: EnvOverrides;280 readonly hasExplicitServerPort: boolean;
281 readonly hasExplicitDevUrl: boolean;
255 readonly checkPortAvailability?: PortAvailabilityCheck<R>;282 readonly checkPortAvailability?: PortAvailabilityCheck<R>;
256}283}
257284
258export function resolveModePortOffsets<R = NetService>({285export function resolveModePortOffsets<R = NetService>({
259 mode,286 mode,
260 startOffset,287 startOffset,
261 envOverrides,288 hasExplicitServerPort,
289 hasExplicitDevUrl,
262 checkPortAvailability,290 checkPortAvailability,
263}: ResolveModePortOffsetsInput<R>): Effect.Effect<291}: ResolveModePortOffsetsInput<R>): Effect.Effect<
264 { readonly serverOffset: number; readonly webOffset: number },292 { readonly serverOffset: number; readonly webOffset: number },
...@@ -266,14 +294,14 @@ export function resolveModePortOffsets<R = NetService>({...@@ -266,14 +294,14 @@ export function resolveModePortOffsets<R = NetService>({
266 R294 R
267> {295> {
268 return Effect.gen(function* () {296 return Effect.gen(function* () {
269 const checkPort = (checkPortAvailability ?? defaultCheckPortAvailability) as PortAvailabilityCheck<R>;297 const checkPort = (checkPortAvailability ??
270 const hasExplicitServerPort = typeof envOverrides.T3CODE_PORT === "string";298 defaultCheckPortAvailability) as PortAvailabilityCheck<R>;
271 const hasExplicitDevUrl = typeof envOverrides.VITE_DEV_SERVER_URL === "string";
272299
273 if (mode === "dev:web") {300 if (mode === "dev:web") {
274 if (hasExplicitDevUrl) {301 if (hasExplicitDevUrl) {
275 return { serverOffset: startOffset, webOffset: startOffset };302 return { serverOffset: startOffset, webOffset: startOffset };
276 }303 }
304
277 const webOffset = yield* findFirstAvailableOffset({305 const webOffset = yield* findFirstAvailableOffset({
278 startOffset,306 startOffset,
279 requireServerPort: false,307 requireServerPort: false,
...@@ -287,6 +315,7 @@ export function resolveModePortOffsets<R = NetService>({...@@ -287,6 +315,7 @@ export function resolveModePortOffsets<R = NetService>({
287 if (hasExplicitServerPort) {315 if (hasExplicitServerPort) {
288 return { serverOffset: startOffset, webOffset: startOffset };316 return { serverOffset: startOffset, webOffset: startOffset };
289 }317 }
318
290 const serverOffset = yield* findFirstAvailableOffset({319 const serverOffset = yield* findFirstAvailableOffset({
291 startOffset,320 startOffset,
292 requireServerPort: true,321 requireServerPort: true,
...@@ -302,71 +331,39 @@ export function resolveModePortOffsets<R = NetService>({...@@ -302,71 +331,39 @@ export function resolveModePortOffsets<R = NetService>({
302 requireWebPort: !hasExplicitDevUrl,331 requireWebPort: !hasExplicitDevUrl,
303 checkPortAvailability: checkPort,332 checkPortAvailability: checkPort,
304 });333 });
334
305 return { serverOffset: sharedOffset, webOffset: sharedOffset };335 return { serverOffset: sharedOffset, webOffset: sharedOffset };
306 });336 });
307}337}
308338
309const optionValue = <A>(option: Option.Option<A>): A | undefined =>
310 Option.isSome(option) ? option.value : undefined;
311
312interface DevRunnerCliInput {339interface DevRunnerCliInput {
313 readonly mode: DevMode;340 readonly mode: DevMode;
314 readonly stateDir: Option.Option<string>;341 readonly stateDir: string | undefined;
315 readonly authToken: Option.Option<string>;342 readonly authToken: string | undefined;
316 readonly noBrowser: Option.Option<string>;343 readonly noBrowser: boolean | undefined;
317 readonly autoBootstrapProjectFromCwd: Option.Option<string>;344 readonly autoBootstrapProjectFromCwd: boolean | undefined;
318 readonly logWebSocketEvents: Option.Option<string>;345 readonly logWebSocketEvents: boolean | undefined;
319 readonly host: Option.Option<string>;346 readonly host: string | undefined;
320 readonly port: Option.Option<string>;347 readonly port: number | undefined;
321 readonly devUrl: Option.Option<string>;348 readonly devUrl: URL | undefined;
322 readonly dryRun: boolean;349 readonly dryRun: boolean;
323 readonly turboArgs: ReadonlyArray<string>;350 readonly turboArgs: ReadonlyArray<string>;
324}351}
325352
326function envOverridesFromCliInput(input: DevRunnerCliInput): EnvOverrides {
327 const envOverrides: EnvOverrides = {};
328
329 const stateDir = optionValue(input.stateDir);
330 if (stateDir !== undefined) envOverrides.T3CODE_STATE_DIR = stateDir;
331
332 const authToken = optionValue(input.authToken);
333 if (authToken !== undefined) envOverrides.T3CODE_AUTH_TOKEN = authToken;
334
335 const host = optionValue(input.host);
336 if (host !== undefined) envOverrides.T3CODE_HOST = host;
337
338 const port = optionValue(input.port);
339 if (port !== undefined) envOverrides.T3CODE_PORT = port;
340
341 const devUrl = optionValue(input.devUrl);
342 if (devUrl !== undefined) envOverrides.VITE_DEV_SERVER_URL = devUrl;
343
344 const noBrowser = optionValue(input.noBrowser);
345 if (noBrowser !== undefined) {
346 envOverrides.T3CODE_NO_BROWSER = toBooleanEnvValue(noBrowser);
347 }
348
349 const autoBootstrapProjectFromCwd = optionValue(input.autoBootstrapProjectFromCwd);
350 if (autoBootstrapProjectFromCwd !== undefined) {
351 envOverrides.T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD = toBooleanEnvValue(
352 autoBootstrapProjectFromCwd,
353 );
354 }
355
356 const logWebSocketEvents = optionValue(input.logWebSocketEvents);
357 if (logWebSocketEvents !== undefined) {
358 envOverrides.T3CODE_LOG_WS_EVENTS = toBooleanEnvValue(logWebSocketEvents);
359 }
360
361 return envOverrides;
362}
363
364export function runDevRunnerWithInput(input: DevRunnerCliInput) {353export function runDevRunnerWithInput(input: DevRunnerCliInput) {
365 return Effect.gen(function* () {354 return Effect.gen(function* () {
366 const envOverrides = envOverridesFromCliInput(input);355 const { portOffset, devInstance } = yield* OffsetConfig.asEffect().pipe(
367 const turboArgs = Array.from(input.turboArgs);356 Effect.mapError(
357 (cause) =>
358 new DevRunnerError({
359 message: "Failed to read T3CODE_PORT_OFFSET/T3CODE_DEV_INSTANCE configuration.",
360 cause,
361 }),
362 ),
363 );
364
368 const { offset, source } = yield* Effect.try({365 const { offset, source } = yield* Effect.try({
369 try: () => resolveOffset(),366 try: () => resolveOffset({ portOffset, devInstance }),
370 catch: (cause) =>367 catch: (cause) =>
371 new DevRunnerError({368 new DevRunnerError({
372 message: cause instanceof Error ? cause.message : String(cause),369 message: cause instanceof Error ? cause.message : String(cause),
...@@ -377,46 +374,32 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) {...@@ -377,46 +374,32 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) {
377 const { serverOffset, webOffset } = yield* resolveModePortOffsets({374 const { serverOffset, webOffset } = yield* resolveModePortOffsets({
378 mode: input.mode,375 mode: input.mode,
379 startOffset: offset,376 startOffset: offset,
380 envOverrides,377 hasExplicitServerPort: input.port !== undefined,
378 hasExplicitDevUrl: input.devUrl !== undefined,
381 });379 });
382380
383 const mergedEnvOverrides = { ...envOverrides };
384 if (!("VITE_DEV_SERVER_URL" in mergedEnvOverrides)) {
385 const { webPort } = portPairForOffset(webOffset);
386 mergedEnvOverrides.PORT = String(webPort);
387 mergedEnvOverrides.ELECTRON_RENDERER_PORT = String(webPort);
388 mergedEnvOverrides.VITE_DEV_SERVER_URL = `http://localhost:${webPort}`;
389 }
390
391 const env = yield* createDevRunnerEnv({381 const env = yield* createDevRunnerEnv({
392 mode: input.mode,382 mode: input.mode,
393 env: process.env,383 baseEnv: process.env,
394 offset: serverOffset,384 serverOffset,
395 envOverrides: mergedEnvOverrides,385 webOffset,
386 stateDir: input.stateDir,
387 authToken: input.authToken,
388 noBrowser: input.noBrowser,
389 autoBootstrapProjectFromCwd: input.autoBootstrapProjectFromCwd,
390 logWebSocketEvents: input.logWebSocketEvents,
391 host: input.host,
392 port: input.port,
393 devUrl: input.devUrl,
396 });394 });
397395
398 const parsedServerPort = Number(env.T3CODE_PORT);
399 const parsedWebPort = Number(env.PORT);
400 if (
401 !Number.isInteger(parsedServerPort) ||
402 parsedServerPort < 1 ||
403 parsedServerPort > MAX_PORT ||
404 !Number.isInteger(parsedWebPort) ||
405 parsedWebPort < 1 ||
406 parsedWebPort > MAX_PORT
407 ) {
408 return yield* new DevRunnerError({
409 message: `Invalid computed dev ports: server='${String(env.T3CODE_PORT)}' web='${String(env.PORT)}'.`,
410 });
411 }
412
413 const selectionSuffix =396 const selectionSuffix =
414 serverOffset !== offset || webOffset !== offset397 serverOffset !== offset || webOffset !== offset
415 ? ` selectedOffset(server=${serverOffset},web=${webOffset})`398 ? ` selectedOffset(server=${serverOffset},web=${webOffset})`
416 : "";399 : "";
417400
418 yield* Effect.logInfo(401 yield* Effect.logInfo(
419 `[dev-runner] mode=${input.mode} source=${source}${selectionSuffix} serverPort=${parsedServerPort} webPort=${parsedWebPort} stateDir=${String(env.T3CODE_STATE_DIR)}`,402 `[dev-runner] mode=${input.mode} source=${source}${selectionSuffix} serverPort=${String(env.T3CODE_PORT)} webPort=${String(env.PORT)} stateDir=${String(env.T3CODE_STATE_DIR)}`,
420 );403 );
421404
422 if (input.dryRun) {405 if (input.dryRun) {
...@@ -424,7 +407,7 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) {...@@ -424,7 +407,7 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) {
424 }407 }
425408
426 const command = process.platform === "win32" ? "turbo.cmd" : "turbo";409 const command = process.platform === "win32" ? "turbo.cmd" : "turbo";
427 const child = yield* ChildProcess.make(command, [...MODE_ARGS[input.mode], ...turboArgs], {410 const child = yield* ChildProcess.make(command, [...MODE_ARGS[input.mode], ...input.turboArgs], {
428 stdin: "inherit",411 stdin: "inherit",
429 stdout: "inherit",412 stdout: "inherit",
430 stderr: "inherit",413 stderr: "inherit",
...@@ -432,7 +415,7 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) {...@@ -432,7 +415,7 @@ export function runDevRunnerWithInput(input: DevRunnerCliInput) {
432 extendEnv: false,415 extendEnv: false,
433 });416 });
434417
435 const exitCode = Number(yield* child.exitCode);418 const exitCode = yield* child.exitCode;
436 if (exitCode !== 0) {419 if (exitCode !== 0) {
437 return yield* new DevRunnerError({420 return yield* new DevRunnerError({
438 message: `turbo exited with code ${exitCode}`,421 message: `turbo exited with code ${exitCode}`,
...@@ -456,37 +439,41 @@ const devRunnerCli = Command.make("dev-runner", {...@@ -456,37 +439,41 @@ const devRunnerCli = Command.make("dev-runner", {
456 ),439 ),
457 stateDir: Flag.string("state-dir").pipe(440 stateDir: Flag.string("state-dir").pipe(
458 Flag.withDescription("State directory path (forwards to T3CODE_STATE_DIR)."),441 Flag.withDescription("State directory path (forwards to T3CODE_STATE_DIR)."),
459 Flag.optional,442 Flag.withFallbackConfig(optionalStringConfig("T3CODE_STATE_DIR")),
460 ),443 ),
461 authToken: Flag.string("auth-token").pipe(444 authToken: Flag.string("auth-token").pipe(
462 Flag.withDescription("Auth token (forwards to T3CODE_AUTH_TOKEN)."),445 Flag.withDescription("Auth token (forwards to T3CODE_AUTH_TOKEN)."),
463 Flag.withAlias("token"),446 Flag.withAlias("token"),
464 Flag.optional,447 Flag.withFallbackConfig(optionalStringConfig("T3CODE_AUTH_TOKEN")),
465 ),448 ),
466 noBrowser: Flag.string("no-browser").pipe(449 noBrowser: Flag.boolean("no-browser").pipe(
467 Flag.withDescription("Browser auto-open toggle. Accepts true/false/1/0/no."),450 Flag.withDescription("Browser auto-open toggle (equivalent to T3CODE_NO_BROWSER)."),
468 Flag.optional,451 Flag.withFallbackConfig(optionalBooleanConfig("T3CODE_NO_BROWSER")),
469 ),452 ),
470 autoBootstrapProjectFromCwd: Flag.string("auto-bootstrap-project-from-cwd").pipe(453 autoBootstrapProjectFromCwd: Flag.boolean("auto-bootstrap-project-from-cwd").pipe(
471 Flag.withDescription("Auto-bootstrap toggle. Accepts true/false/1/0/no."),454 Flag.withDescription(
472 Flag.optional,455 "Auto-bootstrap toggle (equivalent to T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD).",
456 ),
457 Flag.withFallbackConfig(optionalBooleanConfig("T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD")),
473 ),458 ),
474 logWebSocketEvents: Flag.string("log-websocket-events").pipe(459 logWebSocketEvents: Flag.boolean("log-websocket-events").pipe(
475 Flag.withDescription("WebSocket event logging toggle. Accepts true/false/1/0/no."),460 Flag.withDescription("WebSocket event logging toggle (equivalent to T3CODE_LOG_WS_EVENTS)."),
476 Flag.withAlias("log-ws-events"),461 Flag.withAlias("log-ws-events"),
477 Flag.optional,462 Flag.withFallbackConfig(optionalBooleanConfig("T3CODE_LOG_WS_EVENTS")),
478 ),463 ),
479 host: Flag.string("host").pipe(464 host: Flag.string("host").pipe(
480 Flag.withDescription("Server host/interface override (forwards to T3CODE_HOST)."),465 Flag.withDescription("Server host/interface override (forwards to T3CODE_HOST)."),
481 Flag.optional,466 Flag.withFallbackConfig(optionalStringConfig("T3CODE_HOST")),
482 ),467 ),
483 port: Flag.string("port").pipe(468 port: Flag.integer("port").pipe(
469 Flag.withSchema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }))),
484 Flag.withDescription("Server port override (forwards to T3CODE_PORT)."),470 Flag.withDescription("Server port override (forwards to T3CODE_PORT)."),
485 Flag.optional,471 Flag.withFallbackConfig(optionalPortConfig("T3CODE_PORT")),
486 ),472 ),
487 devUrl: Flag.string("dev-url").pipe(473 devUrl: Flag.string("dev-url").pipe(
474 Flag.withSchema(Schema.URLFromString),
488 Flag.withDescription("Web dev URL override (forwards to VITE_DEV_SERVER_URL)."),475 Flag.withDescription("Web dev URL override (forwards to VITE_DEV_SERVER_URL)."),
489 Flag.optional,476 Flag.withFallbackConfig(optionalUrlConfig("VITE_DEV_SERVER_URL")),
490 ),477 ),
491 dryRun: Flag.boolean("dry-run").pipe(478 dryRun: Flag.boolean("dry-run").pipe(
492 Flag.withDescription("Resolve mode/ports/env and print, but do not spawn turbo."),479 Flag.withDescription("Resolve mode/ports/env and print, but do not spawn turbo."),
...@@ -501,26 +488,16 @@ const devRunnerCli = Command.make("dev-runner", {...@@ -501,26 +488,16 @@ const devRunnerCli = Command.make("dev-runner", {
501 Command.withHandler((input) => runDevRunnerWithInput(input)),488 Command.withHandler((input) => runDevRunnerWithInput(input)),
502);489);
503490
504export function runDevRunner(argv = process.argv) {491const cliRuntimeLayer = Layer.mergeAll(
505 return Command.runWith(devRunnerCli, { version: "0.0.0" })(argv.slice(2)).pipe(
506 Effect.mapError((cause) =>
507 cause instanceof DevRunnerError
508 ? cause
509 : new DevRunnerError({
510 message: cause instanceof Error ? cause.message : "dev-runner argument parsing failed",
511 cause,
512 }),
513 ),
514 );
515}
516
517const runtimeLayer = Layer.mergeAll(
518 Logger.layer([Logger.consolePretty()]),492 Logger.layer([Logger.consolePretty()]),
519 NodeServices.layer,493 NodeServices.layer,
520 NetService.layer,494 NetService.layer,
521);495);
522496
523const runtimeProgram = runDevRunner().pipe(Effect.scoped, Effect.provide(runtimeLayer));497const runtimeProgram = Command.run(devRunnerCli, { version: "0.0.0" }).pipe(
498 Effect.scoped,
499 Effect.provide(cliRuntimeLayer),
500);
524501
525if (import.meta.main) {502if (import.meta.main) {
526 NodeRuntime.runMain(runtimeProgram);503 NodeRuntime.runMain(runtimeProgram);