use std::{env, ffi::OsString, fmt, io, mem, process, sync::Arc};

use camino::{Utf8Path, Utf8PathBuf};
use clap::{ArgAction, ArgGroup, CommandFactory, Parser, Subcommand, ValueEnum};
use clap_complete::{generate, Shell};
pub use error::Error;
use serde::Serialize;
use tracing::{debug, error, log::warn};
use turbopath::AbsoluteSystemPathBuf;
use turborepo_api_client::SharedHttpClient;
use turborepo_repository::inference::{RepoMode, RepoState};
use turborepo_shim::TurboState;
use turborepo_telemetry::{
    events::{command::CommandEventBuilder, generic::GenericEventBuilder, EventBuilder, EventType},
    init_telemetry, track_usage, TelemetryHandle,
};
use turborepo_types::{
    ContinueMode, DryRunMode, EnvMode, LogOrder, LogPrefix, OutputLogsMode, UIMode,
};
use turborepo_ui::{ColorConfig, GREY};

use crate::{
    cli::error::print_potential_tasks,
    commands::{
        bin, boundaries, config, daemon, docs, generate, get_mfe_port, info, link, login, logout,
        ls, prune, query, run, telemetry, unlink, CommandBase,
    },
    get_version,
    run::watch::WatchClient,
    tracing::TurboSubscriber,
};

mod error;
mod observability;

fn exit_with_heap_profile(code: i32) -> ! {
    #[cfg(feature = "heap-dhat")]
    crate::heap_profile::finish_global();

    process::exit(code);
}

// Global turbo sets this environment variable to its cwd so that local
// turbo can use it for package inference.
pub const INVOCATION_DIR_ENV_VAR: &str = "TURBO_INVOCATION_DIR";

// Default value for the --cache-workers argument
const DEFAULT_NUM_WORKERS: u32 = 10;

/// Returns a scaled thread count for rayon's global pool based on
/// available CPU cores, capped at
/// [`crate::rayon_compat::MAX_RAYON_THREADS`].
///
/// See [`init_rayon_pool`] and <https://github.com/vercel/turborepo/issues/12251>
fn rayon_pool_size() -> usize {
    let cpus = std::thread::available_parallelism()
        .map(|n| n.get())
        .unwrap_or(1);
    crate::rayon_compat::scale_thread_count(cpus)
}

/// Explicitly initialize rayon's global thread pool early so we control
/// its size and initialization timing.
///
/// If `RAYON_NUM_THREADS` is set, its value is still clamped to
/// [`crate::rayon_compat::MAX_RAYON_THREADS`] to prevent the known
/// deadlock on high-core-count machines.
fn init_rayon_pool() {
    let pool_size = match std::env::var("RAYON_NUM_THREADS")
        .ok()
        .and_then(|v| v.parse().ok())
    {
        Some(user_val) => {
            let clamped = crate::rayon_compat::scale_thread_count(user_val);
            if clamped < user_val {
                tracing::debug!(
                    requested = user_val,
                    clamped,
                    max = crate::rayon_compat::MAX_RAYON_THREADS,
                    "RAYON_NUM_THREADS exceeds safe limit, clamping"
                );
            }
            clamped
        }
        None => rayon_pool_size(),
    };

    tracing::info!(
        rayon_pool_size = pool_size,
        "initializing rayon global pool"
    );

    let builder = rayon::ThreadPoolBuilder::new().num_threads(pool_size);
    if let Err(e) = builder.build_global() {
        tracing::warn!("rayon global pool already initialized: {e}");
    }
}

const SUPPORTED_GRAPH_FILE_EXTENSIONS: [&str; 8] =
    ["svg", "png", "jpg", "pdf", "json", "html", "mermaid", "dot"];

/// The parsed arguments from the command line. In general we should avoid using
/// or mutating this directly, and instead use the fully canonicalized `Opts`
/// struct.
#[derive(Parser, Clone, Default, Debug, PartialEq)]
#[clap(author, about = "The build system that makes ship happen", long_about = None)]
#[clap(disable_help_subcommand = true)]
#[clap(disable_version_flag = true)]
#[clap(arg_required_else_help = true)]
#[command(name = "turbo")]
pub struct Args {
    #[clap(long, global = true)]
    pub version: bool,
    #[clap(long, global = true)]
    /// Skip any attempts to infer which version of Turbo the project is
    /// configured to use
    pub skip_infer: bool,
    /// Disable the turbo update notification
    #[clap(long, global = true)]
    pub no_update_notifier: bool,
    /// Override the endpoint for API calls
    #[clap(long, global = true, value_parser)]
    pub api: Option<String>,
    /// Force color usage in the terminal
    #[clap(long, global = true)]
    pub color: bool,
    /// The directory in which to run turbo
    #[clap(long, global = true, value_parser)]
    pub cwd: Option<Utf8PathBuf>,
    /// Specify a file to save a pprof heap profile
    #[clap(long, global = true, value_parser)]
    pub heap: Option<String>,
    /// Specify whether to use the streaming UI or TUI
    #[clap(long, global = true, value_enum)]
    pub ui: Option<UIMode>,
    /// Override the login endpoint
    #[clap(long, global = true, value_parser)]
    pub login: Option<String>,
    /// Suppress color usage in the terminal
    #[clap(long, global = true)]
    pub no_color: bool,
    /// When enabled, turbo will precede HTTP requests with an OPTIONS request
    /// for authorization
    #[clap(long, global = true)]
    pub preflight: bool,
    /// Set a timeout for all HTTP requests.
    #[clap(long, value_name = "TIMEOUT", global = true, value_parser)]
    pub remote_cache_timeout: Option<u64>,
    /// Set the team slug for API calls
    #[clap(long, global = true, value_parser)]
    pub team: Option<String>,
    /// Set the auth token for API calls
    #[clap(long, global = true, value_parser)]
    pub token: Option<String>,
    /// Specify a file to save a pprof trace
    #[clap(long, global = true, value_parser)]
    pub trace: Option<String>,
    /// verbosity
    #[clap(flatten)]
    pub verbosity: Verbosity,
    #[clap(flatten)]
    pub experimental_otel_args: observability::ExperimentalOtelCliArgs,
    /// Force a check for a new version of turbo
    #[clap(long, global = true, hide = true)]
    pub check_for_update: bool,
    #[clap(long = "__test-run", global = true, hide = true)]
    pub test_run: bool,
    /// Allow for missing `packageManager` in `package.json`.
    ///
    /// `turbo` will use hints from codebase to guess which package manager
    /// should be used.
    #[clap(long, global = true)]
    pub dangerously_disable_package_manager_check: bool,
    #[clap(long = "experimental-allow-no-turbo-json", hide = true, global = true)]
    pub allow_no_turbo_json: bool,
    /// Use the `turbo.json` located at the provided path instead of one at the
    /// root of the repository.
    #[clap(long, global = true)]
    pub root_turbo_json: Option<Utf8PathBuf>,
    #[clap(flatten, next_help_heading = "Run Arguments")]
    // DO NOT MAKE THIS VISIBLE
    // This is explicitly set to None in `run`
    run_args: Option<RunArgs>,
    // This should be inside `RunArgs` but clap currently has a bug
    // around nested flattened optional args: https://github.com/clap-rs/clap/issues/4697
    #[clap(flatten)]
    // DO NOT MAKE THIS VISIBLE
    // Instead use the getter method execution_args()
    execution_args: Option<ExecutionArgs>,
    #[clap(subcommand)]
    pub command: Option<Command>,
}

#[derive(Debug, Parser, Clone, Copy, PartialEq, Eq, Default)]
pub struct Verbosity {
    #[clap(
        long = "verbosity",
        global = true,
        conflicts_with = "v",
        value_name = "COUNT"
    )]
    /// Verbosity level. Useful when debugging Turborepo or creating logs for
    /// issue reports
    pub verbosity: Option<u8>,
    #[clap(
        short = 'v',
        action = clap::ArgAction::Count,
        global = true,
        hide = true,
        conflicts_with = "verbosity"
    )]
    pub v: u8,
}

impl From<Verbosity> for u8 {
    fn from(val: Verbosity) -> Self {
        let Verbosity { verbosity, v } = val;
        verbosity.unwrap_or(v)
    }
}

#[derive(Subcommand, Copy, Clone, Debug, PartialEq)]
pub enum DaemonCommand {
    /// Restarts the turbo daemon
    Restart,
    /// Ensures that the turbo daemon is running
    Start,
    /// Reports the status of the turbo daemon
    Status {
        /// Pass --json to report status in JSON format
        #[clap(long)]
        json: bool,
    },
    /// Stops the turbo daemon
    Stop,
    /// Stops the turbo daemon if it is already running, and removes any stale
    /// daemon state
    Clean {
        /// Clean
        #[clap(long, default_value_t = true)]
        clean_logs: bool,
    },
    /// Shows the daemon logs
    Logs,
}

#[derive(Copy, Clone, Debug, Default, ValueEnum, Serialize, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum OutputFormat {
    /// Output in a human-readable format
    #[default]
    Pretty,
    /// Output in JSON format for direct parsing
    Json,
}

impl fmt::Display for OutputFormat {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            OutputFormat::Pretty => "pretty",
            OutputFormat::Json => "json",
        })
    }
}

#[derive(Subcommand, Copy, Clone, Debug, PartialEq)]
pub enum TelemetryCommand {
    /// Enables anonymous telemetry
    Enable,
    /// Disables anonymous telemetry
    Disable,
    /// Reports the status of telemetry
    Status,
}

/// Returns formatted RunArgs options derived from clap's command definition.
/// These options aren't included in the usage line by clap because they're all
/// optional.
fn get_run_args_options() -> Vec<String> {
    RunArgs::command()
        .get_arguments()
        .filter(|arg| arg.get_long().is_some())
        .map(|arg| {
            let Some(long) = arg.get_long() else {
                return String::new();
            };
            let value_names: Vec<_> = arg.get_value_names().unwrap_or_default().to_vec();

            if value_names.is_empty() {
                // Boolean flag
                format!("--{}", long)
            } else {
                // Check if value is optional via num_args (0..=1 means optional)
                let is_optional = arg
                    .get_num_args()
                    .is_some_and(|range| range.min_values() == 0);

                let value_str = value_names
                    .iter()
                    .map(|v| v.to_string())
                    .collect::<Vec<_>>()
                    .join("> <");

                if is_optional {
                    format!("--{} [<{}>]", long, value_str)
                } else {
                    format!("--{} <{}>", long, value_str)
                }
            }
        })
        .collect()
}

/// Formats clap error messages to improve readability of pipe-separated options
fn format_error_message(mut err_str: String) -> String {
    // Replace pipe separators in usage line with newlines for better readability
    // The usage line typically looks like: "Usage: turbo <--opt1|--opt2|--opt3>"
    if let Some(usage_start) = err_str.find("Usage: ") {
        if let Some(usage_end) = err_str[usage_start..].find('\n') {
            let usage_end = usage_start + usage_end;
            let usage_line = &err_str[usage_start..usage_end];

            // Check if this usage line contains the pipe-separated options pattern
            if usage_line.contains('<') && usage_line.contains('>') && usage_line.contains('|') {
                // Find the angle bracket enclosed section
                if let Some(bracket_start) = usage_line.find('<') {
                    if let Some(bracket_end) = usage_line.rfind('>') {
                        let prefix = &usage_line[..bracket_start];
                        let options_str = &usage_line[bracket_start + 1..bracket_end];

                        // Split the options by pipe and format them as a list
                        let mut formatted_options: Vec<String> = options_str
                            .split('|')
                            .map(|opt| format!("    {}", opt))
                            .collect();

                        // Add RunArgs options that clap doesn't include
                        for opt in get_run_args_options() {
                            formatted_options.push(format!("    {}", opt));
                        }

                        // Build the new usage string
                        let new_usage = format!(
                            "{} [OPTIONS] [TASKS]... [-- <PASS_THROUGH_ARGS>...]\n\nOptions:\n{}",
                            prefix.trim_end(),
                            formatted_options.join("\n")
                        );

                        // Replace the old usage line with the new formatted one
                        err_str.replace_range(usage_start..usage_end, &new_usage);
                    }
                }
            }
        }
    }
    err_str
}

impl Args {
    #[tracing::instrument(skip_all)]
    pub fn new(os_args: Vec<OsString>) -> Self {
        let clap_args = match Args::parse(os_args) {
            Ok(args) => args,
            // Don't use error logger when displaying help text
            Err(e)
                if matches!(
                    e.kind(),
                    clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
                ) =>
            {
                let _ = e.print();
                exit_with_heap_profile(1);
            }
            Err(e) if e.use_stderr() => {
                let err_str = format_error_message(e.to_string());
                // A cleaner solution would be to implement our own clap::error::ErrorFormatter
                // but that would require copying the default formatter just to remove this
                // line: https://docs.rs/clap/latest/src/clap/error/format.rs.html#100
                error!(
                    "{}",
                    err_str.strip_prefix("error: ").unwrap_or(err_str.as_str())
                );
                exit_with_heap_profile(1);
            }
            // If the clap error shouldn't be printed to stderr it indicates help text
            Err(e) => {
                let _ = e.print();
                exit_with_heap_profile(0);
            }
        };
        // We have to override the --version flag because we use `get_version`
        // instead of a hard-coded version or the crate version
        if clap_args.version {
            println!("{}", get_version());
            exit_with_heap_profile(0);
        }

        if let Some(run_args) = clap_args.run_args() {
            if run_args.no_cache {
                warn!(
                    "--no-cache is deprecated and will be removed in a future major version. Use \
                     --cache=local:r,remote:r"
                );
            }
            if run_args.remote_only.is_some() {
                warn!(
                    "--remote-only is deprecated and will be removed in a future major version. \
                     Use --cache=remote:rw"
                );
            }
            if run_args.remote_cache_read_only.is_some() {
                warn!(
                    "--remote-cache-read-only is deprecated and will be removed in a future major \
                     version. Use --cache=local:rw,remote:r"
                );
            }
            if run_args.daemon {
                warn!(
                    "--daemon is deprecated and will be removed in version 3.0. The daemon is no \
                     longer used for `turbo run`."
                );
            }
            if run_args.no_daemon {
                warn!(
                    "--no-daemon is deprecated and will be removed in version 3.0. The daemon is \
                     no longer used for `turbo run`."
                );
            }
            if run_args.parallel {
                warn!(
                    "--parallel is deprecated and will be removed in a future major version. \
                     Instead, define task behavior in your turbo.json task definitions using \
                     `persistent` and `with`."
                );
            }
            if let Some(graph) = &run_args.graph {
                match Utf8Path::new(graph).extension() {
                    Some(ext @ ("png" | "jpg" | "pdf")) => {
                        warn!(
                            "--graph with .{ext} output is deprecated and will be removed in \
                             version 3.0. Use .svg, .html, .mermaid, or .dot instead.",
                        );
                    }
                    Some("json") => {
                        warn!(
                            "--graph with .json output is deprecated and will be removed in \
                             version 3.0. Use `turbo query` for programmatic access to the task \
                             graph."
                        );
                    }
                    _ => {}
                }
            }
        }

        if let Some(Command::Prune { ref scope, .. }) = clap_args.command {
            if scope.is_some() {
                warn!(
                    "--scope is deprecated and will be removed in a future major version. Use \
                     positional arguments instead (e.g. `turbo prune web`)"
                );
            }
        }

        clap_args
    }

    pub(crate) fn parse(os_args: Vec<OsString>) -> Result<Self, clap::Error> {
        let (is_single_package, single_package_free) = Self::remove_single_package(os_args);
        let mut args = Args::try_parse_from(single_package_free)?;
        // --single-package is stripped before clap parsing, so we need to
        // propagate it back. The value can appear in two places in the struct.
        // We defensively attempt to set both.
        if let Some(ref mut execution_args) = args.execution_args {
            execution_args.single_package = is_single_package
        }

        if let Some(
            Command::Run {
                ref mut execution_args,
                ..
            }
            | Command::Watch {
                ref mut execution_args,
                ..
            },
        ) = args.command.as_mut()
        {
            execution_args.single_package = is_single_package;
        }

        if env::var("TEST_RUN").is_ok() {
            args.test_run = true;
        }

        args.validate()?;

        Ok(args)
    }

    pub fn track(&self, tel: &GenericEventBuilder) {
        // track usage only
        track_usage!(tel, self.skip_infer, |val| val);
        track_usage!(tel, self.no_update_notifier, |val| val);
        track_usage!(tel, self.color, |val| val);
        track_usage!(tel, self.no_color, |val| val);
        track_usage!(tel, self.preflight, |val| val);
        track_usage!(tel, &self.login, Option::is_some);
        track_usage!(tel, &self.cwd, Option::is_some);
        track_usage!(tel, &self.heap, Option::is_some);
        track_usage!(tel, &self.team, Option::is_some);
        track_usage!(tel, &self.token, Option::is_some);
        track_usage!(tel, &self.trace, Option::is_some);
        track_usage!(tel, &self.api, Option::is_some);

        // track values
        if let Some(remote_cache_timeout) = self.remote_cache_timeout {
            tel.track_arg_value(
                "remote-cache-timeout",
                remote_cache_timeout,
                turborepo_telemetry::events::EventType::NonSensitive,
            );
        }
        if self.verbosity.v > 0 {
            tel.track_arg_value(
                "v",
                self.verbosity.v,
                turborepo_telemetry::events::EventType::NonSensitive,
            );
        }
        if let Some(verbosity) = self.verbosity.verbosity {
            tel.track_arg_value(
                "verbosity",
                verbosity,
                turborepo_telemetry::events::EventType::NonSensitive,
            );
        }
    }

    /// Fetch the run args supplied to the command
    pub fn run_args(&self) -> Option<&RunArgs> {
        if let Some(Command::Run { run_args, .. }) = &self.command {
            Some(run_args)
        } else {
            self.run_args.as_ref()
        }
    }

    /// Fetch the execution args supplied to the command
    pub fn execution_args(&self) -> Option<&ExecutionArgs> {
        match &self.command {
            Some(Command::Run { execution_args, .. }) => Some(execution_args),
            Some(Command::Watch { execution_args, .. }) => Some(execution_args),
            _ => self.execution_args.as_ref(),
        }
    }

    fn remove_single_package(args: Vec<OsString>) -> (bool, impl Iterator<Item = OsString>) {
        // We always pass --single-package in from the shim.
        // We need to omit it, and then add it in for run.
        let arg_separator_position = args.iter().position(|input_token| input_token == "--");

        let single_package_position = args
            .iter()
            .position(|input_token| input_token == "--single-package");

        let is_single_package = match (arg_separator_position, single_package_position) {
            (_, None) => false,
            (None, Some(_)) => true,
            (Some(arg_separator_position), Some(single_package_position)) => {
                single_package_position < arg_separator_position
            }
        };

        // Clap supports arbitrary iterators as input.
        // We can remove all instances of --single-package
        let single_package_free = args
            .into_iter()
            .enumerate()
            .filter(move |(index, input_token)| {
                arg_separator_position
                    .is_some_and(|arg_separator_position| index > &arg_separator_position)
                    || input_token != "--single-package"
            })
            .map(|(_, input_token)| input_token);

        (is_single_package, single_package_free)
    }

    fn validate(&self) -> Result<(), clap::Error> {
        if self.run_args.is_some()
            && !matches!(
                self.command,
                None | Some(Command::Run { .. })
                    | Some(Command::Config)
                    | Some(Command::Boundaries { .. })
            )
        {
            let mut cmd = Self::command();
            Err(cmd.error(
                clap::error::ErrorKind::UnknownArgument,
                "Cannot use run arguments outside of run command",
            ))
        } else if self.execution_args.is_some()
            && matches!(self.command, Some(Command::Watch { .. }))
        {
            let mut cmd = Self::command();
            Err(cmd.error(
                clap::error::ErrorKind::ArgumentConflict,
                "Cannot use watch arguments before `watch` subcommand",
            ))
        } else if matches!(self.command, Some(Command::Run { .. }))
            && (self.run_args.is_some() || self.execution_args.is_some())
        {
            let mut cmd = Self::command();
            Err(cmd.error(
                clap::error::ErrorKind::ArgumentConflict,
                "Cannot use run arguments before `run` subcommand",
            ))
        } else if matches!(self.command, Some(Command::Boundaries { .. }))
            && (self.run_args.is_some() || self.execution_args.is_some())
        {
            let mut cmd = Self::command();
            Err(cmd.error(
                clap::error::ErrorKind::ArgumentConflict,
                "Cannot use run arguments before `boundaries` subcommand",
            ))
        } else {
            Ok(())
        }
    }
}

/// Defines the subcommands for CLI
#[derive(Subcommand, Clone, Debug, PartialEq)]
pub enum Command {
    /// Get the path to the Turbo binary
    Bin,
    /// Get the port assigned to the current microfrontend
    #[clap(name = "get-mfe-port")]
    GetMfePort,
    #[clap(hide = true)]
    Boundaries {
        #[clap(short = 'F', long, group = "scope-filter-group")]
        filter: Vec<String>,
        #[clap(long, value_enum, default_missing_value = "prompt", num_args = 0..=1, require_equals = true)]
        ignore: Option<BoundariesIgnore>,
        #[clap(long, requires = "ignore")]
        reason: Option<String>,
    },
    /// Generate the autocompletion script for the specified shell
    Completion { shell: Shell },
    /// Runs the Turborepo background daemon
    Daemon {
        /// Set the idle timeout for turbod
        #[clap(long, default_value_t = String::from("4h0m0s"))]
        idle_time: String,
        /// Path to a custom turbo.json file to watch from --root-turbo-json
        #[clap(long)]
        turbo_json_path: Option<Utf8PathBuf>,
        #[clap(subcommand)]
        command: Option<DaemonCommand>,
    },
    /// Visualize your monorepo's package graph in the browser
    Devtools {
        /// Port for the WebSocket server
        #[clap(long, default_value_t = turborepo_devtools::DEFAULT_PORT)]
        port: u16,
        /// Don't automatically open the browser
        #[clap(long)]
        no_open: bool,
    },
    /// Search the Turborepo documentation
    Docs {
        /// The search query
        query: String,
        /// Override the docs version (minimum: 2.7.5)
        #[clap(long)]
        docs_version: Option<String>,
    },
    /// Generate a new app / package
    #[clap(aliases = ["g", "gen"])]
    Generate {
        #[clap(long, hide = true)]
        tag: Option<String>,
        /// The name of the generator to run
        generator_name: Option<String>,
        /// Generator configuration file
        #[clap(short = 'c', long)]
        config: Option<String>,
        /// The root of your repository (default: directory with root
        /// turbo.json)
        #[clap(short = 'r', long)]
        root: Option<String>,
        /// Answers passed directly to generator
        #[clap(short = 'a', long, num_args = 1..)]
        args: Vec<String>,

        #[clap(subcommand)]
        command: Option<Box<GenerateCommand>>,
    },
    /// Enable or disable anonymous telemetry
    Telemetry {
        #[clap(subcommand)]
        command: Option<TelemetryCommand>,
    },
    /// [DEPRECATED] `turbo scan` has been removed. This command will be
    /// fully removed in a future major version.
    #[clap(hide = true)]
    Scan,
    #[clap(hide = true)]
    Config,
    /// EXPERIMENTAL: List packages in your monorepo.
    Ls {
        /// Show only packages that are affected by changes between
        /// the current branch and `main`
        #[clap(long)]
        affected: bool,
        /// Use the given selector to specify package(s) to act as
        /// entry points. The syntax mirrors pnpm's syntax, and
        /// additional documentation and examples can be found in
        /// turbo's documentation https://turborepo.dev/docs/reference/command-line-reference/run#--filter
        #[clap(short = 'F', long)]
        filter: Vec<String>,
        /// Get insight into a specific package, such as
        /// its dependencies and tasks
        packages: Vec<String>,
        /// Output format
        #[clap(long, value_enum)]
        output: Option<OutputFormat>,
    },
    /// Link your local directory to a Vercel organization and enable remote
    /// caching.
    Link {
        /// Do not create or modify .gitignore (default false)
        #[clap(long)]
        no_gitignore: bool,

        /// The scope, i.e. Vercel team, to which you are linking
        #[clap(long)]
        scope: Option<String>,

        /// Answer yes to all prompts (default false)
        #[clap(long, short)]
        yes: bool,
    },
    /// Login to your Vercel account
    Login {
        #[clap(long = "sso-team")]
        sso_team: Option<String>,
        /// Deprecated, no-op. Previously forced a new login even if a valid
        /// token existed.
        #[clap(long = "force", short = 'f', hide = true)]
        force: bool,
        /// Manually enter token instead of requesting one from the login
        /// service.
        #[clap(long, conflicts_with = "sso_team")]
        manual: bool,
    },
    /// Logout to your Vercel account
    Logout {
        /// Invalidate the token on the server. Pass `--invalidate=false` to
        /// skip the remote revoke.
        #[clap(long, value_name = "BOOL", action = ArgAction::Set, default_value = "true", default_missing_value = "true", num_args = 0..=1)]
        invalidate: bool,
    },
    /// Print debugging information
    Info,
    /// Prepare a subset of your monorepo.
    Prune {
        /// DEPRECATED: Use positional arguments instead
        /// (e.g. `turbo prune web`)
        #[clap(hide = true, long)]
        scope: Option<Vec<String>>,
        /// Workspaces that should be included in the subset
        #[clap(
            required_unless_present("scope"),
            conflicts_with("scope"),
            value_name = "SCOPE"
        )]
        scope_arg: Option<Vec<String>>,
        #[clap(long)]
        docker: bool,
        #[clap(long = "out-dir", default_value_t = String::from(prune::DEFAULT_OUTPUT_DIR), value_parser)]
        output_dir: String,
        /// Respect `.gitignore` when copying files to <OUT-DIR>
        #[clap(long, default_missing_value = "true", num_args = 0..=1, require_equals = true)]
        use_gitignore: Option<bool>,
    },

    /// Run tasks across projects in your monorepo
    ///
    /// By default, turbo executes tasks in topological order (i.e.
    /// dependencies first) and then caches the results. Re-running commands for
    /// tasks already in the cache will skip re-execution and immediately move
    /// artifacts from the cache into the correct output folders (as if the task
    /// occurred again).
    ///
    /// Arguments passed after '--' will be passed through to the named tasks.
    Run {
        #[clap(flatten)]
        run_args: Box<RunArgs>,
        #[clap(flatten)]
        execution_args: Box<ExecutionArgs>,
    },
    /// Query your monorepo using GraphQL. If no query is provided, spins up a
    /// GraphQL server with GraphiQL.
    #[command(args_conflicts_with_subcommands = true)]
    Query {
        #[clap(subcommand)]
        subcommand: Option<QuerySubcommand>,
        /// Pass variables to the query via a JSON file
        #[clap(short = 'V', long, requires = "query")]
        variables: Option<Utf8PathBuf>,
        #[clap(long, conflicts_with = "query")]
        schema: bool,
        /// The query to run, either a file path or a query string
        query: Option<String>,
    },
    Watch {
        #[clap(flatten)]
        execution_args: Box<ExecutionArgs>,
        /// EXPERIMENTAL: Write to cache in watch mode.
        #[clap(long)]
        experimental_write_cache: bool,
    },
    /// Unlink the current directory from your Vercel organization and disable
    /// Remote Caching
    Unlink,
}

#[derive(Copy, Clone, Debug, Default, ValueEnum, Serialize, Eq, PartialEq)]
pub enum BoundariesIgnore {
    /// Adds a `@boundaries-ignore` comment everywhere possible
    All,
    /// Prompts user if they want to add `@boundaries-ignore` comment
    #[default]
    Prompt,
}

#[derive(Parser, Clone, Debug, Default, Serialize, PartialEq)]
pub struct GenerateWorkspaceArgs {
    /// Name for the new workspace
    #[clap(short = 'n', long)]
    pub name: Option<String>,
    /// Generate an empty workspace
    #[clap(short = 'b', long, conflicts_with = "copy", default_value_t = true)]
    pub empty: bool,
    /// Generate a workspace using an existing workspace as a template. Can be
    /// the name of a local workspace within your monorepo, or a fully
    /// qualified GitHub URL with any branch and/or subdirectory
    #[clap(short = 'c', long, conflicts_with = "empty", num_args = 0..=1, default_missing_value = "")]
    pub copy: Option<String>,
    /// Where the new workspace should be created
    #[clap(short = 'd', long)]
    pub destination: Option<String>,
    /// The type of workspace to create
    #[clap(short = 't', long)]
    pub r#type: Option<String>,
    /// The root of your repository (default: directory with root turbo.json)
    #[clap(short = 'r', long)]
    pub root: Option<String>,
    /// In a rare case, your GitHub URL might contain a branch name with a slash
    /// (e.g. bug/fix-1) and the path to the example (e.g. foo/bar). In this
    /// case, you must specify the path to the example separately:
    /// --example-path foo/bar
    #[clap(short = 'p', long)]
    pub example_path: Option<String>,
    /// Do not filter available dependencies by the workspace type
    #[clap(long, default_value_t = false)]
    pub show_all_dependencies: bool,
}

#[derive(Parser, Clone, Debug, Default, PartialEq, Serialize)]
pub struct GeneratorCustomArgs {
    /// The name of the generator to run
    generator_name: Option<String>,
    /// Generator configuration file
    #[clap(short = 'c', long)]
    config: Option<String>,
    /// The root of your repository (default: directory with root
    /// turbo.json)
    #[clap(short = 'r', long)]
    root: Option<String>,
    /// Answers passed directly to generator
    #[clap(short = 'a', long, value_delimiter = ' ', num_args = 1..)]
    args: Vec<String>,
}

#[derive(Subcommand, Clone, Debug, PartialEq)]
pub enum GenerateCommand {
    /// Add a new package or app to your project
    #[clap(name = "workspace", alias = "w")]
    Workspace(GenerateWorkspaceArgs),
    #[clap(name = "run", alias = "r")]
    Run(GeneratorCustomArgs),
}

#[derive(Subcommand, Clone, Debug, PartialEq)]
pub enum QuerySubcommand {
    /// Check which packages or tasks are affected by changes between two git
    /// refs
    Affected(AffectedArgs),
    /// List packages in your monorepo (shorthand for a packages query)
    Ls(LsArgs),
}

#[derive(clap::Args, Clone, Debug, PartialEq)]
pub struct LsArgs {
    /// Show only packages that are affected by changes between
    /// the current branch and `main`
    #[clap(long)]
    pub affected: bool,
    /// Use the given selector to specify package(s) to act as
    /// entry points. The syntax mirrors pnpm's syntax, and
    /// additional documentation and examples can be found in
    /// turbo's documentation https://turborepo.dev/docs/reference/command-line-reference/run#--filter
    #[clap(short = 'F', long)]
    pub filter: Vec<String>,
    /// Get insight into a specific package, such as
    /// its dependencies and tasks
    pub packages: Vec<String>,
    /// Output format
    #[clap(long, value_enum)]
    pub output: Option<OutputFormat>,
}

#[derive(clap::Args, Clone, Debug, PartialEq)]
pub struct AffectedArgs {
    /// Return affected packages instead of tasks. Optionally filter by name.
    /// When combined with --tasks, returns affected tasks that match both
    /// the task name and package filters.
    #[clap(long, num_args = 0..)]
    pub packages: Option<Vec<String>>,
    /// Filter to specific task names (e.g. build, test).
    /// When combined with --packages, returns affected tasks that match both
    /// the task name and package filters.
    #[clap(long, num_args = 0..)]
    pub tasks: Option<Vec<String>>,
    /// Base git ref for comparison
    #[clap(long)]
    pub base: Option<String>,
    /// Head git ref for comparison
    #[clap(long)]
    pub head: Option<String>,
    /// Exit with code 1 when affected packages or tasks are found, 0 when
    /// none are found, or 2 on errors. Useful for CI gating. We recommend
    /// parsing the JSON output directly for more flexibility.
    #[clap(long)]
    pub exit_code: bool,
}

fn validate_graph_extension(s: &str) -> Result<String, String> {
    match s.is_empty() {
        true => Ok(s.to_string()),
        _ => match Utf8Path::new(s).extension() {
            Some(ext) if SUPPORTED_GRAPH_FILE_EXTENSIONS.contains(&ext) => Ok(s.to_string()),
            Some(ext) => Err(format!(
                "Invalid file extension: '{ext}'. Allowed extensions are: \
                 {SUPPORTED_GRAPH_FILE_EXTENSIONS:?}"
            )),
            None => Err(format!(
                "The provided filename is missing a file extension. Allowed extensions are: \
                 {SUPPORTED_GRAPH_FILE_EXTENSIONS:?}"
            )),
        },
    }
}

fn path_non_empty(s: &str) -> Result<Utf8PathBuf, String> {
    if s.is_empty() {
        Err("path must not be empty".to_string())
    } else {
        Ok(Utf8Path::new(s).to_path_buf())
    }
}

/// Arguments used in run and watch
#[derive(Parser, Clone, Debug, Default, PartialEq)]
#[command(groups = [
ArgGroup::new("scope-filter-group").multiple(true).required(false),
])]
pub struct ExecutionArgs {
    /// Override the filesystem cache directory.
    #[clap(long, value_parser = path_non_empty)]
    pub cache_dir: Option<Utf8PathBuf>,
    /// Limit the concurrency of task execution. Use 1 for serial (i.e.
    /// one-at-a-time) execution.
    #[clap(long)]
    pub concurrency: Option<String>,
    /// Specify how task execution should proceed when an error occurs.
    /// Use "never" to cancel all tasks. Use "dependencies-successful" to
    /// continue running tasks whose dependencies have succeeded. Use "always"
    /// to continue running all tasks, even those whose dependencies have
    /// failed.
    #[clap(long = "continue", value_name = "CONTINUE", num_args = 0..=1, default_value = "never", default_missing_value = "always", require_equals = true)]
    pub continue_execution: ContinueMode,
    /// Run turbo in single-package mode
    #[clap(long)]
    pub single_package: bool,
    /// Specify whether or not to do framework inference for tasks
    #[clap(long, value_name = "BOOL", action = ArgAction::Set, default_value = "true", default_missing_value = "true", num_args = 0..=1)]
    pub framework_inference: bool,
    /// Specify glob of global filesystem dependencies to be hashed. Useful
    /// for .env and files
    #[clap(long = "global-deps", action = ArgAction::Append)]
    pub global_deps: Vec<String>,
    /// Environment variable mode.
    /// Use "loose" to pass the entire existing environment.
    /// Use "strict" to use an allowlist specified in turbo.json.
    #[clap(long = "env-mode", num_args = 0..=1, default_missing_value = "strict")]
    pub env_mode: Option<EnvMode>,
    /// Use the given selector to specify package(s) to act as
    /// entry points. The syntax mirrors pnpm's syntax, and
    /// additional documentation and examples can be found in
    /// turbo's documentation https://turborepo.dev/docs/reference/command-line-reference/run#--filter
    #[clap(short = 'F', long, group = "scope-filter-group")]
    pub filter: Vec<String>,

    /// Filter to only packages that are affected by changes between
    /// the current branch and `main`
    #[clap(long, group = "scope-filter-group")]
    pub affected: bool,

    /// Set type of process output logging. Use "full" to show
    /// all output. Use "hash-only" to show only turbo-computed
    /// task hashes. Use "new-only" to show only new output with
    /// only hashes for cached tasks. Use "none" to hide process
    /// output. (default full)
    #[clap(long, value_enum)]
    pub output_logs: Option<OutputLogsMode>,
    /// Set type of task output order. Use "stream" to show
    /// output as soon as it is available. Use "grouped" to
    /// show output when a command has finished execution. Use "auto" to let
    /// turbo decide based on its own heuristics. (default auto)
    #[clap(long, value_enum)]
    pub log_order: Option<LogOrder>,
    /// Output machine-readable NDJSON to stdout instead of human-readable
    /// text. Disables the TUI and forces stream mode.
    #[clap(long)]
    pub json: bool,
    /// Write structured JSON logs to a file. If no path is given, writes to
    /// `.turbo/logs/<epoch_millis>.json`.
    #[clap(long)]
    pub log_file: Option<Option<String>>,
    /// Only executes the tasks specified, does not execute parent tasks.
    #[clap(long)]
    pub only: bool,
    #[clap(long, hide = true)]
    pub pkg_inference_root: Option<String>,
    /// Use "none" to remove prefixes from task logs. Use "task" to get task id
    /// prefixing. Use "auto" to let turbo decide how to prefix the logs
    /// based on the execution environment. In most cases this will be the same
    /// as "task". Note that tasks running in parallel interleave their
    /// logs, so removing prefixes can make it difficult to associate logs
    /// with tasks. Use --log-order=grouped to prevent interleaving. (default
    /// auto)
    #[clap(long, value_enum, default_value_t = LogPrefix::Auto)]
    pub log_prefix: LogPrefix,
    // NOTE: The following two are hidden because clap displays them in the help text incorrectly:
    // > Usage: turbo [OPTIONS] [TASKS]... [-- <FORWARDED_ARGS>...] [COMMAND]
    #[clap(hide = true)]
    pub tasks: Vec<String>,
    #[clap(last = true, hide = true)]
    pub pass_through_args: Vec<String>,
}

impl ExecutionArgs {
    fn track(&self, telemetry: &CommandEventBuilder) {
        // default to false
        track_usage!(telemetry, self.framework_inference, |val: bool| !val);

        track_usage!(telemetry, self.continue_execution, |val| matches!(
            val,
            ContinueMode::Always | ContinueMode::DependenciesSuccessful
        ));
        telemetry.track_arg_value(
            "continue-execution-strategy",
            self.continue_execution,
            EventType::NonSensitive,
        );

        track_usage!(telemetry, self.single_package, |val| val);
        track_usage!(telemetry, self.only, |val| val);
        track_usage!(telemetry, &self.cache_dir, Option::is_some);
        track_usage!(telemetry, &self.pkg_inference_root, Option::is_some);

        if let Some(concurrency) = &self.concurrency {
            telemetry.track_arg_value("concurrency", concurrency, EventType::NonSensitive);
        }

        if !self.global_deps.is_empty() {
            telemetry.track_arg_value(
                "global-deps",
                self.global_deps.join(", "),
                EventType::NonSensitive,
            );
        }

        if let Some(env_mode) = self.env_mode {
            telemetry.track_arg_value("env-mode", env_mode, EventType::NonSensitive);
        }

        if let Some(output_logs) = &self.output_logs {
            telemetry.track_arg_value("output-logs", output_logs, EventType::NonSensitive);
        }

        if let Some(log_order) = self.log_order {
            telemetry.track_arg_value("log-order", log_order, EventType::NonSensitive);
        }

        if self.log_prefix != LogPrefix::default() {
            telemetry.track_arg_value("log-prefix", self.log_prefix, EventType::NonSensitive);
        }

        // track sizes
        if !self.filter.is_empty() {
            telemetry.track_arg_value("filter:length", self.filter.len(), EventType::NonSensitive);
        }
    }
}

#[derive(Parser, Clone, Debug, PartialEq)]
#[command(groups = [
    ArgGroup::new("daemon-group").multiple(false).required(false),
])]
pub struct RunArgs {
    /// Set the cache behavior for this run. Pass a list of comma-separated key,
    /// value pairs to enable reading and writing to either the local or
    /// remote cache.
    #[clap(long, conflicts_with_all = &["force", "remote_only", "remote_cache_read_only", "no_cache"])]
    pub cache: Option<String>,
    /// Ignore the existing cache (to force execution). Equivalent to
    /// `--cache=local:w,remote:w`
    #[clap(long, default_missing_value = "true")]
    pub force: Option<Option<bool>>,
    /// Ignore the local filesystem cache for all tasks. Only
    /// allow reading and caching artifacts using the remote cache.
    /// Equivalent to `--cache=remote:rw`
    #[clap(long, default_missing_value = "true", group = "cache-group")]
    pub remote_only: Option<Option<bool>>,
    /// Treat remote cache as read only. Equivalent to
    /// `--cache=remote:r;local:rw`
    #[clap(long, default_missing_value = "true")]
    pub remote_cache_read_only: Option<Option<bool>>,
    /// Avoid saving task results to the cache. Useful for development/watch
    /// tasks. Equivalent to `--cache=local:r,remote:r`
    #[clap(long)]
    pub no_cache: bool,

    /// Set the number of concurrent cache operations (default 10)
    #[clap(long, default_value_t = DEFAULT_NUM_WORKERS)]
    pub cache_workers: u32,
    #[clap(alias = "dry", long = "dry-run", num_args = 0..=1, default_missing_value = "text")]
    pub dry_run: Option<DryRunMode>,
    /// Generate a graph of the task execution and output to a file when a
    /// filename is specified (.svg, .html, .mermaid, .dot). Outputs dot graph
    /// to stdout when no filename is provided.
    /// [DEPRECATED formats: .png, .jpg, .pdf, .json -- will be removed in 3.0]
    #[clap(long, num_args = 0..=1, default_missing_value = "", value_parser = validate_graph_extension)]
    pub graph: Option<String>,
    // clap does not have negation flags such as --daemon and --no-daemon
    // so we need to use a group to enforce that only one of them is set.
    // -----------------------
    /// [DEPRECATED] The daemon is no longer used for `turbo run`.
    /// This flag will be removed in version 3.0.
    #[clap(long, group = "daemon-group")]
    pub daemon: bool,

    /// [DEPRECATED] The daemon is no longer used for `turbo run`.
    /// This flag will be removed in version 3.0.
    #[clap(long, group = "daemon-group")]
    pub no_daemon: bool,

    /// File to write turbo's performance profile output into.
    /// You can load the file up in chrome://tracing to see
    /// which parts of your build were slow.
    #[clap(long, num_args = 0..=1, default_missing_value = "", conflicts_with = "anon_profile")]
    pub profile: Option<String>,
    /// File to write turbo's performance profile output into.
    /// All identifying data omitted from the profile.
    #[clap(long, num_args = 0..=1, default_missing_value = "", conflicts_with = "profile")]
    pub anon_profile: Option<String>,
    /// Generate a summary of the turbo run
    #[clap(long, default_missing_value = "true")]
    pub summarize: Option<Option<bool>>,

    /// [DEPRECATED] Execute all tasks in parallel. Use task configuration
    /// (`persistent`, `with`) instead.
    #[clap(long)]
    pub parallel: bool,
}

impl Default for RunArgs {
    fn default() -> Self {
        Self {
            remote_only: None,
            cache: None,
            force: None,
            cache_workers: DEFAULT_NUM_WORKERS,
            dry_run: None,
            graph: None,
            no_cache: false,
            daemon: false,
            no_daemon: false,
            profile: None,
            anon_profile: None,
            remote_cache_read_only: None,
            summarize: None,
            parallel: false,
        }
    }
}

impl RunArgs {
    pub fn remote_only(&self) -> Option<bool> {
        let remote_only = self.remote_only?;
        Some(remote_only.unwrap_or(true))
    }

    /// Some(true) means force the daemon
    /// Some(false) means force no daemon
    /// None means use the default detection
    pub fn daemon(&self) -> Option<bool> {
        match (self.daemon, self.no_daemon) {
            (true, false) => Some(true),
            (false, true) => Some(false),
            (false, false) => None,
            (true, true) => unreachable!(), // guaranteed by mutually exclusive `ArgGroup`
        }
    }

    pub fn profile_file_and_include_args(&self) -> Option<(String, bool)> {
        let resolve = |file: &str| -> String {
            if file.is_empty() {
                let now = std::time::SystemTime::now()
                    .duration_since(std::time::UNIX_EPOCH)
                    .map_or(0, |duration| duration.as_millis());
                format!("profile.{now}")
            } else {
                file.to_string()
            }
        };

        match (self.profile.as_deref(), self.anon_profile.as_deref()) {
            (Some(file), None) => Some((resolve(file), true)),
            (None, Some(file)) => Some((resolve(file), false)),
            (Some(_), Some(_)) => unreachable!(),
            (None, None) => None,
        }
    }

    pub fn remote_cache_read_only(&self) -> Option<bool> {
        let remote_cache_read_only = self.remote_cache_read_only?;
        Some(remote_cache_read_only.unwrap_or(true))
    }

    pub fn summarize(&self) -> Option<bool> {
        let summarize = self.summarize?;
        Some(summarize.unwrap_or(true))
    }

    pub fn track(&self, telemetry: &CommandEventBuilder) {
        // default to true
        track_usage!(telemetry, self.no_cache, |val| val);
        track_usage!(telemetry, self.remote_only().unwrap_or_default(), |val| val);
        track_usage!(telemetry, &self.force, Option::is_some);
        track_usage!(telemetry, self.daemon, |val| val);
        track_usage!(telemetry, self.no_daemon, |val| val);
        track_usage!(telemetry, self.parallel, |val| val);
        track_usage!(
            telemetry,
            self.remote_cache_read_only().unwrap_or_default(),
            |val| val
        );

        // default to None
        track_usage!(telemetry, &self.profile, Option::is_some);
        track_usage!(telemetry, &self.anon_profile, Option::is_some);
        track_usage!(telemetry, &self.summarize, Option::is_some);

        // track values
        if let Some(dry_run) = &self.dry_run {
            telemetry.track_arg_value("dry-run", dry_run, EventType::NonSensitive);
        }

        if self.cache_workers != DEFAULT_NUM_WORKERS {
            telemetry.track_arg_value("cache-workers", self.cache_workers, EventType::NonSensitive);
        }

        if let Some(graph) = &self.graph {
            // track the extension used only
            let extension = Utf8Path::new(graph).extension().unwrap_or("stdout");
            telemetry.track_arg_value("graph", extension, EventType::NonSensitive);
        }
    }
}

fn initialize_deferred_telemetry_client(
    http_client: SharedHttpClient,
    color_config: ColorConfig,
    version: &str,
) -> Option<TelemetryHandle> {
    let deferred_client = turborepo_api_client::telemetry::DeferredTelemetryClient::new(
        http_client.clone(),
        "https://telemetry.vercel.com",
        version,
    );
    match init_telemetry(deferred_client, color_config) {
        Ok((handle, enabled)) => {
            if enabled {
                http_client.activate();
            }
            Some(handle)
        }
        Err(error) => {
            debug!("failed to start telemetry: {:?}", error);
            None
        }
    }
}

#[derive(PartialEq)]
enum PrintVersionState {
    Enabled,
    Disabled,
}

fn get_print_version_state() -> PrintVersionState {
    env::var("TURBO_PRINT_VERSION_DISABLED")
        .map(|var| match var.as_str() {
            "1" | "true" => PrintVersionState::Disabled,
            _ => PrintVersionState::Enabled,
        })
        .unwrap_or(PrintVersionState::Enabled)
}

#[derive(PartialEq)]
enum CIState {
    Inside,
    Outside,
}

fn get_ci_state() -> CIState {
    match turborepo_ci::is_ci() {
        true => CIState::Inside,
        _ => CIState::Outside,
    }
}

fn should_print_version() -> bool {
    let print_version_state = get_print_version_state();
    let ci_state = get_ci_state();

    print_version_state == PrintVersionState::Enabled && ci_state == CIState::Outside
}

fn set_run_flags<'a>(
    command: &'a mut Command,
    repo_state: &'a Option<RepoState>,
    cli_args: &'a Args,
) -> Result<&'a mut Command, Error> {
    match command {
        Command::Run {
            run_args: _,
            execution_args,
        }
        | Command::Watch { execution_args, .. } => {
            // Don't overwrite the flag if it's already been set for whatever reason
            execution_args.single_package = execution_args.single_package
                || repo_state
                    .as_ref()
                    .map(|repo_state| matches!(repo_state.mode, RepoMode::SinglePackage))
                    .unwrap_or(false);
            // If this is a run command, and we know the actual invocation path, set the
            // inference root, as long as the user hasn't overridden the cwd
            if cli_args.cwd.is_none() {
                if let Ok(invocation_dir) = env::var(INVOCATION_DIR_ENV_VAR) {
                    // TODO: this calculation can probably be wrapped into the path library
                    // and made a little more robust or clear
                    let invocation_path = Utf8Path::new(&invocation_dir);

                    // If repo state doesn't exist, we're either local turbo running at the root
                    // (cwd), or inference failed.
                    // If repo state does exist, we're global turbo, and want to calculate
                    // package inference based on the repo root
                    let this_dir = AbsoluteSystemPathBuf::cwd()?;
                    let repo_root = repo_state.as_ref().map_or(&this_dir, |r| &r.root);
                    if let Ok(relative_path) = invocation_path.strip_prefix(repo_root) {
                        if !relative_path.as_str().is_empty() {
                            debug!("pkg_inference_root set to \"{}\"", relative_path);
                            execution_args.pkg_inference_root = Some(relative_path.to_string());
                        }
                    }
                } else {
                    debug!("{} not set", INVOCATION_DIR_ENV_VAR);
                }
            }
        }
        _ => {}
    }
    Ok(command)
}

fn default_to_run_command(cli_args: &Args) -> Result<Command, Error> {
    let run_args = cli_args.run_args.clone().unwrap_or_default();
    let execution_args = cli_args
        .execution_args
        // We clone instead of take as take would leave the command base a copy of cli_args
        // missing any execution args.
        .clone()
        .ok_or_else(|| Error::NoCommand)?;

    if execution_args.tasks.is_empty() {
        let mut cmd = <Args as CommandFactory>::command();
        let _ = cmd.print_help();
        exit_with_heap_profile(1);
    }

    Ok(Command::Run {
        run_args: Box::new(run_args),
        execution_args: Box::new(execution_args),
    })
}

#[tracing::instrument(skip_all)]
fn get_command(cli_args: &mut Args) -> Result<Command, Error> {
    if let Some(command) = mem::take(&mut cli_args.command) {
        Ok(command)
    } else {
        // If there is no command, we set the command to `Command::Run` with
        // `self.parsed_args.run_args` as arguments.
        default_to_run_command(cli_args)
    }
}

/// Runs the CLI by parsing arguments with clap, then either calling Rust code
/// directly or returning a payload for the Go code to use.
///
/// Scenarios:
/// 1. inference failed, we're running this global turbo. no repo state
/// 2. --skip-infer was passed, assume we're local turbo and run. no repo state
/// 3. There is no local turbo, we're running the global one. repo state exists
/// 4. turbo binary path is set, and it's this one. repo state exists
///
/// # Arguments
///
/// * `repo_state`: If we have done repository inference and NOT executed local
///   turbo, such as in the case where `TURBO_BINARY_PATH` is set, we use it
///   here to modify clap's arguments.
/// * `logger`: The logger to use for the run.
/// * `color_config`: The color configuration to use for the run, i.e. whether
///   we should colorize output.
///
/// returns: Result<i32, Error>
pub fn run(
    repo_state: Option<RepoState>,
    logger: &TurboSubscriber,
    color_config: ColorConfig,
    query_server: Option<Arc<dyn turborepo_query_api::QueryServer>>,
) -> Result<i32, Error> {
    // Initialize rayon's global pool before the tokio runtime so we
    // control thread count and avoid lazy initialization during a hot path.
    init_rayon_pool();

    let runtime = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .map_err(Error::Runtime)?;

    runtime.block_on(run_main(repo_state, logger, color_config, query_server))
}

#[tracing::instrument(skip_all)]
async fn run_main(
    repo_state: Option<RepoState>,
    #[allow(unused_variables)] logger: &TurboSubscriber,
    color_config: ColorConfig,
    query_server: Option<Arc<dyn turborepo_query_api::QueryServer>>,
) -> Result<i32, Error> {
    let _cli_run_span = tracing::info_span!("cli_run").entered();
    let http_client = SharedHttpClient::new();

    let mut cli_args = {
        let _span = tracing::info_span!("cli_arg_parsing").entered();
        Args::new(env::args_os().collect())
    };
    let version = get_version();

    // Initialize telemetry immediately so events are captured from startup.
    // The shared HTTP client is only activated if telemetry is actually
    // enabled for this invocation.
    let telemetry_handle = {
        let _span = tracing::info_span!("telemetry_init").entered();
        initialize_deferred_telemetry_client(http_client.clone(), color_config, version)
    };

    let mut command = get_command(&mut cli_args)?;

    // Suppress the version banner in --json mode — all output on stdout
    // must be machine-readable NDJSON.
    let is_json_mode = matches!(
        &command,
        Command::Run { execution_args, .. } | Command::Watch { execution_args, .. }
            if execution_args.json
    );
    if should_print_version() && !is_json_mode {
        eprintln!("{}", GREY.apply_to(format!("• turbo {}", get_version())));
    }

    // Set some run flags if we have the data and are executing a Run
    set_run_flags(&mut command, &repo_state, &cli_args)?;

    let cwd = repo_state
        .as_ref()
        .map(|state| state.root.as_path())
        .or(cli_args.cwd.as_deref());

    let repo_root = if let Some(cwd) = cwd {
        AbsoluteSystemPathBuf::from_cwd(cwd)?
    } else {
        AbsoluteSystemPathBuf::cwd()?
    };

    // Windows has this annoying habit of abbreviating paths
    // like `C:\Users\Admini~1` instead of `C:\Users\Administrator`
    // We canonicalize to get the proper, full length path
    let repo_root = if cfg!(windows) {
        repo_root.to_realpath()?
    } else {
        repo_root
    };

    cli_args.command = Some(command);

    let root_telemetry = GenericEventBuilder::new();
    root_telemetry.track_start();

    // track system info
    root_telemetry.track_platform(TurboState::platform_name());
    root_telemetry.track_version(TurboState::version());
    root_telemetry.track_ai_agent(turborepo_ai_agents::get_agent());
    root_telemetry.track_cpus(
        std::thread::available_parallelism()
            .map(|n| n.get())
            .unwrap_or(1),
    );
    // track args
    cli_args.track(&root_telemetry);

    let Some(command) = cli_args.command.as_ref() else {
        return Err(Error::NoCommand);
    };

    let cli_result = match command {
        Command::Bin => {
            CommandEventBuilder::new("bin")
                .with_parent(&root_telemetry)
                .track_call();
            bin::run()?;

            Ok(0)
        }
        Command::GetMfePort => {
            let event = CommandEventBuilder::new("get-mfe-port").with_parent(&root_telemetry);
            event.track_call();

            let base = CommandBase::new(cli_args.clone(), repo_root, version, color_config)?;
            get_mfe_port::run(&base).await?;

            Ok(0)
        }
        Command::Boundaries { ignore, reason, .. } => {
            let event = CommandEventBuilder::new("boundaries").with_parent(&root_telemetry);
            let ignore = *ignore;
            let reason = reason.clone();

            event.track_call();
            let base = CommandBase::new(cli_args.clone(), repo_root, version, color_config)?;

            Ok(boundaries::run(base, event, ignore, reason).await?)
        }
        #[allow(unused_variables)]
        Command::Daemon {
            command,
            idle_time,
            turbo_json_path,
        } => {
            let event = CommandEventBuilder::new("daemon").with_parent(&root_telemetry);
            event.track_call();
            let base = CommandBase::new(cli_args.clone(), repo_root, version, color_config)?;
            event.track_ui_mode(base.opts.run_opts.ui_mode);

            match command {
                Some(command) => {
                    daemon::daemon_client(command, &base, turbo_json_path.clone()).await
                }
                None => {
                    daemon::daemon_server(&base, idle_time, turbo_json_path.clone(), logger).await
                }
            }?;

            Ok(0)
        }
        Command::Devtools { port, no_open } => {
            let event = CommandEventBuilder::new("devtools").with_parent(&root_telemetry);
            event.track_call();

            crate::commands::devtools::run(repo_root, *port, *no_open).await?;
            Ok(0)
        }
        Command::Docs {
            query,
            docs_version,
        } => {
            let event = CommandEventBuilder::new("docs").with_parent(&root_telemetry);
            event.track_call();

            docs::run(query, docs_version.as_deref()).await?;
            Ok(0)
        }
        Command::Generate {
            tag,
            generator_name,
            config,
            root,
            args,
            command,
        } => {
            let event = CommandEventBuilder::new("generate").with_parent(&root_telemetry);
            event.track_call();
            let tag = tag.clone().unwrap_or_else(|| get_version().to_string());
            // build GeneratorCustomArgs struct
            let args = GeneratorCustomArgs {
                generator_name: generator_name.clone(),
                config: config.clone(),
                root: root.clone(),
                args: args.clone(),
            };
            let child_event = event.child();
            generate::run(&tag, command, &args, child_event)?;
            Ok(0)
        }
        Command::Info => {
            let event = CommandEventBuilder::new("info").with_parent(&root_telemetry);

            event.track_call();
            let base = CommandBase::new(cli_args.clone(), repo_root, version, color_config)?;
            event.track_ui_mode(base.opts.run_opts.ui_mode);

            info::run(base).await;
            Ok(0)
        }
        Command::Telemetry { command } => {
            let event = CommandEventBuilder::new("telemetry").with_parent(&root_telemetry);
            event.track_call();
            let mut base = CommandBase::new(cli_args.clone(), repo_root, version, color_config)?;
            event.track_ui_mode(base.opts.run_opts.ui_mode);
            let child_event = event.child();
            telemetry::configure(command, &mut base, child_event);
            Ok(0)
        }
        Command::Scan => {
            let event = CommandEventBuilder::new("scan").with_parent(&root_telemetry);
            event.track_call();
            warn!("`turbo scan` is deprecated and will be removed in a future major version.");
            Ok(1)
        }
        Command::Config => {
            CommandEventBuilder::new("config")
                .with_parent(&root_telemetry)
                .track_call();
            config::run(repo_root, cli_args).await?;
            Ok(0)
        }
        Command::Ls {
            packages, output, ..
        } => {
            let Some(ref query_server) = query_server else {
                return Err(error::Error::QueryNotAvailable);
            };
            let event = CommandEventBuilder::new("info").with_parent(&root_telemetry);

            event.track_call();
            let output = *output;
            let packages = packages.clone();
            let base = CommandBase::new(cli_args, repo_root, version, color_config)?;
            event.track_ui_mode(base.opts.run_opts.ui_mode);
            ls::run(base, packages, event, output, query_server.as_ref()).await?;

            Ok(0)
        }
        Command::Link {
            no_gitignore,
            scope,
            yes,
        } => {
            let event = CommandEventBuilder::new("link").with_parent(&root_telemetry);
            event.track_call();

            if cli_args.team.is_some() {
                warn!("team flag does not set the scope for linking. Use --scope instead.");
            }

            if cli_args.test_run {
                println!("Link test run successful");
                return Ok(0);
            }

            let modify_gitignore = !*no_gitignore;
            let yes = *yes;
            let scope = scope.clone();
            let mut base = CommandBase::new(cli_args, repo_root, version, color_config)?;
            event.track_ui_mode(base.opts.run_opts.ui_mode);

            link::link(&mut base, scope, modify_gitignore, yes).await?;

            Ok(0)
        }
        Command::Logout { invalidate } => {
            let event = CommandEventBuilder::new("logout").with_parent(&root_telemetry);
            event.track_call();
            let invalidate = *invalidate;

            let mut base = CommandBase::new(cli_args, repo_root, version, color_config)?;
            event.track_ui_mode(base.opts.run_opts.ui_mode);
            let event_child = event.child();

            logout::logout(&mut base, invalidate, event_child).await?;

            Ok(0)
        }
        Command::Login {
            sso_team,
            force,
            manual,
        } => {
            let event = CommandEventBuilder::new("login").with_parent(&root_telemetry);
            event.track_call();
            if cli_args.test_run {
                println!("Login test run successful");
                return Ok(0);
            }

            let sso_team = sso_team.clone();
            let force = *force;
            let manual = *manual;

            let mut base = CommandBase::new(cli_args, repo_root, version, color_config)?;
            event.track_ui_mode(base.opts.run_opts.ui_mode);
            let event_child = event.child();

            login::login(&mut base, event_child, sso_team.as_deref(), force, manual).await?;

            Ok(0)
        }
        Command::Unlink => {
            let event = CommandEventBuilder::new("unlink").with_parent(&root_telemetry);
            event.track_call();
            if cli_args.test_run {
                println!("Unlink test run successful");
                return Ok(0);
            }

            let mut base = CommandBase::new(cli_args, repo_root, version, color_config)?;
            event.track_ui_mode(base.opts.run_opts.ui_mode);

            unlink::unlink(&mut base)?;

            Ok(0)
        }
        Command::Run {
            run_args,
            execution_args,
        } => {
            let event = CommandEventBuilder::new("run").with_parent(&root_telemetry);
            event.track_call();

            let base = {
                let _span = tracing::info_span!("command_base_new").entered();
                CommandBase::new(cli_args.clone(), repo_root, version, color_config)?
            };
            event.track_ui_mode(base.opts.run_opts.ui_mode);

            if execution_args.tasks.is_empty() {
                print_potential_tasks(base, event).await?;
                finalize_chrome_profile(logger, version);
                return Ok(1);
            }

            run_args.track(&event);
            let verbosity: u8 = cli_args.verbosity.into();
            let exit_code = run::run(
                base,
                event,
                http_client,
                query_server.clone(),
                logger,
                verbosity,
            )
            .await
            .inspect(|code| {
                if *code != 0 {
                    error!("run failed: command  exited ({code})");
                }
            })?;

            // Chrome tracing is enabled early in shim::run(). Here we just
            // flush and generate the markdown summary.
            finalize_chrome_profile(logger, version);

            Ok(exit_code)
        }
        Command::Query {
            subcommand,
            query,
            variables,
            schema,
        } => {
            let Some(ref query_server) = query_server else {
                return Err(error::Error::QueryNotAvailable);
            };
            let subcommand = subcommand.clone();
            let query = query.clone();
            let variables = variables.clone();
            let schema = *schema;
            let event = CommandEventBuilder::new("query").with_parent(&root_telemetry);
            event.track_call();

            let base = CommandBase::new(cli_args, repo_root, version, color_config)?;
            event.track_ui_mode(base.opts.run_opts.ui_mode);

            let query = query::run(
                base,
                event,
                subcommand,
                query,
                variables.as_deref(),
                schema,
                query_server.as_ref(),
            )
            .await?;

            Ok(query)
        }
        Command::Watch {
            execution_args,
            experimental_write_cache,
        } => {
            let event = CommandEventBuilder::new("watch").with_parent(&root_telemetry);
            event.track_call();
            let base = CommandBase::new(cli_args.clone(), repo_root, version, color_config)?;
            event.track_ui_mode(base.opts.run_opts.ui_mode);

            if execution_args.tasks.is_empty() {
                print_potential_tasks(base, event).await?;
                return Ok(1);
            }

            let verbosity: u8 = cli_args.verbosity.into();
            let mut client = WatchClient::new(
                base,
                *experimental_write_cache,
                event,
                query_server.clone(),
                logger,
                verbosity,
            )
            .await?;
            match client.start().await {
                Ok(()) => {}
                Err(crate::run::watch::Error::SignalInterrupt) => {
                    // Normal shutdown via Ctrl+C — not an error.
                }
                Err(e) => {
                    client.shutdown().await;
                    return Err(e.into());
                }
            }
            client.shutdown().await;
            if let Some(path) = logger.stderr_redirect_path() {
                logger.restore_stderr();
                println!("Verbose logs written to {path}");
            }
            return Ok(0);
        }
        Command::Prune {
            scope,
            scope_arg,
            docker,
            output_dir,
            use_gitignore,
        } => {
            let event = CommandEventBuilder::new("prune").with_parent(&root_telemetry);
            event.track_call();
            let scope = scope_arg
                .as_ref()
                .or(scope.as_ref())
                .cloned()
                .unwrap_or_default();
            let docker = *docker;
            let output_dir = output_dir.clone();
            let use_gitignore = use_gitignore.unwrap_or(true);
            let base = CommandBase::new(cli_args, repo_root, version, color_config)?;
            event.track_ui_mode(base.opts.run_opts.ui_mode);
            let event_child = event.child();
            prune::prune(
                &base,
                &scope,
                docker,
                &output_dir,
                use_gitignore,
                event_child,
            )
            .await?;
            Ok(0)
        }
        Command::Completion { shell } => {
            CommandEventBuilder::new("completion")
                .with_parent(&root_telemetry)
                .track_call();
            generate(*shell, &mut Args::command(), "turbo", &mut io::stdout());
            Ok(0)
        }
    };

    root_telemetry.track_end();
    match telemetry_handle {
        Some(handle) => handle.close_with_timeout().await,
        None => debug!("Skipping telemetry close - not initialized"),
    }

    cli_result
}

fn finalize_chrome_profile(logger: &TurboSubscriber, version: &str) {
    let Some(file_path) = logger.chrome_tracing_file() else {
        return;
    };

    let _ = logger.flush_chrome_tracing();

    if let Err(e) = crate::tracing::inject_trace_metadata(std::path::Path::new(&file_path), version)
    {
        warn!("Failed to inject trace metadata: {e}");
    }

    let md_path = format!("{file_path}.md");
    if let Err(e) = turborepo_profile_md::trace_to_markdown(
        std::path::Path::new(&file_path),
        std::path::Path::new(&md_path),
    ) {
        warn!("Failed to generate profile markdown: {e}");
    }
}

#[cfg(test)]
mod test {
    use std::{assert_matches, ffi::OsString};

    use camino::Utf8PathBuf;
    use clap::{CommandFactory, Parser};
    use insta::assert_snapshot;
    use itertools::Itertools;
    use pretty_assertions::assert_eq;

    use crate::cli::{ContinueMode, ExecutionArgs, RunArgs};

    fn get_subcommand(name: &str) -> clap::Command {
        Args::command()
            .find_subcommand(name)
            .unwrap_or_else(|| panic!("subcommand '{name}' not found"))
            .clone()
    }

    #[test]
    fn turbo_short_help() {
        let mut cmd = Args::command();
        let mut buf = Vec::new();
        cmd.write_help(&mut buf).unwrap();
        assert_snapshot!(String::from_utf8(buf).unwrap());
    }

    #[test]
    fn turbo_long_help() {
        let mut cmd = Args::command();
        let mut buf = Vec::new();
        cmd.write_long_help(&mut buf).unwrap();
        assert_snapshot!(String::from_utf8(buf).unwrap());
    }

    #[test]
    fn link_short_help() {
        let mut cmd = get_subcommand("link");
        let mut buf = Vec::new();
        cmd.write_help(&mut buf).unwrap();
        assert_snapshot!(String::from_utf8(buf).unwrap());
    }

    #[test]
    fn unlink_short_help() {
        let mut cmd = get_subcommand("unlink");
        let mut buf = Vec::new();
        cmd.write_help(&mut buf).unwrap();
        assert_snapshot!(String::from_utf8(buf).unwrap());
    }

    #[test]
    fn login_short_help() {
        let mut cmd = get_subcommand("login");
        let mut buf = Vec::new();
        cmd.write_help(&mut buf).unwrap();
        assert_snapshot!(String::from_utf8(buf).unwrap());
    }

    #[test]
    fn logout_short_help() {
        let mut cmd = get_subcommand("logout");
        let mut buf = Vec::new();
        cmd.write_help(&mut buf).unwrap();
        assert_snapshot!(String::from_utf8(buf).unwrap());
    }

    struct CommandTestCase {
        command: &'static str,
        command_args: Vec<Vec<&'static str>>,
        global_args: Vec<Vec<&'static str>>,
        expected_output: Args,
    }

    fn get_default_run_args() -> RunArgs {
        RunArgs {
            cache_workers: 10,
            ..RunArgs::default()
        }
    }

    fn get_default_execution_args() -> ExecutionArgs {
        ExecutionArgs {
            output_logs: None,
            framework_inference: true,
            ..ExecutionArgs::default()
        }
    }

    impl CommandTestCase {
        fn test(&self) {
            let permutations = self.create_all_arg_permutations();
            for command in permutations {
                assert_eq!(Args::try_parse_from(command).unwrap(), self.expected_output)
            }
        }

        fn create_all_arg_permutations(&self) -> Vec<Vec<&'static str>> {
            let mut permutations = Vec::new();
            let mut global_args = vec![vec![self.command]];
            global_args.extend(self.global_args.clone());
            let global_args_len = global_args.len();
            let command_args_len = self.command_args.len();

            // Iterate through all the different permutations of args
            for global_args_permutation in global_args.into_iter().permutations(global_args_len) {
                let command_args = self.command_args.clone();
                for command_args_permutation in
                    command_args.into_iter().permutations(command_args_len)
                {
                    let mut command = vec![vec!["turbo"]];
                    command.extend(global_args_permutation.clone());
                    command.extend(command_args_permutation);
                    permutations.push(command.into_iter().flatten().collect())
                }
            }

            permutations
        }
    }

    use turborepo_types::{DryRunMode, EnvMode, LogOrder, OutputLogsMode};

    use crate::cli::{Args, Command, LogPrefix};

    #[test_case::test_case(
        &["turbo", "run", "build"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "default case"
    )]
    #[test_case::test_case(
        &["turbo", "run", "build"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    framework_inference: true,
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "framework_inference: default to true"
    )]
    #[test_case::test_case(
		&["turbo", "run", "build", "--framework-inference"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                     tasks: vec!["build".to_string()],
                     framework_inference: true,
                     ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "framework_inference: flag only"
    )]
    #[test_case::test_case(
		&["turbo", "run", "build", "--framework-inference", "true"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    framework_inference: true,
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
		} ;
        "framework_inference: flag set to true"
    )]
    #[test_case::test_case(
		&["turbo", "run", "build", "--framework-inference",
    "false"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    framework_inference: false,
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
		} ;
        "framework_inference: flag set to false"
	)]
    #[test_case::test_case(
        &["turbo", "run", "build", "--env-mode"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    env_mode: Some(EnvMode::Strict),
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "env_mode: not fully-specified"
    )]
    #[test_case::test_case(
		&["turbo", "run", "build", "--env-mode", "loose"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    env_mode: Some(EnvMode::Loose),
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
		} ;
        "env_mode: specified loose"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--env-mode", "strict"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    env_mode: Some(EnvMode::Strict),
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
		} ;
        "env_mode: specified strict"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "lint", "test"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string(), "lint".to_string(), "test".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "multiple tasks"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--cache-dir", "foobar"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    cache_dir: Some(Utf8PathBuf::from("foobar")),
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "cache dir"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--cache-workers", "100"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec ! ["build".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(RunArgs {
                    cache_workers: 100,
                    ..get_default_run_args()
                })
            }),
            ..Args::default()
        } ;
        "cache workers"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--concurrency", "20"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    concurrency: Some("20".to_string()),
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "concurrency"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--continue"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    continue_execution: ContinueMode::Always,
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "continue option with no value"
	)]
    #[test_case::test_case(
		&["turbo", "run", "--continue", "build"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    continue_execution: ContinueMode::Always,
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "continue option with no value before task"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--continue=dependencies-successful"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    continue_execution: ContinueMode::DependenciesSuccessful,
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "continue option with explicit value"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--dry-run"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(RunArgs {
                    dry_run: Some(DryRunMode::Text),
                    ..get_default_run_args()
                })
            }),
            ..Args::default()
        } ;
        "dry run"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--dry-run", "json"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(RunArgs {
                    dry_run: Some(DryRunMode::Json),
                    ..get_default_run_args()
                })
            }),
            ..Args::default()
        } ;
        "dry run json"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--filter", "water", "--filter", "earth", "--filter", "fire", "--filter", "air"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    filter: vec![
                        "water".to_string(),
                        "earth".to_string(),
                        "fire".to_string(),
                        "air".to_string()
                    ],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "multiple filters"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "-F", "water", "-F", "earth", "-F", "fire", "-F", "air"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    filter: vec![
                        "water".to_string(),
                        "earth".to_string(),
                        "fire".to_string(),
                        "air".to_string()
                    ],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "multiple filters short"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--filter", "water", "-F", "earth", "--filter", "fire", "-F", "air"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    filter: vec![
                        "water".to_string(),
                        "earth".to_string(),
                        "fire".to_string(),
                        "air".to_string()
                    ],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "multiple filters short and long"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--force"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(RunArgs {
                    force: Some(Some(true)),
                    ..get_default_run_args()
                })
            }),
            ..Args::default()
        } ;
        "force"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--global-deps", ".env"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    global_deps: vec![".env".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "global deps"
	)]
    #[test_case::test_case(
		&[ "turbo", "run", "build", "--global-deps", ".env", "--global-deps", ".env.development"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    global_deps: vec![".env".to_string(), ".env.development".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "multiple global deps"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--graph"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(RunArgs {
                    graph: Some("".to_string()),
                    ..get_default_run_args()
                })
            }),
            ..Args::default()
        } ;
        "graph"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--graph", "out.html"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(RunArgs {
                    graph: Some("out.html".to_string()),
                    ..get_default_run_args()
                })
            }),
            ..Args::default()
        } ;
        "graph with output"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--no-cache"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(RunArgs {
                    no_cache: true,
                    ..get_default_run_args()
                })
            }),
            ..Args::default()
        } ;
        "no cache"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--no-daemon"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(RunArgs {
                    no_daemon: true,
                    ..get_default_run_args()
                })
            }),
            ..Args::default()
        } ;
        "no daemon"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--daemon"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(RunArgs {
                    daemon: true,
                    ..get_default_run_args()
                })
            }),
            ..Args::default()
        } ;
        "daemon"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--output-logs", "full"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    output_logs: Some(OutputLogsMode::Full),
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "output logs full"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--output-logs", "none"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    output_logs: Some(OutputLogsMode::None),
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "output logs none"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--output-logs", "hash-only"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    output_logs: Some(OutputLogsMode::HashOnly),
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "output logs hash only"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--log-order", "stream"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    log_order: Some(LogOrder::Stream),
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "log order stream"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--log-order", "grouped"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    log_order: Some(LogOrder::Grouped),
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        };
        "log order grouped"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--log-prefix", "auto"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    log_prefix: LogPrefix::Auto,
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "log prefix auto"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--log-prefix", "none"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    log_prefix: LogPrefix::None,
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "log prefix none"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--log-prefix", "task"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                     tasks: vec!["build".to_string()],
                     log_prefix: LogPrefix::Task,
                     ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "log prefix task"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(get_default_run_args())
            }),
            ..Args::default()
        } ;
        "just build"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--parallel"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(RunArgs {
                    parallel: true,
                    ..get_default_run_args()
                })
            }),
            ..Args::default()
        } ;
        "parallel"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--profile", "profile_out"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(RunArgs {
                  profile: Some("profile_out".to_string()),
                  ..get_default_run_args()
                })
            }),
            ..Args::default()
        } ;
        "profile"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--profile"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(RunArgs {
                  profile: Some(String::new()),
                  ..get_default_run_args()
                })
            }),
            ..Args::default()
        } ;
        "profile_no_value"
	)]
    // remote-only flag tests
    #[test_case::test_case(
		&["turbo", "run", "build"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(RunArgs {
                    remote_only: None,
                    ..get_default_run_args()
                })
            }),
            ..Args::default()
		} ;
        "remote_only default to false"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--remote-only"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(RunArgs {
                    remote_only: Some(Some(true)),
                    ..get_default_run_args()
                })
            }),
            ..Args::default()
		} ;
        "remote_only with no value, means true"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--remote-only", "true"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(RunArgs {
                    remote_only: Some(Some(true)),
                    ..get_default_run_args()
                })
            }),
            ..Args::default()
		} ;
        "remote_only=true works"
	)]
    #[test_case::test_case(
		&["turbo", "run", "build", "--remote-only", "false"],
        Args {
            command: Some(Command::Run {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    ..get_default_execution_args()
                }),
                run_args: Box::new(RunArgs {
                    remote_only: Some(Some(false)),
                    ..get_default_run_args()
                })
            }),
            ..Args::default()
		} ;
        "remote_only=false works"
	)]
    #[test_case::test_case(
		&["turbo", "build"],
        Args {
            execution_args: Some(ExecutionArgs {
                tasks: vec!["build".to_string()],
                ..get_default_execution_args()
            }),
            ..Args::default()
        } ;
        "build no run prefix"
    )]
    #[test_case::test_case(
    	&["turbo", "build", "lint", "test"],
        Args {
            execution_args: Some(ExecutionArgs {
                tasks: vec!["build".to_string(), "lint".to_string(), "test".to_string()],
                ..get_default_execution_args()
            }),
            ..Args::default()
        } ;
        "multiple tasks no run prefix"
    )]
    fn test_parse_run(args: &[&str], expected: Args) {
        assert_eq!(Args::try_parse_from(args).unwrap(), expected);
    }

    #[test_case::test_case(
        &["turbo", "watch", "build"],
        Args {
            command: Some(Command::Watch {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    ..get_default_execution_args()
                }),
                experimental_write_cache: false
            }),
            ..Args::default()
        };
        "default watch"
    )]
    #[test_case::test_case(
        &["turbo", "watch", "build", "--cache-dir", "foobar"],
        Args {
            command: Some(Command::Watch {
                execution_args: Box::new(ExecutionArgs {
                    tasks: vec!["build".to_string()],
                    cache_dir: Some(Utf8PathBuf::from("foobar")),
                    ..get_default_execution_args()
                }),
                experimental_write_cache: false
            }),
            ..Args::default()
        };
        "with cache-dir"
    )]
    #[test_case::test_case(
        &["turbo", "watch", "build", "lint", "check"],
        Args {
            command: Some(Command::Watch {
                execution_args: Box::new(ExecutionArgs {
                  tasks: vec!["build".to_string(), "lint".to_string(), "check".to_string()],
                  ..get_default_execution_args()
                }),
                experimental_write_cache: false
            }),
            ..Args::default()
        };
        "with multiple tasks"
    )]
    #[test_case::test_case(
        &["turbo", "watch", "build", "--experimental-write-cache"],
        Args {
            command: Some(Command::Watch {
                execution_args: Box::new(ExecutionArgs {
                  tasks: vec!["build".to_string()],
                  ..get_default_execution_args()
                }),
                experimental_write_cache: true
            }),
            ..Args::default()
        };
        "with experimental-write-cache"
    )]
    fn test_parse_watch(args: &[&str], expected: Args) {
        assert_eq!(Args::try_parse_from(args).unwrap(), expected);
    }

    #[test_case::test_case(
        &["turbo", "run", "build", "--daemon", "--no-daemon"],
        "cannot be used with '--no-daemon'" ;
        "daemon and no-daemon at the same time"
    )]
    #[test_case::test_case(
        &["turbo", "run", "build", "--since", "foo"],
        "unexpected argument '--since' found" ;
        "since without filter or scope"
    )]
    #[test_case::test_case(
        &["turbo", "run", "build", "--include-dependencies"],
        "unexpected argument '--include-dependencies' found" ;
        "include-dependencies without filter or scope"
    )]
    #[test_case::test_case(
        &["turbo", "run", "build", "--no-deps"],
        "unexpected argument '--no-deps' found" ;
        "no-deps without filter or scope"
    )]
    fn test_parse_run_failures(args: &[&str], expected: &str) {
        assert_matches!(
            Args::try_parse_from(args),
            Err(err) if err.to_string().contains(expected)
        );
    }

    #[test]
    fn test_parse_bin() {
        assert_eq!(
            Args::try_parse_from(["turbo", "bin"]).unwrap(),
            Args {
                command: Some(Command::Bin {}),
                ..Args::default()
            }
        );

        CommandTestCase {
            command: "bin",
            command_args: vec![],
            global_args: vec![vec!["--cwd", "../examples/with-yarn"]],
            expected_output: Args {
                command: Some(Command::Bin {}),
                cwd: Some(Utf8PathBuf::from("../examples/with-yarn")),
                ..Args::default()
            },
        }
        .test();
    }

    #[test]
    fn test_parse_link() {
        assert_eq!(
            Args::try_parse_from(["turbo", "link"]).unwrap(),
            Args {
                command: Some(Command::Link {
                    no_gitignore: false,
                    scope: None,
                    yes: false,
                }),
                ..Args::default()
            }
        );

        CommandTestCase {
            command: "link",
            command_args: vec![],
            global_args: vec![vec!["--cwd", "../examples/with-yarn"]],
            expected_output: Args {
                command: Some(Command::Link {
                    no_gitignore: false,
                    scope: None,
                    yes: false,
                }),
                cwd: Some(Utf8PathBuf::from("../examples/with-yarn")),
                ..Args::default()
            },
        }
        .test();

        CommandTestCase {
            command: "link",
            command_args: vec![vec!["--yes"]],
            global_args: vec![vec!["--cwd", "../examples/with-yarn"]],
            expected_output: Args {
                command: Some(Command::Link {
                    yes: true,
                    no_gitignore: false,
                    scope: None,
                }),
                cwd: Some(Utf8PathBuf::from("../examples/with-yarn")),
                ..Args::default()
            },
        }
        .test();

        CommandTestCase {
            command: "link",
            command_args: vec![vec!["--scope", "foo"]],
            global_args: vec![vec!["--cwd", "../examples/with-yarn"]],
            expected_output: Args {
                command: Some(Command::Link {
                    yes: false,
                    no_gitignore: false,
                    scope: Some("foo".to_string()),
                }),
                cwd: Some(Utf8PathBuf::from("../examples/with-yarn")),
                ..Args::default()
            },
        }
        .test();

        CommandTestCase {
            command: "link",
            command_args: vec![vec!["--no-gitignore"]],
            global_args: vec![vec!["--cwd", "../examples/with-yarn"]],
            expected_output: Args {
                command: Some(Command::Link {
                    yes: false,
                    no_gitignore: true,
                    scope: None,
                }),
                cwd: Some(Utf8PathBuf::from("../examples/with-yarn")),
                ..Args::default()
            },
        }
        .test();
    }

    #[test]
    fn test_parse_login() {
        assert_eq!(
            Args::try_parse_from(["turbo", "login"]).unwrap(),
            Args {
                command: Some(Command::Login {
                    sso_team: None,
                    force: false,
                    manual: false,
                }),
                ..Args::default()
            }
        );

        CommandTestCase {
            command: "login",
            command_args: vec![],
            global_args: vec![vec!["--cwd", "../examples/with-yarn"]],
            expected_output: Args {
                command: Some(Command::Login {
                    sso_team: None,
                    force: false,
                    manual: false,
                }),
                cwd: Some(Utf8PathBuf::from("../examples/with-yarn")),
                ..Args::default()
            },
        }
        .test();

        CommandTestCase {
            command: "login",
            command_args: vec![vec!["--sso-team", "my-team"]],
            global_args: vec![vec!["--cwd", "../examples/with-yarn"]],
            expected_output: Args {
                command: Some(Command::Login {
                    sso_team: Some("my-team".to_string()),
                    force: false,
                    manual: false,
                }),
                cwd: Some(Utf8PathBuf::from("../examples/with-yarn")),
                ..Args::default()
            },
        }
        .test();
    }

    #[test]
    fn test_parse_logout() {
        assert_eq!(
            Args::try_parse_from(["turbo", "logout"]).unwrap(),
            Args {
                command: Some(Command::Logout { invalidate: true }),
                ..Args::default()
            }
        );

        CommandTestCase {
            command: "logout",
            command_args: vec![],
            global_args: vec![vec!["--cwd", "../examples/with-yarn"]],
            expected_output: Args {
                command: Some(Command::Logout { invalidate: true }),
                cwd: Some(Utf8PathBuf::from("../examples/with-yarn")),
                ..Args::default()
            },
        }
        .test();

        assert_eq!(
            Args::try_parse_from(["turbo", "logout", "--invalidate=false"]).unwrap(),
            Args {
                command: Some(Command::Logout { invalidate: false }),
                ..Args::default()
            }
        );
    }

    #[test]
    fn test_parse_unlink() {
        assert_eq!(
            Args::try_parse_from(["turbo", "unlink"]).unwrap(),
            Args {
                command: Some(Command::Unlink),
                ..Args::default()
            }
        );

        CommandTestCase {
            command: "unlink",
            command_args: vec![],
            global_args: vec![vec!["--cwd", "../examples/with-yarn"]],
            expected_output: Args {
                command: Some(Command::Unlink),
                cwd: Some(Utf8PathBuf::from("../examples/with-yarn")),
                ..Args::default()
            },
        }
        .test();
    }

    #[test]
    fn test_parse_prune() {
        let default_prune = Command::Prune {
            scope: None,
            scope_arg: Some(vec!["foo".into()]),
            docker: false,
            output_dir: "out".to_string(),
            use_gitignore: None,
        };

        assert_eq!(
            Args::try_parse_from(["turbo", "prune", "foo"]).unwrap(),
            Args {
                command: Some(default_prune.clone()),
                ..Args::default()
            }
        );

        CommandTestCase {
            command: "prune",
            command_args: vec![vec!["foo"]],
            global_args: vec![vec!["--cwd", "../examples/with-yarn"]],
            expected_output: Args {
                command: Some(default_prune),
                cwd: Some(Utf8PathBuf::from("../examples/with-yarn")),
                ..Args::default()
            },
        }
        .test();

        assert_eq!(
            Args::try_parse_from(["turbo", "prune", "--scope", "bar"]).unwrap(),
            Args {
                command: Some(Command::Prune {
                    scope: Some(vec!["bar".to_string()]),
                    scope_arg: None,
                    docker: false,
                    output_dir: "out".to_string(),
                    use_gitignore: None,
                }),
                ..Args::default()
            }
        );

        assert_eq!(
            Args::try_parse_from(["turbo", "prune", "foo", "bar"]).unwrap(),
            Args {
                command: Some(Command::Prune {
                    scope: None,
                    scope_arg: Some(vec!["foo".to_string(), "bar".to_string()]),
                    docker: false,
                    output_dir: "out".to_string(),
                    use_gitignore: None,
                }),
                ..Args::default()
            }
        );

        assert_eq!(
            Args::try_parse_from(["turbo", "prune", "--docker", "foo"]).unwrap(),
            Args {
                command: Some(Command::Prune {
                    scope: None,
                    scope_arg: Some(vec!["foo".into()]),
                    docker: true,
                    output_dir: "out".to_string(),
                    use_gitignore: None,
                }),
                ..Args::default()
            }
        );

        assert_eq!(
            Args::try_parse_from(["turbo", "prune", "--out-dir", "dist", "foo"]).unwrap(),
            Args {
                command: Some(Command::Prune {
                    scope: None,
                    scope_arg: Some(vec!["foo".into()]),
                    docker: false,
                    output_dir: "dist".to_string(),
                    use_gitignore: None,
                }),
                ..Args::default()
            }
        );

        CommandTestCase {
            command: "prune",
            command_args: vec![vec!["foo"], vec!["--out-dir", "dist"], vec!["--docker"]],
            global_args: vec![],
            expected_output: Args {
                command: Some(Command::Prune {
                    scope: None,
                    scope_arg: Some(vec!["foo".into()]),
                    docker: true,
                    output_dir: "dist".to_string(),
                    use_gitignore: None,
                }),
                ..Args::default()
            },
        }
        .test();

        CommandTestCase {
            command: "prune",
            command_args: vec![vec!["foo"], vec!["--out-dir", "dist"], vec!["--docker"]],
            global_args: vec![vec!["--cwd", "../examples/with-yarn"]],
            expected_output: Args {
                command: Some(Command::Prune {
                    scope: None,
                    scope_arg: Some(vec!["foo".into()]),
                    docker: true,
                    output_dir: "dist".to_string(),
                    use_gitignore: None,
                }),
                cwd: Some(Utf8PathBuf::from("../examples/with-yarn")),
                ..Args::default()
            },
        }
        .test();

        CommandTestCase {
            command: "prune",
            command_args: vec![
                vec!["--out-dir", "dist"],
                vec!["--docker"],
                vec!["--scope", "foo"],
            ],
            global_args: vec![],
            expected_output: Args {
                command: Some(Command::Prune {
                    scope: Some(vec!["foo".to_string()]),
                    scope_arg: None,
                    docker: true,
                    output_dir: "dist".to_string(),
                    use_gitignore: None,
                }),
                ..Args::default()
            },
        }
        .test();

        CommandTestCase {
            command: "prune",
            command_args: vec![vec!["foo"], vec!["--use-gitignore"]],
            global_args: vec![],
            expected_output: Args {
                command: Some(Command::Prune {
                    scope: None,
                    scope_arg: Some(vec!["foo".to_string()]),
                    docker: false,
                    output_dir: "out".to_string(),
                    use_gitignore: Some(true),
                }),
                ..Args::default()
            },
        }
        .test();

        CommandTestCase {
            command: "prune",
            command_args: vec![vec!["foo"], vec!["--use-gitignore=true"]],
            global_args: vec![],
            expected_output: Args {
                command: Some(Command::Prune {
                    scope: None,
                    scope_arg: Some(vec!["foo".to_string()]),
                    docker: false,
                    output_dir: "out".to_string(),
                    use_gitignore: Some(true),
                }),
                ..Args::default()
            },
        }
        .test();

        CommandTestCase {
            command: "prune",
            command_args: vec![vec!["foo"], vec!["--use-gitignore=false"]],
            global_args: vec![],
            expected_output: Args {
                command: Some(Command::Prune {
                    scope: None,
                    scope_arg: Some(vec!["foo".to_string()]),
                    docker: false,
                    output_dir: "out".to_string(),
                    use_gitignore: Some(false),
                }),
                ..Args::default()
            },
        }
        .test();
    }

    #[test]
    fn test_pass_through_args() {
        assert_eq!(
            Args::try_parse_from(["turbo", "run", "build", "--", "--script-arg=42"]).unwrap(),
            Args {
                command: Some(Command::Run {
                    run_args: Box::new(RunArgs {
                        ..get_default_run_args()
                    }),
                    execution_args: Box::new(ExecutionArgs {
                        tasks: vec!["build".to_string()],
                        pass_through_args: vec!["--script-arg=42".to_string()],
                        ..get_default_execution_args()
                    }),
                }),
                ..Args::default()
            }
        );

        assert_eq!(
            Args::try_parse_from([
                "turbo",
                "run",
                "build",
                "--",
                "--script-arg=42",
                "--foo",
                "--bar",
                "bat"
            ])
            .unwrap(),
            Args {
                command: Some(Command::Run {
                    run_args: Box::new(RunArgs {
                        ..get_default_run_args()
                    }),
                    execution_args: Box::new(ExecutionArgs {
                        tasks: vec!["build".to_string()],
                        pass_through_args: vec![
                            "--script-arg=42".to_string(),
                            "--foo".to_string(),
                            "--bar".to_string(),
                            "bat".to_string()
                        ],
                        ..get_default_execution_args()
                    }),
                }),
                ..Args::default()
            }
        );
    }

    #[test]
    fn test_parse_prune_no_mixed_arg_and_flag() {
        assert!(Args::try_parse_from(["turbo", "prune", "foo", "--scope", "bar"]).is_err(),);
    }

    #[test]
    fn test_parse_gen() {
        let default_gen = Command::Generate {
            tag: None,
            generator_name: None,
            config: None,
            root: None,
            args: vec![],
            command: None,
        };

        assert_eq!(
            Args::try_parse_from(["turbo", "gen"]).unwrap(),
            Args {
                command: Some(default_gen.clone()),
                ..Args::default()
            }
        );

        assert_eq!(
            Args::try_parse_from([
                "turbo",
                "gen",
                "--args",
                "my long arg string",
                "my-second-arg"
            ])
            .unwrap(),
            Args {
                command: Some(Command::Generate {
                    tag: None,
                    generator_name: None,
                    config: None,
                    root: None,
                    args: vec![
                        "my long arg string".to_string(),
                        "my-second-arg".to_string()
                    ],
                    command: None,
                }),
                ..Args::default()
            }
        );

        assert_eq!(
            Args::try_parse_from([
                "turbo",
                "gen",
                "--tag",
                "canary",
                "--config",
                "~/custom-gen-config/gen",
                "my-generator"
            ])
            .unwrap(),
            Args {
                command: Some(Command::Generate {
                    tag: Some("canary".to_string()),
                    generator_name: Some("my-generator".to_string()),
                    config: Some("~/custom-gen-config/gen".to_string()),
                    root: None,
                    args: vec![],
                    command: None,
                }),
                ..Args::default()
            }
        );
    }

    #[test]
    fn test_gen_default_tag_is_not_latest() {
        let args = Args::try_parse_from(["turbo", "gen"]).unwrap();
        let tag = match args.command {
            Some(Command::Generate { tag, .. }) => tag,
            _ => panic!("expected Generate command"),
        };
        assert_eq!(tag, None, "default tag should be None, not \"latest\"");
    }

    #[test]
    fn test_gen_default_tag_resolves_to_current_version() {
        let args = Args::try_parse_from(["turbo", "gen"]).unwrap();
        let tag = match args.command {
            Some(Command::Generate { tag, .. }) => tag,
            _ => panic!("expected Generate command"),
        };
        let resolved = tag.unwrap_or_else(|| crate::get_version().to_string());
        assert_eq!(
            resolved,
            crate::get_version(),
            "gen tag should resolve to the turbo binary version, not \"latest\""
        );
    }

    #[test]
    fn test_gen_explicit_tag_is_preserved() {
        let args = Args::try_parse_from(["turbo", "gen", "--tag", "1.2.3"]).unwrap();
        let tag = match args.command {
            Some(Command::Generate { tag, .. }) => tag,
            _ => panic!("expected Generate command"),
        };
        assert_eq!(tag, Some("1.2.3".to_string()));
        let resolved = tag.unwrap_or_else(|| crate::get_version().to_string());
        assert_eq!(
            resolved, "1.2.3",
            "explicit --tag should override the default version"
        );
    }

    #[test]
    fn test_profile_usage() {
        // Without a filename, profile should still be accepted
        assert!(Args::try_parse_from(["turbo", "build", "--profile"]).is_ok());
        assert!(Args::try_parse_from(["turbo", "build", "--anon-profile"]).is_ok());
        // With a filename, profile should be accepted
        assert!(Args::try_parse_from(["turbo", "build", "--profile", "foo.json"]).is_ok());
        assert!(Args::try_parse_from(["turbo", "build", "--anon-profile", "foo.json"]).is_ok());
        // Both flags simultaneously should be rejected
        assert!(Args::try_parse_from([
            "turbo",
            "build",
            "--profile",
            "foo.json",
            "--anon-profile",
            "bar.json"
        ])
        .is_err());
    }

    #[test]
    fn test_profile_default_filename() {
        let run_args = RunArgs {
            profile: Some(String::new()),
            ..get_default_run_args()
        };
        let (file, include_args) = run_args.profile_file_and_include_args().unwrap();
        assert!(
            file.starts_with("profile."),
            "expected default profile filename, got: {file}"
        );
        assert!(include_args);

        let run_args = RunArgs {
            anon_profile: Some(String::new()),
            ..get_default_run_args()
        };
        let (file, include_args) = run_args.profile_file_and_include_args().unwrap();
        assert!(
            file.starts_with("profile."),
            "expected default profile filename, got: {file}"
        );
        assert!(!include_args);

        let run_args = RunArgs {
            profile: Some("custom.json".to_string()),
            ..get_default_run_args()
        };
        let (file, include_args) = run_args.profile_file_and_include_args().unwrap();
        assert_eq!(file, "custom.json");
        assert!(include_args);
    }

    #[test]
    fn test_empty_cache_dir() {
        assert!(Args::try_parse_from(["turbo", "build", "--cache-dir"]).is_err());
        assert!(Args::try_parse_from(["turbo", "build", "--cache-dir="]).is_err());
        assert!(Args::try_parse_from(["turbo", "build", "--cache-dir", ""]).is_err());
    }

    #[test]
    fn test_preflight() {
        assert!(!Args::try_parse_from(["turbo", "build",]).unwrap().preflight);
        assert!(
            Args::try_parse_from(["turbo", "build", "--preflight"])
                .unwrap()
                .preflight
        );
        assert!(Args::try_parse_from(["turbo", "build", "--preflight=true"]).is_err());
    }

    #[test]
    fn test_log_stream_tui_compatibility() {
        assert!(LogOrder::Auto.compatible_with_tui());
        assert!(!LogOrder::Stream.compatible_with_tui());
        assert!(!LogOrder::Grouped.compatible_with_tui());
    }

    #[test]
    fn test_dangerously_allow_no_package_manager() {
        assert!(
            !Args::try_parse_from(["turbo", "build",])
                .unwrap()
                .dangerously_disable_package_manager_check
        );
        assert!(
            Args::try_parse_from([
                "turbo",
                "build",
                "--dangerously-disable-package-manager-check"
            ])
            .unwrap()
            .dangerously_disable_package_manager_check
        );
    }

    #[test]
    fn test_affected_and_filter_can_be_combined() {
        assert!(
            Args::try_parse_from(["turbo", "run", "build", "--affected", "--filter", "foo"])
                .is_ok(),
        );
        assert!(Args::try_parse_from(["turbo", "build", "--affected", "--filter", "foo"]).is_ok(),);
        assert!(Args::try_parse_from(["turbo", "build", "--filter", "foo", "--affected"]).is_ok(),);
        assert!(Args::try_parse_from(["turbo", "ls", "--filter", "foo", "--affected"]).is_ok(),);
    }

    struct SinglePackageTestCase {
        args: &'static [&'static str],
        expected_is_single: bool,
        expected: &'static [&'static str],
    }

    impl SinglePackageTestCase {
        pub const fn new<const N: usize>(args: &'static [&'static str; N]) -> Self {
            let args = args.as_slice();
            Self {
                args,
                expected_is_single: false,
                expected: args,
            }
        }

        pub const fn expected<const N: usize>(
            mut self,
            expected: &'static [&'static str; N],
        ) -> Self {
            self.expected_is_single = true;
            self.expected = expected.as_slice();
            self
        }

        pub fn os_args(&self) -> Vec<OsString> {
            self.args.iter().map(|s| OsString::from(*s)).collect()
        }

        pub fn assert_actual(&self, actual: (bool, impl Iterator<Item = OsString>)) {
            let (is_single, args) = actual;
            assert_eq!(is_single, self.expected_is_single);
            assert_eq!(
                args.map(|s| s.to_str().unwrap().to_string())
                    .collect::<Vec<_>>()
                    .as_slice(),
                self.expected
            );
        }
    }

    const NO_SINGLE_PKG: SinglePackageTestCase =
        SinglePackageTestCase::new(&["turbo", "--version"]);
    const SINGLE_PKG_AFTER_PASS: SinglePackageTestCase =
        SinglePackageTestCase::new(&["turbo", "--", "--single-package"]);
    const SINGLE_PKG: SinglePackageTestCase =
        SinglePackageTestCase::new(&["turbo", "--single-package", "run", "build"])
            .expected(&["turbo", "run", "build"]);
    const SINGLE_PKG_BEFORE_AFTER: SinglePackageTestCase = SinglePackageTestCase::new(&[
        "turbo",
        "--single-package",
        "run",
        "build",
        "--",
        "--single-package",
    ])
    .expected(&["turbo", "run", "build", "--", "--single-package"]);

    #[test_case::test_case(NO_SINGLE_PKG)]
    #[test_case::test_case(SINGLE_PKG_AFTER_PASS)]
    #[test_case::test_case(SINGLE_PKG)]
    #[test_case::test_case(SINGLE_PKG_BEFORE_AFTER)]
    fn test_single_package_removal(test: SinglePackageTestCase) {
        let os_args = test.os_args();
        let actual = Args::remove_single_package(os_args);
        test.assert_actual(actual);
    }

    #[test]
    fn test_set_single_package() {
        let inferred_run = Args::parse(
            ["turbo", "--single-package", "build"]
                .iter()
                .map(|s| OsString::from(*s))
                .collect(),
        )
        .unwrap();
        let explicit_run = Args::parse(
            ["turbo", "run", "--single-package", "build"]
                .iter()
                .map(|s| OsString::from(*s))
                .collect(),
        )
        .unwrap();
        assert!(inferred_run
            .execution_args
            .as_ref()
            .is_some_and(|e| e.single_package));
        assert!(explicit_run
            .command
            .as_ref()
            .and_then(|cmd| if let Command::Run { execution_args, .. } = cmd {
                Some(execution_args.single_package)
            } else {
                None
            })
            .unwrap_or(false));

        let watch = Args::parse(
            ["turbo", "watch", "--single-package", "build"]
                .iter()
                .map(|s| OsString::from(*s))
                .collect(),
        )
        .unwrap();
        assert!(watch
            .command
            .as_ref()
            .and_then(|cmd| if let Command::Watch { execution_args, .. } = cmd {
                Some(execution_args.single_package)
            } else {
                None
            })
            .unwrap_or(false));
    }

    #[test_case::test_case(&["turbo", "watch", "build", "--no-daemon"]; "after watch")]
    #[test_case::test_case(&["turbo", "--no-daemon", "watch", "build"]; "before watch")]
    fn test_no_run_args_outside_of_run(args: &[&str]) {
        let os_args = args.iter().map(|s| OsString::from(*s)).collect();
        let err = Args::parse(os_args).unwrap_err();
        assert_snapshot!(args.join("-").as_str(), err);
    }

    #[test_case::test_case(&["turbo", "--filter=foo", "run", "build"], false; "execution args")]
    #[test_case::test_case(&["turbo", "--no-daemon", "run", "build"], false; "run args")]
    #[test_case::test_case(&["turbo", "build", "run"], true; "task")]
    #[test_case::test_case(&["turbo", "--filter=web", "watch", "build"], false; "execution before watch")]
    fn test_no_run_args_before_run(args: &[&str], is_okay: bool) {
        let os_args = args.iter().map(|s| OsString::from(*s)).collect();
        let cli = Args::parse(os_args);
        if is_okay {
            cli.unwrap();
        } else {
            let err = cli.unwrap_err();
            assert_snapshot!(args.join("-").as_str(), err);
        }
    }

    #[test_case::test_case(&["turbo", "--filter=foo", "boundaries"], false; "execution args")]
    #[test_case::test_case(&["turbo", "--no-daemon", "boundaries"], false; "run args")]
    fn test_no_run_args_before_boundaries(args: &[&str], is_okay: bool) {
        let os_args = args.iter().map(|s| OsString::from(*s)).collect();
        let cli = Args::parse(os_args);
        if is_okay {
            cli.unwrap();
        } else {
            let err = cli.unwrap_err();
            assert_snapshot!(args.join("-").as_str(), err);
        }
    }

    #[test_case::test_case(&["turbo", "boundaries"], true; "empty")]
    #[test_case::test_case(&["turbo", "boundaries", "--ignore"], true; "with ignore")]
    #[test_case::test_case(&["turbo", "boundaries", "--ignore=all"], true; "with ignore all")]
    #[test_case::test_case(&["turbo", "boundaries", "--ignore=prompt"], true; "with ignore prompt")]
    #[test_case::test_case(&["turbo", "boundaries", "--filter", "ui"], true; "with filter")]
    fn test_boundaries(args: &[&str], is_okay: bool) {
        let os_args = args.iter().map(|s| OsString::from(*s)).collect();
        let cli = Args::parse(os_args);
        if is_okay {
            cli.unwrap();
        } else {
            let err = cli.unwrap_err();
            assert_snapshot!(args.join("-").as_str(), err);
        }
    }

    #[test]
    fn test_query_affected_no_args() {
        let args = Args::try_parse_from(["turbo", "query", "affected"]).unwrap();
        assert_eq!(
            args.command,
            Some(Command::Query {
                subcommand: Some(super::QuerySubcommand::Affected(super::AffectedArgs {
                    packages: None,
                    tasks: None,
                    base: None,
                    head: None,
                    exit_code: false,
                })),
                query: None,
                variables: None,
                schema: false,
            })
        );
    }

    #[test]
    fn test_query_affected_bare_packages_flag() {
        let args = Args::try_parse_from(["turbo", "query", "affected", "--packages"]).unwrap();
        assert_matches!(
            args.command,
            Some(Command::Query {
                subcommand: Some(super::QuerySubcommand::Affected(ref a)),
                ..
            }) if a.packages == Some(vec![])
        );
    }

    #[test]
    fn test_query_affected_with_packages() {
        let args =
            Args::try_parse_from(["turbo", "query", "affected", "--packages", "web"]).unwrap();
        assert_matches!(
            args.command,
            Some(Command::Query {
                subcommand: Some(super::QuerySubcommand::Affected(ref a)),
                ..
            }) if a.packages == Some(vec!["web".to_string()])
        );
    }

    #[test]
    fn test_query_affected_with_multiple_packages() {
        let args =
            Args::try_parse_from(["turbo", "query", "affected", "--packages", "web", "docs"])
                .unwrap();
        assert_matches!(
            args.command,
            Some(Command::Query {
                subcommand: Some(super::QuerySubcommand::Affected(ref a)),
                ..
            }) if a.packages == Some(vec!["web".to_string(), "docs".to_string()])
        );
    }

    #[test]
    fn test_query_affected_bare_tasks_flag() {
        let args = Args::try_parse_from(["turbo", "query", "affected", "--tasks"]).unwrap();
        assert_matches!(
            args.command,
            Some(Command::Query {
                subcommand: Some(super::QuerySubcommand::Affected(ref a)),
                ..
            }) if a.tasks == Some(vec![])
        );
    }

    #[test]
    fn test_query_affected_with_tasks() {
        let args =
            Args::try_parse_from(["turbo", "query", "affected", "--tasks", "build"]).unwrap();
        assert_matches!(
            args.command,
            Some(Command::Query {
                subcommand: Some(super::QuerySubcommand::Affected(ref a)),
                ..
            }) if a.tasks == Some(vec!["build".to_string()])
        );
    }

    #[test]
    fn test_query_affected_with_base_head() {
        let args = Args::try_parse_from([
            "turbo", "query", "affected", "--base", "main", "--head", "HEAD",
        ])
        .unwrap();
        assert_matches!(
            args.command,
            Some(Command::Query {
                subcommand: Some(super::QuerySubcommand::Affected(ref a)),
                ..
            }) if a.base == Some("main".to_string()) && a.head == Some("HEAD".to_string())
        );
    }

    #[test]
    fn test_query_affected_combined_packages_and_tasks() {
        let args = Args::try_parse_from([
            "turbo",
            "query",
            "affected",
            "--packages",
            "web",
            "--tasks",
            "build",
        ])
        .unwrap();
        assert_matches!(
            args.command,
            Some(Command::Query {
                subcommand: Some(super::QuerySubcommand::Affected(ref a)),
                ..
            }) if a.packages == Some(vec!["web".to_string()])
                && a.tasks == Some(vec!["build".to_string()])
        );
    }

    #[test]
    fn test_query_affected_combined_bare_packages_and_bare_tasks() {
        let args =
            Args::try_parse_from(["turbo", "query", "affected", "--packages", "--tasks"]).unwrap();
        assert_matches!(
            args.command,
            Some(Command::Query {
                subcommand: Some(super::QuerySubcommand::Affected(ref a)),
                ..
            }) if a.packages == Some(vec![]) && a.tasks == Some(vec![])
        );
    }

    #[test]
    fn test_query_affected_exit_code_flag() {
        let args = Args::try_parse_from(["turbo", "query", "affected", "--exit-code"]).unwrap();
        assert_matches!(
            args.command,
            Some(Command::Query {
                subcommand: Some(super::QuerySubcommand::Affected(ref a)),
                ..
            }) if a.exit_code
        );
    }

    #[test]
    fn test_query_raw_graphql_still_works() {
        let args =
            Args::try_parse_from(["turbo", "query", "{ packages { items { name } } }"]).unwrap();
        assert_eq!(
            args.command,
            Some(Command::Query {
                subcommand: None,
                query: Some("{ packages { items { name } } }".to_string()),
                variables: None,
                schema: false,
            })
        );
    }

    #[test]
    fn test_query_schema_still_works() {
        let args = Args::try_parse_from(["turbo", "query", "--schema"]).unwrap();
        assert_eq!(
            args.command,
            Some(Command::Query {
                subcommand: None,
                query: None,
                variables: None,
                schema: true,
            })
        );
    }
}
