@@ -63,6 +63,23 @@ type BrowserFrontendRuntimeState = {
6363 readonly webState : BrowserFrontendStateFile | null
6464}
6565
66+ export interface BrowserFrontendCommandOptions {
67+ readonly daemon : boolean
68+ }
69+
70+ const browserFrontendForegroundOptions : BrowserFrontendCommandOptions = { daemon : false }
71+
72+ type BrowserFrontendLaunch = {
73+ readonly env : Readonly < Record < string , string > >
74+ readonly localUrl : string
75+ }
76+
77+ type BrowserFrontendRunnerEffect = Effect . Effect <
78+ void ,
79+ ControllerBootstrapError | PlatformError ,
80+ CommandExecutor . CommandExecutor
81+ >
82+
6683const browserEnv = ( decision : BrowserFrontendStartDecision ) : Readonly < Record < string , string > > => ( {
6784 ...copyProcessEnv ( ) ,
6885 DOCKER_GIT_API_URL : decision . apiBaseUrl ,
@@ -76,19 +93,56 @@ const runStreaming = (
7693 args : ReadonlyArray < string > ,
7794 env : Readonly < Record < string , string > >
7895) : Effect . Effect < number , PlatformError , CommandExecutor . CommandExecutor > =>
79- runCommandExitCodeStreaming ( {
80- args,
81- command : "bun" ,
82- cwd : process . cwd ( ) ,
83- env
84- } )
96+ runCommandExitCodeStreaming ( { args, command : "bun" , cwd : process . cwd ( ) , env } )
8597
8698const parsePids = ( output : string ) : ReadonlyArray < string > =>
8799 output
88100 . split ( / \s + / u)
89101 . map ( ( pid ) => pid . trim ( ) )
90102 . filter ( ( pid ) => / ^ \d + $ / u. test ( pid ) )
91103
104+ // CHANGE: derive a stable daemon log path beside the browser runtime state file.
105+ // WHY: detached mode must preserve diagnostics after the parent CLI exits.
106+ // QUOTE(ТЗ): "Run browser with support dameon mode, like a flag -d"
107+ // REF: issue-373
108+ // SOURCE: n/a
109+ // FORMAT THEOREM: suffix(statePath,".json") -> logPath = prefix(statePath,".json") + ".log"
110+ // PURITY: CORE
111+ // EFFECT: n/a
112+ // INVARIANT: every state path maps deterministically to exactly one log path
113+ // COMPLEXITY: O(n)/O(n) where n = |statePath|
114+ const browserFrontendLogPath = ( statePath : string ) : string =>
115+ statePath . endsWith ( ".json" ) ? `${ statePath . slice ( 0 , - ".json" . length ) } .log` : `${ statePath } .log`
116+
117+ const parseDaemonPid = ( output : string ) : Effect . Effect < string , ControllerBootstrapError > => {
118+ const pid = parsePids ( output ) [ 0 ]
119+ return pid === undefined
120+ ? Effect . fail ( browserFrontendError ( "Browser frontend daemon did not report a pid." ) )
121+ : Effect . succeed ( pid )
122+ }
123+
124+ const startDaemon = (
125+ args : ReadonlyArray < string > ,
126+ env : Readonly < Record < string , string > > ,
127+ logPath : string
128+ ) : Effect . Effect < string , ControllerBootstrapError | PlatformError , CommandExecutor . CommandExecutor > => {
129+ const script = [
130+ "log_path=\"$1\"" ,
131+ "shift" ,
132+ "command -v nohup >/dev/null 2>&1 || exit 127" ,
133+ "command -v \"$1\" >/dev/null 2>&1 || exit 127" ,
134+ "mkdir -p \"$(dirname \"$log_path\")\"" ,
135+ "nohup \"$@\" >>\"$log_path\" 2>&1 < /dev/null &" ,
136+ String . raw `printf '%s\n' "$!"`
137+ ] . join ( "\n" )
138+
139+ return runCommandCapture (
140+ { args : [ "-c" , script , "sh" , logPath , "bun" , ...args ] , command : "sh" , cwd : process . cwd ( ) , env } ,
141+ [ 0 ] ,
142+ ( ) => browserFrontendError ( "Failed to start browser frontend daemon." )
143+ ) . pipe ( Effect . flatMap ( ( output ) => parseDaemonPid ( output ) ) )
144+ }
145+
92146const findWebServerPids = ( ) : Effect . Effect < ReadonlyArray < string > , never , CommandExecutor . CommandExecutor > => {
93147 const script = [
94148 "port=\"$1\"" ,
@@ -271,27 +325,48 @@ const ensureSuccess = (
271325 ? Effect . void
272326 : Effect . fail ( browserFrontendError ( `${ action } failed with exit code ${ exitCode } .` ) )
273327
274- export const runBrowserFrontend = (
328+ // CHANGE: share the browser frontend build phase between foreground and daemon modes.
329+ // WHY: daemon mode must not drift from foreground mode in revision, environment, or build failure semantics.
330+ // QUOTE(ТЗ): "Run browser with support dameon mode, like a flag -d"
331+ // REF: issue-373
332+ // SOURCE: n/a
333+ // FORMAT THEOREM: forall mode in {foreground,daemon}: launch(mode) -> built(webRevision)
334+ // PURITY: SHELL
335+ // EFFECT: Effect<BrowserFrontendLaunch, ControllerBootstrapError | PlatformError, CommandExecutor>
336+ // INVARIANT: launch env is derived exactly once from BrowserFrontendStartDecision
337+ // COMPLEXITY: O(build)/O(env)
338+ const buildBrowserFrontendLaunch = (
275339 decision : BrowserFrontendStartDecision
276- ) : Effect . Effect <
277- void ,
278- ControllerBootstrapError | PlatformError ,
279- CommandExecutor . CommandExecutor
280- > =>
340+ ) : Effect . Effect < BrowserFrontendLaunch , ControllerBootstrapError | PlatformError , CommandExecutor . CommandExecutor > =>
281341 Effect . gen ( function * ( _ ) {
282342 const env = browserEnv ( decision )
283343 const localUrl = `http://${ decision . host } :${ decision . port } /`
284344
285345 yield * _ ( Effect . log ( `Building docker-git browser frontend ${ decision . webRevision } for API ${ decision . apiBaseUrl } .` ) )
286346 const buildExitCode = yield * _ ( runStreaming ( [ "run" , "--cwd" , "packages/app" , "build:web" ] , env ) )
287347 yield * _ ( ensureSuccess ( buildExitCode , "Browser frontend build" ) )
348+ return { env, localUrl }
349+ } )
288350
289- yield * _ ( Effect . log ( `docker-git browser frontend: ${ localUrl } ` ) )
351+ export const runBrowserFrontend = ( decision : BrowserFrontendStartDecision ) : BrowserFrontendRunnerEffect =>
352+ Effect . gen ( function * ( _ ) {
353+ const launch = yield * _ ( buildBrowserFrontendLaunch ( decision ) )
354+ yield * _ ( Effect . log ( `docker-git browser frontend: ${ launch . localUrl } ` ) )
290355 yield * _ ( Effect . log ( "Press Ctrl+C to stop the browser frontend." ) )
291- const serveExitCode = yield * _ ( runStreaming ( [ "run" , "--cwd" , "packages/app" , "serve:web" ] , env ) )
356+ const serveExitCode = yield * _ ( runStreaming ( [ "run" , "--cwd" , "packages/app" , "serve:web" ] , launch . env ) )
292357 yield * _ ( ensureSuccess ( serveExitCode , "Browser frontend server" ) )
293358 } )
294359
360+ export const runBrowserFrontendDaemon = ( decision : BrowserFrontendStartDecision ) : BrowserFrontendRunnerEffect =>
361+ Effect . gen ( function * ( _ ) {
362+ const launch = yield * _ ( buildBrowserFrontendLaunch ( decision ) )
363+ const logPath = browserFrontendLogPath ( decision . statePath )
364+
365+ const pid = yield * _ ( startDaemon ( [ "run" , "--cwd" , "packages/app" , "serve:web" ] , launch . env , logPath ) )
366+ yield * _ ( Effect . log ( `docker-git browser frontend daemon: ${ launch . localUrl } (pid ${ pid } )` ) )
367+ yield * _ ( Effect . log ( `docker-git browser frontend daemon log: ${ logPath } ` ) )
368+ } )
369+
295370// CHANGE: make `docker-git browser` idempotent for local development
296371// WHY: repeated invocations should deploy only changed API or browser code
297372// QUOTE(ТЗ): "Надо перезапускать только те контейнеры у которых изменился код"
@@ -302,15 +377,25 @@ export const runBrowserFrontend = (
302377// EFFECT: Effect<void, ControllerBootstrapError | PlatformError, ControllerRuntime>
303378// INVARIANT: controller readiness is checked independently from browser runtime reuse
304379// COMPLEXITY: O(total_bytes(web_inputs) + processes + controller_probe)
305- export const runBrowserFrontendCommand : Effect . Effect <
380+ export const runBrowserFrontendCommandWithOptions = (
381+ options : BrowserFrontendCommandOptions
382+ ) : Effect . Effect <
306383 void ,
307384 ControllerBootstrapError | PlatformError ,
308385 ControllerRuntime
309- > = pipe (
310- prepareBrowserStack ( ) ,
311- Effect . flatMap ( ( decision ) =>
312- decision . shouldStartWeb
313- ? runBrowserFrontend ( decision )
314- : Effect . log ( `docker-git browser frontend is already running at http://${ decision . host } :${ decision . port } /` )
386+ > =>
387+ pipe (
388+ prepareBrowserStack ( ) ,
389+ Effect . flatMap ( ( decision ) => {
390+ if ( ! decision . shouldStartWeb ) {
391+ return Effect . log ( `docker-git browser frontend is already running at http://${ decision . host } :${ decision . port } /` )
392+ }
393+ return options . daemon ? runBrowserFrontendDaemon ( decision ) : runBrowserFrontend ( decision )
394+ } )
315395 )
316- )
396+
397+ export const runBrowserFrontendCommand : Effect . Effect <
398+ void ,
399+ ControllerBootstrapError | PlatformError ,
400+ ControllerRuntime
401+ > = runBrowserFrontendCommandWithOptions ( browserFrontendForegroundOptions )
0 commit comments