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};
49
50#[derive(Parser, Debug, Clone)]
56#[clap(
57 name = "maintain",
58 author,
59 version,
60 about = "Land Build System - Configuration-based builds",
61 long_about = "A configuration-driven build system that enables triggering builds \
62 directly with Cargo instead of shell scripts. Reads configuration \
63 from .vscode/land-config.json and supports multiple build profiles."
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)]
99 pub dry_run: bool,
100
101 #[clap(long, short = 'v', global = true)]
103 pub verbose: bool,
104
105 #[clap(long, default_value = "true", global = true)]
107 pub merge_env: bool,
108
109 #[clap(last = true)]
111 pub build_args: Vec<String>,
112}
113
114#[derive(Subcommand, Debug, Clone)]
116pub enum Commands {
117 Build {
119 #[clap(long, short = 'p', value_parser = parse_profile_name)]
121 profile: String,
122
123 #[clap(long)]
125 dry_run: bool,
126 },
127
128 ListProfiles {
130 #[clap(long, short = 'v')]
132 verbose: bool,
133 },
134
135 ShowProfile {
137 profile: String,
139 },
140
141 ValidateProfile {
143 profile: String,
145 },
146
147 Resolve {
149 #[clap(long, short = 'p')]
151 profile: String,
152
153 #[clap(long, short = 'f', default_value = "table")]
155 format: OutputFormat,
156 },
157}
158
159#[derive(Debug, Clone, ValueEnum)]
161pub enum OutputFormat {
162 Table,
163 Json,
164 Env,
165}
166
167impl std::fmt::Display for OutputFormat {
168 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169 match self {
170 OutputFormat::Table => write!(f, "table"),
171 OutputFormat::Json => write!(f, "json"),
172 OutputFormat::Env => write!(f, "env"),
173 }
174 }
175}
176
177impl Cli {
182 pub fn execute(&self) -> Result<(), String> {
184 let config_path = self.config.clone().unwrap_or_else(|| {
185 PathBuf::from(".vscode/land-config.json")
186 });
187
188 let config = load_config(&config_path)
190 .map_err(|e| format!("Failed to load configuration: {}", e))?;
191
192 if let Some(command) = &self.command {
194 return self.execute_command(command, &config);
195 }
196
197 if let Some(profile_name) = &self.profile {
199 return self.execute_build(profile_name, &config, self.dry_run);
200 }
201
202 Err("No command specified. Use --profile <name> to build or --help for usage.".to_string())
204 }
205
206 fn execute_command(&self, command: &Commands, config: &LandConfig) -> Result<(), String> {
208 match command {
209 Commands::Build { profile, dry_run } => {
210 self.execute_build(profile, config, *dry_run)
211 }
212 Commands::ListProfiles { verbose } => {
213 self.execute_list_profiles(config, *verbose)
214 }
215 Commands::ShowProfile { profile } => {
216 self.execute_show_profile(profile, config)
217 }
218 Commands::ValidateProfile { profile } => {
219 self.execute_validate_profile(profile, config)
220 }
221 Commands::Resolve { profile, format } => {
222 self.execute_resolve(profile, config, Some(format.to_string()))
223 }
224 }
225 }
226
227 fn execute_build(
229 &self,
230 profile_name: &str,
231 config: &LandConfig,
232 dry_run: bool,
233 ) -> Result<(), String> {
234 let resolved_profile = resolve_profile_name(profile_name, config);
236
237 let profile = config.profiles.get(&resolved_profile).ok_or_else(|| {
239 format!(
240 "Profile '{}' not found. Available profiles: {}",
241 resolved_profile,
242 config.profiles.keys().cloned().collect::<Vec<_>>().join(", ")
243 )
244 })?;
245
246 print_build_header(&resolved_profile, profile);
248
249 let env_vars = resolve_environment_dual_path(
251 profile,
252 config,
253 self.merge_env,
254 &self.env_override,
255 );
256
257 let env_vars = apply_overrides(
259 env_vars,
260 &self.workbench,
261 &self.node_version,
262 &self.environment,
263 &self.dependency,
264 );
265
266 if self.verbose || dry_run {
268 print_resolved_environment(&env_vars);
269 }
270
271 if dry_run {
273 println!("\n{}", "Dry run complete. No changes made.");
274 return Ok(());
275 }
276
277 execute_build_command(&resolved_profile, config, &env_vars, &self.build_args)
279 }
280
281 fn execute_list_profiles(&self, config: &LandConfig, verbose: bool) -> Result<(), String> {
283 println!("\n{}", "Land Build System - Available Profiles");
284 println!("{}\n", "=".repeat(50));
285
286 let mut debug_profiles: Vec<_> = config
288 .profiles
289 .iter()
290 .filter(|(k, _)| k.starts_with("debug"))
291 .collect();
292 let mut release_profiles: Vec<_> = config
293 .profiles
294 .iter()
295 .filter(|(k, _)| {
296 k.starts_with("production") || k.starts_with("release") || k.starts_with("web")
297 })
298 .collect();
299 let mut bundler_profiles: Vec<_> = config
300 .profiles
301 .iter()
302 .filter(|(k, _)| k.contains("bundler") || k.contains("swc") || k.contains("oxc"))
303 .collect();
304
305 debug_profiles.sort_by_key(|(k, _)| k.as_str());
307 release_profiles.sort_by_key(|(k, _)| k.as_str());
308 bundler_profiles.sort_by_key(|(k, _)| k.as_str());
309
310 println!("{}:", "Debug Profiles".yellow());
312 println!();
313 for (name, profile) in &debug_profiles {
314 let default_profile = config.cli.as_ref()
315 .and_then(|cli| cli.default_profile.as_ref())
316 .map(|s| s.as_str())
317 .unwrap_or("");
318 let recommended = default_profile == name.as_str();
319 let marker = if recommended { " [RECOMMENDED]" } else { "" };
320 println!(
321 " {:<20} - {}{}",
322 name.green(),
323 profile.description.as_ref().map(|d| d.as_str()).unwrap_or("No description"),
324 marker.bright_magenta()
325 );
326 if verbose {
327 if let Some(workbench) = &profile.workbench {
328 println!(" Workbench: {}", workbench);
329 }
330 if let Some(features) = &profile.features {
331 for (feature, enabled) in features {
332 let status = if *enabled { "[X]" } else { "[ ]" };
333 println!(" {:>20} {} = {}", feature.cyan(), status, enabled);
334 }
335 }
336 }
337 }
338
339 println!("\n{}:", "Release Profiles");
341 for (name, profile) in &release_profiles {
342 println!(
343 " {:<20} - {}",
344 name.green(),
345 profile.description.as_ref().map(|d| d.as_str()).unwrap_or("No description")
346 );
347 if verbose {
348 if let Some(workbench) = &profile.workbench {
349 println!(" Workbench: {}", workbench);
350 }
351 }
352 }
353
354 if !bundler_profiles.is_empty() {
356 println!("\n{}:", "Bundler Profiles");
357 for (name, profile) in &bundler_profiles {
358 println!(
359 " {:<20} - {}",
360 name.green(),
361 profile.description.as_ref().map(|d| d.as_str()).unwrap_or("No description")
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 if let Some(wb_config) = &config.workbench {
402 if let Some(features) = &wb_config.features {
403 if let Some(wb_features) = features.get(workbench) {
404 if let Some(coverage) = &wb_features.coverage {
405 println!(" Coverage: {}", coverage);
406 }
407 if let Some(complexity) = &wb_features.complexity {
408 println!(" Complexity: {}", complexity);
409 }
410 if wb_features.polyfills.unwrap_or(false) {
411 println!(" Polyfills: enabled");
412 }
413 if wb_features.mountain_providers.unwrap_or(false) {
414 println!(" Mountain Providers: enabled");
415 }
416 if wb_features.wind_services.unwrap_or(false) {
417 println!(" Wind Services: enabled");
418 }
419 }
420 }
421 }
422 }
423
424 println!("\nEnvironment Variables:");
426 if let Some(env) = &profile.env {
427 let mut sorted_env: Vec<_> = env.iter().collect();
428 sorted_env.sort_by_key(|(k, _)| k.as_str());
429 for (key, value) in sorted_env {
430 println!(" {:<25} = {}", key, value);
431 }
432 }
433
434 if let Some(features) = &profile.features {
436 println!("\nFeatures:");
437 println!("\n Enabled:");
438 let mut sorted_features: Vec<_> = features.iter()
439 .filter(|(_, enabled)| **enabled)
440 .collect();
441 sorted_features.sort_by_key(|(k, _)| k.as_str());
442 for (feature, _) in &sorted_features {
443 println!(" {:<30}", feature.green());
444 }
445 }
446
447 let build_cmd = get_build_command(&resolved_profile, &config);
449 println!("\nBuild Command: {}", build_cmd);
450
451 if let Some(script) = &profile.rhai_script {
453 println!("\nRhai Script: {}", script);
454 }
455
456 println!();
457 Ok(())
458 }
459
460 fn execute_validate_profile(&self, profile_name: &str, config: &LandConfig) -> Result<(), String> {
462 let resolved_profile = resolve_profile_name(profile_name, config);
463
464 let profile = config.profiles.get(&resolved_profile).ok_or_else(|| {
465 format!("Profile '{}' not found.", resolved_profile)
466 })?;
467
468 println!("\n{}: {}", "Validating Profile:", resolved_profile.green());
469 println!("{}\n", "=".repeat(50));
470
471 let mut issues = Vec::new();
472 let mut warnings = Vec::new();
473
474 if profile.description.is_none() {
476 warnings.push("Profile has no description".to_string());
477 }
478
479 if profile.workbench.is_none() {
481 issues.push("Profile has no workbench type specified".to_string());
482 } else if let Some(workbench) = &profile.workbench {
483 if let Some(wb_config) = &config.workbench {
484 if let Some(available) = &wb_config.available {
485 if !available.contains(workbench) {
486 issues.push(format!("Workbench '{}' not defined in workbench configuration", workbench));
487 }
488 }
489 }
490 }
491
492 if profile.env.is_none() || profile.env.as_ref().unwrap().is_empty() {
494 warnings.push("Profile has no environment variables defined".to_string());
495 }
496
497 if issues.is_empty() && warnings.is_empty() {
499 println!("{}", "Profile is valid!".green());
500 } else {
501 if !warnings.is_empty() {
502 println!("\n{} Warnings:", warnings.len().to_string().yellow());
503 for warning in &warnings {
504 println!(" - {}", warning.yellow());
505 }
506 }
507 if !issues.is_empty() {
508 println!("\n{} Issues:", issues.len().to_string().red());
509 for issue in &issues {
510 println!(" - {}", issue.red());
511 }
512 }
513 }
514
515 println!();
516 Ok(())
517 }
518
519 fn execute_resolve(&self, profile_name: &str, config: &LandConfig, _format: Option<String>) -> Result<(), String> {
521 let resolved_profile = resolve_profile_name(profile_name, config);
522
523 let profile = config.profiles.get(&resolved_profile).ok_or_else(|| {
524 format!("Profile '{}' not found.", resolved_profile)
525 })?;
526
527 println!("\n{}: {}", "Resolved Profile:", resolved_profile.green());
528 println!("{}\n", "=".repeat(50));
529
530 if let Some(desc) = &profile.description {
532 println!("Description: {}", desc);
533 }
534
535 if let Some(workbench) = &profile.workbench {
536 println!("Workbench: {}", workbench);
537 }
538
539 if let Some(env) = &profile.env {
541 println!("\nEnvironment Variables ({}):", env.len());
542 for (key, value) in env {
543 println!(" {} = {}", key.green(), value);
544 }
545 }
546
547 if let Some(features) = &profile.features {
549 println!("\nFeatures ({}):", features.len());
550 for (feature, enabled) in features {
551 let status = if *enabled { "[X]" } else { "[ ]" };
552 println!(" {} {}", status, feature);
553 }
554 }
555
556 println!();
557 Ok(())
558 }
559}
560
561fn print_build_header(profile_name: &str, profile: &Profile) {
567 println!("\n{}", "========================================");
568 println!("Land Build: {}", profile_name);
569 println!("========================================");
570
571 if let Some(desc) = &profile.description {
572 println!("Description: {}", desc);
573 }
574
575 if let Some(workbench) = &profile.workbench {
576 println!("Workbench: {}", workbench);
577 }
578}
579
580fn print_resolved_environment(env: &HashMap<String, String>) {
582 println!("\nResolved Environment:");
583 let mut sorted_env: Vec<_> = env.iter().collect();
584 sorted_env.sort_by_key(|(k, _)| k.as_str());
585
586 for (key, value) in sorted_env {
587 let display_value = if value.is_empty() {
588 "(empty)"
589 } else {
590 value
591 };
592 println!(" {:<25} = {}", key, display_value);
593 }
594}
595
596fn parse_profile_name(s: &str) -> Result<String, String> {
598 let name = s.trim().to_lowercase();
599
600 if name.is_empty() {
601 return Err("Profile name cannot be empty".to_string());
602 }
603
604 if name.contains(' ') {
605 return Err("Profile name cannot contain spaces".to_string());
606 }
607
608 Ok(name)
609}
610
611fn resolve_profile_name(name: &str, config: &LandConfig) -> String {
613 if let Some(cli_config) = &config.cli {
614 if let Some(resolved) = cli_config.profile_aliases.get(name) {
615 return resolved.clone();
616 }
617 }
618 name.to_string()
619}
620
621fn get_build_command(profile_name: &str, config: &LandConfig) -> String {
623 let command_key = if profile_name.starts_with("production")
625 || profile_name.starts_with("release")
626 || profile_name.starts_with("web")
627 {
628 "production"
629 } else {
630 profile_name.split('-').next().unwrap_or("debug")
631 };
632
633 if let Some(build_commands) = &config.build_commands {
634 build_commands.get(command_key).cloned().unwrap_or_else(|| {
635 if profile_name.starts_with("production") {
636 "pnpm tauri build".to_string()
637 } else {
638 "pnpm tauri build --debug".to_string()
639 }
640 })
641 } else {
642 if profile_name.starts_with("production") {
644 "pnpm tauri build".to_string()
645 } else {
646 "pnpm tauri build --debug".to_string()
647 }
648 }
649}
650
651fn resolve_environment_dual_path(
674 profile: &Profile,
675 config: &LandConfig,
676 merge_env: bool,
677 cli_overrides: &[(String, String)],
678) -> HashMap<String, String> {
679 let mut env = HashMap::new();
680
681 if let Some(templates) = &config.templates {
683 for (key, value) in &templates.env {
684 env.insert(key.clone(), value.clone());
685 }
686 }
687
688 if merge_env {
690 for (key, value) in std::env::vars() {
691 if is_build_env_var(&key) {
694 env.insert(key, value);
695 }
696 }
697 }
698
699 if let Some(profile_env) = &profile.env {
701 for (key, value) in profile_env {
702 env.insert(key.clone(), value.clone());
703 }
704 }
705
706 for (key, value) in cli_overrides {
708 env.insert(key.clone(), value.clone());
709 }
710
711 env
712}
713
714fn is_build_env_var(key: &str) -> bool {
716 matches!(
717 key,
718 "Browser"
719 | "Bundle"
720 | "Clean"
721 | "Compile"
722 | "Debug"
723 | "Dependency"
724 | "Mountain"
725 | "Wind"
726 | "Electron"
727 | "BrowserProxy"
728 | "NODE_ENV"
729 | "NODE_VERSION"
730 | "NODE_OPTIONS"
731 | "RUST_LOG"
732 | "AIR_LOG_JSON"
733 | "AIR_LOG_FILE"
734 | "Level"
735 | "Name"
736 | "Prefix"
737 )
738}
739
740fn parse_key_val<K, V>(s: &str) -> Result<(K, V), String>
742where
743 K: std::str::FromStr,
744 V: std::str::FromStr,
745 K::Err: std::fmt::Display,
746 V::Err: std::fmt::Display,
747{
748 let pos = s
749 .find('=')
750 .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
751 Ok((
752 s[..pos].parse().map_err(|e| format!("key parse error: {e}"))?,
753 s[pos + 1..].parse().map_err(|e| format!("value parse error: {e}"))?,
754 ))
755}
756
757fn apply_overrides(
759 mut env: HashMap<String, String>,
760 workbench: &Option<String>,
761 node_version: &Option<String>,
762 environment: &Option<String>,
763 dependency: &Option<String>,
764) -> HashMap<String, String> {
765 if let Some(workbench) = workbench {
766 env.remove("Browser");
768 env.remove("Wind");
769 env.remove("Mountain");
770 env.remove("Electron");
771 env.remove("BrowserProxy");
772
773 env.insert(workbench.clone(), "true".to_string());
775 }
776
777 if let Some(version) = node_version {
778 env.insert("NODE_VERSION".to_string(), version.clone());
779 }
780
781 if let Some(environment) = environment {
782 env.insert("NODE_ENV".to_string(), environment.clone());
783 }
784
785 if let Some(dependency) = dependency {
786 env.insert("Dependency".to_string(), dependency.clone());
787 }
788
789 env
790}
791
792fn execute_build_command(
811 profile_name: &str,
812 config: &LandConfig,
813 env_vars: &HashMap<String, String>,
814 build_args: &[String],
815) -> Result<(), String> {
816 use std::process::Command as StdCommand;
817
818 let build_command = get_build_command(profile_name, config);
820
821 let is_debug = build_command.to_lowercase().contains("--debug");
823
824 let mut maintain_args = vec!["--".to_string()];
827
828 maintain_args.push("pnpm".to_string());
830 maintain_args.push("tauri".to_string());
831 maintain_args.push("build".to_string());
832
833 if is_debug {
834 maintain_args.push("--debug".to_string());
835 }
836
837 maintain_args.extend(build_args.iter().cloned());
839
840 println!("Executing: {}", maintain_args.join(" "));
841 println!("With environment variables:");
842 for (key, value) in env_vars.iter().take(10) {
843 println!(" {}={}", key, value);
844 }
845 if env_vars.len() > 10 {
846 println!(" ... and {} more", env_vars.len() - 10);
847 }
848
849 let maintain_binary = find_maintain_binary();
852
853 let mut cmd = StdCommand::new(&maintain_binary);
860 cmd.args(&maintain_args);
861
862 cmd.envs(env_vars.iter());
865
866 cmd.env("MAINTAIN_CLI_MERGED", "true");
868
869 cmd.stderr(std::process::Stdio::inherit())
870 .stdout(std::process::Stdio::inherit());
871
872 let status = cmd.status()
873 .map_err(|e| format!("Failed to execute Maintain binary ({}): {}", maintain_binary, e))?;
874
875 if status.success() {
876 println!("\n{}", "Build completed successfully!".green());
877 Ok(())
878 } else {
879 Err(format!("Build failed with exit code: {:?}", status.code()))
880 }
881}
882
883fn find_maintain_binary() -> String {
891 use std::path::Path;
892
893 let release_path = "./Target/release/Maintain";
895 if Path::new(release_path).exists() {
896 return release_path.to_string();
897 }
898
899 let debug_path = "./Target/debug/Maintain";
901 if Path::new(debug_path).exists() {
902 return debug_path.to_string();
903 }
904
905 "maintain".to_string()
907}
908
909pub fn get_all_profiles(config: &LandConfig) -> Vec<&str> {
911 let mut profiles: Vec<&str> = config.profiles.keys().map(|s| s.as_str()).collect();
912 profiles.sort();
913 profiles
914}