Skip to main content

Maintain/Run/
CLI.rs

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