foundry_mcp/cli/commands/
delete_spec.rs

1//! Implementation of the delete_spec command
2
3use crate::cli::args::DeleteSpecArgs;
4use crate::core::{project, spec};
5use crate::types::responses::{DeleteSpecResponse, FoundryResponse, ValidationStatus};
6use anyhow::{Context, Result};
7use std::fs;
8
9pub async fn execute(args: DeleteSpecArgs) -> Result<FoundryResponse<DeleteSpecResponse>> {
10    // Validate inputs
11    validate_args(&args)?;
12
13    // Validate project exists
14    validate_project_exists(&args.project_name)?;
15
16    // Validate spec exists
17    if !spec::spec_exists(&args.project_name, &args.spec_name)? {
18        return Err(anyhow::anyhow!(
19            "Spec '{}' not found in project '{}'. Use 'foundry load-project {}' to see available specs.",
20            args.spec_name,
21            args.project_name,
22            args.project_name
23        ));
24    }
25
26    // Get spec path and files before deletion for response
27    let spec_path = spec::get_spec_path(&args.project_name, &args.spec_name)?;
28    let files_to_delete = get_spec_files(&spec_path)?;
29
30    // Validate confirmation
31    if args.confirm.to_lowercase() != "true" {
32        return Err(anyhow::anyhow!(
33            "Deletion not confirmed. Set --confirm true to proceed with deleting spec '{}' and all its files. Got: '{}'",
34            args.spec_name,
35            args.confirm
36        ));
37    }
38
39    // Delete the spec
40    spec::delete_spec(&args.project_name, &args.spec_name)
41        .with_context(|| format!("Failed to delete spec '{}'", args.spec_name))?;
42
43    let response_data = DeleteSpecResponse {
44        project_name: args.project_name.clone(),
45        spec_name: args.spec_name.clone(),
46        spec_path: spec_path.to_string_lossy().to_string(),
47        files_deleted: files_to_delete,
48    };
49
50    Ok(FoundryResponse {
51        data: response_data,
52        next_steps: generate_next_steps(&args),
53        validation_status: ValidationStatus::Complete,
54        workflow_hints: generate_workflow_hints(&args),
55    })
56}
57
58/// Validate command arguments
59fn validate_args(args: &DeleteSpecArgs) -> Result<()> {
60    if args.project_name.trim().is_empty() {
61        return Err(anyhow::anyhow!("Project name cannot be empty"));
62    }
63
64    if args.spec_name.trim().is_empty() {
65        return Err(anyhow::anyhow!("Spec name cannot be empty"));
66    }
67
68    // Validate spec name format (basic check)
69    if !args.spec_name.contains('_') {
70        return Err(anyhow::anyhow!(
71            "Invalid spec name format '{}'. Expected format: YYYYMMDD_HHMMSS_feature_name",
72            args.spec_name
73        ));
74    }
75
76    Ok(())
77}
78
79/// Validate that project exists
80fn validate_project_exists(project_name: &str) -> Result<()> {
81    if !project::project_exists(project_name)? {
82        return Err(anyhow::anyhow!(
83            "Project '{}' not found. Use 'foundry list-projects' to see available projects.",
84            project_name
85        ));
86    }
87    Ok(())
88}
89
90/// Get list of files that will be deleted from a spec directory
91fn get_spec_files(spec_path: &std::path::Path) -> Result<Vec<String>> {
92    let mut files = Vec::new();
93
94    if !spec_path.exists() {
95        return Ok(files);
96    }
97
98    // Check for standard spec files
99    let expected_files = ["spec.md", "task-list.md", "notes.md"];
100
101    for file_name in &expected_files {
102        let file_path = spec_path.join(file_name);
103        if file_path.exists() {
104            files.push(file_path.to_string_lossy().to_string());
105        }
106    }
107
108    // Also check for any other files in the spec directory
109    if let Ok(entries) = fs::read_dir(spec_path) {
110        for entry in entries.flatten() {
111            let path = entry.path();
112            if path.is_file() {
113                let file_name = path
114                    .file_name()
115                    .and_then(|name| name.to_str())
116                    .unwrap_or("unknown");
117
118                // Only add if not already in our expected files list
119                if !expected_files.contains(&file_name) {
120                    files.push(path.to_string_lossy().to_string());
121                }
122            }
123        }
124    }
125
126    Ok(files)
127}
128
129/// Generate next steps for the response
130fn generate_next_steps(args: &DeleteSpecArgs) -> Vec<String> {
131    vec![
132        format!(
133            "Successfully deleted spec '{}' from project '{}'",
134            args.spec_name, args.project_name
135        ),
136        "All spec files have been permanently removed".to_string(),
137        format!(
138            "You can view remaining specs: foundry load-project {}",
139            args.project_name
140        ),
141        format!(
142            "You can create a new spec: foundry create-spec {} <feature_name>",
143            args.project_name
144        ),
145        "Deletion cannot be undone - you might consider backing up important specs before deletion"
146            .to_string(),
147    ]
148}
149
150/// Generate workflow hints for the response
151fn generate_workflow_hints(args: &DeleteSpecArgs) -> Vec<String> {
152    vec![
153        format!("Deleted spec: {}", args.spec_name),
154        "This action cannot be undone".to_string(),
155        "All associated files (spec.md, task-list.md, notes.md) have been removed".to_string(),
156        "You can use 'foundry list-projects' to see project status after deletion".to_string(),
157        "You might consider archiving completed specs rather than deleting for future reference"
158            .to_string(),
159        "You can create new specs to continue feature development".to_string(),
160    ]
161}