Skip to main content

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