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 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(|| PathBuf::from(".vscode/land-config.json"));
185
186 let config = load_config(&config_path).map_err(|e| format!("Failed to load configuration: {}", e))?;
188
189 if let Some(command) = &self.command {
191 return self.execute_command(command, &config);
192 }
193
194 if let Some(profile_name) = &self.profile {
196 return self.execute_build(profile_name, &config, self.dry_run);
197 }
198
199 Err("No command specified. Use --profile <name> to build or --help for usage.".to_string())
201 }
202
203 fn execute_command(&self, command:&Commands, config:&LandConfig) -> Result<(), String> {
205 match command {
206 Commands::Build { profile, dry_run } => self.execute_build(profile, config, *dry_run),
207 Commands::ListProfiles { verbose } => self.execute_list_profiles(config, *verbose),
208 Commands::ShowProfile { profile } => self.execute_show_profile(profile, config),
209 Commands::ValidateProfile { profile } => self.execute_validate_profile(profile, config),
210 Commands::Resolve { profile, format } => self.execute_resolve(profile, config, Some(format.to_string())),
211 }
212 }
213
214 fn execute_build(&self, profile_name:&str, config:&LandConfig, dry_run:bool) -> Result<(), String> {
216 let resolved_profile = resolve_profile_name(profile_name, config);
218
219 let profile = config.profiles.get(&resolved_profile).ok_or_else(|| {
221 format!(
222 "Profile '{}' not found. Available profiles: {}",
223 resolved_profile,
224 config.profiles.keys().cloned().collect::<Vec<_>>().join(", ")
225 )
226 })?;
227
228 print_build_header(&resolved_profile, profile);
230
231 let env_vars = resolve_environment_dual_path(profile, config, self.merge_env, &self.env_override);
233
234 let env_vars = apply_overrides(
236 env_vars,
237 &self.workbench,
238 &self.node_version,
239 &self.environment,
240 &self.dependency,
241 );
242
243 if self.verbose || dry_run {
245 print_resolved_environment(&env_vars);
246 }
247
248 if dry_run {
250 println!("\n{}", "Dry run complete. No changes made.");
251 return Ok(());
252 }
253
254 execute_build_command(&resolved_profile, config, &env_vars, &self.build_args)
256 }
257
258 fn execute_list_profiles(&self, config:&LandConfig, verbose:bool) -> Result<(), String> {
260 println!("\n{}", "Land Build System - Available Profiles");
261 println!("{}\n", "=".repeat(50));
262
263 let mut debug_profiles:Vec<_> = config.profiles.iter().filter(|(k, _)| k.starts_with("debug")).collect();
265 let mut release_profiles:Vec<_> = config
266 .profiles
267 .iter()
268 .filter(|(k, _)| k.starts_with("production") || k.starts_with("release") || k.starts_with("web"))
269 .collect();
270 let mut bundler_profiles:Vec<_> = config
271 .profiles
272 .iter()
273 .filter(|(k, _)| k.contains("bundler") || k.contains("swc") || k.contains("oxc"))
274 .collect();
275
276 debug_profiles.sort_by_key(|(k, _)| k.as_str());
278 release_profiles.sort_by_key(|(k, _)| k.as_str());
279 bundler_profiles.sort_by_key(|(k, _)| k.as_str());
280
281 println!("{}:", "Debug Profiles".yellow());
283 println!();
284 for (name, profile) in &debug_profiles {
285 let default_profile = config
286 .cli
287 .as_ref()
288 .and_then(|cli| cli.default_profile.as_ref())
289 .map(|s| s.as_str())
290 .unwrap_or("");
291 let recommended = default_profile == name.as_str();
292 let marker = if recommended { " [RECOMMENDED]" } else { "" };
293 println!(
294 " {:<20} - {}{}",
295 name.green(),
296 profile.description.as_ref().map(|d| d.as_str()).unwrap_or("No description"),
297 marker.bright_magenta()
298 );
299 if verbose {
300 if let Some(workbench) = &profile.workbench {
301 println!(" Workbench: {}", workbench);
302 }
303 if let Some(features) = &profile.features {
304 for (feature, enabled) in features {
305 let status = if *enabled { "[X]" } else { "[ ]" };
306 println!(" {:>20} {} = {}", feature.cyan(), status, enabled);
307 }
308 }
309 }
310 }
311
312 println!("\n{}:", "Release Profiles");
314 for (name, profile) in &release_profiles {
315 println!(
316 " {:<20} - {}",
317 name.green(),
318 profile.description.as_ref().map(|d| d.as_str()).unwrap_or("No description")
319 );
320 if verbose {
321 if let Some(workbench) = &profile.workbench {
322 println!(" Workbench: {}", workbench);
323 }
324 }
325 }
326
327 if !bundler_profiles.is_empty() {
329 println!("\n{}:", "Bundler Profiles");
330 for (name, profile) in &bundler_profiles {
331 println!(
332 " {:<20} - {}",
333 name.green(),
334 profile.description.as_ref().map(|d| d.as_str()).unwrap_or("No description")
335 );
336 }
337 }
338
339 if let Some(cli_config) = &config.cli {
341 if !cli_config.profile_aliases.is_empty() {
342 println!("\n{}:", "Profile Aliases");
343 for (alias, target) in &cli_config.profile_aliases {
344 println!(" {:<10} -> {}", alias.cyan(), target);
345 }
346 }
347 }
348
349 println!();
350 Ok(())
351 }
352
353 fn execute_show_profile(&self, profile_name:&str, config:&LandConfig) -> Result<(), String> {
355 let resolved_profile = resolve_profile_name(profile_name, config);
356
357 let profile = config
358 .profiles
359 .get(&resolved_profile)
360 .ok_or_else(|| format!("Profile '{}' not found.", resolved_profile))?;
361
362 println!("\n{}: {}", "Profile:", resolved_profile.green());
363 println!("{}\n", "=".repeat(50));
364
365 if let Some(desc) = &profile.description {
367 println!("Description: {}", desc);
368 }
369
370 if let Some(workbench) = &profile.workbench {
372 println!("\nWorkbench:");
373 println!(" Type: {}", workbench);
374
375 if let Some(wb_config) = &config.workbench {
376 if let Some(features) = &wb_config.features {
377 if let Some(wb_features) = features.get(workbench) {
378 if let Some(coverage) = &wb_features.coverage {
379 println!(" Coverage: {}", coverage);
380 }
381 if let Some(complexity) = &wb_features.complexity {
382 println!(" Complexity: {}", complexity);
383 }
384 if wb_features.polyfills.unwrap_or(false) {
385 println!(" Polyfills: enabled");
386 }
387 if wb_features.mountain_providers.unwrap_or(false) {
388 println!(" Mountain Providers: enabled");
389 }
390 if wb_features.wind_services.unwrap_or(false) {
391 println!(" Wind Services: enabled");
392 }
393 }
394 }
395 }
396 }
397
398 println!("\nEnvironment Variables:");
400 if let Some(env) = &profile.env {
401 let mut sorted_env:Vec<_> = env.iter().collect();
402 sorted_env.sort_by_key(|(k, _)| k.as_str());
403 for (key, value) in sorted_env {
404 println!(" {:<25} = {}", key, value);
405 }
406 }
407
408 if let Some(features) = &profile.features {
410 println!("\nFeatures:");
411 println!("\n Enabled:");
412 let mut sorted_features:Vec<_> = features.iter().filter(|(_, enabled)| **enabled).collect();
413 sorted_features.sort_by_key(|(k, _)| k.as_str());
414 for (feature, _) in &sorted_features {
415 println!(" {:<30}", feature.green());
416 }
417 }
418
419 let build_cmd = get_build_command(&resolved_profile, &config);
421 println!("\nBuild Command: {}", build_cmd);
422
423 if let Some(script) = &profile.rhai_script {
425 println!("\nRhai Script: {}", script);
426 }
427
428 println!();
429 Ok(())
430 }
431
432 fn execute_validate_profile(&self, profile_name:&str, config:&LandConfig) -> Result<(), String> {
434 let resolved_profile = resolve_profile_name(profile_name, config);
435
436 let profile = config
437 .profiles
438 .get(&resolved_profile)
439 .ok_or_else(|| format!("Profile '{}' not found.", resolved_profile))?;
440
441 println!("\n{}: {}", "Validating Profile:", resolved_profile.green());
442 println!("{}\n", "=".repeat(50));
443
444 let mut issues = Vec::new();
445 let mut warnings = Vec::new();
446
447 if profile.description.is_none() {
449 warnings.push("Profile has no description".to_string());
450 }
451
452 if profile.workbench.is_none() {
454 issues.push("Profile has no workbench type specified".to_string());
455 } else if let Some(workbench) = &profile.workbench {
456 if let Some(wb_config) = &config.workbench {
457 if let Some(available) = &wb_config.available {
458 if !available.contains(workbench) {
459 issues.push(format!("Workbench '{}' not defined in workbench configuration", workbench));
460 }
461 }
462 }
463 }
464
465 if profile.env.is_none() || profile.env.as_ref().unwrap().is_empty() {
467 warnings.push("Profile has no environment variables defined".to_string());
468 }
469
470 if issues.is_empty() && warnings.is_empty() {
472 println!("{}", "Profile is valid!".green());
473 } else {
474 if !warnings.is_empty() {
475 println!("\n{} Warnings:", warnings.len().to_string().yellow());
476 for warning in &warnings {
477 println!(" - {}", warning.yellow());
478 }
479 }
480 if !issues.is_empty() {
481 println!("\n{} Issues:", issues.len().to_string().red());
482 for issue in &issues {
483 println!(" - {}", issue.red());
484 }
485 }
486 }
487
488 println!();
489 Ok(())
490 }
491
492 fn execute_resolve(&self, profile_name:&str, config:&LandConfig, _format:Option<String>) -> Result<(), String> {
494 let resolved_profile = resolve_profile_name(profile_name, config);
495
496 let profile = config
497 .profiles
498 .get(&resolved_profile)
499 .ok_or_else(|| format!("Profile '{}' not found.", resolved_profile))?;
500
501 println!("\n{}: {}", "Resolved Profile:", resolved_profile.green());
502 println!("{}\n", "=".repeat(50));
503
504 if let Some(desc) = &profile.description {
506 println!("Description: {}", desc);
507 }
508
509 if let Some(workbench) = &profile.workbench {
510 println!("Workbench: {}", workbench);
511 }
512
513 if let Some(env) = &profile.env {
515 println!("\nEnvironment Variables ({}):", env.len());
516 for (key, value) in env {
517 println!(" {} = {}", key.green(), value);
518 }
519 }
520
521 if let Some(features) = &profile.features {
523 println!("\nFeatures ({}):", features.len());
524 for (feature, enabled) in features {
525 let status = if *enabled { "[X]" } else { "[ ]" };
526 println!(" {} {}", status, feature);
527 }
528 }
529
530 println!();
531 Ok(())
532 }
533}
534
535fn print_build_header(profile_name:&str, profile:&Profile) {
541 println!("\n{}", "========================================");
542 println!("Land Build: {}", profile_name);
543 println!("========================================");
544
545 if let Some(desc) = &profile.description {
546 println!("Description: {}", desc);
547 }
548
549 if let Some(workbench) = &profile.workbench {
550 println!("Workbench: {}", workbench);
551 }
552}
553
554fn print_resolved_environment(env:&HashMap<String, String>) {
556 println!("\nResolved Environment:");
557 let mut sorted_env:Vec<_> = env.iter().collect();
558 sorted_env.sort_by_key(|(k, _)| k.as_str());
559
560 for (key, value) in sorted_env {
561 let display_value = if value.is_empty() { "(empty)" } else { value };
562 println!(" {:<25} = {}", key, display_value);
563 }
564}
565
566fn parse_profile_name(s:&str) -> Result<String, String> {
568 let name = s.trim().to_lowercase();
569
570 if name.is_empty() {
571 return Err("Profile name cannot be empty".to_string());
572 }
573
574 if name.contains(' ') {
575 return Err("Profile name cannot contain spaces".to_string());
576 }
577
578 Ok(name)
579}
580
581fn resolve_profile_name(name:&str, config:&LandConfig) -> String {
583 if let Some(cli_config) = &config.cli {
584 if let Some(resolved) = cli_config.profile_aliases.get(name) {
585 return resolved.clone();
586 }
587 }
588 name.to_string()
589}
590
591fn get_build_command(profile_name:&str, config:&LandConfig) -> String {
593 let command_key = if profile_name.starts_with("production")
595 || profile_name.starts_with("release")
596 || profile_name.starts_with("web")
597 {
598 "production"
599 } else {
600 profile_name.split('-').next().unwrap_or("debug")
601 };
602
603 if let Some(build_commands) = &config.build_commands {
604 build_commands.get(command_key).cloned().unwrap_or_else(|| {
605 if profile_name.starts_with("production") {
606 "pnpm tauri build".to_string()
607 } else {
608 "pnpm tauri build --debug".to_string()
609 }
610 })
611 } else {
612 if profile_name.starts_with("production") {
614 "pnpm tauri build".to_string()
615 } else {
616 "pnpm tauri build --debug".to_string()
617 }
618 }
619}
620
621fn resolve_environment_dual_path(
644 profile:&Profile,
645 config:&LandConfig,
646 merge_env:bool,
647 cli_overrides:&[(String, String)],
648) -> HashMap<String, String> {
649 let mut env = HashMap::new();
650
651 if let Some(templates) = &config.templates {
653 for (key, value) in &templates.env {
654 env.insert(key.clone(), value.clone());
655 }
656 }
657
658 if merge_env {
660 for (key, value) in std::env::vars() {
661 if is_build_env_var(&key) {
664 env.insert(key, value);
665 }
666 }
667 }
668
669 if let Some(profile_env) = &profile.env {
671 for (key, value) in profile_env {
672 env.insert(key.clone(), value.clone());
673 }
674 }
675
676 for (key, value) in cli_overrides {
678 env.insert(key.clone(), value.clone());
679 }
680
681 env
682}
683
684fn is_build_env_var(key:&str) -> bool {
686 matches!(
687 key,
688 "Browser"
689 | "Bundle"
690 | "Clean" | "Compile"
691 | "Debug" | "Dependency"
692 | "Mountain"
693 | "Wind" | "Electron"
694 | "BrowserProxy"
695 | "NODE_ENV"
696 | "NODE_VERSION"
697 | "NODE_OPTIONS"
698 | "RUST_LOG"
699 | "AIR_LOG_JSON"
700 | "AIR_LOG_FILE"
701 | "Level" | "Name"
702 | "Prefix"
703 )
704}
705
706fn parse_key_val<K, V>(s:&str) -> Result<(K, V), String>
708where
709 K: std::str::FromStr,
710 V: std::str::FromStr,
711 K::Err: std::fmt::Display,
712 V::Err: std::fmt::Display, {
713 let pos = s.find('=').ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
714 Ok((
715 s[..pos].parse().map_err(|e| format!("key parse error: {e}"))?,
716 s[pos + 1..].parse().map_err(|e| format!("value parse error: {e}"))?,
717 ))
718}
719
720fn apply_overrides(
722 mut env:HashMap<String, String>,
723 workbench:&Option<String>,
724 node_version:&Option<String>,
725 environment:&Option<String>,
726 dependency:&Option<String>,
727) -> HashMap<String, String> {
728 if let Some(workbench) = workbench {
729 env.remove("Browser");
731 env.remove("Wind");
732 env.remove("Mountain");
733 env.remove("Electron");
734 env.remove("BrowserProxy");
735
736 env.insert(workbench.clone(), "true".to_string());
738 }
739
740 if let Some(version) = node_version {
741 env.insert("NODE_VERSION".to_string(), version.clone());
742 }
743
744 if let Some(environment) = environment {
745 env.insert("NODE_ENV".to_string(), environment.clone());
746 }
747
748 if let Some(dependency) = dependency {
749 env.insert("Dependency".to_string(), dependency.clone());
750 }
751
752 env
753}
754
755fn execute_build_command(
776 profile_name:&str,
777 config:&LandConfig,
778 env_vars:&HashMap<String, String>,
779 build_args:&[String],
780) -> Result<(), String> {
781 use std::process::Command as StdCommand;
782
783 let build_command = get_build_command(profile_name, config);
785
786 let is_debug = build_command.to_lowercase().contains("--debug");
788
789 let mut maintain_args = vec!["--".to_string()];
792
793 maintain_args.push("pnpm".to_string());
795 maintain_args.push("tauri".to_string());
796 maintain_args.push("build".to_string());
797
798 if is_debug {
799 maintain_args.push("--debug".to_string());
800 }
801
802 maintain_args.extend(build_args.iter().cloned());
804
805 println!("Executing: {}", maintain_args.join(" "));
806 println!("With environment variables:");
807 for (key, value) in env_vars.iter().take(10) {
808 println!(" {}={}", key, value);
809 }
810 if env_vars.len() > 10 {
811 println!(" ... and {} more", env_vars.len() - 10);
812 }
813
814 let maintain_binary = find_maintain_binary();
817
818 let mut cmd = StdCommand::new(&maintain_binary);
825 cmd.args(&maintain_args);
826
827 cmd.envs(env_vars.iter());
830
831 cmd.env("MAINTAIN_CLI_MERGED", "true");
833
834 cmd.stderr(std::process::Stdio::inherit())
835 .stdout(std::process::Stdio::inherit());
836
837 let status = cmd
838 .status()
839 .map_err(|e| format!("Failed to execute Maintain binary ({}): {}", maintain_binary, e))?;
840
841 if status.success() {
842 println!("\n{}", "Build completed successfully!".green());
843 Ok(())
844 } else {
845 Err(format!("Build failed with exit code: {:?}", status.code()))
846 }
847}
848
849fn find_maintain_binary() -> String {
857 use std::path::Path;
858
859 let release_path = "./Target/release/Maintain";
861 if Path::new(release_path).exists() {
862 return release_path.to_string();
863 }
864
865 let debug_path = "./Target/debug/Maintain";
867 if Path::new(debug_path).exists() {
868 return debug_path.to_string();
869 }
870
871 "maintain".to_string()
873}
874
875pub fn get_all_profiles(config:&LandConfig) -> Vec<&str> {
877 let mut profiles:Vec<&str> = config.profiles.keys().map(|s| s.as_str()).collect();
878 profiles.sort();
879 profiles
880}