lean_ctx/tools/
ctx_package.rs1use std::path::Path;
4
5use crate::core::context_package;
6use crate::core::session::SessionState;
7
8pub fn handle(
9 project_root: &str,
10 session: Option<&SessionState>,
11 action: &str,
12 path: Option<&str>,
13 agent_id: Option<&str>,
14 description: Option<&str>,
15) -> String {
16 match action.trim() {
17 "save" => handle_save(project_root, session, path, agent_id, description),
18 "resume" => handle_resume(project_root, session, path),
19 "list" => handle_list(project_root),
20 "info" => handle_info(path),
21 other => format!(
22 "ERR: unknown package action '{other}'. Use: save | resume <path> | list | info <path>"
23 ),
24 }
25}
26
27fn handle_save(
28 project_root: &str,
29 session: Option<&SessionState>,
30 path: Option<&str>,
31 agent_id: Option<&str>,
32 description: Option<&str>,
33) -> String {
34 let Some(session) = session else {
35 return "ERR: no active session to save".to_string();
36 };
37 let output_path = path.map(Path::new);
38 match context_package::save_package(session, project_root, agent_id, description, output_path) {
39 Ok(p) => format!("package saved: {}", p.display()),
40 Err(e) => format!("ERR: {e}"),
41 }
42}
43
44fn handle_resume(project_root: &str, session: Option<&SessionState>, path: Option<&str>) -> String {
45 let Some(path_str) = path else {
46 return "ERR: resume requires a path to the .ctx.json package".to_string();
47 };
48 let pkg_path = Path::new(path_str);
49 if !pkg_path.exists() {
50 let hash = crate::core::project_hash::hash_project_root(project_root);
52 let alt = crate::core::data_dir::lean_ctx_data_dir()
53 .unwrap_or_else(|_| std::path::PathBuf::from(".lean-ctx"))
54 .join("packages")
55 .join(hash)
56 .join(path_str);
57 if !alt.exists() {
58 return format!("ERR: package not found: {path_str}");
59 }
60 return do_resume(session, &alt);
61 }
62 do_resume(session, pkg_path)
63}
64
65fn do_resume(session: Option<&SessionState>, path: &Path) -> String {
66 let mut target = match session {
67 Some(base) => base.clone(),
68 None => SessionState::new(),
69 };
70 match context_package::resume_package(&mut target, path) {
71 Ok(report) => report.format(),
72 Err(e) => format!("ERR: {e}"),
73 }
74}
75
76fn handle_list(project_root: &str) -> String {
77 let hash = crate::core::project_hash::hash_project_root(project_root);
78 let dir = crate::core::data_dir::lean_ctx_data_dir()
79 .unwrap_or_else(|_| std::path::PathBuf::from(".lean-ctx"))
80 .join("packages")
81 .join(hash);
82 if !dir.exists() {
83 return "No saved packages yet.".to_string();
84 }
85 let mut entries: Vec<String> = Vec::new();
86 if let Ok(rd) = std::fs::read_dir(&dir) {
87 for entry in rd.flatten() {
88 let p = entry.path();
89 if p.extension().and_then(|e| e.to_str()) == Some("json")
90 && let Ok(json) = std::fs::read_to_string(&p)
91 && let Ok(pkg) = serde_json::from_str::<context_package::ContextPackage>(&json)
92 {
93 entries.push(format!(
94 " {} — {}",
95 p.file_name().unwrap_or_default().to_string_lossy(),
96 pkg.summary_line()
97 ));
98 }
99 }
100 }
101 if entries.is_empty() {
102 return "No saved packages yet.".to_string();
103 }
104 entries.sort();
105 format!("packages ({}):\n{}", entries.len(), entries.join("\n"))
106}
107
108fn handle_info(path: Option<&str>) -> String {
109 let Some(path_str) = path else {
110 return "ERR: info requires a path".to_string();
111 };
112 let p = Path::new(path_str);
113 if !p.exists() {
114 return format!("ERR: not found: {path_str}");
115 }
116 match std::fs::read_to_string(p) {
117 Ok(json) => match serde_json::from_str::<context_package::ContextPackage>(&json) {
118 Ok(pkg) => format!(
119 "format_version: {}\ncreated: {}\nproject: {}\n{}",
120 pkg.format_version,
121 pkg.created_at.format("%Y-%m-%d %H:%M"),
122 pkg.project_root,
123 pkg.summary_line()
124 ),
125 Err(e) => format!("ERR: parse: {e}"),
126 },
127 Err(e) => format!("ERR: read: {e}"),
128 }
129}