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 Json,
180 Env,
181}
182
183impl std::fmt::Display for OutputFormat {
184 fn fmt(&self, f:&mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 match self {
186 OutputFormat::Table => write!(f, "table"),
187 OutputFormat::Json => write!(f, "json"),
188 OutputFormat::Env => write!(f, "env"),
189 }
190 }
191}
192
193impl Cli {
198 pub fn execute(&self) -> Result<(), String> {
200 let config_path = self.config.clone().unwrap_or_else(|| PathBuf::from(".vscode/land-config.json"));
201
202 let config = load_config(&config_path).map_err(|e| format!("Failed to load configuration: {}", e))?;
204
205 if let Some(command) = &self.command {
207 return self.execute_command(command, &config);
208 }
209
210 if let Some(profile_name) = &self.profile {
212 return self.execute_run(profile_name, &config, self.dry_run);
213 }
214
215 Err("No command specified. Use --profile <name> to run or --help for usage.".to_string())
217 }
218
219 fn execute_command(&self, command:&Commands, config:&LandConfig) -> Result<(), String> {
221 match command {
222 Commands::Run { profile, hot_reload, dry_run } => {
223 let _ = hot_reload; self.execute_run(profile, config, *dry_run)
225 },
226 Commands::ListProfiles { verbose } => self.execute_list_profiles(config, *verbose),
227 Commands::ShowProfile { profile } => self.execute_show_profile(profile, config),
228 Commands::ValidateProfile { profile } => self.execute_validate_profile(profile, config),
229 Commands::Resolve { profile, format } => self.execute_resolve(profile, config, Some(format.to_string())),
230 }
231 }
232
233 fn execute_run(&self, profile_name:&str, config:&LandConfig, dry_run:bool) -> Result<(), String> {
235 let resolved_profile = resolve_profile_name(profile_name, config);
237
238 let profile = config.profiles.get(&resolved_profile).ok_or_else(|| {
240 format!(
241 "Profile '{}' not found. Available profiles: {}",
242 resolved_profile,
243 config.profiles.keys().cloned().collect::<Vec<_>>().join(", ")
244 )
245 })?;
246
247 print_run_header(&resolved_profile, profile);
249
250 let env_vars = resolve_environment_dual_path(profile, config, self.merge_env, &self.env_override);
252
253 let env_vars = apply_overrides(
255 env_vars,
256 &self.workbench,
257 &self.node_version,
258 &self.environment,
259 &self.dependency,
260 );
261
262 if self.verbose || dry_run {
264 print_resolved_environment(&env_vars);
265 }
266
267 if dry_run {
269 println!("\n{}", "Dry run complete. No changes made.");
270 return Ok(());
271 }
272
273 execute_run_command(&resolved_profile, config, &env_vars, &self.run_args)
275 }
276
277 fn execute_list_profiles(&self, config:&LandConfig, verbose:bool) -> Result<(), String> {
279 println!("\n{}", "Land Run System - Available Profiles");
280 println!("{}\n", "=".repeat(50));
281
282 let mut debug_profiles:Vec<_> = config.profiles.iter().filter(|(k, _)| k.starts_with("debug")).collect();
284 let mut release_profiles:Vec<_> = config
285 .profiles
286 .iter()
287 .filter(|(k, _)| k.starts_with("production") || k.starts_with("release") || k.starts_with("web"))
288 .collect();
289
290 debug_profiles.sort_by_key(|(k, _)| k.as_str());
292 release_profiles.sort_by_key(|(k, _)| k.as_str());
293
294 println!("{}:", "Debug Profiles".yellow());
296 println!();
297 for (name, profile) in &debug_profiles {
298 let default_profile = config
299 .cli
300 .as_ref()
301 .and_then(|cli| cli.default_profile.as_ref())
302 .map(|s| s.as_str())
303 .unwrap_or("");
304 let recommended = default_profile == name.as_str();
305 let marker = if recommended { " [RECOMMENDED]" } else { "" };
306 println!(
307 " {:<20} - {}{}",
308 name.green(),
309 profile.description.as_ref().map(|d| d.as_str()).unwrap_or("No description"),
310 marker.bright_magenta()
311 );
312 if verbose {
313 if let Some(workbench) = &profile.workbench {
314 println!(" Workbench: {}", workbench);
315 }
316 if let Some(features) = &profile.features {
317 for (feature, enabled) in features {
318 let status = if *enabled { "[X]" } else { "[ ]" };
319 println!(" {:>20} {} = {}", feature.cyan(), status, enabled);
320 }
321 }
322 }
323 }
324
325 println!("\n{}:", "Release Profiles".yellow());
327 for (name, profile) in &release_profiles {
328 println!(
329 " {:<20} - {}",
330 name.green(),
331 profile.description.as_ref().map(|d| d.as_str()).unwrap_or("No description")
332 );
333 if verbose {
334 if let Some(workbench) = &profile.workbench {
335 println!(" Workbench: {}", workbench);
336 }
337 }
338 }
339
340 if let Some(cli_config) = &config.cli {
342 if !cli_config.profile_aliases.is_empty() {
343 println!("\n{}:", "Profile Aliases");
344 for (alias, target) in &cli_config.profile_aliases {
345 println!(" {:<10} -> {}", alias.cyan(), target);
346 }
347 }
348 }
349
350 println!();
351 Ok(())
352 }
353
354 fn execute_show_profile(&self, profile_name:&str, config:&LandConfig) -> Result<(), String> {
356 let resolved_profile = resolve_profile_name(profile_name, config);
357
358 let profile = config
359 .profiles
360 .get(&resolved_profile)
361 .ok_or_else(|| format!("Profile '{}' not found.", resolved_profile))?;
362
363 println!("\n{}: {}", "Profile:", resolved_profile.green());
364 println!("{}\n", "=".repeat(50));
365
366 if let Some(desc) = &profile.description {
368 println!("Description: {}", desc);
369 }
370
371 if let Some(workbench) = &profile.workbench {
373 println!("\nWorkbench:");
374 println!(" Type: {}", workbench);
375 }
376
377 println!("\nEnvironment Variables:");
379 if let Some(env) = &profile.env {
380 let mut sorted_env:Vec<_> = env.iter().collect();
381 sorted_env.sort_by_key(|(k, _)| k.as_str());
382 for (key, value) in sorted_env {
383 println!(" {:<25} = {}", key, value);
384 }
385 }
386
387 if let Some(features) = &profile.features {
389 println!("\nFeatures:");
390 println!("\n Enabled:");
391 let mut sorted_features:Vec<_> = features.iter().filter(|(_, enabled)| **enabled).collect();
392 sorted_features.sort_by_key(|(k, _)| k.as_str());
393 for (feature, _) in &sorted_features {
394 println!(" {:<30}", feature.green());
395 }
396 }
397
398 if let Some(script) = &profile.rhai_script {
400 println!("\nRhai Script: {}", script);
401 }
402
403 println!();
404 Ok(())
405 }
406
407 fn execute_validate_profile(&self, profile_name:&str, config:&LandConfig) -> Result<(), String> {
409 let resolved_profile = resolve_profile_name(profile_name, config);
410
411 let profile = config
412 .profiles
413 .get(&resolved_profile)
414 .ok_or_else(|| format!("Profile '{}' not found.", resolved_profile))?;
415
416 println!("\n{}: {}", "Validating Profile:", resolved_profile.green());
417 println!("{}\n", "=".repeat(50));
418
419 let mut issues = Vec::new();
420 let mut warnings = Vec::new();
421
422 if profile.description.is_none() {
424 warnings.push("Profile has no description".to_string());
425 }
426
427 if profile.workbench.is_none() {
429 issues.push("Profile has no workbench type specified".to_string());
430 }
431
432 if profile.env.is_none() || profile.env.as_ref().unwrap().is_empty() {
434 warnings.push("Profile has no environment variables defined".to_string());
435 }
436
437 if issues.is_empty() && warnings.is_empty() {
439 println!("{}", "Profile is valid!".green());
440 } else {
441 if !warnings.is_empty() {
442 println!("\n{} Warnings:", warnings.len().to_string().yellow());
443 for warning in &warnings {
444 println!(" - {}", warning.yellow());
445 }
446 }
447 if !issues.is_empty() {
448 println!("\n{} Issues:", issues.len().to_string().red());
449 for issue in &issues {
450 println!(" - {}", issue.red());
451 }
452 }
453 }
454
455 println!();
456 Ok(())
457 }
458
459 fn execute_resolve(&self, profile_name:&str, config:&LandConfig, _format:Option<String>) -> Result<(), String> {
461 let resolved_profile = resolve_profile_name(profile_name, config);
462
463 let profile = config
464 .profiles
465 .get(&resolved_profile)
466 .ok_or_else(|| format!("Profile '{}' not found.", resolved_profile))?;
467
468 println!("\n{}: {}", "Resolved Profile:", resolved_profile.green());
469 println!("{}\n", "=".repeat(50));
470
471 if let Some(desc) = &profile.description {
473 println!("Description: {}", desc);
474 }
475
476 if let Some(workbench) = &profile.workbench {
477 println!("Workbench: {}", workbench);
478 }
479
480 if let Some(env) = &profile.env {
482 println!("\nEnvironment Variables ({}):", env.len());
483 for (key, value) in env {
484 println!(" {} = {}", key.green(), value);
485 }
486 }
487
488 if let Some(features) = &profile.features {
490 println!("\nFeatures ({}):", features.len());
491 for (feature, enabled) in features {
492 let status = if *enabled { "[X]" } else { "[ ]" };
493 println!(" {} {}", status, feature);
494 }
495 }
496
497 println!();
498 Ok(())
499 }
500}
501
502fn print_run_header(profile_name:&str, profile:&Profile) {
508 println!("\n{}", "========================================");
509 println!("Land Run: {}", profile_name);
510 println!("========================================");
511
512 if let Some(desc) = &profile.description {
513 println!("Description: {}", desc);
514 }
515
516 if let Some(workbench) = &profile.workbench {
517 println!("Workbench: {}", workbench);
518 }
519}
520
521fn print_resolved_environment(env:&HashMap<String, String>) {
523 println!("\nResolved Environment:");
524 let mut sorted_env:Vec<_> = env.iter().collect();
525 sorted_env.sort_by_key(|(k, _)| k.as_str());
526
527 for (key, value) in sorted_env {
528 let display_value = if value.is_empty() { "(empty)" } else { value };
529 println!(" {:<25} = {}", key, display_value);
530 }
531}
532
533fn parse_profile_name(s:&str) -> Result<String, String> {
535 let name = s.trim().to_lowercase();
536
537 if name.is_empty() {
538 return Err("Profile name cannot be empty".to_string());
539 }
540
541 if name.contains(' ') {
542 return Err("Profile name cannot contain spaces".to_string());
543 }
544
545 Ok(name)
546}
547
548fn resolve_profile_name(name:&str, config:&LandConfig) -> String {
550 if let Some(cli_config) = &config.cli {
551 if let Some(resolved) = cli_config.profile_aliases.get(name) {
552 return resolved.clone();
553 }
554 }
555 name.to_string()
556}
557
558fn resolve_environment_dual_path(
581 profile:&Profile,
582 config:&LandConfig,
583 merge_env:bool,
584 cli_overrides:&[(String, String)],
585) -> HashMap<String, String> {
586 let mut env = HashMap::new();
587
588 if let Some(templates) = &config.templates {
590 for (key, value) in &templates.env {
591 env.insert(key.clone(), value.clone());
592 }
593 }
594
595 if merge_env {
597 for (key, value) in std::env::vars() {
598 if is_run_env_var(&key) {
601 env.insert(key, value);
602 }
603 }
604 }
605
606 if let Some(profile_env) = &profile.env {
608 for (key, value) in profile_env {
609 env.insert(key.clone(), value.clone());
610 }
611 }
612
613 for (key, value) in cli_overrides {
615 env.insert(key.clone(), value.clone());
616 }
617
618 env
619}
620
621fn is_run_env_var(key:&str) -> bool {
623 matches!(
624 key,
625 "Browser"
626 | "Bundle"
627 | "Clean" | "Compile"
628 | "Debug" | "Dependency"
629 | "Mountain"
630 | "Wind" | "Electron"
631 | "BrowserProxy"
632 | "NODE_ENV"
633 | "NODE_VERSION"
634 | "NODE_OPTIONS"
635 | "RUST_LOG"
636 | "AIR_LOG_JSON"
637 | "AIR_LOG_FILE"
638 | "Level" | "Name"
639 | "Prefix"
640 | "HOT_RELOAD"
641 | "WATCH"
642 )
643}
644
645fn parse_key_val<K, V>(s:&str) -> Result<(K, V), String>
647where
648 K: std::str::FromStr,
649 V: std::str::FromStr,
650 K::Err: std::fmt::Display,
651 V::Err: std::fmt::Display, {
652 let pos = s.find('=').ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
653 Ok((
654 s[..pos].parse().map_err(|e| format!("key parse error: {e}"))?,
655 s[pos + 1..].parse().map_err(|e| format!("value parse error: {e}"))?,
656 ))
657}
658
659fn apply_overrides(
661 mut env:HashMap<String, String>,
662 workbench:&Option<String>,
663 node_version:&Option<String>,
664 environment:&Option<String>,
665 dependency:&Option<String>,
666) -> HashMap<String, String> {
667 if let Some(workbench) = workbench {
668 env.remove("Browser");
670 env.remove("Wind");
671 env.remove("Mountain");
672 env.remove("Electron");
673 env.remove("BrowserProxy");
674
675 env.insert(workbench.clone(), "true".to_string());
677 }
678
679 if let Some(version) = node_version {
680 env.insert("NODE_VERSION".to_string(), version.clone());
681 }
682
683 if let Some(environment) = environment {
684 env.insert("NODE_ENV".to_string(), environment.clone());
685 }
686
687 if let Some(dependency) = dependency {
688 env.insert("Dependency".to_string(), dependency.clone());
689 }
690
691 env
692}
693
694fn execute_run_command(
712 profile_name:&str,
713 _config:&LandConfig,
714 env_vars:&HashMap<String, String>,
715 run_args:&[String],
716) -> Result<(), String> {
717 use std::process::Command as StdCommand;
718
719 let is_debug = profile_name.starts_with("debug");
721
722 let run_command = if is_debug { "pnpm tauri dev" } else { "pnpm dev" };
725
726 let mut cmd_args:Vec<String> = run_command.split_whitespace().map(|s| s.to_string()).collect();
728 cmd_args.extend(run_args.iter().cloned());
729
730 println!("Executing: {}", cmd_args.join(" "));
731 println!("With environment variables:");
732 for (key, value) in env_vars.iter().take(10) {
733 println!(" {}={}", key, value);
734 }
735 if env_vars.len() > 10 {
736 println!(" ... and {} more", env_vars.len() - 10);
737 }
738
739 let (shell_cmd, args) = cmd_args.split_first().ok_or("Empty command")?;
741
742 let mut cmd = StdCommand::new(shell_cmd);
744 cmd.args(args);
745 cmd.envs(env_vars.iter());
746
747 cmd.env("MAINTAIN_RUN_MODE", "true");
749
750 cmd.stderr(std::process::Stdio::inherit())
751 .stdout(std::process::Stdio::inherit());
752
753 let status = cmd
754 .status()
755 .map_err(|e| format!("Failed to execute run command ({}): {}", shell_cmd, e))?;
756
757 if status.success() {
758 println!("\n{}", "Run completed successfully!".green());
759 Ok(())
760 } else {
761 Err(format!("Run failed with exit code: {:?}", status.code()))
762 }
763}