authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-12 21:36:07-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-12 21:41:42-08:00
log15380f2b5f99065efb0100cc47ad96255594ae4e
tree8da3f6754a591ee9ab60d7640ad475dce75548cb
parent48b1f7c38862e3672cf5b8ec9ca6edbc6f48e5ef

Add dev runner for deterministic multi-instance port offsets

- Route `dev*` scripts through `scripts/dev-runner.mjs` to compute shared server/web port offsets - Support `T3CODE_DEV_INSTANCE` (hashed or numeric) and explicit `T3CODE_PORT_OFFSET` - Document multi-instance usage and allow server `VITE_DEV_SERVER_URL` override

5 files changed, 143 insertions(+), 5 deletions(-)

README.md+13
...@@ -49,6 +49,9 @@ bun run dev...@@ -49,6 +49,9 @@ bun run dev
49# Desktop development49# Desktop development
50bun run dev:desktop50bun run dev:desktop
5151
52# Desktop development on an isolated port set
53T3CODE_DEV_INSTANCE=feature-xyz bun run dev:desktop
54
52# Production55# Production
53bun run build56bun run build
54bun run start57bun run start
...@@ -67,6 +70,16 @@ npx t3...@@ -67,6 +70,16 @@ npx t3
67- `bun run typecheck` — Strict TypeScript checks for all packages.70- `bun run typecheck` — Strict TypeScript checks for all packages.
68- `bun run test` — Runs workspace tests.71- `bun run test` — Runs workspace tests.
6972
73### Running multiple dev instances
74
75Set `T3CODE_DEV_INSTANCE` to any value to deterministically shift all dev ports together.
76
77- Default ports: server `3773`, web `5173`
78- Shifted ports: `base + offset` (offset is hashed from `T3CODE_DEV_INSTANCE`)
79- Example: `T3CODE_DEV_INSTANCE=branch-a bun run dev:desktop`
80
81If you want full control instead of hashing, set `T3CODE_PORT_OFFSET` to a numeric offset.
82
70## Runtime modes83## Runtime modes
7184
72T3 Code has a global runtime mode switch in the chat toolbar:85T3 Code has a global runtime mode switch in the chat toolbar:
apps/server/package.json+1-1
...@@ -10,7 +10,7 @@...@@ -10,7 +10,7 @@
10 "type": "module",10 "type": "module",
11 "main": "./dist/index.mjs",11 "main": "./dist/index.mjs",
12 "scripts": {12 "scripts": {
13 "dev": "VITE_DEV_SERVER_URL=http://localhost:5173 bun run src/index.ts",13 "dev": "VITE_DEV_SERVER_URL=${VITE_DEV_SERVER_URL:-http://localhost:5173} bun run src/index.ts",
14 "build": "tsdown && node scripts/bundle-client.mjs",14 "build": "tsdown && node scripts/bundle-client.mjs",
15 "start": "node dist/index.mjs",15 "start": "node dist/index.mjs",
16 "typecheck": "tsc --noEmit",16 "typecheck": "tsc --noEmit",
package.json+4-4
...@@ -6,10 +6,10 @@...@@ -6,10 +6,10 @@
6 "packages/*"6 "packages/*"
7 ],7 ],
8 "scripts": {8 "scripts": {
9 "dev": "T3CODE_LOG_WS_EVENTS=1 VITE_WS_URL=ws://localhost:3773 turbo run dev --ui=tui --filter=@t3tools/contracts --filter=@t3tools/web --filter=t3 --parallel",9 "dev": "bun run scripts/dev-runner.mjs dev",
10 "dev:server": "turbo run dev --filter=t3",10 "dev:server": "bun run scripts/dev-runner.mjs dev:server",
11 "dev:web": "VITE_WS_URL=ws://localhost:3773 turbo run dev --filter=@t3tools/web",11 "dev:web": "bun run scripts/dev-runner.mjs dev:web",
12 "dev:desktop": "turbo run dev --filter=@t3tools/desktop --filter=@t3tools/web --parallel",12 "dev:desktop": "bun run scripts/dev-runner.mjs dev:desktop",
13 "start": "turbo run start --filter=t3",13 "start": "turbo run start --filter=t3",
14 "start:desktop": "turbo run start --filter=@t3tools/desktop",14 "start:desktop": "turbo run start --filter=@t3tools/desktop",
15 "build": "turbo run build",15 "build": "turbo run build",
scripts/dev-runner.mjs created+124
...@@ -0,0 +1,124 @@
1import { spawn } from "node:child_process";
2
3const BASE_SERVER_PORT = 3773;
4const BASE_WEB_PORT = 5173;
5const MAX_HASH_OFFSET = 3000;
6const MODE_ARGS = {
7 dev: [
8 "run",
9 "dev",
10 "--ui=tui",
11 "--filter=@t3tools/contracts",
12 "--filter=@t3tools/web",
13 "--filter=t3",
14 "--parallel",
15 ],
16 "dev:server": ["run", "dev", "--filter=t3"],
17 "dev:web": ["run", "dev", "--filter=@t3tools/web"],
18 "dev:desktop": ["run", "dev", "--filter=@t3tools/desktop", "--filter=@t3tools/web", "--parallel"],
19};
20
21function parseInteger(value, envName) {
22 const parsed = Number(value);
23 if (!Number.isInteger(parsed)) {
24 throw new Error(`Invalid ${envName}: ${value}`);
25 }
26 return parsed;
27}
28
29function hashSeed(seed) {
30 let hash = 0x811c9dc5;
31 for (let i = 0; i < seed.length; i += 1) {
32 hash ^= seed.charCodeAt(i);
33 hash = Math.imul(hash, 0x01000193);
34 }
35 return hash >>> 0;
36}
37
38function resolveOffset() {
39 const explicitOffset = process.env.T3CODE_PORT_OFFSET?.trim();
40 if (explicitOffset) {
41 const parsed = parseInteger(explicitOffset, "T3CODE_PORT_OFFSET");
42 if (parsed < 0) {
43 throw new Error(`Invalid T3CODE_PORT_OFFSET: ${explicitOffset}`);
44 }
45 return { offset: parsed, source: `T3CODE_PORT_OFFSET=${explicitOffset}` };
46 }
47
48 const seed = process.env.T3CODE_DEV_INSTANCE?.trim();
49 if (!seed) {
50 return { offset: 0, source: "default ports" };
51 }
52
53 if (/^\d+$/.test(seed)) {
54 return { offset: Number(seed), source: `numeric T3CODE_DEV_INSTANCE=${seed}` };
55 }
56
57 const offset = (hashSeed(seed) % MAX_HASH_OFFSET) + 1;
58 return { offset, source: `hashed T3CODE_DEV_INSTANCE=${seed}` };
59}
60
61function main() {
62 const mode = process.argv[2];
63 const isDryRun = process.argv.includes("--dry-run");
64 if (!mode || !(mode in MODE_ARGS)) {
65 const supportedModes = Object.keys(MODE_ARGS).join(", ");
66 throw new Error(`Usage: bun scripts/dev-runner.mjs <mode>. Supported modes: ${supportedModes}`);
67 }
68
69 const { offset, source } = resolveOffset();
70 const serverPort = BASE_SERVER_PORT + offset;
71 const webPort = BASE_WEB_PORT + offset;
72
73 if (serverPort > 65535 || webPort > 65535) {
74 throw new Error(
75 `Port offset too large (${offset}). Computed ports: server=${serverPort}, web=${webPort}`,
76 );
77 }
78
79 const env = {
80 ...process.env,
81 T3CODE_PORT: String(serverPort),
82 PORT: String(webPort),
83 ELECTRON_RENDERER_PORT: String(webPort),
84 VITE_WS_URL: `ws://localhost:${serverPort}`,
85 VITE_DEV_SERVER_URL: `http://localhost:${webPort}`,
86 };
87 if (mode === "dev" && !env.T3CODE_LOG_WS_EVENTS) {
88 env.T3CODE_LOG_WS_EVENTS = "1";
89 }
90
91 console.info(
92 `[dev-runner] mode=${mode} source=${source} serverPort=${serverPort} webPort=${webPort}`,
93 );
94
95 if (isDryRun) {
96 return;
97 }
98
99 const command = process.platform === "win32" ? "turbo.cmd" : "turbo";
100 const child = spawn(command, MODE_ARGS[mode], {
101 stdio: "inherit",
102 env,
103 });
104
105 child.on("exit", (code, signal) => {
106 if (signal) {
107 process.kill(process.pid, signal);
108 return;
109 }
110 process.exit(code ?? 0);
111 });
112
113 child.on("error", (error) => {
114 console.error("[dev-runner] failed to start turbo", error);
115 process.exit(1);
116 });
117}
118
119try {
120 main();
121} catch (error) {
122 console.error("[dev-runner]", error);
123 process.exit(1);
124}
turbo.json+1
...@@ -4,6 +4,7 @@...@@ -4,6 +4,7 @@
4 "PORT",4 "PORT",
5 "VITE_WS_URL",5 "VITE_WS_URL",
6 "VITE_DEV_SERVER_URL",6 "VITE_DEV_SERVER_URL",
7 "ELECTRON_RENDERER_PORT",
7 "T3CODE_LOG_WS_EVENTS",8 "T3CODE_LOG_WS_EVENTS",
8 "T3CODE_MODE",9 "T3CODE_MODE",
9 "T3CODE_PORT",10 "T3CODE_PORT",