Skip to main content

sift_queue/cli/commands/
edit.rs

1use crate::queue::{Queue, Source, UpdateAttrs, VALID_STATUSES};
2use crate::EditArgs;
3use anyhow::Result;
4use std::path::PathBuf;
5
6/// Execute the `sq edit` command.
7pub fn execute(args: &EditArgs, queue_path: PathBuf) -> Result<i32> {
8    let queue = Queue::new(queue_path);
9
10    let id = match &args.id {
11        Some(ref id) => id.as_str(),
12        None => {
13            eprintln!("Error: Item ID is required");
14            return Ok(1);
15        }
16    };
17
18    let item = match queue.find(id) {
19        Some(item) => item,
20        None => {
21            eprintln!("Error: Item not found: {}", id);
22            return Ok(1);
23        }
24    };
25
26    let mut attrs = UpdateAttrs::default();
27    let mut has_changes = false;
28
29    if args.set_metadata.is_some() && args.merge_metadata.is_some() {
30        eprintln!("Error: --set-metadata and --merge-metadata are mutually exclusive");
31        return Ok(1);
32    }
33
34    // Status
35    if let Some(ref status) = args.set_status {
36        if !VALID_STATUSES.contains(&status.as_str()) {
37            eprintln!(
38                "Error: Invalid status: {}. Valid: {}",
39                status,
40                VALID_STATUSES.join(", ")
41            );
42            return Ok(1);
43        }
44        attrs.status = Some(status.clone());
45        has_changes = true;
46    }
47
48    // Title
49    if let Some(ref title) = args.set_title {
50        attrs.title = Some(title.clone());
51        has_changes = true;
52    }
53
54    // Description
55    if let Some(ref description) = args.set_description {
56        attrs.description = Some(description.clone());
57        has_changes = true;
58    }
59
60    // Metadata (full replace)
61    if let Some(ref json_str) = args.set_metadata {
62        match serde_json::from_str::<serde_json::Value>(json_str) {
63            Ok(v) => {
64                attrs.metadata = Some(v);
65                has_changes = true;
66            }
67            Err(e) => {
68                eprintln!("Error: Invalid JSON for metadata: {}", e);
69                return Ok(1);
70            }
71        }
72    }
73
74    // Metadata (deep merge)
75    if let Some(ref json_str) = args.merge_metadata {
76        let patch = match serde_json::from_str::<serde_json::Value>(json_str) {
77            Ok(v) => v,
78            Err(e) => {
79                eprintln!("Error: Invalid JSON for merge metadata: {}", e);
80                return Ok(1);
81            }
82        };
83
84        if !patch.is_object() {
85            eprintln!("Error: --merge-metadata must be a JSON object");
86            return Ok(1);
87        }
88
89        let merged = deep_merge(item.metadata.clone(), patch);
90        attrs.metadata = Some(merged);
91        has_changes = true;
92    }
93
94    // Blocked by
95    if let Some(ref ids_str) = args.set_blocked_by {
96        let blocked_by: Vec<String> = ids_str
97            .split(',')
98            .map(|s: &str| s.trim().to_string())
99            .filter(|s: &String| !s.is_empty())
100            .collect();
101        attrs.blocked_by = Some(blocked_by);
102        has_changes = true;
103    }
104
105    // Source modifications
106    let has_source_adds = !args.add_diff.is_empty()
107        || !args.add_file.is_empty()
108        || !args.add_text.is_empty()
109        || !args.add_directory.is_empty()
110        || !args.add_transcript.is_empty();
111    let has_source_removes = !args.rm_source.is_empty();
112
113    if has_source_adds || has_source_removes {
114        has_changes = true;
115        let mut sources: Vec<serde_json::Value> =
116            item.sources.iter().map(|s| s.to_json_value()).collect();
117
118        // Remove sources (sort indices in reverse to preserve correctness)
119        let mut rm_indices: Vec<usize> = args.rm_source.clone();
120        rm_indices.sort();
121        rm_indices.reverse();
122        for index in rm_indices {
123            if index < sources.len() {
124                sources.remove(index);
125            } else {
126                eprintln!("Warning: Source index {} out of range", index);
127            }
128        }
129
130        // Add new sources
131        for path in &args.add_diff {
132            sources.push(source_value("diff", Some(path.as_str()), None));
133        }
134        for path in &args.add_file {
135            sources.push(source_value("file", Some(path.as_str()), None));
136        }
137        for text in &args.add_text {
138            sources.push(source_value("text", None, Some(text.as_str())));
139        }
140        for path in &args.add_directory {
141            sources.push(source_value("directory", Some(path.as_str()), None));
142        }
143        for path in &args.add_transcript {
144            sources.push(source_value("transcript", Some(path.as_str()), None));
145        }
146
147        if sources.is_empty() {
148            eprintln!("Error: Cannot remove all sources");
149            return Ok(1);
150        }
151
152        // Convert back to Source structs
153        let new_sources: Vec<Source> = sources
154            .into_iter()
155            .filter_map(|v| serde_json::from_value(v).ok())
156            .collect();
157        attrs.sources = Some(new_sources);
158    }
159
160    if !has_changes {
161        eprintln!("Error: No changes specified");
162        return Ok(1);
163    }
164
165    match queue.update(id, attrs)? {
166        Some(updated) => {
167            if args.json {
168                let json = serde_json::to_string_pretty(&updated.to_json_value())?;
169                println!("{}", json);
170            } else {
171                println!("{}", updated.id);
172                eprintln!("Updated item {}", updated.id);
173            }
174            Ok(0)
175        }
176        None => {
177            eprintln!("Error: Item not found: {}", id);
178            Ok(1)
179        }
180    }
181}
182
183fn deep_merge(current: serde_json::Value, patch: serde_json::Value) -> serde_json::Value {
184    match (current, patch) {
185        (serde_json::Value::Object(mut cur_map), serde_json::Value::Object(patch_map)) => {
186            for (k, patch_v) in patch_map {
187                let next = match cur_map.remove(&k) {
188                    Some(cur_v) => deep_merge(cur_v, patch_v),
189                    None => patch_v,
190                };
191                cur_map.insert(k, next);
192            }
193            serde_json::Value::Object(cur_map)
194        }
195        (_, patch_non_object) => patch_non_object,
196    }
197}
198
199fn source_value(
200    type_: &str,
201    path: Option<&str>,
202    content: Option<&str>,
203) -> serde_json::Value {
204    let mut map = serde_json::Map::new();
205    map.insert(
206        "type".to_string(),
207        serde_json::Value::String(type_.to_string()),
208    );
209    if let Some(p) = path {
210        map.insert(
211            "path".to_string(),
212            serde_json::Value::String(p.to_string()),
213        );
214    }
215    if let Some(c) = content {
216        map.insert(
217            "content".to_string(),
218            serde_json::Value::String(c.to_string()),
219        );
220    }
221    serde_json::Value::Object(map)
222}