@@ -80,27 +80,104 @@ def run_compose_command(service_name, command, build=False):
8080 console .print (f"[red]❌ Docker compose file not found: { compose_file } [/red]" )
8181 return False
8282
83+ # Step 1: If build is requested, run build separately first (no timeout for CUDA builds)
84+ if build and command == 'up' :
85+ # Build command - need to specify profiles for build too
86+ build_cmd = ['docker' , 'compose' ]
87+
88+ # Add profiles to build command (needed for profile-specific services)
89+ if service_name == 'backend' :
90+ caddyfile_path = service_path / 'Caddyfile'
91+ if caddyfile_path .exists () and caddyfile_path .is_file ():
92+ build_cmd .extend (['--profile' , 'https' ])
93+
94+ obsidian_enabled = False
95+ config_data = load_config_yml ()
96+ if config_data :
97+ memory_config = config_data .get ('memory' , {})
98+ obsidian_config = memory_config .get ('obsidian' , {})
99+ if obsidian_config .get ('enabled' , False ):
100+ obsidian_enabled = True
101+
102+ if not obsidian_enabled :
103+ env_file = service_path / '.env'
104+ if env_file .exists ():
105+ env_values = dotenv_values (env_file )
106+ if env_values .get ('OBSIDIAN_ENABLED' , 'false' ).lower () == 'true' :
107+ obsidian_enabled = True
108+
109+ if obsidian_enabled :
110+ build_cmd .extend (['--profile' , 'obsidian' ])
111+
112+ elif service_name == 'speaker-recognition' :
113+ env_file = service_path / '.env'
114+ if env_file .exists ():
115+ env_values = dotenv_values (env_file )
116+ compute_mode = env_values .get ('COMPUTE_MODE' , 'cpu' )
117+ build_cmd .extend (['--profile' , compute_mode ])
118+
119+ build_cmd .append ('build' )
120+
121+ # Run build with streaming output (no timeout)
122+ console .print (f"[cyan]🔨 Building { service_name } (this may take several minutes for CUDA/GPU builds)...[/cyan]" )
123+ try :
124+ process = subprocess .Popen (
125+ build_cmd ,
126+ cwd = service_path ,
127+ stdout = subprocess .PIPE ,
128+ stderr = subprocess .STDOUT ,
129+ text = True ,
130+ bufsize = 1
131+ )
132+
133+ if process .stdout is None :
134+ raise RuntimeError ("Process stdout is None - unable to read command output" )
135+
136+ for line in process .stdout :
137+ line = line .rstrip ()
138+ if not line :
139+ continue
140+
141+ if 'error' in line .lower () or 'failed' in line .lower ():
142+ console .print (f" [red]{ line } [/red]" )
143+ elif 'Successfully' in line or 'built' in line .lower ():
144+ console .print (f" [green]{ line } [/green]" )
145+ elif 'Building' in line or 'Step' in line :
146+ console .print (f" [cyan]{ line } [/cyan]" )
147+ elif 'warning' in line .lower ():
148+ console .print (f" [yellow]{ line } [/yellow]" )
149+ else :
150+ console .print (f" [dim]{ line } [/dim]" )
151+
152+ process .wait ()
153+
154+ if process .returncode != 0 :
155+ console .print (f"\n [red]❌ Build failed for { service_name } [/red]" )
156+ return False
157+
158+ console .print (f"[green]✅ Build completed for { service_name } [/green]" )
159+
160+ except Exception as e :
161+ console .print (f"[red]❌ Error building { service_name } : { e } [/red]" )
162+ return False
163+
164+ # Step 2: Run the actual command (up/down/restart/status)
83165 cmd = ['docker' , 'compose' ]
84166
85- # For backend service, check if HTTPS is configured (Caddyfile exists)
167+ # Add profiles for backend service
86168 if service_name == 'backend' :
87169 caddyfile_path = service_path / 'Caddyfile'
88170 if caddyfile_path .exists () and caddyfile_path .is_file ():
89- # Enable HTTPS profile to start Caddy service
90171 cmd .extend (['--profile' , 'https' ])
91172
92- # Check if Obsidian/Neo4j is enabled
93173 obsidian_enabled = False
94-
95- # Method 1: Check config.yml (preferred)
96174 config_data = load_config_yml ()
97175 if config_data :
98176 memory_config = config_data .get ('memory' , {})
99177 obsidian_config = memory_config .get ('obsidian' , {})
100178 if obsidian_config .get ('enabled' , False ):
101179 obsidian_enabled = True
102180
103- # Method 2: Fallback to .env for backward compatibility
104181 if not obsidian_enabled :
105182 env_file = service_path / '.env'
106183 if env_file .exists ():
@@ -114,30 +191,22 @@ def run_compose_command(service_name, command, build=False):
114191
115192 # Handle speaker-recognition service specially
116193 if service_name == 'speaker-recognition' and command in ['up' , 'down' ]:
117- # Read configuration to determine profile
118194 env_file = service_path / '.env'
119195 if env_file .exists ():
120196 env_values = dotenv_values (env_file )
121197 compute_mode = env_values .get ('COMPUTE_MODE' , 'cpu' )
122198
123- # Add profile flag for both up and down commands
124- if compute_mode == 'gpu' :
125- cmd .extend (['--profile' , 'gpu' ])
126- else :
127- cmd .extend (['--profile' , 'cpu' ])
199+ cmd .extend (['--profile' , compute_mode ])
128200
129201 if command == 'up' :
130202 https_enabled = env_values .get ('REACT_UI_HTTPS' , 'false' )
131203 if https_enabled .lower () == 'true' :
132- # HTTPS mode: start with profile for all services (includes nginx)
133204 cmd .extend (['up' , '-d' ])
134205 else :
135- # HTTP mode: start specific services with profile (no nginx)
136206 cmd .extend (['up' , '-d' , 'speaker-service-gpu' if compute_mode == 'gpu' else 'speaker-service-cpu' , 'web-ui' ])
137207 elif command == 'down' :
138208 cmd .extend (['down' ])
139209 else :
140- # Fallback: no profile
141210 if command == 'up' :
142211 cmd .extend (['up' , '-d' ])
143212 elif command == 'down' :
@@ -152,90 +221,73 @@ def run_compose_command(service_name, command, build=False):
152221 cmd .extend (['restart' ])
153222 elif command == 'status' :
154223 cmd .extend (['ps' ])
155-
156- if command == 'up' and build :
157- cmd .append ('--build' )
158-
224+
159225 try :
160- # For commands that need real-time output (build), stream to console
161- if build and command == 'up' :
162- console .print (f"[dim]Building { service_name } containers...[/dim]" )
163- process = subprocess .Popen (
164- cmd ,
165- cwd = service_path ,
166- stdout = subprocess .PIPE ,
167- stderr = subprocess .STDOUT ,
168- text = True ,
169- bufsize = 1
170- )
171-
172- # Simply stream all output with coloring
173- all_output = []
174-
175- if process .stdout is None :
176- raise RuntimeError ("Process stdout is None - unable to read command output" )
177- for line in process .stdout :
178- line = line .rstrip ()
179- if not line :
180- continue
181-
182- # Store for error context
183- all_output .append (line )
184-
185- # Print with appropriate coloring
186- if 'error' in line .lower () or 'failed' in line .lower ():
187- console .print (f" [red]{ line } [/red]" )
188- elif 'Successfully' in line or 'Started' in line or 'Created' in line :
189- console .print (f" [green]{ line } [/green]" )
190- elif 'Building' in line or 'Creating' in line :
191- console .print (f" [cyan]{ line } [/cyan]" )
192- elif 'warning' in line .lower ():
193- console .print (f" [yellow]{ line } [/yellow]" )
194- else :
195- console .print (f" [dim]{ line } [/dim]" )
196-
197- # Wait for process to complete
198- process .wait ()
199-
200- # If build failed, show error summary
201- if process .returncode != 0 :
202- console .print (f"\n [red]❌ Build failed for { service_name } [/red]" )
203- return False
204-
226+ # Run the command with timeout (build already done if needed)
227+ result = subprocess .run (
228+ cmd ,
229+ cwd = service_path ,
230+ capture_output = True ,
231+ text = True ,
232+ check = False ,
233+ timeout = 120 # 2 minute timeout
234+ )
235+
236+ if result .returncode == 0 :
205237 return True
206238 else :
207- # For non-build commands, run silently unless there's an error
208- result = subprocess .run (
209- cmd ,
210- cwd = service_path ,
211- capture_output = True ,
212- text = True ,
213- check = False ,
214- timeout = 120 # 2 minute timeout for service status checks
215- )
216-
217- if result .returncode == 0 :
218- return True
219- else :
220- console .print (f"[red]❌ Command failed[/red]" )
221- if result .stderr :
222- console .print ("[red]Error output:[/red]" )
223- # Show all error output
224- for line in result .stderr .splitlines ():
225- console .print (f" [dim]{ line } [/dim]" )
226- return False
227-
239+ console .print (f"[red]❌ Command failed[/red]" )
240+ if result .stderr :
241+ console .print ("[red]Error output:[/red]" )
242+ for line in result .stderr .splitlines ():
243+ console .print (f" [dim]{ line } [/dim]" )
244+ return False
245+
228246 except subprocess .TimeoutExpired :
229247 console .print (f"[red]❌ Command timed out after 2 minutes for { service_name } [/red]" )
230248 return False
231249 except Exception as e :
232250 console .print (f"[red]❌ Error running command: { e } [/red]" )
233251 return False
234252
253+ def ensure_docker_network ():
254+ """Ensure chronicle-network exists"""
255+ try :
256+ # Check if network already exists
257+ result = subprocess .run (
258+ ['docker' , 'network' , 'inspect' , 'chronicle-network' ],
259+ capture_output = True ,
260+ check = False
261+ )
262+
263+ if result .returncode != 0 :
264+ # Network doesn't exist, create it
265+ console .print ("[blue]📡 Creating chronicle-network...[/blue]" )
266+ subprocess .run (
267+ ['docker' , 'network' , 'create' , 'chronicle-network' ],
268+ check = True ,
269+ capture_output = True
270+ )
271+ console .print ("[green]✅ chronicle-network created[/green]" )
272+ else :
273+ console .print ("[dim]📡 chronicle-network already exists[/dim]" )
274+ return True
275+ except subprocess .CalledProcessError as e :
276+ console .print (f"[red]❌ Failed to create network: { e } [/red]" )
277+ return False
278+ except Exception as e :
279+ console .print (f"[red]❌ Error checking/creating network: { e } [/red]" )
280+ return False
281+
235282def start_services (services , build = False ):
236283 """Start specified services"""
237284 console .print (f"🚀 [bold]Starting { len (services )} services...[/bold]" )
238-
285+
286+ # Ensure Docker network exists before starting services
287+ if not ensure_docker_network ():
288+ console .print ("[red]❌ Cannot start services without Docker network[/red]" )
289+ return
290+
239291 success_count = 0
240292 for service_name in services :
241293 if service_name not in SERVICES :
0 commit comments