sift_queue/cli/commands/
edit.rs1use crate::queue::{parse_priority_value, Queue, Source, UpdateAttrs, VALID_STATUSES};
2use crate::EditArgs;
3use anyhow::Result;
4use std::path::PathBuf;
5
6pub 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 if args.set_priority.is_some() && args.clear_priority {
35 eprintln!("Error: --set-priority and --clear-priority are mutually exclusive");
36 return Ok(1);
37 }
38
39 if let Some(ref status) = args.set_status {
41 if !VALID_STATUSES.contains(&status.as_str()) {
42 eprintln!(
43 "Error: Invalid status: {}. Valid: {}",
44 status,
45 VALID_STATUSES.join(", ")
46 );
47 return Ok(1);
48 }
49 attrs.status = Some(status.clone());
50 has_changes = true;
51 }
52
53 if let Some(ref title) = args.set_title {
55 attrs.title = Some(title.clone());
56 has_changes = true;
57 }
58
59 if let Some(ref description) = args.set_description {
61 attrs.description = Some(description.clone());
62 has_changes = true;
63 }
64
65 if let Some(ref priority_str) = args.set_priority {
67 match parse_priority_value(priority_str) {
68 Ok(priority) => {
69 attrs.priority = Some(Some(priority));
70 has_changes = true;
71 }
72 Err(err) => {
73 eprintln!("Error: {}", err);
74 return Ok(1);
75 }
76 }
77 }
78
79 if args.clear_priority {
80 attrs.priority = Some(None);
81 has_changes = true;
82 }
83
84 if let Some(ref json_str) = args.set_metadata {
86 match serde_json::from_str::<serde_json::Value>(json_str) {
87 Ok(v) => {
88 attrs.metadata = Some(v);
89 has_changes = true;
90 }
91 Err(e) => {
92 eprintln!("Error: Invalid JSON for metadata: {}", e);
93 return Ok(1);
94 }
95 }
96 }
97
98 if let Some(ref json_str) = args.merge_metadata {
100 let patch = match serde_json::from_str::<serde_json::Value>(json_str) {
101 Ok(v) => v,
102 Err(e) => {
103 eprintln!("Error: Invalid JSON for merge metadata: {}", e);
104 return Ok(1);
105 }
106 };
107
108 if !patch.is_object() {
109 eprintln!("Error: --merge-metadata must be a JSON object");
110 return Ok(1);
111 }
112
113 let merged = deep_merge(item.metadata.clone(), patch);
114 attrs.metadata = Some(merged);
115 has_changes = true;
116 }
117
118 if let Some(ref ids_str) = args.set_blocked_by {
120 let blocked_by: Vec<String> = ids_str
121 .split(',')
122 .map(|s: &str| s.trim().to_string())
123 .filter(|s: &String| !s.is_empty())
124 .collect();
125 attrs.blocked_by = Some(blocked_by);
126 has_changes = true;
127 }
128
129 let has_source_adds = !args.add_diff.is_empty()
131 || !args.add_file.is_empty()
132 || !args.add_text.is_empty()
133 || !args.add_directory.is_empty()
134 || !args.add_transcript.is_empty();
135 let has_source_removes = !args.rm_source.is_empty();
136
137 if has_source_adds || has_source_removes {
138 has_changes = true;
139 let mut sources: Vec<serde_json::Value> =
140 item.sources.iter().map(|s| s.to_json_value()).collect();
141
142 let mut rm_indices: Vec<usize> = args.rm_source.clone();
144 rm_indices.sort();
145 rm_indices.reverse();
146 for index in rm_indices {
147 if index < sources.len() {
148 sources.remove(index);
149 } else {
150 eprintln!("Warning: Source index {} out of range", index);
151 }
152 }
153
154 for path in &args.add_diff {
156 sources.push(source_value("diff", Some(path.as_str()), None));
157 }
158 for path in &args.add_file {
159 sources.push(source_value("file", Some(path.as_str()), None));
160 }
161 for text in &args.add_text {
162 sources.push(source_value("text", None, Some(text.as_str())));
163 }
164 for path in &args.add_directory {
165 sources.push(source_value("directory", Some(path.as_str()), None));
166 }
167 for path in &args.add_transcript {
168 sources.push(source_value("transcript", Some(path.as_str()), None));
169 }
170
171 if sources.is_empty() {
172 eprintln!("Error: Cannot remove all sources");
173 return Ok(1);
174 }
175
176 let new_sources: Vec<Source> = sources
178 .into_iter()
179 .filter_map(|v| serde_json::from_value(v).ok())
180 .collect();
181 attrs.sources = Some(new_sources);
182 }
183
184 if !has_changes {
185 eprintln!("Error: No changes specified");
186 return Ok(1);
187 }
188
189 match queue.update(id, attrs)? {
190 Some(updated) => {
191 if args.json {
192 let json = serde_json::to_string_pretty(&updated.to_json_value())?;
193 println!("{}", json);
194 } else {
195 println!("{}", updated.id);
196 eprintln!("Updated item {}", updated.id);
197 }
198 Ok(0)
199 }
200 None => {
201 eprintln!("Error: Item not found: {}", id);
202 Ok(1)
203 }
204 }
205}
206
207fn deep_merge(current: serde_json::Value, patch: serde_json::Value) -> serde_json::Value {
208 match (current, patch) {
209 (serde_json::Value::Object(mut cur_map), serde_json::Value::Object(patch_map)) => {
210 for (k, patch_v) in patch_map {
211 let next = match cur_map.remove(&k) {
212 Some(cur_v) => deep_merge(cur_v, patch_v),
213 None => patch_v,
214 };
215 cur_map.insert(k, next);
216 }
217 serde_json::Value::Object(cur_map)
218 }
219 (_, patch_non_object) => patch_non_object,
220 }
221}
222
223fn source_value(type_: &str, path: Option<&str>, content: Option<&str>) -> serde_json::Value {
224 let mut map = serde_json::Map::new();
225 map.insert(
226 "type".to_string(),
227 serde_json::Value::String(type_.to_string()),
228 );
229 if let Some(p) = path {
230 map.insert("path".to_string(), serde_json::Value::String(p.to_string()));
231 }
232 if let Some(c) = content {
233 map.insert(
234 "content".to_string(),
235 serde_json::Value::String(c.to_string()),
236 );
237 }
238 serde_json::Value::Object(map)
239}