@@ -236,6 +236,68 @@ def get_subprocess_command(self, env):
236236 return list (self ._subprocess_argv )
237237
238238
239+ def capture_output (
240+ command : str ,
241+ namespace : str = config .DEFAULT_NAMESPACE ,
242+ timeout : int = config .COMMAND_TIMEOUT ,
243+ ):
244+ """
245+ Runs a command (string or argv) with the given stack namespace and captures stdout/stderr.
246+
247+ Returns: (returncode, stdout, stderr)
248+ """
249+ import errno
250+
251+ shellname = os .path .basename (config .SHELL ).lower ()
252+ argv = list (command ) if isinstance (command , (list , tuple )) else to_args (command )
253+
254+ # build env exactly like Wrapper.launch()
255+ env = os .environ .copy ()
256+ env .update (resolve_environ (load_environ (namespace )))
257+
258+ # prefer argv execution where possible
259+ if shellname in ["bash" , "sh" , "zsh" ]:
260+ needs_shell = any (re .search (r"\{(\w+)\}" , a ) for a in argv )
261+ if needs_shell :
262+ expr_argv = [re .sub (r"\{(\w+)\}" , r"${\1}" , a ) for a in argv ]
263+ expr = shell_join (expr_argv )
264+ cmd = [config .SHELL , "-c" , expr ]
265+ else :
266+ cmd = argv
267+
268+ # for cmd always use original command string
269+ elif shellname in ["cmd" ]:
270+ cmd = command
271+
272+ else :
273+ cmd = argv
274+
275+ try :
276+ proc = subprocess .run (
277+ cmd ,
278+ env = encode (env ),
279+ shell = False ,
280+ check = False ,
281+ capture_output = True ,
282+ text = True ,
283+ timeout = timeout ,
284+ )
285+ return proc .returncode , proc .stdout , proc .stderr
286+ except FileNotFoundError as e :
287+ # no process ran; synthesize a bash-like error and code
288+ # 127 is the conventional "command not found" code in shells
289+ missing = e .filename or (
290+ cmd [0 ] if isinstance (cmd , list ) and cmd else str (command )
291+ )
292+ return 127 , "" , f"{ missing } : command not found"
293+ except OSError as e :
294+ # Other OS-level execution errors (permission, exec format, etc.)
295+ rc = 126 if getattr (e , "errno" , None ) in (errno .EACCES ,) else 1
296+ return rc , "" , str (e )
297+ except subprocess .TimeoutExpired as e :
298+ return 124 , "" , f"Command timed out after { timeout } seconds"
299+
300+
239301def run_command (command : str , namespace : str = config .DEFAULT_NAMESPACE ):
240302 """
241303 Runs a given command with the given stack namespace.
0 commit comments