oxi/cli/commands/
export.rs1use anyhow::Result;
4use std::path::Path;
5
6pub fn handle_export(session_id: Option<&str>, output_path: Option<&Path>) -> Result<()> {
8 use crate::store::session::SessionManager;
9
10 let cwd = std::env::current_dir()
11 .unwrap_or_else(|_| std::path::PathBuf::from("."))
12 .to_string_lossy()
13 .to_string();
14
15 let session_path = if let Some(sid) = session_id {
17 let direct = std::path::Path::new(sid);
19 if direct.exists() {
20 direct.to_path_buf()
21 } else {
22 anyhow::bail!("Session not found: {}", sid);
23 }
24 } else {
25 let sessions = std::thread::scope(|s| {
28 s.spawn(|| {
29 let rt = tokio::runtime::Builder::new_current_thread()
30 .enable_all()
31 .build()?;
32 rt.block_on(SessionManager::list(&cwd, None))
33 })
34 .join()
35 .map_err(|e| anyhow::anyhow!("thread panicked: {:?}", e))?
36 })?;
37 let most_recent = sessions
38 .first()
39 .ok_or_else(|| anyhow::anyhow!("No sessions found for this project"))?;
40 let session_dir: std::path::PathBuf =
42 crate::store::session::get_default_session_dir(&cwd).into();
43 session_dir.join(format!("{}.jsonl", most_recent.id))
44 };
45
46 if !session_path.exists() {
47 anyhow::bail!("Session file not found: {}", session_path.display());
48 }
49
50 let sm = SessionManager::open(&session_path.to_string_lossy(), None, Some(&cwd));
52 let branch = sm.get_branch(None);
53
54 let meta = crate::storage::export::ExportMeta {
56 model: None,
57 provider: None,
58 exported_at: chrono::Utc::now().timestamp_millis(),
59 total_user_tokens: None,
60 total_assistant_tokens: None,
61 };
62
63 let entries: Vec<crate::store::session::SessionEntry> = branch.into_iter().collect();
64 let html = crate::storage::export::export_to_html(
65 &entries,
66 &meta,
67 &crate::storage::export::HtmlExportOptions::default(),
68 )?;
69
70 let out = if let Some(p) = output_path {
72 p.to_path_buf()
73 } else {
74 let sid_short = session_path
75 .file_stem()
76 .and_then(|s| s.to_str())
77 .unwrap_or("session");
78 let short = &sid_short[..8.min(sid_short.len())];
79 std::path::PathBuf::from(format!("oxi-export-{}.html", short))
80 };
81
82 std::fs::write(&out, &html)?;
83 println!(
84 "Exported {} entries to {} ({} bytes)",
85 entries.len(),
86 out.display(),
87 html.len()
88 );
89 Ok(())
90}
91
92pub fn handle_import(path: &Path) -> Result<()> {
94 if !path.exists() {
95 anyhow::bail!("File not found: {}", path.display());
96 }
97
98 let cwd = std::env::current_dir()
99 .unwrap_or_else(|_| std::path::PathBuf::from("."))
100 .to_string_lossy()
101 .to_string();
102
103 let resolved = crate::store::session::resolve_session_path(&path.to_string_lossy(), &cwd)
104 .map_err(|e| anyhow::anyhow!("Error resolving path: {}", e))?;
105
106 if !std::path::Path::new(&resolved).exists() {
107 anyhow::bail!("File not found: {}", resolved);
108 }
109
110 let sessions_dir: std::path::PathBuf =
112 crate::store::session::get_default_session_dir(&cwd).into();
113 std::fs::create_dir_all(&sessions_dir)?;
114
115 let filename = path
116 .file_name()
117 .unwrap_or_else(|| std::ffi::OsStr::new("imported.jsonl"));
118 let dest = sessions_dir.join(filename);
119
120 if dest.exists() {
122 let stem = dest
123 .file_stem()
124 .and_then(|s| s.to_str())
125 .unwrap_or("imported");
126 let ext = dest.extension().and_then(|s| s.to_str()).unwrap_or("jsonl");
127 let unique_name = format!(
128 "{}-{}.{}",
129 stem,
130 chrono::Utc::now().format("%Y%m%d%H%M%S"),
131 ext
132 );
133 let alt_dest = sessions_dir.join(&unique_name);
134 std::fs::copy(path, &alt_dest)?;
135 println!("Imported session to {}", alt_dest.display());
136 } else {
137 std::fs::copy(path, &dest)?;
138 println!("Imported session to {}", dest.display());
139 }
140 Ok(())
141}
142
143pub async fn handle_share(session_id: Option<&str>) -> Result<()> {
145 let gh_check = std::process::Command::new("gh")
147 .args(["auth", "status"])
148 .output()?;
149
150 if !gh_check.status.success() {
151 anyhow::bail!("GitHub CLI (gh) is not authenticated. Run: gh auth login");
152 }
153
154 let cwd = std::env::current_dir()
155 .unwrap_or_else(|_| std::path::PathBuf::from("."))
156 .to_string_lossy()
157 .to_string();
158
159 let session_path = if let Some(sid) = session_id {
161 let direct = std::path::Path::new(sid);
162 if direct.exists() {
163 direct.to_path_buf()
164 } else {
165 anyhow::bail!("Session not found: {}", sid);
166 }
167 } else {
168 let sessions = crate::store::session::SessionManager::list(&cwd, None).await?;
169 let most_recent = sessions
170 .first()
171 .ok_or_else(|| anyhow::anyhow!("No sessions found for this project"))?;
172 let session_dir: std::path::PathBuf =
173 crate::store::session::get_default_session_dir(&cwd).into();
174 session_dir.join(format!("{}.jsonl", most_recent.id))
175 };
176
177 if !session_path.exists() {
178 anyhow::bail!("Session file not found: {}", session_path.display());
179 }
180
181 let sm = crate::store::session::SessionManager::open(
182 &session_path.to_string_lossy(),
183 None,
184 Some(&cwd),
185 );
186 let branch = sm.get_branch(None);
187 let entries: Vec<crate::store::session::SessionEntry> = branch.into_iter().collect();
188
189 let meta = crate::storage::export::ExportMeta {
190 model: None,
191 provider: None,
192 exported_at: chrono::Utc::now().timestamp_millis(),
193 total_user_tokens: None,
194 total_assistant_tokens: None,
195 };
196
197 let html = crate::storage::export::export_to_html(
198 &entries,
199 &meta,
200 &crate::storage::export::HtmlExportOptions::default(),
201 )?;
202
203 let temp_path = std::env::temp_dir().join("oxi-share-export.html");
204 std::fs::write(&temp_path, &html)?;
205
206 let output = tokio::process::Command::new("gh")
208 .args(["gist", "create", &temp_path.to_string_lossy()])
209 .output()
210 .await?;
211
212 let _ = std::fs::remove_file(&temp_path);
213
214 if output.status.success() {
215 let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
216 println!("Gist created: {}", url);
217 } else {
218 let stderr = String::from_utf8_lossy(&output.stderr);
219 anyhow::bail!("Failed to create gist: {}", stderr.trim());
220 }
221 Ok(())
222}