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