@@ -260,6 +260,12 @@ Without --, flags like --version are interpreted by morloc-manager itself.")]
260260 /// Port mapping HOST:CONTAINER (default: 8080:8080)
261261 #[ arg( short, long, value_parser = parse_port) ]
262262 port : Vec < ( u16 , u16 ) > ,
263+ /// Pass environment variable to the container (KEY=VALUE)
264+ #[ arg( short, long = "env" ) ]
265+ env_vars : Vec < String > ,
266+ /// Read environment variables from a file (one KEY=VALUE per line)
267+ #[ arg( long) ]
268+ env_file : Option < String > ,
263269 } ,
264270 /// Stop a running serve container
265271 #[ command( display_order = 21 ) ]
@@ -288,6 +294,9 @@ Without --, flags like --version are interpreted by morloc-manager itself.")]
288294 /// Overwrite existing output directory
289295 #[ arg( long) ]
290296 force : bool ,
297+ /// Declare an expected environment variable name (recorded in manifest, no value stored)
298+ #[ arg( short, long = "env" ) ]
299+ env_vars : Vec < String > ,
291300 } ,
292301 /// Build a serve image from frozen state
293302 #[ command( display_order = 24 ) ]
@@ -310,8 +319,20 @@ Without --, flags like --version are interpreted by morloc-manager itself.")]
310319 #[ arg( long) ]
311320 rebuild : bool ,
312321 } ,
313- /// List running serve containers
322+ /// Evaluate a morloc expression against a running serve container
314323 #[ command( display_order = 25 ) ]
324+ #[ command( after_help = "Examples:\n morloc-manager eval 'add 1 2'\n morloc-manager eval myenv 'map (add 1) [1,2,3]'\n morloc-manager eval -p 9090 'greet \" world\" '" ) ]
325+ Eval {
326+ /// Expression to evaluate (or environment name if two positional args)
327+ first : String ,
328+ /// Expression to evaluate (when first arg is environment name)
329+ second : Option < String > ,
330+ /// Port of the serve container (default: 8080)
331+ #[ arg( short, long, default_value = "8080" ) ]
332+ port : u16 ,
333+ } ,
334+ /// List running serve containers
335+ #[ command( display_order = 26 ) ]
315336 #[ command( after_help = "Examples:\n morloc-manager status" ) ]
316337 Status ,
317338 /// Check environment health and diagnose issues
@@ -358,6 +379,44 @@ fn parse_port(s: &str) -> std::result::Result<(u16, u16), String> {
358379 Ok ( ( host, container) )
359380}
360381
382+ /// Parse env vars from --env flags and --env-file, returning (key, value) pairs.
383+ fn collect_env_vars (
384+ env_flags : & [ String ] ,
385+ env_file : Option < & str > ,
386+ ) -> Result < Vec < ( String , String ) > > {
387+ let mut result = Vec :: new ( ) ;
388+
389+ if let Some ( path) = env_file {
390+ let contents = std:: fs:: read_to_string ( path) . map_err ( |e| {
391+ ManagerError :: EnvError ( format ! ( "Cannot read env file {path}: {e}" ) )
392+ } ) ?;
393+ for line in contents. lines ( ) {
394+ let trimmed = line. trim ( ) ;
395+ if trimmed. is_empty ( ) || trimmed. starts_with ( '#' ) {
396+ continue ;
397+ }
398+ if let Some ( ( k, v) ) = trimmed. split_once ( '=' ) {
399+ result. push ( ( k. to_string ( ) , v. to_string ( ) ) ) ;
400+ }
401+ }
402+ }
403+
404+ for entry in env_flags {
405+ if let Some ( ( k, v) ) = entry. split_once ( '=' ) {
406+ result. push ( ( k. to_string ( ) , v. to_string ( ) ) ) ;
407+ } else {
408+ // Bare key — pass through from host environment
409+ if let Ok ( v) = std:: env:: var ( entry) {
410+ result. push ( ( entry. clone ( ) , v) ) ;
411+ } else {
412+ eprintln ! ( "Warning: env var '{entry}' not set in host environment, skipping" ) ;
413+ }
414+ }
415+ }
416+
417+ Ok ( result)
418+ }
419+
361420// ======================================================================
362421// Main
363422// ======================================================================
@@ -1293,7 +1352,7 @@ fn dispatch(verbose: bool, cmd: Cmd) -> Result<()> {
12931352 }
12941353
12951354 // ---- freeze ----
1296- Cmd :: Freeze { output, force } => {
1355+ Cmd :: Freeze { output, force, env_vars } => {
12971356 let output_dir = output. as_deref ( ) . unwrap_or ( "./morloc-freeze" ) ;
12981357 // Protect against silently overwriting a previous freeze
12991358 let existing_tar = std:: path:: Path :: new ( output_dir) . join ( "state.tar.gz" ) ;
@@ -1330,7 +1389,7 @@ fn dispatch(verbose: bool, cmd: Cmd) -> Result<()> {
13301389 } ;
13311390 let data_dir = cfg:: env_data_dir ( env_scope, & env_name) ;
13321391 let image = ec. active_image ( ) . to_string ( ) ;
1333- let result = freeze:: freeze_from_dir ( env_scope, ver. clone ( ) , engine, & image, & data_dir. to_string_lossy ( ) , output_dir, verbose) ;
1392+ let result = freeze:: freeze_from_dir ( env_scope, ver. clone ( ) , engine, & image, & data_dir. to_string_lossy ( ) , output_dir, verbose, & env_vars ) ;
13341393 if result. is_ok ( ) && ec. morloc_version . as_ref ( ) != Some ( & ver) {
13351394 let mut updated = ec. clone ( ) ;
13361395 updated. morloc_version = Some ( ver) ;
@@ -1360,13 +1419,27 @@ fn dispatch(verbose: bool, cmd: Cmd) -> Result<()> {
13601419 let engine = match engine_override {
13611420 Some ( EngineArg :: Docker ) => ContainerEngine :: Docker ,
13621421 Some ( EngineArg :: Podman ) => ContainerEngine :: Podman ,
1363- None => ensure_engine ( ) ?,
1422+ None => {
1423+ let e = ensure_engine ( ) ?;
1424+ eprintln ! (
1425+ "Note: using {} engine from global config. Override with --engine if needed." ,
1426+ display_engine( e)
1427+ ) ;
1428+ e
1429+ }
13641430 } ;
1431+ if !manifest. env_vars . is_empty ( ) {
1432+ eprintln ! (
1433+ "Note: frozen state expects env vars: {}" ,
1434+ manifest. env_vars. join( ", " )
1435+ ) ;
1436+ eprintln ! ( " Pass at runtime with: -e KEY=VALUE when running the image" ) ;
1437+ }
13651438 serve:: build_serve_image ( engine, verbose, & from, & tag, manifest. morloc_version , base. as_deref ( ) , rebuild, & manifest. programs )
13661439 }
13671440
13681441 // ---- start ----
1369- Cmd :: Start { name, port } => {
1442+ Cmd :: Start { name, port, env_vars , env_file } => {
13701443 let ( env_name, env_scope, ec) = resolve_env_or_active ( name) ?;
13711444 let image = ec. active_image ( ) . to_string ( ) ;
13721445 let data_dir = cfg:: env_data_dir ( env_scope, & env_name) ;
@@ -1382,10 +1455,12 @@ fn dispatch(verbose: bool, cmd: Cmd) -> Result<()> {
13821455 } ;
13831456 let flags_path = cfg:: env_flags_path ( env_scope, & env_name) ;
13841457 let extra_flags = cfg:: read_flags_file ( & flags_path) ;
1458+ let user_env = collect_env_vars ( & env_vars, env_file. as_deref ( ) ) ?;
13851459 serve:: serve_environment (
13861460 ec. engine , verbose, & image,
13871461 & data_dir. to_string_lossy ( ) , & container_name,
13881462 & port_mappings, & extra_flags, & Some ( ec. shm_size . clone ( ) ) ,
1463+ & user_env,
13891464 )
13901465 }
13911466
@@ -1439,6 +1514,50 @@ fn dispatch(verbose: bool, cmd: Cmd) -> Result<()> {
14391514 Ok ( ( ) )
14401515 }
14411516
1517+ // ---- eval ----
1518+ Cmd :: Eval { first, second, port } => {
1519+ let expr = if let Some ( ref expr_arg) = second {
1520+ // first is env name — validate it exists and its serve container is running
1521+ let ( env_name, _, ec) = resolve_env_or_active ( Some ( first) ) ?;
1522+ let container_name = format ! ( "morloc-serve-{env_name}" ) ;
1523+ if !container:: container_exists ( ec. engine , & container_name) {
1524+ return Err ( ManagerError :: EnvError ( format ! (
1525+ "No serve container running for '{env_name}'. Start with: morloc-manager start {env_name}"
1526+ ) ) ) ;
1527+ }
1528+ expr_arg. clone ( )
1529+ } else {
1530+ first
1531+ } ;
1532+ use std:: io:: { Read as IoRead , Write as IoWrite } ;
1533+ let body = format ! ( "{{\" expr\" :{}}}" , serde_json:: to_string( & expr) . unwrap_or_default( ) ) ;
1534+ let request = format ! (
1535+ "POST /eval HTTP/1.1\r \n Host: localhost\r \n Content-Type: application/json\r \n Content-Length: {}\r \n Connection: close\r \n \r \n {}" ,
1536+ body. len( ) , body
1537+ ) ;
1538+ let addr = format ! ( "127.0.0.1:{port}" ) ;
1539+ let mut stream = std:: net:: TcpStream :: connect ( & addr) . map_err ( |e| {
1540+ ManagerError :: EnvError ( format ! (
1541+ "Cannot connect to serve container on {addr}: {e}\n Is a serve container running? Start with: morloc-manager start"
1542+ ) )
1543+ } ) ?;
1544+ stream. write_all ( request. as_bytes ( ) ) . map_err ( |e| {
1545+ ManagerError :: EnvError ( format ! ( "Failed to send request: {e}" ) )
1546+ } ) ?;
1547+ let mut response = String :: new ( ) ;
1548+ stream. read_to_string ( & mut response) . map_err ( |e| {
1549+ ManagerError :: EnvError ( format ! ( "Failed to read response: {e}" ) )
1550+ } ) ?;
1551+ // Extract body from HTTP response (after \r\n\r\n)
1552+ if let Some ( pos) = response. find ( "\r \n \r \n " ) {
1553+ let body = & response[ pos + 4 ..] ;
1554+ println ! ( "{body}" ) ;
1555+ } else {
1556+ println ! ( "{response}" ) ;
1557+ }
1558+ Ok ( ( ) )
1559+ }
1560+
14421561 // ---- status ----
14431562 Cmd :: Status => {
14441563 // Only show engines that have running/stopped serve containers.
@@ -1907,6 +2026,7 @@ mod tests {
19072026 content_hash : "abc" . to_string ( ) ,
19082027 image_digest : None ,
19092028 } ) ,
2029+ env_vars : vec ! [ "API_KEY" . to_string( ) , "DB_URL" . to_string( ) ] ,
19102030 } ;
19112031 cfg:: write_config ( & path, & fm) . unwrap ( ) ;
19122032 let fm2: FreezeManifest = cfg:: read_config ( & path) . unwrap ( ) ;
0 commit comments