11// @effect -diagnostics nodeBuiltinImport:off
2+ import * as NodeChildProcess from "node:child_process" ;
3+ import * as NodeEvents from "node:events" ;
24import * as NodeFS from "node:fs" ;
35import * as NodeOS from "node:os" ;
46
@@ -10,6 +12,7 @@ import * as Layer from "effect/Layer";
1012import * as Option from "effect/Option" ;
1113import * as Path from "effect/Path" ;
1214import * as Redacted from "effect/Redacted" ;
15+ import * as Result from "effect/Result" ;
1316import * as Schema from "effect/Schema" ;
1417
1518import {
@@ -58,6 +61,22 @@ const makeDesktopBootstrap = (
5861 ...overrides ,
5962} ) ;
6063
64+ const liveProcess = Effect . acquireRelease (
65+ Effect . promise ( async ( ) => {
66+ const child = NodeChildProcess . spawn ( process . execPath , [ "-e" , "process.stdin.resume()" ] , {
67+ stdio : [ "pipe" , "ignore" , "ignore" ] ,
68+ } ) ;
69+ await NodeEvents . EventEmitter . once ( child , "spawn" ) ;
70+ return child ;
71+ } ) ,
72+ ( child ) =>
73+ Effect . promise ( async ( ) => {
74+ const exited = NodeEvents . EventEmitter . once ( child , "exit" ) ;
75+ child . kill ( ) ;
76+ await exited ;
77+ } ) ,
78+ ) ;
79+
6180it . layer ( NodeServices . layer ) ( "cli config resolution" , ( it ) => {
6281 const defaultObservabilityConfig = {
6382 traceMinLevel : "Info" ,
@@ -100,10 +119,13 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
100119 const fs = yield * FileSystem . FileSystem ;
101120 const path = yield * Path . Path ;
102121 const root = yield * fs . makeTempDirectoryScoped ( { prefix : "t3-cli-preflight-" } ) ;
122+ const child = yield * liveProcess ;
123+ assert ( child . pid !== undefined ) ;
103124 for ( const [ name , pid , mode , rejectRunningServer ] of [
104125 [ "stale" , 2_147_483_647 , "web" , true ] ,
105- [ "desktop" , process . pid , "desktop" , true ] ,
106- [ "serve" , process . pid , "web" , false ] ,
126+ [ "own-pid" , process . pid , "web" , true ] ,
127+ [ "desktop" , child . pid , "desktop" , true ] ,
128+ [ "serve" , child . pid , "web" , false ] ,
107129 ] as const ) {
108130 const baseDir = path . join ( root , name ) ;
109131 const stateDir = path . join ( baseDir , "userdata" ) ;
@@ -119,7 +141,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
119141 } ) ,
120142 ) ;
121143 const cwd = path . join ( root , `${ name } -project` ) ;
122- const config = yield * resolveServerConfig (
144+ const configResult = yield * resolveServerConfig (
123145 {
124146 ...minimalWebFlags ( baseDir ) ,
125147 mode : Option . some ( mode ) ,
@@ -135,13 +157,56 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
135157 ConfigProvider . layer ( ConfigProvider . fromEnv ( { env : { } } ) ) ,
136158 ) ,
137159 ) ,
160+ Effect . result ,
138161 ) ;
139- expect ( config . cwd ) . toBe ( cwd ) ;
162+ assert ( Result . isSuccess ( configResult ) ) ;
163+ expect ( configResult . success . cwd ) . toBe ( cwd ) ;
140164 expect ( yield * fs . exists ( cwd ) ) . toBe ( true ) ;
141165 }
142166 } ) ,
143167 ) ;
144168
169+ it . effect (
170+ "refuses a manual web launch when a runtime record belongs to another live process" ,
171+ ( ) =>
172+ Effect . gen ( function * ( ) {
173+ const fs = yield * FileSystem . FileSystem ;
174+ const path = yield * Path . Path ;
175+ const baseDir = yield * fs . makeTempDirectoryScoped ( { prefix : "t3-cli-preflight-live-" } ) ;
176+ const child = yield * liveProcess ;
177+ assert ( child . pid !== undefined ) ;
178+ const stateDir = path . join ( baseDir , "userdata" ) ;
179+ yield * fs . makeDirectory ( stateDir , { recursive : true } ) ;
180+ yield * fs . writeFileString (
181+ path . join ( stateDir , "server-runtime.json" ) ,
182+ yield * encodeUnknownJson ( {
183+ version : 1 ,
184+ pid : child . pid ,
185+ port : 3773 ,
186+ origin : "http://127.0.0.1:3773" ,
187+ startedAt : "2026-10-01T00:00:00.000Z" ,
188+ } ) ,
189+ ) ;
190+ const cwd = path . join ( baseDir , "project" ) ;
191+ const error = yield * resolveServerConfig (
192+ { ...minimalWebFlags ( baseDir ) , cwd : Option . some ( cwd ) } ,
193+ Option . none ( ) ,
194+ { rejectRunningServer : true } ,
195+ ) . pipe (
196+ Effect . provide (
197+ Layer . merge (
198+ NetService . layer ,
199+ ConfigProvider . layer ( ConfigProvider . fromEnv ( { env : { } } ) ) ,
200+ ) ,
201+ ) ,
202+ Effect . flip ,
203+ ) ;
204+ expect ( String ( error ) ) . toContain ( "A T3 Code server is already running" ) ;
205+ expect ( String ( error ) ) . toContain ( `pid ${ child . pid } ` ) ;
206+ expect ( yield * fs . exists ( cwd ) ) . toBe ( false ) ;
207+ } ) ,
208+ ) ;
209+
145210 it . effect ( "enables a trimmed reusable auth token only for web dev mode" , ( ) =>
146211 Effect . gen ( function * ( ) {
147212 const baseDir = yield * FileSystem . FileSystem . pipe (
0 commit comments