foundry_mcp/core/ops/
delete_spec.rs1use anyhow::{Context, Result};
4
5use crate::core::foundry;
6use crate::types::responses::{DeleteSpecResponse, FoundryResponse, ValidationStatus};
7
8#[derive(Debug, Clone)]
9pub struct Input {
10 pub project_name: String,
11 pub spec_name: String,
12 pub confirm: String,
13}
14
15pub async fn run(input: Input) -> Result<FoundryResponse<DeleteSpecResponse>> {
16 let foundry = foundry::get_default_foundry()?;
17
18 validate_args(&input)?;
19 validate_project_exists(&foundry, &input.project_name).await?;
20
21 let response_data = match foundry
23 .load_spec(&input.project_name, &input.spec_name)
24 .await
25 {
26 Ok(spec) => {
27 let files_to_delete = vec![
28 format!("{}/spec.md", spec.name),
29 format!("{}/task-list.md", spec.name),
30 format!("{}/notes.md", spec.name),
31 ];
32
33 if input.confirm.to_lowercase() != "true" {
34 return Err(anyhow::anyhow!(
35 "Deletion not confirmed. Set --confirm true to proceed with deleting spec '{}' and all its files. Got: '{}'",
36 input.spec_name,
37 input.confirm
38 ));
39 }
40
41 foundry
42 .delete_spec(&input.project_name, &input.spec_name)
43 .await
44 .with_context(|| format!("Failed to delete spec '{}'", input.spec_name))?;
45
46 DeleteSpecResponse {
47 project_name: input.project_name.clone(),
48 spec_name: input.spec_name.clone(),
49 spec_path: format!(
50 "~/.foundry/{}/specs/{}",
51 input.project_name, input.spec_name
52 ),
53 files_deleted: files_to_delete,
54 }
55 }
56 Err(_) => {
57 return Err(anyhow::anyhow!(
58 "Spec '{}' not found in project '{}'. Use 'mcp_foundry_load_project {}' to see available specs.",
59 input.spec_name,
60 input.project_name,
61 input.project_name
62 ));
63 }
64 };
65
66 Ok(FoundryResponse {
67 data: response_data,
68 next_steps: generate_next_steps(&input),
69 validation_status: ValidationStatus::Complete,
70 workflow_hints: generate_workflow_hints(&input),
71 })
72}
73
74fn validate_args(input: &Input) -> Result<()> {
75 if input.project_name.trim().is_empty() {
76 return Err(anyhow::anyhow!("Project name cannot be empty"));
77 }
78 if input.spec_name.trim().is_empty() {
79 return Err(anyhow::anyhow!("Spec name cannot be empty"));
80 }
81 if !input.spec_name.contains('_') {
82 return Err(anyhow::anyhow!(
83 "Invalid spec name format '{}'. Expected format: YYYYMMDD_HHMMSS_feature_name",
84 input.spec_name
85 ));
86 }
87 Ok(())
88}
89
90async fn validate_project_exists(
91 foundry: &foundry::Foundry<crate::core::backends::filesystem::FilesystemBackend>,
92 project_name: &str,
93) -> Result<()> {
94 if !foundry.project_exists(project_name).await? {
95 return Err(anyhow::anyhow!(
96 "Project '{}' not found. Use 'mcp_foundry_list_projects' to see available projects.",
97 project_name
98 ));
99 }
100 Ok(())
101}
102
103fn generate_next_steps(input: &Input) -> Vec<String> {
104 vec![
105 format!(
106 "Successfully deleted spec '{}' from project '{}'",
107 input.spec_name, input.project_name
108 ),
109 "All spec files have been permanently removed".to_string(),
110 format!(
111 "You can view remaining specs: mcp_foundry_load_project {}",
112 input.project_name
113 ),
114 format!(
115 "You can create a new spec: mcp_foundry_create_spec {} <feature_name>",
116 input.project_name
117 ),
118 "Deletion cannot be undone - you might consider backing up important specs before deletion"
119 .to_string(),
120 ]
121}
122
123fn generate_workflow_hints(input: &Input) -> Vec<String> {
124 vec![
125 format!("Deleted spec: {}", input.spec_name),
126 "This action cannot be undone".to_string(),
127 "All associated files (spec.md, task-list.md, notes.md) have been removed".to_string(),
128 "You can use 'mcp_foundry_list_projects' to see project status after deletion".to_string(),
129 "You might consider archiving completed specs rather than deleting for future reference"
130 .to_string(),
131 "You can create new specs to continue feature development".to_string(),
132 ]
133}