1use 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#[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 #[clap(long, short = 'p', value_parser = parse_profile_name)]
72 pub profile: Option<String>,
73
74 #[clap(long, short = 'c', global = true)]
76 pub config: Option<PathBuf>,
77
78 #[clap(long, short = 'w', global = true)]
80 pub workbench: Option<String>,
81
82 #[clap(long, short = 'n', global = true)]
84 pub node_version: Option<String>,
85
86 #[clap(long, short = 'e', global = true)]
88 pub environment: Option<String>,
89
90 #[clap(long, short = 'd', global = true)]
92 pub dependency: Option<String>,
93
94 #[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 #[clap(long, global = true, default_value = "true")]
100 pub hot_reload: bool,
101
102 #[clap(long, global = true, default_value = "true")]
104 pub watch: bool,
105
106 #[clap(long, global = true, default_value = "3001")]
108 pub live_reload_port: u16,
109
110 #[clap(long, global = true)]
112 pub dry_run: bool,
113
114 #[clap(long, short = 'v', global = true)]
116 pub verbose: bool,
117
118 #[clap(long, default_value = "true", global = true)]
120 pub merge_env: bool,
121
122 #[clap(last = true)]
124 pub run_args: Vec<String>,
125}
126
127#[derive(Subcommand, Debug, Clone)]
129pub enum Commands {
130 Run {
132 #[clap(long, short = 'p', value_parser = parse_profile_name)]
134 profile: String,
135
136 #[clap(long, default_value = "true")]
138 hot_reload: bool,
139
140 #[clap(long)]
142 dry_run: bool,
143 },
144
145 ListProfiles {
147 #[clap(long, short = 'v')]
149 verbose: bool,
150 },
151
152 ShowProfile {
154 profile: String,
156 },
157
158 ValidateProfile {
160 profile: String,
162 },
163
164 Resolve {
166 #[clap(long, short = 'p')]
168 profile: String,
169
170 #[clap(long, short = 'f', default_value = "table")]
172 format: OutputFormat,
173 },
174}
175
176#[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
194impl Cli {
199 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 let config = load_config(&config_path)
207 .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 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 fn execute_run(
247 &self,
248 profile_name: &str,
249 config: &LandConfig,
250 dry_run: bool,
251 ) -> Result<(), String> {
252 let resolved_profile = resolve_profile_name(profile_name, config);
254
255 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(&resolved_profile, profile);
266
267 let env_vars = resolve_environment_dual_path(
269 profile,
270 config,
271 self.merge_env,
272 &self.env_override,
273 );
274
275 let env_vars = apply_overrides(
277 env_vars,
278 &self.workbench,
279 &self.node_version,
280 &self.environment,
281 &self.dependency,
282 );
283
284 if self.verbose || dry_run {
286 print_resolved_environment(&env_vars);
287 }
288
289 if dry_run {
291 println!("\n{}", "Dry run complete. No changes made.");
292 return Ok(());
293 }
294
295 execute_run_command(&resolved_profile, config, &env_vars, &self.run_args)
297 }
298
299 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 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 debug_profiles.sort_by_key(|(k, _)| k.as_str());
320 release_profiles.sort_by_key(|(k, _)| k.as_str());
321
322 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 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 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 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 if let Some(desc) = &profile.description {
393 println!("Description: {}", desc);
394 }
395
396 if let Some(workbench) = &profile.workbench {
398 println!("\nWorkbench:");
399 println!(" Type: {}", workbench);
400 }
401
402 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 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 if let Some(script) = &profile.rhai_script {
427 println!("\nRhai Script: {}", script);
428 }
429
430 println!();
431 Ok(())
432 }
433
434 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 if profile.description.is_none() {
450 warnings.push("Profile has no description".to_string());
451 }
452
453 if profile.workbench.is_none() {
455 issues.push("Profile has no workbench type specified".to_string());
456 }
457
458 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 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 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 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 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 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
527fn 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
546fn 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
562fn 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
577fn 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
587fn 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 if let Some(templates) = &config.templates {
619 for (key, value) in &templates.env {
620 env.insert(key.clone(), value.clone());
621 }
622 }
623
624 if merge_env {
626 for (key, value) in std::env::vars() {
627 if is_run_env_var(&key) {
630 env.insert(key, value);
631 }
632 }
633 }
634
635 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 for (key, value) in cli_overrides {
644 env.insert(key.clone(), value.clone());
645 }
646
647 env
648}
649
650fn 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
678fn 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
695fn 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 env.remove("Browser");
706 env.remove("Wind");
707 env.remove("Mountain");
708 env.remove("Electron");
709 env.remove("BrowserProxy");
710
711 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
730fn 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 let is_debug = profile_name.starts_with("debug");
757
758 let run_command = if is_debug {
761 "pnpm tauri dev"
762 } else {
763 "pnpm dev"
764 };
765
766 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 let (shell_cmd, args) = cmd_args.split_first()
781 .ok_or("Empty command")?;
782
783 let mut cmd = StdCommand::new(shell_cmd);
785 cmd.args(args);
786 cmd.envs(env_vars.iter());
787
788 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}