sift_queue/cli/commands/
edit.rs1use crate::queue::{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 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 if let Some(ref title) = args.set_title {
50 attrs.title = Some(title.clone());
51 has_changes = true;
52 }
53
54 if let Some(ref description) = args.set_description {
56 attrs.description = Some(description.clone());
57 has_changes = true;
58 }
59
60 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 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 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 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 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 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 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}