Maintain/Build/
CLI.rs

1//=============================================================================//
2// File Path: Element/Maintain/Source/Build/CLI/mod.rs
3//=============================================================================//
4// Module: CLI - Command Line Interface for Configuration-Based Builds
5//
6// This module provides the cargo-first CLI interface that enables triggering
7// builds directly with the Cargo utility instead of shell scripts.
8//
9// RESPONSIBILITIES:
10// ================
11//
12// Primary:
13// - Parse command-line arguments for profile-based builds
14// - Load and validate configuration from land-config.json
15// - Resolve environment variables from configuration
16// - Execute builds with resolved configuration
17//
18// Secondary:
19// - Provide utility commands (--list-profiles, --show-profile)
20// - Support dry-run mode for configuration preview
21// - Enable profile aliases for quick access
22//
23// USAGE:
24// ======
25//
26// Basic usage:
27// ```bash
28// cargo run --bin Maintain -- --profile debug-mountain
29// ```
30//
31// List profiles:
32// ```bash
33// cargo run --bin Maintain -- --list-profiles
34// ```
35//
36// Dry run:
37// ```bash
38// cargo run --bin Maintain -- --profile debug --dry-run
39// ```
40//
41//===================================================================================
42
43use clap::{Parser, Subcommand, ValueEnum};
44use colored::Colorize;
45use std::collections::HashMap;
46use std::path::PathBuf;
47
48use crate::Build::Rhai::{load_config, LandConfig, Profile};
49
50//=============================================================================
51// CLI Argument Definitions
52//=============================================================================
53
54/// Land Build System - Configuration-based builds via Cargo
55#[derive(Parser, Debug, Clone)]
56#[clap(
57    name = "maintain",
58    author,
59    version,
60    about = "Land Build System - Configuration-based builds",
61    long_about = "A configuration-driven build system that enables triggering builds \
62                  directly with Cargo instead of shell scripts. Reads configuration \
63                  from .vscode/land-config.json and supports multiple build profiles."
64)]
65pub struct Cli {
66    #[clap(subcommand)]
67    pub command: Option<Commands>,
68
69    /// Build profile to use (shortcut for 'build' subcommand)
70    #[clap(long, short = 'p', value_parser = parse_profile_name)]
71    pub profile: Option<String>,
72
73    /// Configuration file path (default: .vscode/land-config.json)
74    #[clap(long, short = 'c', global = true)]
75    pub config: Option<PathBuf>,
76
77    /// Override workbench type
78    #[clap(long, short = 'w', global = true)]
79    pub workbench: Option<String>,
80
81    /// Override Node.js version
82    #[clap(long, short = 'n', global = true)]
83    pub node_version: Option<String>,
84
85    /// Override Node.js environment
86    #[clap(long, short = 'e', global = true)]
87    pub environment: Option<String>,
88
89    /// Override dependency source
90    #[clap(long, short = 'd', global = true)]
91    pub dependency: Option<String>,
92
93    /// Override environment variables (key=value pairs)
94    #[clap(long = "env", value_parser = parse_key_val::<String, String>, global = true, action = clap::ArgAction::Append)]
95    pub env_override: Vec<(String, String)>,
96
97    /// Enable dry-run mode (show config without building)
98    #[clap(long, global = true)]
99    pub dry_run: bool,
100
101    /// Enable verbose output
102    #[clap(long, short = 'v', global = true)]
103    pub verbose: bool,
104
105    /// Merge with shell environment (default: true)
106    #[clap(long, default_value = "true", global = true)]
107    pub merge_env: bool,
108
109    /// Additional build arguments (passed through to build command)
110    #[clap(last = true)]
111    pub build_args: Vec<String>,
112}
113
114/// Available subcommands
115#[derive(Subcommand, Debug, Clone)]
116pub enum Commands {
117    /// Execute a build with the specified profile
118    Build {
119        /// Build profile to use
120        #[clap(long, short = 'p', value_parser = parse_profile_name)]
121        profile: String,
122
123        /// Enable dry-run mode
124        #[clap(long)]
125        dry_run: bool,
126    },
127
128    /// List all available build profiles
129    ListProfiles {
130        /// Show detailed information for each profile
131        #[clap(long, short = 'v')]
132        verbose: bool,
133    },
134
135    /// Show details for a specific profile
136    ShowProfile {
137        /// Profile name to show
138        profile: String,
139    },
140
141    /// Validate a build profile
142    ValidateProfile {
143        /// Profile name to validate
144        profile: String,
145    },
146
147    /// Show current environment variable resolution
148    Resolve {
149        /// Profile name to resolve
150        #[clap(long, short = 'p')]
151        profile: String,
152
153        /// Output format
154        #[clap(long, short = 'f', default_value = "table")]
155        format: OutputFormat,
156    },
157}
158
159/// Output format options
160#[derive(Debug, Clone, ValueEnum)]
161pub enum OutputFormat {
162    Table,
163    Json,
164    Env,
165}
166
167impl std::fmt::Display for OutputFormat {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        match self {
170            OutputFormat::Table => write!(f, "table"),
171            OutputFormat::Json => write!(f, "json"),
172            OutputFormat::Env => write!(f, "env"),
173        }
174    }
175}
176
177//=============================================================================
178// CLI Implementation
179//=============================================================================//
180
181impl Cli {
182    /// Execute the CLI command
183    pub fn execute(&self) -> Result<(), String> {
184        let config_path = self.config.clone().unwrap_or_else(|| {
185            PathBuf::from(".vscode/land-config.json")
186        });
187
188        // Load configuration
189        let config = load_config(&config_path)
190            .map_err(|e| format!("Failed to load configuration: {}", e))?;
191
192        // Handle subcommands
193        if let Some(command) = &self.command {
194            return self.execute_command(command, &config);
195        }
196
197        // Handle direct profile argument
198        if let Some(profile_name) = &self.profile {
199            return self.execute_build(profile_name, &config, self.dry_run);
200        }
201
202        // Default: show help
203        Err("No command specified. Use --profile <name> to build or --help for usage.".to_string())
204    }
205
206    /// Execute a subcommand
207    fn execute_command(&self, command: &Commands, config: &LandConfig) -> Result<(), String> {
208        match command {
209            Commands::Build { profile, dry_run } => {
210                self.execute_build(profile, config, *dry_run)
211            }
212            Commands::ListProfiles { verbose } => {
213                self.execute_list_profiles(config, *verbose)
214            }
215            Commands::ShowProfile { profile } => {
216                self.execute_show_profile(profile, config)
217            }
218            Commands::ValidateProfile { profile } => {
219                self.execute_validate_profile(profile, config)
220            }
221            Commands::Resolve { profile, format } => {
222                self.execute_resolve(profile, config, Some(format.to_string()))
223            }
224        }
225    }
226
227    /// Execute a build with the specified profile
228    fn execute_build(
229        &self,
230        profile_name: &str,
231        config: &LandConfig,
232        dry_run: bool,
233    ) -> Result<(), String> {
234        // Resolve profile name (handle aliases)
235        let resolved_profile = resolve_profile_name(profile_name, config);
236
237        // Get profile from config
238        let profile = config.profiles.get(&resolved_profile).ok_or_else(|| {
239            format!(
240                "Profile '{}' not found. Available profiles: {}",
241                resolved_profile,
242                config.profiles.keys().cloned().collect::<Vec<_>>().join(", ")
243            )
244        })?;
245
246        // Print build header
247        print_build_header(&resolved_profile, profile);
248
249        // Resolve environment variables with dual-path merge
250        let env_vars = resolve_environment_dual_path(
251            profile,
252            config,
253            self.merge_env,
254            &self.env_override,
255        );
256
257        // Apply CLI overrides for explicit flags
258        let env_vars = apply_overrides(
259            env_vars,
260            &self.workbench,
261            &self.node_version,
262            &self.environment,
263            &self.dependency,
264        );
265
266        // Print resolved configuration
267        if self.verbose || dry_run {
268            print_resolved_environment(&env_vars);
269        }
270
271        // Dry run: stop here
272        if dry_run {
273            println!("\n{}", "Dry run complete. No changes made.");
274            return Ok(());
275        }
276
277        // Execute build
278        execute_build_command(&resolved_profile, config, &env_vars, &self.build_args)
279    }
280
281    /// List all available profiles
282    fn execute_list_profiles(&self, config: &LandConfig, verbose: bool) -> Result<(), String> {
283        println!("\n{}", "Land Build System - Available Profiles");
284        println!("{}\n", "=".repeat(50));
285
286        // Group profiles by type
287        let mut debug_profiles: Vec<_> = config
288            .profiles
289            .iter()
290            .filter(|(k, _)| k.starts_with("debug"))
291            .collect();
292        let mut release_profiles: Vec<_> = config
293            .profiles
294            .iter()
295            .filter(|(k, _)| {
296                k.starts_with("production") || k.starts_with("release") || k.starts_with("web")
297            })
298            .collect();
299        let mut bundler_profiles: Vec<_> = config
300            .profiles
301            .iter()
302            .filter(|(k, _)| k.contains("bundler") || k.contains("swc") || k.contains("oxc"))
303            .collect();
304
305        // Sort profiles
306        debug_profiles.sort_by_key(|(k, _)| k.as_str());
307        release_profiles.sort_by_key(|(k, _)| k.as_str());
308        bundler_profiles.sort_by_key(|(k, _)| k.as_str());
309
310        // Print debug profiles
311        println!("{}:", "Debug Profiles".yellow());
312        println!();
313        for (name, profile) in &debug_profiles {
314            let default_profile = config.cli.as_ref()
315                .and_then(|cli| cli.default_profile.as_ref())
316                .map(|s| s.as_str())
317                .unwrap_or("");
318            let recommended = default_profile == name.as_str();
319            let marker = if recommended { " [RECOMMENDED]" } else { "" };
320            println!(
321                "  {:<20} - {}{}",
322                name.green(),
323                profile.description.as_ref().map(|d| d.as_str()).unwrap_or("No description"),
324                marker.bright_magenta()
325            );
326            if verbose {
327                if let Some(workbench) = &profile.workbench {
328                    println!("    Workbench: {}", workbench);
329                }
330                if let Some(features) = &profile.features {
331                    for (feature, enabled) in features {
332                            let status = if *enabled { "[X]" } else { "[ ]" };
333                            println!("    {:>20} {} = {}", feature.cyan(), status, enabled);
334                    }
335                }
336            }
337        }
338
339        // Print release profiles
340        println!("\n{}:", "Release Profiles");
341        for (name, profile) in &release_profiles {
342            println!(
343                "  {:<20} - {}",
344                name.green(),
345                profile.description.as_ref().map(|d| d.as_str()).unwrap_or("No description")
346            );
347            if verbose {
348                if let Some(workbench) = &profile.workbench {
349                    println!("    Workbench: {}", workbench);
350                }
351            }
352        }
353
354        // Print bundler profiles
355        if !bundler_profiles.is_empty() {
356            println!("\n{}:", "Bundler Profiles");
357            for (name, profile) in &bundler_profiles {
358                println!(
359                    "  {:<20} - {}",
360                    name.green(),
361                    profile.description.as_ref().map(|d| d.as_str()).unwrap_or("No description")
362                );
363            }
364        }
365
366        // Print CLI aliases if available
367        if let Some(cli_config) = &config.cli {
368            if !cli_config.profile_aliases.is_empty() {
369                println!("\n{}:", "Profile Aliases");
370                for (alias, target) in &cli_config.profile_aliases {
371                    println!("  {:<10} -> {}", alias.cyan(), target);
372                }
373            }
374        }
375
376        println!();
377        Ok(())
378    }
379
380    /// Show details for a specific profile
381    fn execute_show_profile(&self, profile_name: &str, config: &LandConfig) -> Result<(), String> {
382        let resolved_profile = resolve_profile_name(profile_name, config);
383
384        let profile = config.profiles.get(&resolved_profile).ok_or_else(|| {
385            format!("Profile '{}' not found.", resolved_profile)
386        })?;
387
388        println!("\n{}: {}", "Profile:", resolved_profile.green());
389        println!("{}\n", "=".repeat(50));
390
391        // Description
392        if let Some(desc) = &profile.description {
393            println!("Description: {}", desc);
394        }
395
396        // Workbench
397        if let Some(workbench) = &profile.workbench {
398            println!("\nWorkbench:");
399            println!("  Type: {}", workbench);
400
401            if let Some(wb_config) = &config.workbench {
402                if let Some(features) = &wb_config.features {
403                    if let Some(wb_features) = features.get(workbench) {
404                        if let Some(coverage) = &wb_features.coverage {
405                            println!("  Coverage: {}", coverage);
406                        }
407                        if let Some(complexity) = &wb_features.complexity {
408                            println!("  Complexity: {}", complexity);
409                        }
410                        if wb_features.polyfills.unwrap_or(false) {
411                            println!("  Polyfills: enabled");
412                        }
413                        if wb_features.mountain_providers.unwrap_or(false) {
414                            println!("  Mountain Providers: enabled");
415                        }
416                        if wb_features.wind_services.unwrap_or(false) {
417                            println!("  Wind Services: enabled");
418                        }
419                    }
420                }
421            }
422        }
423
424        // Environment Variables
425        println!("\nEnvironment Variables:");
426        if let Some(env) = &profile.env {
427            let mut sorted_env: Vec<_> = env.iter().collect();
428            sorted_env.sort_by_key(|(k, _)| k.as_str());
429            for (key, value) in sorted_env {
430                println!("  {:<25} = {}", key, value);
431            }
432        }
433
434        // Features
435        if let Some(features) = &profile.features {
436            println!("\nFeatures:");
437            println!("\n  Enabled:");
438            let mut sorted_features: Vec<_> = features.iter()
439                .filter(|(_, enabled)| **enabled)
440                .collect();
441            sorted_features.sort_by_key(|(k, _)| k.as_str());
442            for (feature, _) in &sorted_features {
443                println!("  {:<30}", feature.green());
444            }
445        }
446
447        // Build Command
448        let build_cmd = get_build_command(&resolved_profile, &config);
449        println!("\nBuild Command: {}", build_cmd);
450
451        // Rhai Script
452        if let Some(script) = &profile.rhai_script {
453            println!("\nRhai Script: {}", script);
454        }
455
456        println!();
457        Ok(())
458    }
459
460    /// Validate a profile's configuration
461    fn execute_validate_profile(&self, profile_name: &str, config: &LandConfig) -> Result<(), String> {
462        let resolved_profile = resolve_profile_name(profile_name, config);
463
464        let profile = config.profiles.get(&resolved_profile).ok_or_else(|| {
465            format!("Profile '{}' not found.", resolved_profile)
466        })?;
467
468        println!("\n{}: {}", "Validating Profile:", resolved_profile.green());
469        println!("{}\n", "=".repeat(50));
470
471        let mut issues = Vec::new();
472        let mut warnings = Vec::new();
473
474        // Check description
475        if profile.description.is_none() {
476            warnings.push("Profile has no description".to_string());
477        }
478
479        // Check workbench
480        if profile.workbench.is_none() {
481            issues.push("Profile has no workbench type specified".to_string());
482        } else if let Some(workbench) = &profile.workbench {
483            if let Some(wb_config) = &config.workbench {
484                if let Some(available) = &wb_config.available {
485                    if !available.contains(workbench) {
486                        issues.push(format!("Workbench '{}' not defined in workbench configuration", workbench));
487                    }
488                }
489            }
490        }
491
492        // Check environment variables
493        if profile.env.is_none() || profile.env.as_ref().unwrap().is_empty() {
494            warnings.push("Profile has no environment variables defined".to_string());
495        }
496
497        // Display results
498        if issues.is_empty() && warnings.is_empty() {
499            println!("{}", "Profile is valid!".green());
500        } else {
501            if !warnings.is_empty() {
502                println!("\n{} Warnings:", warnings.len().to_string().yellow());
503                for warning in &warnings {
504                    println!("  - {}", warning.yellow());
505                }
506            }
507            if !issues.is_empty() {
508                println!("\n{} Issues:", issues.len().to_string().red());
509                for issue in &issues {
510                    println!("  - {}", issue.red());
511                }
512            }
513        }
514
515        println!();
516        Ok(())
517    }
518
519    /// Resolve a profile to its resolved configuration
520    fn execute_resolve(&self, profile_name: &str, config: &LandConfig, _format: Option<String>) -> Result<(), String> {
521        let resolved_profile = resolve_profile_name(profile_name, config);
522
523        let profile = config.profiles.get(&resolved_profile).ok_or_else(|| {
524            format!("Profile '{}' not found.", resolved_profile)
525        })?;
526
527        println!("\n{}: {}", "Resolved Profile:", resolved_profile.green());
528        println!("{}\n", "=".repeat(50));
529
530        // Profile information
531        if let Some(desc) = &profile.description {
532            println!("Description: {}", desc);
533        }
534
535        if let Some(workbench) = &profile.workbench {
536            println!("Workbench: {}", workbench);
537        }
538
539        // Environment Variables
540        if let Some(env) = &profile.env {
541            println!("\nEnvironment Variables ({}):", env.len());
542            for (key, value) in env {
543                println!("  {} = {}", key.green(), value);
544            }
545        }
546
547        // Features
548        if let Some(features) = &profile.features {
549            println!("\nFeatures ({}):", features.len());
550            for (feature, enabled) in features {
551                let status = if *enabled { "[X]" } else { "[ ]" };
552                println!("  {} {}", status, feature);
553            }
554        }
555
556        println!();
557        Ok(())
558    }
559}
560
561//=============================================================================
562// Helper Functions (standalone functions, not methods)
563//=============================================================================
564
565/// Print build header
566fn print_build_header(profile_name: &str, profile: &Profile) {
567    println!("\n{}", "========================================");
568    println!("Land Build: {}", profile_name);
569    println!("========================================");
570
571    if let Some(desc) = &profile.description {
572        println!("Description: {}", desc);
573    }
574
575    if let Some(workbench) = &profile.workbench {
576        println!("Workbench: {}", workbench);
577    }
578}
579
580/// Print resolved environment variables
581fn print_resolved_environment(env: &HashMap<String, String>) {
582    println!("\nResolved Environment:");
583    let mut sorted_env: Vec<_> = env.iter().collect();
584    sorted_env.sort_by_key(|(k, _)| k.as_str());
585
586    for (key, value) in sorted_env {
587        let display_value = if value.is_empty() {
588            "(empty)"
589        } else {
590            value
591        };
592        println!("  {:<25} = {}", key, display_value);
593    }
594}
595
596/// Parse and validate profile name
597fn parse_profile_name(s: &str) -> Result<String, String> {
598    let name = s.trim().to_lowercase();
599
600    if name.is_empty() {
601        return Err("Profile name cannot be empty".to_string());
602    }
603
604    if name.contains(' ') {
605        return Err("Profile name cannot contain spaces".to_string());
606    }
607
608    Ok(name)
609}
610
611/// Resolve profile name (handle aliases)
612fn resolve_profile_name(name: &str, config: &LandConfig) -> String {
613    if let Some(cli_config) = &config.cli {
614        if let Some(resolved) = cli_config.profile_aliases.get(name) {
615            return resolved.clone();
616        }
617    }
618    name.to_string()
619}
620
621/// Get build command for a profile
622fn get_build_command(profile_name: &str, config: &LandConfig) -> String {
623    // Determine the command from NODE_ENV
624    let command_key = if profile_name.starts_with("production")
625        || profile_name.starts_with("release")
626        || profile_name.starts_with("web")
627    {
628        "production"
629    } else {
630        profile_name.split('-').next().unwrap_or("debug")
631    };
632
633    if let Some(build_commands) = &config.build_commands {
634        build_commands.get(command_key).cloned().unwrap_or_else(|| {
635            if profile_name.starts_with("production") {
636                "pnpm tauri build".to_string()
637            } else {
638                "pnpm tauri build --debug".to_string()
639            }
640        })
641    } else {
642        // Fallback command
643        if profile_name.starts_with("production") {
644            "pnpm tauri build".to_string()
645        } else {
646            "pnpm tauri build --debug".to_string()
647        }
648    }
649}
650
651/// Resolve environment variables with dual-path merging.
652///
653/// This function implements the dual-path environment resolution:
654/// - Path A: Shell environment variables (from process)
655/// - Path B: CLI profile configuration (from land-config.json)
656///
657/// Merge priority (lowest to highest):
658/// 1. Template defaults
659/// 2. Shell environment variables (if merge_env is true)
660/// 3. Profile environment variables
661/// 4. CLI --env overrides
662///
663/// # Arguments
664///
665/// * `profile` - The profile configuration
666/// * `config` - The land configuration
667/// * `merge_env` - Whether to merge with shell environment
668/// * `cli_overrides` - CLI --env override pairs
669///
670/// # Returns
671///
672/// Merged HashMap of environment variables
673fn resolve_environment_dual_path(
674    profile: &Profile,
675    config: &LandConfig,
676    merge_env: bool,
677    cli_overrides: &[(String, String)],
678) -> HashMap<String, String> {
679    let mut env = HashMap::new();
680
681    // Layer 1: Start with template defaults (lowest priority)
682    if let Some(templates) = &config.templates {
683        for (key, value) in &templates.env {
684            env.insert(key.clone(), value.clone());
685        }
686    }
687
688    // Layer 2: Merge shell environment variables (if enabled)
689    if merge_env {
690        for (key, value) in std::env::vars() {
691            // Only merge relevant environment variables
692            // that are part of our build system
693            if is_build_env_var(&key) {
694                env.insert(key, value);
695            }
696        }
697    }
698
699    // Layer 3: Apply profile environment (overrides shell)
700    if let Some(profile_env) = &profile.env {
701        for (key, value) in profile_env {
702            env.insert(key.clone(), value.clone());
703        }
704    }
705
706    // Layer 4: Apply CLI --env overrides (highest priority)
707    for (key, value) in cli_overrides {
708        env.insert(key.clone(), value.clone());
709    }
710
711    env
712}
713
714/// Check if an environment variable is a build system variable.
715fn is_build_env_var(key: &str) -> bool {
716    matches!(
717        key,
718        "Browser"
719            | "Bundle"
720            | "Clean"
721            | "Compile"
722            | "Debug"
723            | "Dependency"
724            | "Mountain"
725            | "Wind"
726            | "Electron"
727            | "BrowserProxy"
728            | "NODE_ENV"
729            | "NODE_VERSION"
730            | "NODE_OPTIONS"
731            | "RUST_LOG"
732            | "AIR_LOG_JSON"
733            | "AIR_LOG_FILE"
734            | "Level"
735            | "Name"
736            | "Prefix"
737    )
738}
739
740/// Parse a key=value pair from command line.
741fn parse_key_val<K, V>(s: &str) -> Result<(K, V), String>
742where
743    K: std::str::FromStr,
744    V: std::str::FromStr,
745    K::Err: std::fmt::Display,
746    V::Err: std::fmt::Display,
747{
748    let pos = s
749        .find('=')
750        .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
751    Ok((
752        s[..pos].parse().map_err(|e| format!("key parse error: {e}"))?,
753        s[pos + 1..].parse().map_err(|e| format!("value parse error: {e}"))?,
754    ))
755}
756
757/// Apply CLI overrides to environment
758fn apply_overrides(
759    mut env: HashMap<String, String>,
760    workbench: &Option<String>,
761    node_version: &Option<String>,
762    environment: &Option<String>,
763    dependency: &Option<String>,
764) -> HashMap<String, String> {
765    if let Some(workbench) = workbench {
766        // Clear all workbench flags
767        env.remove("Browser");
768        env.remove("Wind");
769        env.remove("Mountain");
770        env.remove("Electron");
771        env.remove("BrowserProxy");
772
773        // Set the selected workbench
774        env.insert(workbench.clone(), "true".to_string());
775    }
776
777    if let Some(version) = node_version {
778        env.insert("NODE_VERSION".to_string(), version.clone());
779    }
780
781    if let Some(environment) = environment {
782        env.insert("NODE_ENV".to_string(), environment.clone());
783    }
784
785    if let Some(dependency) = dependency {
786        env.insert("Dependency".to_string(), dependency.clone());
787    }
788
789    env
790}
791
792/// Execute the build command with dual-path environment injection.
793///
794/// This function:
795/// 1. Calls the Maintain binary in legacy mode with merged environment variables
796/// 2. The Maintain binary's Process() function generates the extensive product name
797/// 3. The Process() function updates tauri.conf.json with the generated name
798/// 4. The actual tauri build command is executed
799///
800/// # Arguments
801///
802/// * `profile_name` - The resolved profile name
803/// * `config` - Land configuration
804/// * `env_vars` - Merged environment variables from all sources
805/// * `build_args` - Additional build arguments
806///
807/// # Returns
808///
809/// Result indicating success or failure
810fn execute_build_command(
811    profile_name: &str,
812    config: &LandConfig,
813    env_vars: &HashMap<String, String>,
814    build_args: &[String],
815) -> Result<(), String> {
816    use std::process::Command as StdCommand;
817
818    // Get the build command from config
819    let build_command = get_build_command(profile_name, config);
820
821    // Determine if this is a debug build
822    let is_debug = build_command.to_lowercase().contains("--debug");
823
824    // Build the command arguments for the Maintain binary
825    // The Maintain binary expects: -- <build_command> [args...]
826    let mut maintain_args = vec!["--".to_string()];
827
828    // Add pnpm tauri build command
829    maintain_args.push("pnpm".to_string());
830    maintain_args.push("tauri".to_string());
831    maintain_args.push("build".to_string());
832
833    if is_debug {
834        maintain_args.push("--debug".to_string());
835    }
836
837    // Add any additional build arguments
838    maintain_args.extend(build_args.iter().cloned());
839
840    println!("Executing: {}", maintain_args.join(" "));
841    println!("With environment variables:");
842    for (key, value) in env_vars.iter().take(10) {
843        println!("  {}={}", key, value);
844    }
845    if env_vars.len() > 10 {
846        println!("  ... and {} more", env_vars.len() - 10);
847    }
848
849    // Get the path to the Maintain binary
850    // Try to find it in the target directory
851    let maintain_binary = find_maintain_binary();
852
853    // Execute the Maintain binary with merged environment variables
854    // The Maintain binary will:
855    // 1. Parse environment variables via clap (Argument struct)
856    // 2. Generate the extensive product name in Process()
857    // 3. Update tauri.conf.json with productName and identifier
858    // 4. Execute the actual build command
859    let mut cmd = StdCommand::new(&maintain_binary);
860    cmd.args(&maintain_args);
861
862    // Pass ALL resolved environment variables to the Maintain binary
863    // This is critical for the dual-path merge to work
864    cmd.envs(env_vars.iter());
865
866    // Also set the MERGED_ENV_INDICATOR to show that env was merged
867    cmd.env("MAINTAIN_CLI_MERGED", "true");
868
869    cmd.stderr(std::process::Stdio::inherit())
870        .stdout(std::process::Stdio::inherit());
871
872    let status = cmd.status()
873        .map_err(|e| format!("Failed to execute Maintain binary ({}): {}", maintain_binary, e))?;
874
875    if status.success() {
876        println!("\n{}", "Build completed successfully!".green());
877        Ok(())
878    } else {
879        Err(format!("Build failed with exit code: {:?}", status.code()))
880    }
881}
882
883/// Find the Maintain binary path.
884///
885/// Tries multiple locations:
886/// 1. ./Target/release/Maintain
887/// 2. ./Target/debug/Maintain
888/// 3. maintain (from PATH)
889/// 4. cargo run --bin Maintain (fallback)
890fn find_maintain_binary() -> String {
891    use std::path::Path;
892
893    // Try release build first
894    let release_path = "./Target/release/Maintain";
895    if Path::new(release_path).exists() {
896        return release_path.to_string();
897    }
898
899    // Try debug build
900    let debug_path = "./Target/debug/Maintain";
901    if Path::new(debug_path).exists() {
902        return debug_path.to_string();
903    }
904
905    // Fallback to "maintain" in PATH
906    "maintain".to_string()
907}
908
909/// List all available profiles (avoids Self)
910pub fn get_all_profiles(config: &LandConfig) -> Vec<&str> {
911    let mut profiles: Vec<&str> = config.profiles.keys().map(|s| s.as_str()).collect();
912    profiles.sort();
913    profiles
914}