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