use std::fmt::Display;
use anyhow::bail;
use is_terminal::IsTerminal;
use crate::{
errors::RailwayError,
util::{
progress::create_spinner_if,
prompt::{fake_select, prompt_confirm_with_default, prompt_options},
two_factor::validate_two_factor_if_enabled,
},
workspace::{Project, Workspace, workspaces},
};
use super::*;
#[derive(Parser)]
#[clap(
after_help = "Examples:\n\n railway delete --project project-id --yes --json\n railway rm --project project-id --yes --json\n railway project delete --project project-id --yes --json\n\nAutomation notes:\n Project deletion is scheduled by Railway. Treat a successful response as the deletion request being accepted.\n Non-interactive deletion requires --yes. Use --2fa-code when your account requires 2FA."
)]
pub struct Args {
#[clap(short, long)]
project: Option<String>,
#[clap(short = 'y', long = "yes")]
yes: bool,
#[clap(long)]
json: bool,
#[clap(long = "2fa-code")]
two_factor_code: Option<String>,
}
pub async fn command(args: Args) -> Result<()> {
let configs = Configs::new()?;
let client = GQLClient::new_authorized(&configs)?;
let is_terminal = std::io::stdout().is_terminal();
let all_workspaces = workspaces().await?;
let (project_id, project_name) = select_project(args.project, &all_workspaces, is_terminal)?;
if !args.yes {
if !is_terminal {
bail!(
"Cannot prompt for confirmation in non-interactive mode. Use --yes to skip confirmation."
);
}
let confirmed = prompt_confirm_with_default(
format!(
r#"Are you sure you want to delete the project "{}"? This action cannot be undone."#,
project_name.red()
)
.as_str(),
false,
)?;
if !confirmed {
println!("Deletion cancelled.");
return Ok(());
}
}
validate_two_factor_if_enabled(&client, &configs, is_terminal, args.two_factor_code).await?;
let spinner = create_spinner_if(!args.json, "Deleting project...".into());
let vars = mutations::project_schedule_delete::Variables {
id: project_id.clone(),
};
post_graphql::<mutations::ProjectScheduleDelete, _>(&client, &configs.get_backboard(), vars)
.await?;
if args.json {
println!("{}", serde_json::json!({"id": project_id}));
} else if let Some(spinner) = spinner {
spinner.finish_with_message(format!(
"{} {} {}",
"Project".green(),
project_name.magenta().bold(),
"deleted!".green()
));
}
Ok(())
}
fn select_project(
project_arg: Option<String>,
all_workspaces: &[Workspace],
is_terminal: bool,
) -> Result<(String, String)> {
let all_projects: Vec<ProjectWithWorkspace> = all_workspaces
.iter()
.flat_map(|w| {
w.projects()
.into_iter()
.filter(|p| p.deleted_at().is_none())
.map(|p| ProjectWithWorkspace {
project: p,
workspace_name: w.name().to_string(),
})
})
.collect();
if all_projects.is_empty() {
bail!(RailwayError::NoProjects);
}
if let Some(project) = project_arg {
let found = all_projects.iter().find(|p| {
p.project.id().to_lowercase() == project.to_lowercase()
|| p.project.name().to_lowercase() == project.to_lowercase()
});
if let Some(p) = found {
fake_select("Select the project to delete", &p.to_string());
return Ok((p.project.id().to_string(), p.project.name().to_string()));
} else {
bail!("Project \"{}\" not found", project);
}
}
if !is_terminal {
bail!(
"Project must be specified when not running in a terminal. Use --project <id or name>"
);
}
let selected = prompt_options("Select the project to delete", all_projects)?;
Ok((
selected.project.id().to_string(),
selected.project.name().to_string(),
))
}
#[derive(Debug, Clone)]
struct ProjectWithWorkspace {
project: Project,
workspace_name: String,
}
impl Display for ProjectWithWorkspace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", self.project.name(), self.workspace_name)
}
}