use std::path::Path;
use anyhow::{Result, anyhow, bail};
use colored::Colorize;
use rigg_core::workspace::{APIS_DIR, PROJECTS_DIR, STATE_DIR, WORKSPACE_FILE};
use crate::cli::InitArgs;
use crate::commands::discovery;
use crate::commands::{CommandError, GlobalContext};
pub async fn run(ctx: &GlobalContext, args: InitArgs) -> Result<()> {
let root = Path::new(".");
let files_sub = args
.path
.trim_end_matches('/')
.trim_start_matches("./")
.to_string();
let files_sub = (!files_sub.is_empty() && files_sub != ".").then_some(files_sub);
let files_root = match &files_sub {
Some(sub) => root.join(sub),
None => root.to_path_buf(),
};
let ws_file = root.join(WORKSPACE_FILE);
if ws_file.exists() {
bail!(
"{} already exists — this is already a rigg workspace",
ws_file.display()
);
}
let (search, foundry) = if args.search_service.is_some() || args.foundry_account.is_some() {
if args.foundry_account.is_some() != args.foundry_project.is_some() {
return Err(anyhow!(CommandError::Usage(
"--foundry-account and --foundry-project must be given together".to_string()
)));
}
(
args.search_service.clone(),
args.foundry_account
.clone()
.zip(args.foundry_project.clone()),
)
} else if args.no_discovery || !ctx.interactive() {
return Err(anyhow!(CommandError::Usage(
"in non-interactive mode pass --search-service and/or --foundry-account/--foundry-project".to_string()
)));
} else {
discovery::discover_interactive(ctx.no_color).await?
};
if search.is_none() && foundry.is_none() {
return Err(anyhow!(CommandError::Usage(
"nothing to manage: no search service or foundry project selected".to_string()
)));
}
println!();
println!("{}", "Identity guidance".bold());
println!(
" For stacks spanning services (search + storage + foundry), a USER-ASSIGNED managed\n\
\x20 identity is recommended: one identity for the whole pipeline, role assignments\n\
\x20 survive service re-creation, and it works across environments. Use system-assigned\n\
\x20 for simple single-service setups. `rigg auth doctor` (0.19) will verify the wiring."
);
let mut yaml = String::new();
yaml.push_str("# Rigg workspace configuration.\n");
if let Some(sub) = &files_sub {
yaml.push_str(&format!(
"# Resource definitions live in {sub}/projects/<name>/ — see `rigg new project`.\n"
));
yaml.push_str(&format!("root: {sub}\n"));
} else {
yaml.push_str(
"# Resource definitions live in projects/<name>/ — see `rigg new project`.\n",
);
}
yaml.push_str("environments:\n");
yaml.push_str(&format!(" {}:\n", args.env_name));
yaml.push_str(" default: true\n");
if let Some(service) = &search {
yaml.push_str(&format!(" search: {{ service: {service} }}\n"));
}
if let Some((account, project)) = &foundry {
yaml.push_str(&format!(
" foundry: {{ account: {account}, project: {project} }}\n"
));
}
std::fs::write(&ws_file, yaml)?;
std::fs::create_dir_all(files_root.join(PROJECTS_DIR))?;
std::fs::create_dir_all(files_root.join(APIS_DIR))?;
let state_ignore = match &files_sub {
Some(sub) => format!("{sub}/{STATE_DIR}/"),
None => format!("{STATE_DIR}/"),
};
let gitignore = root.join(".gitignore");
let mut gi = if gitignore.exists() {
std::fs::read_to_string(&gitignore)?
} else {
String::new()
};
if !gi.lines().any(|l| l.trim() == state_ignore) {
if !gi.is_empty() && !gi.ends_with('\n') {
gi.push('\n');
}
gi.push_str(&state_ignore);
gi.push('\n');
std::fs::write(&gitignore, gi)?;
}
println!();
println!("{} rigg workspace initialized", "✓".green().bold());
println!(" config: {}", ws_file.display());
if let Some(sub) = &files_sub {
println!(" files: {sub}/ (projects, apis, state)");
}
if let Some(s) = &search {
println!(" search: {s}");
}
if let Some((a, p)) = &foundry {
println!(" foundry: {a}/{p}");
}
println!(
" environment: {} (default) — rigg commands target it unless -e/RIGG_ENV say otherwise; \
add more with `rigg env add`",
args.env_name
);
println!();
println!("Next steps:");
println!(" rigg new project <name> # create your first project");
println!(" rigg new pipeline <name> -p <p> # scaffold an explicit RAG pipeline");
println!(" rigg adopt <project> <selector> # or adopt existing Azure resources");
Ok(())
}