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",
58 author,
59 version,
60 about = "Land Build System - Configuration-based builds",
61 long_about = "A configuration-driven build system that enables triggering builds directly with Cargo instead of \
62 shell scripts. Reads configuration from .vscode/land-config.json and supports multiple build \
63 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
164 Json,
165
166 Env,
167}
168
169impl std::fmt::Display for OutputFormat {
170 fn fmt(&self, f:&mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 match self {
172 OutputFormat::Table => write!(f, "table"),
173
174 OutputFormat::Json => write!(f, "json"),
175
176 OutputFormat::Env => write!(f, "env"),
177 }
178 }
179}
180
181impl Cli {
186 pub fn execute(&self) -> Result<(), String> {
188 let config_path = self.config.clone().unwrap_or_else(|| PathBuf::from(".vscode/land-config.json"));
189
190 let config = load_config(&config_path).map_err(|e| format!("Failed to load configuration: {}", e))?;
192
193 if let Some(command) = &self.command {
195 return self.execute_command(command, &config);
196 }
197
198 if let Some(profile_name) = &self.profile {
200 return self.execute_build(profile_name, &config, self.dry_run);
201 }
202
203 Err("No command specified. Use --profile <name> to build or --help for usage.".to_string())
205 }
206
207 fn execute_command(&self, command:&Commands, config:&LandConfig) -> Result<(), String> {
209 match command {
210 Commands::Build { profile, dry_run } => self.execute_build(profile, config, *dry_run),
211
212 Commands::ListProfiles { verbose } => self.execute_list_profiles(config, *verbose),
213
214 Commands::ShowProfile { profile } => self.execute_show_profile(profile, config),
215
216 Commands::ValidateProfile { profile } => self.execute_validate_profile(profile, config),
217
218 Commands::Resolve { profile, format } => self.execute_resolve(profile, config, Some(format.to_string())),
219 }
220 }
221
222 fn execute_build(&self, profile_name:&str, config:&LandConfig, dry_run:bool) -> Result<(), String> {
224 let resolved_profile = resolve_profile_name(profile_name, config);
226
227 let profile = config.profiles.get(&resolved_profile).ok_or_else(|| {
229 format!(
230 "Profile '{}' not found. Available profiles: {}",
231 resolved_profile,
232 config.profiles.keys().cloned().collect::<Vec<_>>().join(", ")
233 )
234 })?;
235
236 print_build_header(&resolved_profile, profile);
238
239 let env_vars = resolve_environment_dual_path(profile, config, self.merge_env, &self.env_override);
241
242 let env_vars = apply_overrides(
244 env_vars,
245 &self.workbench,
246 &self.node_version,
247 &self.environment,
248 &self.dependency,
249 );
250
251 if self.verbose || dry_run {
253 print_resolved_environment(&env_vars);
254 }
255
256 if dry_run {
258 println!("\n{}", "Dry run complete. No changes made.");
259
260 return Ok(());
261 }
262
263 execute_build_command(&resolved_profile, config, &env_vars, &self.build_args)
265 }
266
267 fn execute_list_profiles(&self, config:&LandConfig, verbose:bool) -> Result<(), String> {
269 println!("\n{}", "Land Build System - Available Profiles");
270
271 println!("{}\n", "=".repeat(50));
272
273 let mut debug_profiles:Vec<_> = config.profiles.iter().filter(|(k, _)| k.starts_with("debug")).collect();
275
276 let mut release_profiles:Vec<_> = config
277 .profiles
278 .iter()
279 .filter(|(k, _)| k.starts_with("production") || k.starts_with("release") || k.starts_with("web"))
280 .collect();
281
282 let mut bundler_profiles:Vec<_> = config
283 .profiles
284 .iter()
285 .filter(|(k, _)| k.contains("bundler") || k.contains("swc") || k.contains("oxc"))
286 .collect();
287
288 debug_profiles.sort_by_key(|(k, _)| k.as_str());
290
291 release_profiles.sort_by_key(|(k, _)| k.as_str());
292
293 bundler_profiles.sort_by_key(|(k, _)| k.as_str());
294
295 println!("{}:", "Debug Profiles".yellow());
297
298 println!();
299
300 for (name, profile) in &debug_profiles {
301 let default_profile = config
302 .cli
303 .as_ref()
304 .and_then(|cli| cli.default_profile.as_ref())
305 .map(|s| s.as_str())
306 .unwrap_or("");
307
308 let recommended = default_profile == name.as_str();
309
310 let marker = if recommended { " [RECOMMENDED]" } else { "" };
311
312 println!(
313 " {:<20} - {}{}",
314 name.green(),
315 profile.description.as_ref().map(|d| d.as_str()).unwrap_or("No description"),
316 marker.bright_magenta()
317 );
318
319 if verbose {
320 if let Some(workbench) = &profile.workbench {
321 println!(" Workbench: {}", workbench);
322 }
323
324 if let Some(features) = &profile.features {
325 for (feature, enabled) in features {
326 let status = if *enabled { "[X]" } else { "[ ]" };
327
328 println!(" {:>20} {} = {}", feature.cyan(), status, enabled);
329 }
330 }
331 }
332 }
333
334 println!("\n{}:", "Release Profiles");
336
337 for (name, profile) in &release_profiles {
338 println!(
339 " {:<20} - {}",
340 name.green(),
341 profile.description.as_ref().map(|d| d.as_str()).unwrap_or("No description")
342 );
343
344 if verbose {
345 if let Some(workbench) = &profile.workbench {
346 println!(" Workbench: {}", workbench);
347 }
348 }
349 }
350
351 if !bundler_profiles.is_empty() {
353 println!("\n{}:", "Bundler Profiles");
354
355 for (name, profile) in &bundler_profiles {
356 println!(
357 " {:<20} - {}",
358 name.green(),
359 profile.description.as_ref().map(|d| d.as_str()).unwrap_or("No description")
360 );
361 }
362 }
363
364 if let Some(cli_config) = &config.cli {
366 if !cli_config.profile_aliases.is_empty() {
367 println!("\n{}:", "Profile Aliases");
368
369 for (alias, target) in &cli_config.profile_aliases {
370 println!(" {:<10} -> {}", alias.cyan(), target);
371 }
372 }
373 }
374
375 println!();
376
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
385 .profiles
386 .get(&resolved_profile)
387 .ok_or_else(|| format!("Profile '{}' not found.", resolved_profile))?;
388
389 println!("\n{}: {}", "Profile:", resolved_profile.green());
390
391 println!("{}\n", "=".repeat(50));
392
393 if let Some(desc) = &profile.description {
395 println!("Description: {}", desc);
396 }
397
398 if let Some(workbench) = &profile.workbench {
400 println!("\nWorkbench:");
401
402 println!(" Type: {}", workbench);
403
404 if let Some(wb_config) = &config.workbench {
405 if let Some(features) = &wb_config.features {
406 if let Some(wb_features) = features.get(workbench) {
407 if let Some(coverage) = &wb_features.coverage {
408 println!(" Coverage: {}", coverage);
409 }
410
411 if let Some(complexity) = &wb_features.complexity {
412 println!(" Complexity: {}", complexity);
413 }
414
415 if wb_features.polyfills.unwrap_or(false) {
416 println!(" Polyfills: enabled");
417 }
418
419 if wb_features.mountain_providers.unwrap_or(false) {
420 println!(" Mountain Providers: enabled");
421 }
422
423 if wb_features.wind_services.unwrap_or(false) {
424 println!(" Wind Services: enabled");
425 }
426 }
427 }
428 }
429 }
430
431 println!("\nEnvironment Variables:");
433
434 if let Some(env) = &profile.env {
435 let mut sorted_env:Vec<_> = env.iter().collect();
436
437 sorted_env.sort_by_key(|(k, _)| k.as_str());
438
439 for (key, value) in sorted_env {
440 println!(" {:<25} = {}", key, value);
441 }
442 }
443
444 if let Some(features) = &profile.features {
446 println!("\nFeatures:");
447
448 println!("\n Enabled:");
449
450 let mut sorted_features:Vec<_> = features.iter().filter(|(_, enabled)| **enabled).collect();
451
452 sorted_features.sort_by_key(|(k, _)| k.as_str());
453
454 for (feature, _) in &sorted_features {
455 println!(" {:<30}", feature.green());
456 }
457 }
458
459 let build_cmd = get_build_command(&resolved_profile, &config);
461
462 println!("\nBuild Command: {}", build_cmd);
463
464 if let Some(script) = &profile.rhai_script {
466 println!("\nRhai Script: {}", script);
467 }
468
469 println!();
470
471 Ok(())
472 }
473
474 fn execute_validate_profile(&self, profile_name:&str, config:&LandConfig) -> Result<(), String> {
476 let resolved_profile = resolve_profile_name(profile_name, config);
477
478 let profile = config
479 .profiles
480 .get(&resolved_profile)
481 .ok_or_else(|| format!("Profile '{}' not found.", resolved_profile))?;
482
483 println!("\n{}: {}", "Validating Profile:", resolved_profile.green());
484
485 println!("{}\n", "=".repeat(50));
486
487 let mut issues = Vec::new();
488
489 let mut warnings = Vec::new();
490
491 if profile.description.is_none() {
493 warnings.push("Profile has no description".to_string());
494 }
495
496 if profile.workbench.is_none() {
498 issues.push("Profile has no workbench type specified".to_string());
499 } else if let Some(workbench) = &profile.workbench {
500 if let Some(wb_config) = &config.workbench {
501 if let Some(available) = &wb_config.available {
502 if !available.contains(workbench) {
503 issues.push(format!("Workbench '{}' not defined in workbench configuration", workbench));
504 }
505 }
506 }
507 }
508
509 if profile.env.is_none() || profile.env.as_ref().unwrap().is_empty() {
511 warnings.push("Profile has no environment variables defined".to_string());
512 }
513
514 if issues.is_empty() && warnings.is_empty() {
516 println!("{}", "Profile is valid!".green());
517 } else {
518 if !warnings.is_empty() {
519 println!("\n{} Warnings:", warnings.len().to_string().yellow());
520
521 for warning in &warnings {
522 println!(" - {}", warning.yellow());
523 }
524 }
525
526 if !issues.is_empty() {
527 println!("\n{} Issues:", issues.len().to_string().red());
528
529 for issue in &issues {
530 println!(" - {}", issue.red());
531 }
532 }
533 }
534
535 println!();
536
537 Ok(())
538 }
539
540 fn execute_resolve(&self, profile_name:&str, config:&LandConfig, _format:Option<String>) -> Result<(), String> {
542 let resolved_profile = resolve_profile_name(profile_name, config);
543
544 let profile = config
545 .profiles
546 .get(&resolved_profile)
547 .ok_or_else(|| format!("Profile '{}' not found.", resolved_profile))?;
548
549 println!("\n{}: {}", "Resolved Profile:", resolved_profile.green());
550
551 println!("{}\n", "=".repeat(50));
552
553 if let Some(desc) = &profile.description {
555 println!("Description: {}", desc);
556 }
557
558 if let Some(workbench) = &profile.workbench {
559 println!("Workbench: {}", workbench);
560 }
561
562 if let Some(env) = &profile.env {
564 println!("\nEnvironment Variables ({}):", env.len());
565
566 for (key, value) in env {
567 println!(" {} = {}", key.green(), value);
568 }
569 }
570
571 if let Some(features) = &profile.features {
573 println!("\nFeatures ({}):", features.len());
574
575 for (feature, enabled) in features {
576 let status = if *enabled { "[X]" } else { "[ ]" };
577
578 println!(" {} {}", status, feature);
579 }
580 }
581
582 println!();
583
584 Ok(())
585 }
586}
587
588fn print_build_header(profile_name:&str, profile:&Profile) {
594 println!("\n{}", "========================================");
595
596 println!("Land Build: {}", profile_name);
597
598 println!("========================================");
599
600 if let Some(desc) = &profile.description {
601 println!("Description: {}", desc);
602 }
603
604 if let Some(workbench) = &profile.workbench {
605 println!("Workbench: {}", workbench);
606 }
607}
608
609fn print_resolved_environment(env:&HashMap<String, String>) {
611 println!("\nResolved Environment:");
612
613 let mut sorted_env:Vec<_> = env.iter().collect();
614
615 sorted_env.sort_by_key(|(k, _)| k.as_str());
616
617 for (key, value) in sorted_env {
618 let display_value = if value.is_empty() { "(empty)" } else { value };
619
620 println!(" {:<25} = {}", key, display_value);
621 }
622}
623
624fn parse_profile_name(s:&str) -> Result<String, String> {
626 let name = s.trim().to_lowercase();
627
628 if name.is_empty() {
629 return Err("Profile name cannot be empty".to_string());
630 }
631
632 if name.contains(' ') {
633 return Err("Profile name cannot contain spaces".to_string());
634 }
635
636 Ok(name)
637}
638
639fn resolve_profile_name(name:&str, config:&LandConfig) -> String {
641 if let Some(cli_config) = &config.cli {
642 if let Some(resolved) = cli_config.profile_aliases.get(name) {
643 return resolved.clone();
644 }
645 }
646
647 name.to_string()
648}
649
650fn get_build_command(profile_name:&str, config:&LandConfig) -> String {
652 let command_key = if profile_name.starts_with("production")
654 || profile_name.starts_with("release")
655 || profile_name.starts_with("web")
656 {
657 "production"
658 } else {
659 profile_name.split('-').next().unwrap_or("debug")
660 };
661
662 if let Some(build_commands) = &config.build_commands {
663 build_commands.get(command_key).cloned().unwrap_or_else(|| {
664 if profile_name.starts_with("production") {
665 "pnpm tauri build".to_string()
666 } else {
667 "pnpm tauri build --debug".to_string()
668 }
669 })
670 } else {
671 if profile_name.starts_with("production") {
673 "pnpm tauri build".to_string()
674 } else {
675 "pnpm tauri build --debug".to_string()
676 }
677 }
678}
679
680fn resolve_environment_dual_path(
703 profile:&Profile,
704
705 config:&LandConfig,
706
707 merge_env:bool,
708
709 cli_overrides:&[(String, String)],
710) -> HashMap<String, String> {
711 let mut env = HashMap::new();
712
713 if let Some(templates) = &config.templates {
715 for (key, value) in &templates.env {
716 env.insert(key.clone(), value.clone());
717 }
718 }
719
720 if merge_env {
722 for (key, value) in std::env::vars() {
723 if is_build_env_var(&key) {
726 env.insert(key, value);
727 }
728 }
729 }
730
731 if let Some(profile_env) = &profile.env {
733 for (key, value) in profile_env {
734 env.insert(key.clone(), value.clone());
735 }
736 }
737
738 for (key, value) in cli_overrides {
740 env.insert(key.clone(), value.clone());
741 }
742
743 env
744}
745
746fn is_build_env_var(key:&str) -> bool {
748 matches!(
749 key,
750 "Browser"
751 | "Bundle"
752 | "CargoFeatures"
753 | "Clean" | "CocoonEsbuildDefine"
754 | "Compile"
755 | "Debug" | "Dependency"
756 | "Mountain"
757 | "Wind" | "Electron"
758 | "BrowserProxy"
759 | "NODE_ENV"
760 | "NODE_VERSION"
761 | "NODE_OPTIONS"
762 | "RUST_LOG"
763 | "AIR_LOG_JSON"
764 | "AIR_LOG_FILE"
765 | "Level" | "Name"
766 | "Prefix"
767 )
768}
769
770fn parse_key_val<K, V>(s:&str) -> Result<(K, V), String>
772where
773 K: std::str::FromStr,
774 V: std::str::FromStr,
775 K::Err: std::fmt::Display,
776 V::Err: std::fmt::Display, {
777 let pos = s.find('=').ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
778
779 Ok((
780 s[..pos].parse().map_err(|e| format!("key parse error: {e}"))?,
781 s[pos + 1..].parse().map_err(|e| format!("value parse error: {e}"))?,
782 ))
783}
784
785fn apply_overrides(
787 mut env:HashMap<String, String>,
788
789 workbench:&Option<String>,
790
791 node_version:&Option<String>,
792
793 environment:&Option<String>,
794
795 dependency:&Option<String>,
796) -> HashMap<String, String> {
797 if let Some(workbench) = workbench {
798 env.remove("Browser");
800
801 env.remove("Wind");
802
803 env.remove("Mountain");
804
805 env.remove("Electron");
806
807 env.remove("BrowserProxy");
808
809 env.insert(workbench.clone(), "true".to_string());
811 }
812
813 if let Some(version) = node_version {
814 env.insert("NODE_VERSION".to_string(), version.clone());
815 }
816
817 if let Some(environment) = environment {
818 env.insert("NODE_ENV".to_string(), environment.clone());
819 }
820
821 if let Some(dependency) = dependency {
822 env.insert("Dependency".to_string(), dependency.clone());
823 }
824
825 env
826}
827
828fn execute_build_command(
849 profile_name:&str,
850
851 config:&LandConfig,
852
853 env_vars:&HashMap<String, String>,
854
855 build_args:&[String],
856) -> Result<(), String> {
857 use std::process::Command as StdCommand;
858
859 let build_command = get_build_command(profile_name, config);
861
862 let is_debug = build_command.to_lowercase().contains("--debug");
864
865 let mut maintain_args = vec!["--".to_string()];
868
869 maintain_args.push("pnpm".to_string());
871
872 maintain_args.push("tauri".to_string());
873
874 maintain_args.push("build".to_string());
875
876 if is_debug {
877 maintain_args.push("--debug".to_string());
878 }
879
880 maintain_args.extend(build_args.iter().cloned());
882
883 println!("Executing: {}", maintain_args.join(" "));
884
885 println!("With environment variables:");
886
887 for (key, value) in env_vars.iter().take(10) {
888 println!(" {}={}", key, value);
889 }
890
891 if env_vars.len() > 10 {
892 println!(" ... and {} more", env_vars.len() - 10);
893 }
894
895 let maintain_binary = find_maintain_binary();
898
899 let mut cmd = StdCommand::new(&maintain_binary);
906
907 cmd.args(&maintain_args);
908
909 cmd.envs(env_vars.iter());
912
913 cmd.env("MAINTAIN_CLI_MERGED", "true");
915
916 cmd.stderr(std::process::Stdio::inherit())
917 .stdout(std::process::Stdio::inherit());
918
919 let status = cmd
920 .status()
921 .map_err(|e| format!("Failed to execute Maintain binary ({}): {}", maintain_binary, e))?;
922
923 if status.success() {
924 println!("\n{}", "Build completed successfully!".green());
925
926 Ok(())
927 } else {
928 Err(format!("Build failed with exit code: {:?}", status.code()))
929 }
930}
931
932fn find_maintain_binary() -> String {
940 use std::path::Path;
941
942 let release_path = "./Target/release/Maintain";
944
945 if Path::new(release_path).exists() {
946 return release_path.to_string();
947 }
948
949 let debug_path = "./Target/debug/Maintain";
951
952 if Path::new(debug_path).exists() {
953 return debug_path.to_string();
954 }
955
956 "maintain".to_string()
958}
959
960pub fn get_all_profiles(config:&LandConfig) -> Vec<&str> {
962 let mut profiles:Vec<&str> = config.profiles.keys().map(|s| s.as_str()).collect();
963
964 profiles.sort();
965
966 profiles
967}