use std::path::PathBuf; use anyhow::Result; use serde_json::{json, Value}; use crate::client::{ authenticated_or_local_trusted_request, list_attached_nodes_if_available_with_session, list_task_events_if_available_with_session, stored_session_for_coordinator, JsonLineSession, }; use crate::config::{ effective_project_scope, effective_scope_value, project_config_file, read_cli_session, read_project_config, write_project_config, ProjectConfig, StoredCliSession, }; use crate::process::process_list_report_with_session; use crate::process_events::project_quota_posture; use crate::{ bundle_inspection, discovered_environment_names, BundleInspectArgs, ProcessListArgs, ProjectInitArgs, ProjectListArgs, ProjectSelectArgs, ProjectStatusArgs, }; pub(crate) fn project_init_report(args: ProjectInitArgs, cwd: PathBuf) -> Result { let stored_session = read_cli_session(&cwd)?; let tenant = session_or_effective_scope_value( stored_session.as_ref(), &args.scope.tenant, |session| session.tenant.as_str(), "tenant", ); let project = args.new_project.clone(); let user = session_or_effective_scope_value( stored_session.as_ref(), &args.scope.user, |session| session.user.as_str(), "user", ); let coordinator = args.scope.coordinator.clone().or_else(|| { stored_session .as_ref() .map(|session| session.coordinator.clone()) }); let name = args.name.clone(); let config = ProjectConfig { tenant: tenant.clone(), project: project.clone(), user: user.clone(), coordinator: coordinator.clone(), }; let config_file = project_config_file(&cwd); if config_file.exists() && !args.yes { anyhow::bail!( "{} already exists; rerun with --yes to update the project link", config_file.display() ); } let mut coordinator_session_requests = 0; let coordinator_response = if let Some(coordinator) = &coordinator { let mut session = JsonLineSession::connect(coordinator)?; let request = authenticated_or_local_trusted_request( coordinator, stored_session.as_ref(), json!({ "type": "create_project", "project": project, "name": name, }), json!({ "type": "create_project", "tenant": tenant, "actor_user": user, "project": project, "name": name, }), )?; let response = session.request(request)?; coordinator_session_requests = session.requests(); Some(response) } else { None }; write_project_config(&cwd, &config)?; let created_or_linked_project = coordinator_response .as_ref() .and_then(|response| response.get("project")) .cloned() .unwrap_or_else(|| { json!({ "id": config.project.clone(), "tenant": config.tenant.clone(), "name": args.name.clone(), }) }); Ok(json!({ "command": "project init", "source": if coordinator.is_some() { "public_coordinator_api" } else { "local_project_config" }, "private_website_required": false, "project_config_written": true, "project_config_write_after_coordinator_acceptance": coordinator.is_some(), "coordinator_create_before_local_write": coordinator.is_some(), "coordinator_session_requests": coordinator_session_requests, "created_or_linked_project": created_or_linked_project, "current_directory_link": { "cwd": cwd, "config_file": config_file, "config_format": "clusterflux_project_config_v1", "links_current_directory": true, "writes_current_directory_only": true, "private_website_required": false, }, "safe_defaults": { "tenant": config.tenant.clone(), "project": config.project.clone(), "user": config.user.clone(), "coordinator": config.coordinator.clone(), "project_name": args.name.clone(), "default_project_id_used": args.new_project == "project", "default_project_name_used": args.name == "Clusterflux Project", "browser_interaction_required": false, "private_website_required": false, }, "project_config": config, "config_file": project_config_file(&cwd), "coordinator_response": coordinator_response, })) } pub(crate) fn project_status_report(args: ProjectStatusArgs, cwd: PathBuf) -> Result { let config = read_project_config(&cwd)?; let stored_session = read_cli_session(&cwd)?; let mut effective_scope = effective_project_scope(&args.scope, config.as_ref()); if effective_scope.coordinator.is_none() { effective_scope.coordinator = stored_session .as_ref() .filter(|session| session.session_secret.is_some()) .map(|session| session.coordinator.clone()); } if let (Some(config), Some(session)) = (config.as_ref(), stored_session.as_ref()) { let same_coordinator = effective_scope .coordinator .as_deref() .is_some_and(|coordinator| { crate::client::control_endpoint_identity(coordinator).ok() == crate::client::control_endpoint_identity(&session.coordinator).ok() }); if same_coordinator && (session.tenant != config.tenant || session.project != config.project || session.user != config.user) { anyhow::bail!( "stored CLI session is for {}/{}/{} but this workspace is configured for {}/{}/{}; run `clusterflux login --browser` from this workspace", session.tenant, session.project, session.user, config.tenant, config.project, config.user, ); } } if let Some(bound_session) = effective_scope .coordinator .as_deref() .and_then(|coordinator| { stored_session_for_coordinator(coordinator, stored_session.as_ref()) }) { effective_scope.tenant = bound_session.tenant.clone(); effective_scope.project = bound_session.project.clone(); effective_scope.user = bound_session.user.clone(); } let inspection = bundle_inspection( BundleInspectArgs { project: Some(cwd.clone()), source_provider: None, disabled_source_providers: Vec::new(), json: true, }, cwd.clone(), ) .ok(); let coordinator = effective_scope.coordinator.clone(); let attached_nodes = list_attached_nodes_if_available_with_session( coordinator.as_deref(), &effective_scope, stored_session.as_ref(), )?; let coordinator_response = list_task_events_if_available_with_session( coordinator.as_deref(), &effective_scope, None, stored_session.as_ref(), )?; let process_report = process_list_report_with_session( ProcessListArgs { scope: effective_scope.clone(), }, stored_session.as_ref(), )?; let discovered_environments = discovered_environment_names(inspection.as_ref()); let active_process = process_report .get("processes") .and_then(Value::as_array) .and_then(|processes| processes.first()) .and_then(|process| process.get("process")) .and_then(Value::as_str) .map(str::to_owned) .unwrap_or_else(|| { if process_report.get("status").and_then(Value::as_str) == Some("ok") { "none".to_owned() } else { "unknown_without_coordinator".to_owned() } }); let quota_posture = project_quota_posture(&attached_nodes, coordinator_response.as_ref()); Ok(json!({ "command": "project status", "cwd": cwd, "tenant": effective_scope.tenant, "project": effective_scope.project, "user": effective_scope.user, "coordinator": coordinator, "project_identity": { "tenant": effective_scope.tenant, "project": effective_scope.project, "user": effective_scope.user, "source": if config.is_some() { "project_config_with_cli_overrides" } else { "cli_scope" } }, "project_config": config, "bundle": inspection, "discovered_environments": discovered_environments, "active_process": active_process, "processes": process_report.get("processes").cloned().unwrap_or_else(|| json!([])), "attached_nodes": attached_nodes, "quota_posture": quota_posture, "coordinator_response": coordinator_response, })) } pub(crate) fn project_list_report(args: ProjectListArgs, cwd: PathBuf) -> Result { let stored_session = read_cli_session(&cwd)?; let coordinator = args.scope.coordinator.clone().or_else(|| { stored_session .as_ref() .map(|session| session.coordinator.clone()) }); let tenant = session_or_effective_scope_value( stored_session.as_ref(), &args.scope.tenant, |session| session.tenant.as_str(), "tenant", ); let user = session_or_effective_scope_value( stored_session.as_ref(), &args.scope.user, |session| session.user.as_str(), "user", ); if let Some(coordinator) = &coordinator { let mut session = JsonLineSession::connect(coordinator)?; let request = authenticated_or_local_trusted_request( coordinator, stored_session.as_ref(), json!({ "type": "list_projects", }), json!({ "type": "list_projects", "tenant": tenant, "actor_user": user, }), )?; let response = session.request(request)?; let projects = response .get("projects") .cloned() .unwrap_or_else(|| json!([])); let project_count = projects.as_array().map(Vec::len).unwrap_or(0); return Ok(json!({ "command": "project list", "source": "public_coordinator_api", "coordinator": coordinator, "tenant": tenant, "user": user, "projects": projects, "project_count": project_count, "private_website_required": false, "response": response, "coordinator_session_requests": session.requests(), })); } let projects = read_project_config(&cwd)?.into_iter().collect::>(); let project_count = projects.len(); Ok(json!({ "command": "project list", "source": "local_project_config", "projects": projects, "project_count": project_count, "private_website_required": false, })) } pub(crate) fn project_select_report(args: ProjectSelectArgs, cwd: PathBuf) -> Result { let stored_session = read_cli_session(&cwd)?; let tenant = session_or_effective_scope_value( stored_session.as_ref(), &args.scope.tenant, |session| session.tenant.as_str(), "tenant", ); let user = session_or_effective_scope_value( stored_session.as_ref(), &args.scope.user, |session| session.user.as_str(), "user", ); let coordinator = args.scope.coordinator.clone().or_else(|| { stored_session .as_ref() .map(|session| session.coordinator.clone()) }); let config = ProjectConfig { tenant: tenant.clone(), project: args.selected_project.clone(), user: user.clone(), coordinator: coordinator.clone(), }; let coordinator_response = if let Some(coordinator) = &coordinator { let mut session = JsonLineSession::connect(coordinator)?; let request = authenticated_or_local_trusted_request( coordinator, stored_session.as_ref(), json!({ "type": "select_project", "project": args.selected_project, }), json!({ "type": "select_project", "tenant": tenant, "actor_user": user, "project": args.selected_project, }), )?; Some(session.request(request)?) } else { None }; write_project_config(&cwd, &config)?; let selected_project = coordinator_response .as_ref() .and_then(|response| response.get("project")) .cloned() .unwrap_or_else(|| { json!({ "id": config.project.clone(), "tenant": config.tenant.clone(), "name": config.project.clone(), }) }); Ok(json!({ "command": "project select", "source": if coordinator.is_some() { "public_coordinator_api" } else { "local_project_config" }, "selected_project": selected_project, "project_config_written": true, "private_website_required": false, "project_config": config, "coordinator_response": coordinator_response, })) } fn session_or_effective_scope_value( stored_session: Option<&StoredCliSession>, cli_value: &str, session_value: impl FnOnce(&StoredCliSession) -> &str, default_value: &str, ) -> String { if let Some(session) = stored_session.filter(|session| session.session_secret.is_some()) { session_value(session).to_owned() } else { effective_scope_value(cli_value, stored_session.map(session_value), default_value) } }