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)
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 fn execute_run(&self, profile_name:&str, config:&LandConfig, dry_run:bool) -> Result<(), String> {
244 let resolved_profile = resolve_profile_name(profile_name, config);
246
247 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(&resolved_profile, profile);
258
259 let env_vars = resolve_environment_dual_path(profile, config, self.merge_env, &self.env_override);
261
262 let env_vars = apply_overrides(
264 env_vars,
265 &self.workbench,
266 &self.node_version,
267 &self.environment,
268 &self.dependency,
269 );
270
271 if self.verbose || dry_run {
273 print_resolved_environment(&env_vars);
274 }
275
276 if dry_run {
278 println!("\n{}", "Dry run complete. No changes made.");
279
280 return Ok(());
281 }
282
283 execute_run_command(&resolved_profile, config, &env_vars, &self.run_args)
285 }
286
287 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 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 debug_profiles.sort_by_key(|(k, _)| k.as_str());
304
305 release_profiles.sort_by_key(|(k, _)| k.as_str());
306
307 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 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 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 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 if let Some(desc) = &profile.description {
394 println!("Description: {}", desc);
395 }
396
397 if let Some(workbench) = &profile.workbench {
399 println!("\nWorkbench:");
400
401 println!(" Type: {}", workbench);
402 }
403
404 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 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 if let Some(script) = &profile.rhai_script {
434 println!("\nRhai Script: {}", script);
435 }
436
437 println!();
438
439 Ok(())
440 }
441
442 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 if profile.description.is_none() {
461 warnings.push("Profile has no description".to_string());
462 }
463
464 if profile.workbench.is_none() {
466 issues.push("Profile has no workbench type specified".to_string());
467 }
468
469 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 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 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 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 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 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
548fn 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
569fn 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
584fn 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
599fn 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
610fn 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 if let Some(templates) = &config.templates {
645 for (key, value) in &templates.env {
646 env.insert(key.clone(), value.clone());
647 }
648 }
649
650 if merge_env {
652 for (key, value) in std::env::vars() {
653 if is_run_env_var(&key) {
656 env.insert(key, value);
657 }
658 }
659 }
660
661 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 for (key, value) in cli_overrides {
670 env.insert(key.clone(), value.clone());
671 }
672
673 env
674}
675
676fn 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
700fn 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
715fn 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 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 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
758fn 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 let is_debug = profile_name.starts_with("debug");
788
789 let run_command = if is_debug { "pnpm tauri dev" } else { "pnpm dev" };
792
793 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 let (shell_cmd, args) = cmd_args.split_first().ok_or("Empty command")?;
812
813 let mut cmd = StdCommand::new(shell_cmd);
815
816 cmd.args(args);
817
818 cmd.envs(env_vars.iter());
819
820 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}