1use 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#[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 #[clap(long, short = 'p', value_parser = parse_profile_name)]
71 pub profile:Option<String>,
72
73 #[clap(long, short = 'c', global = true)]
75 pub config:Option<PathBuf>,
76
77 #[clap(long, short = 'w', global = true)]
79 pub workbench:Option<String>,
80
81 #[clap(long, short = 'n', global = true)]
83 pub node_version:Option<String>,
84
85 #[clap(long, short = 'e', global = true)]
87 pub environment:Option<String>,
88
89 #[clap(long, short = 'd', global = true)]
91 pub dependency:Option<String>,
92
93 #[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 #[clap(long, global = true, default_value = "true")]
99 pub hot_reload:bool,
100
101 #[clap(long, global = true, default_value = "true")]
103 pub watch:bool,
104
105 #[clap(long, global = true, default_value = "3001")]
107 pub live_reload_port:u16,
108
109 #[clap(long, global = true)]
111 pub dry_run:bool,
112
113 #[clap(long, short = 'v', global = true)]
115 pub verbose:bool,
116
117 #[clap(long, default_value = "true", global = true)]
119 pub merge_env:bool,
120
121 #[clap(last = true)]
123 pub run_args:Vec<String>,
124}
125
126#[derive(Subcommand, Debug, Clone)]
128pub enum Commands {
129 Run {
131 #[clap(long, short = 'p', value_parser = parse_profile_name)]
133 profile:String,
134
135 #[clap(long, default_value = "true")]
137 hot_reload:bool,
138
139 #[clap(long)]
141 dry_run:bool,
142 },
143
144 ListProfiles {
146 #[clap(long, short = 'v')]
148 verbose:bool,
149 },
150
151 ShowProfile {
153 profile:String,
155 },
156
157 ValidateProfile {
159 profile:String,
161 },
162
163 Resolve {
165 #[clap(long, short = 'p')]
167 profile:String,
168
169 #[clap(long, short = 'f', default_value = "table")]
171 format:OutputFormat,
172 },
173}
174
175#[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
197impl Cli {
202 pub fn execute(&self) -> Result<(), String> {
204 let config_path = self.config.clone().unwrap_or_else(|| PathBuf::from(".vscode/land-config.json"));
205
206 let config = load_config(&config_path).map_err(|e| format!("Failed to load configuration: {}", e))?;
208
209 if let Some(command) = &self.command {
211 return self.execute_command(command, &config);
212 }
213
214 if let Some(profile_name) = &self.profile {
216 return self.execute_run(profile_name, &config, self.dry_run);
217 }
218
219 Err("No command specified. Use --profile <name> to run or --help for usage.".to_string())
221 }
222
223 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; 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 fn execute_run(&self, profile_name:&str, config:&LandConfig, dry_run:bool) -> Result<(), String> {
243 let resolved_profile = resolve_profile_name(profile_name, config);
245
246 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(&resolved_profile, profile);
257
258 let env_vars = resolve_environment_dual_path(profile, config, self.merge_env, &self.env_override);
260
261 let env_vars = apply_overrides(
263 env_vars,
264 &self.workbench,
265 &self.node_version,
266 &self.environment,
267 &self.dependency,
268 );
269
270 if self.verbose || dry_run {
272 print_resolved_environment(&env_vars);
273 }
274
275 if dry_run {
277 println!("\n{}", "Dry run complete. No changes made.");
278
279 return Ok(());
280 }
281
282 execute_run_command(&resolved_profile, config, &env_vars, &self.run_args)
284 }
285
286 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 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 debug_profiles.sort_by_key(|(k, _)| k.as_str());
303
304 release_profiles.sort_by_key(|(k, _)| k.as_str());
305
306 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 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 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 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 if let Some(desc) = &profile.description {
393 println!("Description: {}", desc);
394 }
395
396 if let Some(workbench) = &profile.workbench {
398 println!("\nWorkbench:");
399
400 println!(" Type: {}", workbench);
401 }
402
403 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 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 if let Some(script) = &profile.rhai_script {
433 println!("\nRhai Script: {}", script);
434 }
435
436 println!();
437
438 Ok(())
439 }
440
441 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 if profile.description.is_none() {
460 warnings.push("Profile has no description".to_string());
461 }
462
463 if profile.workbench.is_none() {
465 issues.push("Profile has no workbench type specified".to_string());
466 }
467
468 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 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 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 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 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 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
547fn 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
568fn 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
583fn 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
598fn 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
609fn 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 if let Some(templates) = &config.templates {
644 for (key, value) in &templates.env {
645 env.insert(key.clone(), value.clone());
646 }
647 }
648
649 if merge_env {
651 for (key, value) in std::env::vars() {
652 if is_run_env_var(&key) {
655 env.insert(key, value);
656 }
657 }
658 }
659
660 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 for (key, value) in cli_overrides {
669 env.insert(key.clone(), value.clone());
670 }
671
672 env
673}
674
675fn 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
699fn 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
714fn 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 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 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
757fn 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 let is_debug = profile_name.starts_with("debug");
787
788 let run_command = if is_debug { "pnpm tauri dev" } else { "pnpm dev" };
791
792 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 let (shell_cmd, args) = cmd_args.split_first().ok_or("Empty command")?;
811
812 let mut cmd = StdCommand::new(shell_cmd);
814
815 cmd.args(args);
816
817 cmd.envs(env_vars.iter());
818
819 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}