start_command/
execution_chain.rs1use crate::execution_identity::{execution_aliases, has_user_names};
7use crate::execution_store::{ExecutionRecord, ExecutionStore};
8use serde_json::{json, Value};
9use std::fs;
10use std::io::Write;
11use std::path::PathBuf;
12
13fn start_millis(record: &ExecutionRecord) -> i64 {
14 chrono::DateTime::parse_from_rfc3339(&record.start_time)
15 .map(|t| t.timestamp_millis())
16 .unwrap_or(i64::MIN)
17}
18
19pub fn build_chain(records: &[ExecutionRecord], record: &ExecutionRecord) -> Vec<ExecutionRecord> {
23 let mut seen = vec![record.uuid.clone()];
24 let mut chain = vec![record.clone()];
25 let mut current = record.clone();
26 while let Some(previous_id) = current.continues.clone() {
27 let Some(previous) = records.iter().find(|r| r.uuid == previous_id) else {
28 break;
29 };
30 if seen.contains(&previous.uuid) {
31 break;
32 }
33 seen.push(previous.uuid.clone());
34 chain.insert(0, previous.clone());
35 current = previous.clone();
36 }
37 let mut current = record.clone();
38 loop {
39 let next = records
40 .iter()
41 .filter(|r| r.continues.as_deref() == Some(¤t.uuid) && !seen.contains(&r.uuid))
42 .max_by_key(|r| start_millis(r));
43 let Some(next) = next else {
44 break;
45 };
46 seen.push(next.uuid.clone());
47 chain.push(next.clone());
48 current = next.clone();
49 }
50 chain
51}
52
53pub fn resolve_continues(
55 store: Option<&ExecutionStore>,
56 identifier: &str,
57 session_id: &str,
58) -> Result<String, String> {
59 let store = store.ok_or("--continues requires execution tracking.")?;
60 let target = store.get(identifier).ok_or_else(|| {
61 format!(
62 "No execution found with UUID or session name: {}",
63 identifier
64 )
65 })?;
66 if target.uuid == session_id {
67 return Err(format!("Execution {} cannot continue itself", session_id));
68 }
69 Ok(target.uuid)
70}
71
72pub fn status_identity(
76 record: &ExecutionRecord,
77 store: Option<&ExecutionStore>,
78) -> Vec<(String, Value)> {
79 let mut fields = Vec::new();
80 if has_user_names(record) {
81 fields.push(("aliases".to_string(), json!(execution_aliases(record))));
82 }
83 if let Some(store) = store {
84 let records = store.get_all();
85 if record.continues.is_some()
86 || records
87 .iter()
88 .any(|r| r.continues.as_deref() == Some(&record.uuid))
89 {
90 let chain: Vec<String> = build_chain(&records, record)
91 .into_iter()
92 .map(|r| r.uuid)
93 .collect();
94 if chain.len() > 1 {
95 fields.push(("chain".to_string(), json!(chain)));
96 }
97 }
98 }
99 fields
100}
101
102fn joined(fields: &[(String, Value)], key: &str, separator: &str) -> Option<String> {
103 let (_, value) = fields.iter().find(|(k, _)| k == key)?;
104 let items: Vec<&str> = value.as_array()?.iter().filter_map(Value::as_str).collect();
105 Some(items.join(separator))
106}
107
108pub fn identity_text_lines(record: &ExecutionRecord, fields: &[(String, Value)]) -> Vec<String> {
110 let mut lines = Vec::new();
111 if let Some(aliases) = joined(fields, "aliases", ", ") {
112 lines.push(format!("Aliases: {}", aliases));
113 }
114 if let Some(continues) = &record.continues {
115 lines.push(format!("Continues: {}", continues));
116 }
117 if let Some(chain) = joined(fields, "chain", " -> ") {
118 lines.push(format!("Chain: {}", chain));
119 }
120 lines
121}
122
123pub struct ChainLog {
125 dir: PathBuf,
126 pub path: PathBuf,
127}
128
129impl Drop for ChainLog {
130 fn drop(&mut self) {
131 let _ = fs::remove_dir_all(&self.dir);
132 }
133}
134
135pub fn write_chain_log(chain: &[ExecutionRecord]) -> std::io::Result<ChainLog> {
138 let dir = std::env::temp_dir().join(format!("start-chain-log-{}", uuid::Uuid::new_v4()));
139 fs::create_dir(&dir)?;
140 let last = chain.last().map(|r| r.uuid.as_str()).unwrap_or("chain");
141 let log = ChainLog {
142 path: dir.join(format!("{}.chain.log", last)),
143 dir,
144 };
145 let mut file = fs::File::create(&log.path)?;
146 for (index, record) in chain.iter().enumerate() {
147 let log_path = (!record.log_path.is_empty()).then_some(&record.log_path);
148 let header = json!({
149 "event": "continuation",
150 "uuid": record.uuid,
151 "chainIndex": index + 1,
152 "chainLength": chain.len(),
153 "continues": record.continues,
154 "command": record.command,
155 "startTime": record.start_time,
156 "logPath": log_path,
157 });
158 write!(file, "\n[Start Command Lifecycle] {}\n", header)?;
159 match log_path.map(fs::read) {
160 Some(Ok(content)) => file.write_all(&content)?,
161 _ => writeln!(file, "Log file not found: {}", record.log_path)?,
162 }
163 }
164 Ok(log)
165}
166
167pub fn prepare_chain_upload(
170 store: &ExecutionStore,
171 record: &ExecutionRecord,
172) -> Result<Option<ChainLog>, String> {
173 let chain = build_chain(&store.get_all(), record);
174 if chain.len() < 2 {
175 return Ok(None);
176 }
177 write_chain_log(&chain)
178 .map(Some)
179 .map_err(|e| format!("Could not join chain logs: {}", e))
180}