mod agents;
mod collect;
mod credentials;
mod execute;
mod explain;
mod report;
use std::collections::HashMap;
use anyhow::Result;
use colored::Colorize;
use tracing::info;
use rigg_client::AzureSearchClient;
use rigg_core::resources::ResourceKind;
use rigg_core::resources::managed;
use rigg_core::service::ServiceDomain;
use rigg_core::state::Checksums;
use rigg_diff::Change;
use crate::cli::ResourceTypeFlags;
use crate::commands::common::{order_by_dependencies, resolve_resource_selection_from_flags};
use crate::commands::confirm::prompt_yes_no;
use crate::commands::load_config_and_env;
use collect::{build_local_managed_map, collect_push_resource};
pub async fn run(
flags: &ResourceTypeFlags,
recursive: bool,
filter: Option<String>,
force: bool,
no_explain: bool,
env_override: Option<&str>,
) -> Result<()> {
let (project_root, config, env) = load_config_and_env(env_override)?;
let files_root = config.files_root(&project_root);
let use_explain = !no_explain && crate::commands::ai::is_ai_active();
let selection = resolve_resource_selection_from_flags(flags, env.sync.include_preview, false);
if selection.is_empty() {
println!("No resource types specified. Use --all or specify types (e.g., --indexes)");
return Ok(());
}
let kinds = selection.kinds();
let search_kinds: Vec<ResourceKind> = kinds
.iter()
.filter(|k| k.domain() == ServiceDomain::Search)
.copied()
.collect();
let has_foundry_kinds = kinds.iter().any(|k| k.domain() == ServiceDomain::Foundry);
let primary_search_svc = env.primary_search_service();
let server_name = primary_search_svc
.map(|s| s.name.as_str())
.unwrap_or("(none)");
let checksums = Checksums::load_env(&project_root, &env.name).unwrap_or_default();
let mut resources_to_push = Vec::new();
let mut validation_errors = Vec::new();
let mut recreate_candidates: Vec<(ResourceKind, String)> = Vec::new();
let mut total_unchanged = 0;
let mut change_details: HashMap<(ResourceKind, String), Vec<Change>> = HashMap::new();
let mut remote_values: HashMap<(ResourceKind, String), serde_json::Value> = HashMap::new();
let mut remote_conflicts: Vec<(ResourceKind, String)> = Vec::new();
if !search_kinds.is_empty() {
let search_svc = primary_search_svc
.ok_or_else(|| anyhow::anyhow!("No search service in environment '{}'", env.name))?;
eprintln!("Comparing local resources against {}...", server_name);
let client = AzureSearchClient::from_service_config(search_svc)?;
info!(
"Connected to {} using {}",
server_name,
client.auth_method()
);
let service_dir = env.search_service_dir(&files_root, search_svc);
let managed_map = build_local_managed_map(&service_dir);
let has_ks = search_kinds.contains(&ResourceKind::KnowledgeSource);
for kind in &search_kinds {
if *kind == ResourceKind::KnowledgeSource {
let ks_base = service_dir.join("agentic-retrieval/knowledge-sources");
if !ks_base.exists() {
continue;
}
for entry in std::fs::read_dir(&ks_base)? {
let entry = entry?;
let path = entry.path();
if !path.is_dir() {
continue;
}
let ks_name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_string(),
None => continue,
};
if let Some(exact_name) = selection.name_filter(*kind) {
if ks_name != exact_name {
continue;
}
}
if let Some(ref pattern) = filter {
if !ks_name.contains(pattern) {
continue;
}
}
let ks_file = path.join(format!("{}.json", ks_name));
if !ks_file.exists() {
continue;
}
let content = std::fs::read_to_string(&ks_file)?;
let local: serde_json::Value = serde_json::from_str(&content)?;
let push_count_before = resources_to_push.len();
collect_push_resource(
&client,
*kind,
&ks_name,
local,
&mut resources_to_push,
&mut validation_errors,
&mut recreate_candidates,
&mut total_unchanged,
&mut change_details,
&mut remote_values,
&checksums,
&mut remote_conflicts,
)
.await;
let ks_is_new = resources_to_push.get(push_count_before).is_some_and(
|(k, n, _, exists)| {
*k == ResourceKind::KnowledgeSource && n == &ks_name && !exists
},
);
if has_ks && !ks_is_new {
let managed_subs = managed::read_managed_sub_resources(&path, &ks_name);
for (sub_kind, sub_name, sub_def) in managed_subs {
collect_push_resource(
&client,
sub_kind,
&sub_name,
sub_def,
&mut resources_to_push,
&mut validation_errors,
&mut recreate_candidates,
&mut total_unchanged,
&mut change_details,
&mut remote_values,
&checksums,
&mut remote_conflicts,
)
.await;
}
}
}
continue;
}
let resource_dir = service_dir.join(kind.directory_name());
if !resource_dir.exists() {
continue;
}
for entry in std::fs::read_dir(&resource_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
let name = path
.file_stem()
.and_then(|n| n.to_str())
.ok_or_else(|| anyhow::anyhow!("Invalid file name"))?;
if let Some(ks_name) = managed::managing_ks(&managed_map, *kind, name) {
info!(
"Skipping managed {} '{}' (owned by knowledge source '{}') — use --knowledgesources to push",
kind.display_name(),
name,
ks_name
);
continue;
}
if let Some(exact_name) = selection.name_filter(*kind) {
if name != exact_name {
continue;
}
}
if let Some(ref pattern) = filter {
if !name.contains(pattern) {
continue;
}
}
let content = std::fs::read_to_string(&path)?;
let local: serde_json::Value = serde_json::from_str(&content)?;
collect_push_resource(
&client,
*kind,
name,
local,
&mut resources_to_push,
&mut validation_errors,
&mut recreate_candidates,
&mut total_unchanged,
&mut change_details,
&mut remote_values,
&checksums,
&mut remote_conflicts,
)
.await;
}
}
report::handle_recreate_candidates(
&recreate_candidates,
&mut resources_to_push,
&service_dir,
force,
)?;
}
if has_foundry_kinds && env.has_foundry() {
agents::collect_foundry_agents(
&env,
&files_root,
&selection,
&filter,
&mut resources_to_push,
&mut total_unchanged,
&mut change_details,
&mut remote_values,
&checksums,
&mut remote_conflicts,
)
.await?;
}
if recursive && !resources_to_push.is_empty() {
let initial_names: std::collections::HashSet<(ResourceKind, String)> = resources_to_push
.iter()
.map(|(k, n, _, _)| (*k, n.clone()))
.collect();
let all_kinds = if env.sync.include_preview {
ResourceKind::all().to_vec()
} else {
ResourceKind::stable().to_vec()
};
let mut all_local = Vec::new();
let recurse_search_svc = env.primary_search_service();
for k in &all_kinds {
let dir = if k.domain() == ServiceDomain::Search {
if let Some(svc) = recurse_search_svc {
env.search_service_dir(&files_root, svc)
.join(k.directory_name())
} else {
continue;
}
} else {
continue;
};
if !dir.exists() {
continue;
}
for entry in std::fs::read_dir(&dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
let n = path
.file_stem()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
if let Ok(content) = std::fs::read_to_string(&path) {
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&content) {
all_local.push((*k, n, val));
}
}
}
}
let selected: Vec<_> = resources_to_push
.iter()
.map(|(k, n, v, _)| (*k, n.clone(), v.clone()))
.collect();
let expanded = rigg_core::copy::expand_recursive(&selected, &all_local);
for (k, n, v) in expanded {
if !initial_names.contains(&(k, n.clone())) {
println!(
" {} {} '{}' (included by --recursive)",
"+".green(),
k.display_name(),
n
);
resources_to_push.push((k, n, v, false));
}
}
}
if !validation_errors.is_empty() {
println!("Validation errors:");
for error in &validation_errors {
println!(" {}", error);
}
println!();
anyhow::bail!(
"Push blocked: {} validation error(s). Fix the issues above and try again.",
validation_errors.len()
);
}
if resources_to_push.is_empty() {
if total_unchanged > 0 {
println!(
"All {} resource(s) match the server, nothing to push.",
total_unchanged
);
} else {
println!("No local resources found to push.");
}
return Ok(());
}
let used_ai_narrative = if use_explain {
explain::generate_push_narrative(
&resources_to_push,
&change_details,
&remote_values,
total_unchanged,
)
.await
.map(|narrative| {
println!("{}", narrative);
})
.is_some()
} else {
false
};
if !used_ai_narrative {
report::print_push_plan(&resources_to_push, &change_details, total_unchanged);
}
println!();
report::print_conflict_warnings(&remote_conflicts, &resources_to_push);
if !force && !prompt_yes_no("Proceed with push?")? {
println!("Aborted.");
return Ok(());
}
let ordered = order_by_dependencies(&resources_to_push);
let search_resources: Vec<_> = ordered
.iter()
.filter(|(k, _, _, _)| k.domain() == ServiceDomain::Search)
.collect();
let foundry_resources: Vec<_> = ordered
.iter()
.filter(|(k, _, _, _)| k.domain() == ServiceDomain::Foundry)
.collect();
let mut success_count = 0;
let mut error_count = 0;
let mut pushed_resources: Vec<(ResourceKind, String)> = Vec::new();
let mut cached_connection_string: Option<String> = None;
if !search_resources.is_empty() {
let (s, e, pushed) = execute::push_search_resources(
&search_resources,
&recreate_candidates,
&env,
&mut cached_connection_string,
)
.await?;
success_count += s;
error_count += e;
pushed_resources.extend(pushed);
}
if !foundry_resources.is_empty() && env.has_foundry() {
let (s, e, pushed) = execute::push_foundry_agents(&foundry_resources, &env).await?;
success_count += s;
error_count += e;
pushed_resources.extend(pushed);
}
if !pushed_resources.is_empty() {
execute::pullback_synced_resources(&pushed_resources, &project_root, &files_root, &env)
.await?;
}
println!();
println!(
"Push complete: {} succeeded, {} failed",
success_count, error_count
);
if error_count > 0 {
std::process::exit(1);
}
Ok(())
}