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