Skip to main content

start_command/
args_parser.rs

1//! Argument Parser for start-command wrapper options
2//!
3//! Supports two syntax patterns:
4//! 1. $ [wrapper-options] -- [command-options]
5//! 2. $ [wrapper-options] command [command-options]
6//!
7//! Wrapper Options:
8//! --isolated, --isolation, -i <backend> Run in isolated environment (screen, tmux, docker, ssh)
9//! --attached, -a                   Run in attached mode (foreground)
10//! --detached, -d                   Run in detached mode (background)
11//! --session, -s <name>             Session name for isolation
12//! --image <image>                  Docker image (optional, defaults to OS-matched image)
13//! --volume, -v <host:container[:mode]> Docker bind mount/volume (repeatable, docker only)
14//! --mount <mount-spec>             Docker --mount spec (repeatable, docker only)
15//! --env, -e <KEY=VALUE>            Environment variable for docker container (repeatable, docker only)
16//! --privileged                     Run docker container in privileged mode (docker only)
17//! --endpoint <endpoint>            SSH endpoint (required for ssh isolation, e.g., user@host)
18//! --isolated-user, -u [username]   Create isolated user with same permissions
19//! --keep-user                      Keep isolated user after command completes
20//! --keep-alive, -k                 Keep isolation environment alive after command exits
21//! --auto-remove-docker-container   Automatically remove docker container after exit
22//! --shell <shell>                  Shell to use in isolation environments: auto, bash, zsh, sh (default: auto)
23//! --status <uuid-or-session-name>  Show status of a tracked execution
24//! --list                           List all tracked command executions
25//! --upload-log <uuid-or-session>   Upload the stored log for a tracked execution
26//! --stop <uuid-or-session-name>    Send CTRL+C/SIGINT to a detached execution
27//! --terminate <uuid-or-session-name> Terminate a detached execution immediately
28
29use std::env;
30
31use crate::isolation::get_default_docker_image;
32
33/// Valid isolation backends
34pub const VALID_BACKENDS: [&str; 4] = ["screen", "tmux", "docker", "ssh"];
35
36/// Valid shell options for --shell
37pub const VALID_SHELLS: [&str; 4] = ["auto", "bash", "zsh", "sh"];
38
39/// Valid output formats for query output
40pub const VALID_OUTPUT_FORMATS: [&str; 3] = ["links-notation", "json", "text"];
41
42/// UUID v4 regex pattern for validation
43const UUID_REGEX: &str = r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$";
44
45/// Check if a string is a valid UUID v4
46pub fn is_valid_uuid(s: &str) -> bool {
47    regex::Regex::new(UUID_REGEX)
48        .map(|re| re.is_match(&s.to_lowercase()))
49        .unwrap_or(false)
50}
51
52/// Generate a UUID v4
53pub fn generate_uuid() -> String {
54    uuid::Uuid::new_v4().to_string()
55}
56
57/// Wrapper options parsed from command line
58#[derive(Debug, Clone)]
59pub struct WrapperOptions {
60    /// Isolation backend: screen, tmux, docker, ssh
61    pub isolated: Option<String>,
62    /// Run in attached mode
63    pub attached: bool,
64    /// Run in detached mode
65    pub detached: bool,
66    /// Session name
67    pub session: Option<String>,
68    /// Session ID (UUID) for tracking - auto-generated if not provided
69    pub session_id: Option<String>,
70    /// Docker image
71    pub image: Option<String>,
72    /// Docker bind mounts/volumes (-v/--volume), applied to docker isolation
73    pub volumes: Vec<String>,
74    /// Docker --mount specs, applied to docker isolation
75    pub mounts: Vec<String>,
76    /// Docker environment variables (-e/--env, KEY=VALUE), applied to docker isolation
77    pub env: Vec<String>,
78    /// Run docker container in privileged mode
79    pub privileged: bool,
80    /// SSH endpoint (e.g., user@host)
81    pub endpoint: Option<String>,
82    /// Create isolated user
83    pub user: bool,
84    /// Optional custom username for isolated user
85    pub user_name: Option<String>,
86    /// Keep isolated user after command completes
87    pub keep_user: bool,
88    /// Keep environment alive after command exits
89    pub keep_alive: bool,
90    /// Auto-remove docker container after exit
91    pub auto_remove_docker_container: bool,
92    /// Shell to use in isolation environments: auto, bash, zsh, sh
93    pub shell: String,
94    /// Use command-stream library for command execution
95    pub use_command_stream: bool,
96    /// UUID to query status for
97    pub status: Option<String>,
98    /// List all tracked execution records
99    pub list: bool,
100    /// UUID/session name whose stored log should be uploaded
101    pub upload_log: Option<String>,
102    /// Output format for status/list (links-notation, json, text)
103    pub output_format: Option<String>,
104    /// UUID/session name to stop gracefully
105    pub stop: Option<String>,
106    /// UUID/session name to terminate immediately
107    pub terminate: Option<String>,
108    /// Clean up stale "executing" records
109    pub cleanup: bool,
110    /// Show what would be cleaned without actually cleaning
111    pub cleanup_dry_run: bool,
112}
113
114impl Default for WrapperOptions {
115    fn default() -> Self {
116        WrapperOptions {
117            isolated: None,
118            attached: false,
119            detached: false,
120            session: None,
121            session_id: None,
122            image: None,
123            volumes: Vec::new(),
124            mounts: Vec::new(),
125            env: Vec::new(),
126            privileged: false,
127            endpoint: None,
128            user: false,
129            user_name: None,
130            keep_user: false,
131            keep_alive: false,
132            auto_remove_docker_container: false,
133            shell: "auto".to_string(),
134            use_command_stream: false,
135            status: None,
136            list: false,
137            upload_log: None,
138            output_format: None,
139            stop: None,
140            terminate: None,
141            cleanup: false,
142            cleanup_dry_run: false,
143        }
144    }
145}
146
147/// Result of parsing arguments
148#[derive(Debug)]
149pub struct ParsedArgs {
150    /// Wrapper options
151    pub wrapper_options: WrapperOptions,
152    /// The command to execute (joined with spaces)
153    pub command: String,
154    /// Raw command arguments
155    pub raw_command: Vec<String>,
156}
157
158/// Parse command line arguments into wrapper options and command
159pub fn parse_args(args: &[String]) -> Result<ParsedArgs, String> {
160    let mut wrapper_options = WrapperOptions::default();
161    let mut command_args: Vec<String> = Vec::new();
162
163    // Find the separator '--' or detect where command starts
164    let separator_index = args.iter().position(|a| a == "--");
165
166    if let Some(sep_idx) = separator_index {
167        // Pattern 1: explicit separator
168        let wrapper_args: Vec<String> = args[..sep_idx].to_vec();
169        command_args = args[sep_idx + 1..].to_vec();
170        parse_wrapper_args(&wrapper_args, &mut wrapper_options)?;
171    } else {
172        // Pattern 2: parse until we hit a non-option argument
173        let mut i = 0;
174        while i < args.len() {
175            let arg = &args[i];
176            if arg.starts_with('-') {
177                match parse_option(args, i, &mut wrapper_options)? {
178                    0 => {
179                        return Err(format!("Unknown wrapper option: {}", arg));
180                    }
181                    consumed => {
182                        i += consumed;
183                    }
184                }
185            } else {
186                // Non-option argument, rest is command
187                command_args = args[i..].to_vec();
188                break;
189            }
190        }
191    }
192
193    // Validate options and apply defaults
194    validate_options(&mut wrapper_options)?;
195
196    Ok(ParsedArgs {
197        wrapper_options,
198        command: command_args.join(" "),
199        raw_command: command_args,
200    })
201}
202
203/// Parse wrapper arguments
204fn parse_wrapper_args(args: &[String], options: &mut WrapperOptions) -> Result<(), String> {
205    let mut i = 0;
206    while i < args.len() {
207        match parse_option(args, i, options)? {
208            0 => {
209                if args[i].starts_with('-') {
210                    return Err(format!("Unknown wrapper option: {}", args[i]));
211                }
212                if env::var("START_DEBUG").is_ok_and(|v| v == "1" || v == "true") {
213                    eprintln!("Unknown wrapper option: {}", args[i]);
214                }
215                i += 1;
216            }
217            consumed => {
218                i += consumed;
219            }
220        }
221    }
222    Ok(())
223}
224
225/// Parse a single option from args array
226/// Returns number of arguments consumed (0 if not recognized)
227fn parse_option(
228    args: &[String],
229    index: usize,
230    options: &mut WrapperOptions,
231) -> Result<usize, String> {
232    let arg = &args[index];
233
234    // --isolated, --isolation, or -i
235    if arg == "--isolated" || arg == "--isolation" || arg == "-i" {
236        if index + 1 < args.len() && !args[index + 1].starts_with('-') {
237            options.isolated = Some(args[index + 1].to_lowercase());
238            return Ok(2);
239        } else {
240            return Err(format!(
241                "Option {} requires a backend argument (screen, tmux, docker, ssh)",
242                arg
243            ));
244        }
245    }
246
247    // --isolated=<value> or --isolation=<value>
248    if arg.starts_with("--isolated=") || arg.starts_with("--isolation=") {
249        options.isolated = Some(arg.split('=').nth(1).unwrap_or("").to_lowercase());
250        return Ok(1);
251    }
252
253    // --attached or -a
254    if arg == "--attached" || arg == "-a" {
255        options.attached = true;
256        return Ok(1);
257    }
258
259    // --detached or -d
260    if arg == "--detached" || arg == "-d" {
261        options.detached = true;
262        return Ok(1);
263    }
264
265    // --session or -s
266    if arg == "--session" || arg == "-s" {
267        if index + 1 < args.len() && !args[index + 1].starts_with('-') {
268            options.session = Some(args[index + 1].clone());
269            return Ok(2);
270        } else {
271            return Err(format!("Option {} requires a session name argument", arg));
272        }
273    }
274
275    // --session=<value>
276    if arg.starts_with("--session=") {
277        options.session = Some(arg.split('=').nth(1).unwrap_or("").to_string());
278        return Ok(1);
279    }
280
281    // --image (for docker)
282    if arg == "--image" {
283        if index + 1 < args.len() && !args[index + 1].starts_with('-') {
284            options.image = Some(args[index + 1].clone());
285            return Ok(2);
286        } else {
287            return Err(format!("Option {} requires an image name argument", arg));
288        }
289    }
290
291    // --image=<value>
292    if arg.starts_with("--image=") {
293        options.image = Some(arg.split('=').nth(1).unwrap_or("").to_string());
294        return Ok(1);
295    }
296
297    // --volume or -v (for docker) - repeatable bind mount / volume
298    if arg == "--volume" || arg == "-v" {
299        if index + 1 < args.len() && !args[index + 1].starts_with('-') {
300            options.volumes.push(args[index + 1].clone());
301            return Ok(2);
302        } else {
303            return Err(format!(
304                "Option {} requires a volume argument (host:container[:mode])",
305                arg
306            ));
307        }
308    }
309
310    // --volume=<value> or -v=<value>
311    if arg.starts_with("--volume=") || arg.starts_with("-v=") {
312        options
313            .volumes
314            .push(arg[arg.find('=').unwrap() + 1..].to_string());
315        return Ok(1);
316    }
317
318    // --mount (for docker) - repeatable mount spec
319    if arg == "--mount" {
320        if index + 1 < args.len() && !args[index + 1].starts_with('-') {
321            options.mounts.push(args[index + 1].clone());
322            return Ok(2);
323        } else {
324            return Err(format!("Option {} requires a mount spec argument", arg));
325        }
326    }
327
328    // --mount=<value>
329    if let Some(value) = arg.strip_prefix("--mount=") {
330        options.mounts.push(value.to_string());
331        return Ok(1);
332    }
333
334    // --env or -e (for docker) - repeatable environment variable
335    if arg == "--env" || arg == "-e" {
336        if index + 1 < args.len() && !args[index + 1].starts_with('-') {
337            options.env.push(args[index + 1].clone());
338            return Ok(2);
339        } else {
340            return Err(format!("Option {} requires a KEY=VALUE argument", arg));
341        }
342    }
343
344    // --env=<value> or -e=<value>
345    if arg.starts_with("--env=") || arg.starts_with("-e=") {
346        options
347            .env
348            .push(arg[arg.find('=').unwrap() + 1..].to_string());
349        return Ok(1);
350    }
351
352    // --privileged (for docker)
353    if arg == "--privileged" {
354        options.privileged = true;
355        return Ok(1);
356    }
357
358    // --endpoint (for ssh)
359    if arg == "--endpoint" {
360        if index + 1 < args.len() && !args[index + 1].starts_with('-') {
361            options.endpoint = Some(args[index + 1].clone());
362            return Ok(2);
363        } else {
364            return Err(format!("Option {} requires an endpoint argument", arg));
365        }
366    }
367
368    // --endpoint=<value>
369    if arg.starts_with("--endpoint=") {
370        options.endpoint = Some(arg.split('=').nth(1).unwrap_or("").to_string());
371        return Ok(1);
372    }
373
374    // --isolated-user or -u [optional-username]
375    if arg == "--isolated-user" || arg == "-u" {
376        options.user = true;
377        // Check if next arg is an optional username (not starting with -)
378        if index + 1 < args.len() && !args[index + 1].starts_with('-') {
379            let next_arg = &args[index + 1];
380            // Check if next arg matches username format
381            let username_regex = regex::Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap();
382            if username_regex.is_match(next_arg) && next_arg.len() <= 32 {
383                options.user_name = Some(next_arg.clone());
384                return Ok(2);
385            }
386        }
387        return Ok(1);
388    }
389
390    // --isolated-user=<value>
391    if arg.starts_with("--isolated-user=") {
392        options.user = true;
393        options.user_name = Some(arg.split('=').nth(1).unwrap_or("").to_string());
394        return Ok(1);
395    }
396
397    // --keep-user
398    if arg == "--keep-user" {
399        options.keep_user = true;
400        return Ok(1);
401    }
402
403    // --keep-alive or -k
404    if arg == "--keep-alive" || arg == "-k" {
405        options.keep_alive = true;
406        return Ok(1);
407    }
408
409    // --auto-remove-docker-container
410    if arg == "--auto-remove-docker-container" {
411        options.auto_remove_docker_container = true;
412        return Ok(1);
413    }
414
415    // --shell <shell>
416    if arg == "--shell" {
417        if index + 1 < args.len() && !args[index + 1].starts_with('-') {
418            options.shell = args[index + 1].to_lowercase();
419            return Ok(2);
420        } else {
421            return Err(format!(
422                "Option {} requires a shell argument (auto, bash, zsh, sh)",
423                arg
424            ));
425        }
426    }
427
428    // --shell=<value>
429    if arg.starts_with("--shell=") {
430        options.shell = arg.split('=').nth(1).unwrap_or("").to_lowercase();
431        return Ok(1);
432    }
433
434    // --use-command-stream
435    if arg == "--use-command-stream" {
436        options.use_command_stream = true;
437        return Ok(1);
438    }
439
440    // --session-id or --session-name (alias) <uuid>
441    if arg == "--session-id" || arg == "--session-name" {
442        if index + 1 < args.len() && !args[index + 1].starts_with('-') {
443            options.session_id = Some(args[index + 1].clone());
444            return Ok(2);
445        } else {
446            return Err(format!("Option {} requires a UUID argument", arg));
447        }
448    }
449
450    // --session-id=<value> or --session-name=<value>
451    if arg.starts_with("--session-id=") || arg.starts_with("--session-name=") {
452        options.session_id = Some(arg.split('=').nth(1).unwrap_or("").to_string());
453        return Ok(1);
454    }
455
456    // --status <uuid-or-session-name>
457    if arg == "--status" {
458        if index + 1 < args.len() && !args[index + 1].starts_with('-') {
459            options.status = Some(args[index + 1].clone());
460            return Ok(2);
461        } else {
462            return Err(format!(
463                "Option {} requires a UUID or session name argument",
464                arg
465            ));
466        }
467    }
468
469    // --status=<value>
470    if let Some(value) = arg.strip_prefix("--status=") {
471        if value.is_empty() {
472            return Err("Option --status requires a UUID or session name argument".to_string());
473        }
474        options.status = Some(value.to_string());
475        return Ok(1);
476    }
477
478    // --upload-log <uuid-or-session-name>
479    if arg == "--upload-log" {
480        if index + 1 < args.len() && !args[index + 1].starts_with('-') {
481            options.upload_log = Some(args[index + 1].clone());
482            return Ok(2);
483        } else {
484            return Err(format!(
485                "Option {} requires a UUID or session name argument",
486                arg
487            ));
488        }
489    }
490
491    // --upload-log=<value>
492    if let Some(value) = arg.strip_prefix("--upload-log=") {
493        if value.is_empty() {
494            return Err("Option --upload-log requires a UUID or session name argument".to_string());
495        }
496        options.upload_log = Some(value.to_string());
497        return Ok(1);
498    }
499
500    // --stop <uuid-or-session-name>
501    if arg == "--stop" {
502        if index + 1 < args.len() && !args[index + 1].starts_with('-') {
503            options.stop = Some(args[index + 1].clone());
504            return Ok(2);
505        } else {
506            return Err(format!(
507                "Option {} requires a UUID or session name argument",
508                arg
509            ));
510        }
511    }
512
513    // --stop=<value>
514    if let Some(value) = arg.strip_prefix("--stop=") {
515        if value.is_empty() {
516            return Err("Option --stop requires a UUID or session name argument".to_string());
517        }
518        options.stop = Some(value.to_string());
519        return Ok(1);
520    }
521
522    // --terminate <uuid-or-session-name>
523    if arg == "--terminate" {
524        if index + 1 < args.len() && !args[index + 1].starts_with('-') {
525            options.terminate = Some(args[index + 1].clone());
526            return Ok(2);
527        } else {
528            return Err(format!(
529                "Option {} requires a UUID or session name argument",
530                arg
531            ));
532        }
533    }
534
535    // --terminate=<value>
536    if let Some(value) = arg.strip_prefix("--terminate=") {
537        if value.is_empty() {
538            return Err("Option --terminate requires a UUID or session name argument".to_string());
539        }
540        options.terminate = Some(value.to_string());
541        return Ok(1);
542    }
543
544    // --list
545    if arg == "--list" {
546        options.list = true;
547        return Ok(1);
548    }
549
550    // --output-format <format>
551    if arg == "--output-format" {
552        if index + 1 < args.len() && !args[index + 1].starts_with('-') {
553            options.output_format = Some(args[index + 1].to_lowercase());
554            return Ok(2);
555        } else {
556            return Err(format!("Option {} requires a format argument", arg));
557        }
558    }
559
560    // --output-format=<value>
561    if arg.starts_with("--output-format=") {
562        options.output_format = Some(arg.split('=').nth(1).unwrap_or("").to_lowercase());
563        return Ok(1);
564    }
565
566    // --cleanup
567    if arg == "--cleanup" {
568        options.cleanup = true;
569        return Ok(1);
570    }
571
572    // --cleanup-dry-run
573    if arg == "--cleanup-dry-run" {
574        options.cleanup = true;
575        options.cleanup_dry_run = true;
576        return Ok(1);
577    }
578
579    // Not a recognized wrapper option
580    Ok(0)
581}
582
583/// Validate parsed options and apply defaults
584pub fn validate_options(options: &mut WrapperOptions) -> Result<(), String> {
585    // Check attached and detached conflict
586    if options.attached && options.detached {
587        return Err(
588            "Cannot use both --attached and --detached at the same time. Please choose only one mode."
589                .to_string(),
590        );
591    }
592
593    // Validate isolation backend
594    if let Some(ref backend) = options.isolated {
595        if !VALID_BACKENDS.contains(&backend.as_str()) {
596            return Err(format!(
597                "Invalid isolation backend: \"{}\". Valid options are: {}",
598                backend,
599                VALID_BACKENDS.join(", ")
600            ));
601        }
602
603        // Docker uses --image or defaults to OS-matched image
604        if backend == "docker" && options.image.is_none() {
605            options.image = Some(get_default_docker_image());
606        }
607
608        // SSH requires --endpoint
609        if backend == "ssh" && options.endpoint.is_none() {
610            return Err(
611                "SSH isolation requires --endpoint option to specify the remote server (e.g., user@host)"
612                    .to_string(),
613            );
614        }
615    }
616
617    // Session name is only valid with isolation
618    if options.session.is_some() && options.isolated.is_none() {
619        return Err("--session option is only valid with --isolated".to_string());
620    }
621
622    // Image is only valid with docker
623    if options.image.is_some() && options.isolated.as_deref() != Some("docker") {
624        return Err("--image option is only valid with --isolated docker".to_string());
625    }
626
627    // Docker runtime options (--volume, --mount, --env, --privileged) are only valid with docker
628    let is_docker = options.isolated.as_deref() == Some("docker");
629    if !options.volumes.is_empty() && !is_docker {
630        return Err("--volume option is only valid with --isolated docker".to_string());
631    }
632    if !options.mounts.is_empty() && !is_docker {
633        return Err("--mount option is only valid with --isolated docker".to_string());
634    }
635    if !options.env.is_empty() && !is_docker {
636        return Err("--env option is only valid with --isolated docker".to_string());
637    }
638    if options.privileged && !is_docker {
639        return Err("--privileged option is only valid with --isolated docker".to_string());
640    }
641
642    // Endpoint is only valid with ssh
643    if options.endpoint.is_some() && options.isolated.as_deref() != Some("ssh") {
644        return Err("--endpoint option is only valid with --isolated ssh".to_string());
645    }
646
647    // Keep-alive is only valid with isolation
648    if options.keep_alive && options.isolated.is_none() {
649        return Err("--keep-alive option is only valid with --isolated".to_string());
650    }
651
652    // Auto-remove-docker-container is only valid with docker isolation
653    if options.auto_remove_docker_container && options.isolated.as_deref() != Some("docker") {
654        return Err(
655            "--auto-remove-docker-container option is only valid with --isolated docker"
656                .to_string(),
657        );
658    }
659
660    // User isolation validation
661    if options.user {
662        // User isolation is not supported with Docker
663        if options.isolated.as_deref() == Some("docker") {
664            return Err(
665                "--isolated-user is not supported with Docker isolation. Docker uses its own user namespace for isolation."
666                    .to_string(),
667            );
668        }
669        // Validate custom username if provided
670        if let Some(ref username) = options.user_name {
671            let username_regex = regex::Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap();
672            if !username_regex.is_match(username) {
673                return Err(format!(
674                    "Invalid username format for --isolated-user: \"{}\". Username should contain only letters, numbers, hyphens, and underscores.",
675                    username
676                ));
677            }
678            if username.len() > 32 {
679                return Err(format!(
680                    "Username too long for --isolated-user: \"{}\". Maximum length is 32 characters.",
681                    username
682                ));
683            }
684        }
685    }
686
687    // Keep-user validation
688    if options.keep_user && !options.user {
689        return Err("--keep-user option is only valid with --isolated-user".to_string());
690    }
691
692    // Validate output format
693    if let Some(ref format) = options.output_format {
694        if !VALID_OUTPUT_FORMATS.contains(&format.as_str()) {
695            return Err(format!(
696                "Invalid output format: \"{}\". Valid options are: {}",
697                format,
698                VALID_OUTPUT_FORMATS.join(", ")
699            ));
700        }
701    }
702
703    // Query/control modes are mutually exclusive
704    let query_modes = [
705        options.status.is_some(),
706        options.list,
707        options.upload_log.is_some(),
708        options.stop.is_some(),
709        options.terminate.is_some(),
710        options.cleanup,
711    ]
712    .into_iter()
713    .filter(|enabled| *enabled)
714    .count();
715
716    if query_modes > 1 {
717        return Err(
718            "Cannot combine --status, --list, --upload-log, --stop, --terminate, or --cleanup in the same invocation"
719                .to_string(),
720        );
721    }
722
723    // Output format is only valid with read-only query modes
724    if options.output_format.is_some() && options.status.is_none() && !options.list {
725        return Err("--output-format option is only valid with --status or --list".to_string());
726    }
727
728    // Validate shell option
729    if !VALID_SHELLS.contains(&options.shell.as_str()) {
730        return Err(format!(
731            "Invalid shell: \"{}\". Valid options are: {}",
732            options.shell,
733            VALID_SHELLS.join(", ")
734        ));
735    }
736
737    // Validate session ID is a valid UUID if provided
738    if let Some(ref session_id) = options.session_id {
739        if !is_valid_uuid(session_id) {
740            return Err(format!(
741                "Invalid session ID: \"{}\". Session ID must be a valid UUID v4.",
742                session_id
743            ));
744        }
745    }
746
747    Ok(())
748}
749
750/// Generate a unique session name
751pub fn generate_session_name(prefix: Option<&str>) -> String {
752    use std::cell::RefCell;
753    use std::time::{SystemTime, UNIX_EPOCH};
754
755    thread_local! {
756        static STATE: RefCell<u64> = RefCell::new(
757            SystemTime::now()
758                .duration_since(UNIX_EPOCH)
759                .unwrap()
760                .as_nanos() as u64
761        );
762    }
763
764    fn next_random() -> u64 {
765        STATE.with(|state| {
766            let mut s = state.borrow_mut();
767            *s ^= *s << 13;
768            *s ^= *s >> 7;
769            *s ^= *s << 17;
770            *s
771        })
772    }
773
774    let prefix = prefix.unwrap_or("start");
775    let timestamp = chrono::Utc::now().timestamp_millis();
776    let random: String = (0..6)
777        .map(|_| {
778            let idx = (next_random() % 36) as u8;
779            if idx < 10 {
780                (b'0' + idx) as char
781            } else {
782                (b'a' + idx - 10) as char
783            }
784        })
785        .collect();
786    format!("{}-{}-{}", prefix, timestamp, random)
787}
788
789/// Check if any isolation options are present
790pub fn has_isolation(options: &WrapperOptions) -> bool {
791    options.isolated.is_some()
792}
793
794/// Get the effective mode for isolation
795/// Multiplexers default to attached, docker defaults to attached
796pub fn get_effective_mode(options: &WrapperOptions) -> &'static str {
797    if options.detached {
798        "detached"
799    } else {
800        // Default to attached for all backends
801        "attached"
802    }
803}
804
805#[cfg(test)]
806#[path = "args_parser_cases.rs"]
807mod tests;