use crate::error::{CliError, Result};
use colored::Colorize;
use lmrc_config_validator::LmrcConfig;
use lmrc_pipeline::steps::BootstrapInitStep;
use lmrc_pipeline::{Pipeline, PipelineContext};
use std::fs;
use std::path::PathBuf;
use toml_edit::{DocumentMut, Item, Table};
pub async fn execute() -> Result<()> {
println!("{}", "LMRC Stack Bootstrap".bright_blue().bold());
println!("{}", "=".repeat(60).bright_blue());
println!();
validate_lmrc_workspace()?;
check_not_bootstrapped()?;
let env_file = check_env_bootstrap_file()?;
let (gitlab_url, gitlab_token, gitlab_project) = load_required_env_vars(&env_file)?;
let config = LmrcConfig::from_file(&PathBuf::from("lmrc.toml"))
.map_err(|e| CliError::Config(format!("Failed to load lmrc.toml: {}", e)))?;
println!(
"{} Starting bootstrap for project: {}",
"→".bright_blue(),
config.project.name.bright_white()
);
println!();
let bootstrap_step = BootstrapInitStep::new(env_file, gitlab_url, gitlab_token, gitlab_project);
let ctx = PipelineContext::new(config)
.map_err(|e| CliError::Pipeline(format!("Failed to create pipeline context: {}", e)))?;
Pipeline::new(ctx)
.add_step(bootstrap_step)
.run()
.await
.map_err(|e| CliError::Pipeline(format!("Bootstrap failed: {}", e)))?;
mark_as_bootstrapped()?;
println!();
println!("{}", "=".repeat(60).bright_green());
println!(
"{}",
"Bootstrap completed successfully!".bright_green().bold()
);
println!("{}", "=".repeat(60).bright_green());
println!();
println!("Your project is now initialized and pushed to GitLab.");
println!(
"The {} field has been added to Cargo.toml to prevent re-running bootstrap.",
"package.metadata.lmrc.bootstrapped".bright_white()
);
println!();
Ok(())
}
fn validate_lmrc_workspace() -> Result<()> {
println!("{} Validating LMRC workspace...", "→".bright_blue());
if !PathBuf::from("lmrc.toml").exists() {
return Err(CliError::Config(
"lmrc.toml not found. This command must be run from an LMRC-generated project root."
.to_string(),
));
}
if !PathBuf::from("Cargo.toml").exists() {
return Err(CliError::Config(
"Cargo.toml not found. This command must be run from an LMRC-generated project root."
.to_string(),
));
}
if !PathBuf::from("infra/pipeline").exists() {
return Err(CliError::Config(
"infra/pipeline directory not found. This doesn't appear to be an LMRC-generated project."
.to_string(),
));
}
let cargo_toml_content = fs::read_to_string("Cargo.toml")
.map_err(|e| CliError::IoError(format!("Failed to read Cargo.toml: {}", e)))?;
let doc = cargo_toml_content
.parse::<DocumentMut>()
.map_err(|e| CliError::Config(format!("Failed to parse Cargo.toml: {}", e)))?;
if !doc.contains_key("workspace") {
return Err(CliError::Config(
"Cargo.toml doesn't have [workspace] section. This doesn't appear to be an LMRC-generated project."
.to_string(),
));
}
println!(" {} Valid LMRC workspace detected", "✓".bright_green());
Ok(())
}
fn check_not_bootstrapped() -> Result<()> {
println!("{} Checking bootstrap status...", "→".bright_blue());
let cargo_toml_content = fs::read_to_string("Cargo.toml")
.map_err(|e| CliError::IoError(format!("Failed to read Cargo.toml: {}", e)))?;
let doc = cargo_toml_content
.parse::<DocumentMut>()
.map_err(|e| CliError::Config(format!("Failed to parse Cargo.toml: {}", e)))?;
if let Some(package) = doc.get("package")
&& let Some(metadata) = package.get("metadata")
&& let Some(lmrc) = metadata.get("lmrc")
&& let Some(bootstrapped) = lmrc.get("bootstrapped")
&& bootstrapped.as_bool() == Some(true)
{
return Err(CliError::Config(
"Project has already been bootstrapped. The 'package.metadata.lmrc.bootstrapped' field is set to true in Cargo.toml.\n\
If you need to re-run bootstrap, manually remove this field from Cargo.toml."
.to_string(),
));
}
println!(" {} Project not yet bootstrapped", "✓".bright_green());
Ok(())
}
fn check_env_bootstrap_file() -> Result<PathBuf> {
println!("{} Checking for .env.bootstrap file...", "→".bright_blue());
let env_file = PathBuf::from(".env.bootstrap");
if !env_file.exists() {
return Err(CliError::Config(
".env.bootstrap file not found.\n\n\
Please create a .env.bootstrap file with your CI/CD variables:\n\n\
# Required variables:\n\
GITLAB_URL=https://gitlab.com\n\
GITLAB_TOKEN=your-gitlab-token\n\
GITLAB_PROJECT=your-org/your-project\n\n\
# Infrastructure tokens:\n\
HETZNER_API_TOKEN=your-hetzner-token\n\
CLOUDFLARE_API_TOKEN=your-cloudflare-token\n\n\
# Add other variables as needed...\n"
.to_string(),
));
}
println!(" {} Found .env.bootstrap file", "✓".bright_green());
Ok(env_file)
}
fn load_required_env_vars(env_file: &PathBuf) -> Result<(String, String, String)> {
println!(
"{} Loading required variables from .env.bootstrap...",
"→".bright_blue()
);
let content = fs::read_to_string(env_file)
.map_err(|e| CliError::IoError(format!("Failed to read .env.bootstrap: {}", e)))?;
let mut gitlab_url = None;
let mut gitlab_token = None;
let mut gitlab_project = None;
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
let key = key.trim();
let value = value.trim();
let value = if (value.starts_with('"') && value.ends_with('"'))
|| (value.starts_with('\'') && value.ends_with('\''))
{
&value[1..value.len() - 1]
} else {
value
};
match key {
"GITLAB_URL" => gitlab_url = Some(value.to_string()),
"GITLAB_TOKEN" => gitlab_token = Some(value.to_string()),
"GITLAB_PROJECT" => gitlab_project = Some(value.to_string()),
_ => {}
}
}
}
let gitlab_url = gitlab_url
.ok_or_else(|| CliError::Config("GITLAB_URL not found in .env.bootstrap".to_string()))?;
let gitlab_token = gitlab_token
.ok_or_else(|| CliError::Config("GITLAB_TOKEN not found in .env.bootstrap".to_string()))?;
let gitlab_project = gitlab_project.ok_or_else(|| {
CliError::Config("GITLAB_PROJECT not found in .env.bootstrap".to_string())
})?;
println!(
" {} GITLAB_URL: {}",
"✓".bright_green(),
gitlab_url.bright_white()
);
println!(
" {} GITLAB_PROJECT: {}",
"✓".bright_green(),
gitlab_project.bright_white()
);
println!(
" {} GITLAB_TOKEN: {}",
"✓".bright_green(),
"***hidden***".dimmed()
);
Ok((gitlab_url, gitlab_token, gitlab_project))
}
fn mark_as_bootstrapped() -> Result<()> {
println!("{} Marking project as bootstrapped...", "→".bright_blue());
let cargo_toml_path = PathBuf::from("Cargo.toml");
let cargo_toml_content = fs::read_to_string(&cargo_toml_path)
.map_err(|e| CliError::IoError(format!("Failed to read Cargo.toml: {}", e)))?;
let mut doc = cargo_toml_content
.parse::<DocumentMut>()
.map_err(|e| CliError::Config(format!("Failed to parse Cargo.toml: {}", e)))?;
if !doc.contains_key("package") {
doc["package"] = Item::Table(Table::new());
}
let package = doc["package"].as_table_mut().ok_or_else(|| {
CliError::Config("Invalid Cargo.toml: package is not a table".to_string())
})?;
if !package.contains_key("metadata") {
package["metadata"] = Item::Table(Table::new());
}
let metadata = package["metadata"].as_table_mut().ok_or_else(|| {
CliError::Config("Invalid Cargo.toml: package.metadata is not a table".to_string())
})?;
if !metadata.contains_key("lmrc") {
metadata["lmrc"] = Item::Table(Table::new());
}
let lmrc = metadata["lmrc"].as_table_mut().ok_or_else(|| {
CliError::Config("Invalid Cargo.toml: package.metadata.lmrc is not a table".to_string())
})?;
lmrc["bootstrapped"] = Item::Value(toml_edit::Value::Boolean(toml_edit::Formatted::new(true)));
fs::write(&cargo_toml_path, doc.to_string())
.map_err(|e| CliError::IoError(format!("Failed to write Cargo.toml: {}", e)))?;
println!(
" {} Added package.metadata.lmrc.bootstrapped = true to Cargo.toml",
"✓".bright_green()
);
Ok(())
}