1use crate::cli_handlers::read_stdin;
5use crate::error::{IntentError, Result};
6use crate::events::EventManager;
7use crate::project::ProjectContext;
8use crate::report::ReportManager;
9use crate::workspace::WorkspaceManager;
10use std::path::PathBuf;
11
12#[allow(dead_code)]
14pub enum CurrentAction {
15 Set { task_id: i64 },
16 Clear,
17}
18
19#[allow(dead_code)]
20pub enum EventCommands {
21 Add {
22 task_id: Option<i64>,
23 log_type: String,
24 data_stdin: bool,
25 },
26 List {
27 task_id: Option<i64>,
28 log_type: Option<String>,
29 since: Option<String>,
30 limit: Option<i64>,
31 },
32}
33
34pub async fn handle_current_command(
35 set: Option<i64>,
36 command: Option<CurrentAction>,
37) -> Result<()> {
38 let ctx = ProjectContext::load().await?;
39 let workspace_mgr = WorkspaceManager::new(&ctx.pool);
40
41 if let Some(task_id) = set {
43 eprintln!("⚠️ Warning: 'ie current --set' is a low-level atomic command.");
44 eprintln!(
45 " For normal use, prefer 'ie task start {}' which ensures data consistency.",
46 task_id
47 );
48 eprintln!();
49 let response = workspace_mgr.set_current_task(task_id, None).await?;
50 println!("✓ Switched to task #{}", task_id);
51 println!("{}", serde_json::to_string_pretty(&response)?);
52 return Ok(());
53 }
54
55 match command {
57 Some(CurrentAction::Set { task_id }) => {
58 eprintln!("⚠️ Warning: 'ie current set' is a low-level atomic command.");
59 eprintln!(
60 " For normal use, prefer 'ie task start {}' which ensures data consistency.",
61 task_id
62 );
63 eprintln!();
64 let response = workspace_mgr.set_current_task(task_id, None).await?;
65 println!("✓ Switched to task #{}", task_id);
66 println!("{}", serde_json::to_string_pretty(&response)?);
67 },
68 Some(CurrentAction::Clear) => {
69 eprintln!("⚠️ Warning: 'ie current clear' is a low-level atomic command.");
70 eprintln!(" For normal use, prefer 'ie task done' or 'ie task switch' which ensures data consistency.");
71 eprintln!();
72 workspace_mgr.clear_current_task(None).await?;
73 println!("✓ Current task cleared");
74 },
75 None => {
76 let response = workspace_mgr.get_current_task(None).await?;
78 println!("{}", serde_json::to_string_pretty(&response)?);
79 },
80 }
81
82 Ok(())
83}
84
85pub async fn handle_report_command(
86 since: Option<String>,
87 status: Option<String>,
88 filter_name: Option<String>,
89 filter_spec: Option<String>,
90 summary_only: bool,
91) -> Result<()> {
92 let ctx = ProjectContext::load().await?;
93 let report_mgr = ReportManager::new(&ctx.pool);
94
95 let report = report_mgr
96 .generate_report(since, status, filter_name, filter_spec, summary_only)
97 .await?;
98 println!("{}", serde_json::to_string_pretty(&report)?);
99
100 Ok(())
101}
102
103pub async fn handle_event_command(cmd: EventCommands) -> Result<()> {
104 match cmd {
105 EventCommands::Add {
106 task_id,
107 log_type,
108 data_stdin,
109 } => {
110 let ctx = ProjectContext::load_or_init().await?;
111 let project_path = ctx.root.to_string_lossy().to_string();
112 let event_mgr = EventManager::with_project_path(&ctx.pool, project_path);
113
114 let data = if data_stdin {
115 read_stdin()?
116 } else {
117 return Err(IntentError::InvalidInput(
118 "--data-stdin is required".to_string(),
119 ));
120 };
121
122 let target_task_id = if let Some(id) = task_id {
124 id
126 } else {
127 let session_id = crate::workspace::resolve_session_id(None);
129 let current_task_id: Option<i64> = sqlx::query_scalar::<_, Option<i64>>(
130 "SELECT current_task_id FROM sessions WHERE session_id = ?",
131 )
132 .bind(&session_id)
133 .fetch_optional(&ctx.pool)
134 .await?
135 .flatten();
136
137 current_task_id
138 .ok_or_else(|| IntentError::InvalidInput(
139 "No current task is set and --task-id was not provided. Use 'current --set <ID>' to set a task first.".to_string(),
140 ))?
141 };
142
143 let event = event_mgr
144 .add_event(target_task_id, &log_type, &data)
145 .await?;
146 println!("{}", serde_json::to_string_pretty(&event)?);
147 },
148
149 EventCommands::List {
150 task_id,
151 limit,
152 log_type,
153 since,
154 } => {
155 let ctx = ProjectContext::load().await?;
156 let event_mgr = EventManager::new(&ctx.pool);
157
158 let events = event_mgr
159 .list_events(task_id, limit, log_type, since)
160 .await?;
161 println!("{}", serde_json::to_string_pretty(&events)?);
162 },
163 }
164
165 Ok(())
166}
167
168fn parse_status_keywords(query: &str) -> Option<Vec<String>> {
171 let query_lower = query.to_lowercase();
172 let words: Vec<&str> = query_lower.split_whitespace().collect();
173
174 if words.is_empty() {
176 return None;
177 }
178
179 let valid_statuses = ["todo", "doing", "done"];
181 let mut statuses: Vec<String> = Vec::new();
182
183 for word in words {
184 if valid_statuses.contains(&word) {
185 if !statuses.iter().any(|s| s == word) {
186 statuses.push(word.to_string());
187 }
188 } else {
189 return None;
191 }
192 }
193
194 Some(statuses)
195}
196
197pub async fn handle_search_command(
198 query: &str,
199 include_tasks: bool,
200 include_events: bool,
201 limit: Option<i64>,
202 offset: Option<i64>,
203 format: &str,
204) -> Result<()> {
205 use crate::search::SearchManager;
206 use crate::tasks::TaskManager;
207
208 let ctx = ProjectContext::load_or_init().await?;
209
210 if let Some(statuses) = parse_status_keywords(query) {
212 let task_mgr = TaskManager::new(&ctx.pool);
214
215 let mut all_tasks = Vec::new();
217 for status in &statuses {
218 let result = task_mgr
219 .find_tasks(Some(status), None, None, limit, offset)
220 .await?;
221 all_tasks.extend(result.tasks);
222 }
223
224 all_tasks.sort_by(|a, b| {
226 let pri_a = a.priority.unwrap_or(999);
227 let pri_b = b.priority.unwrap_or(999);
228 pri_a.cmp(&pri_b).then_with(|| a.id.cmp(&b.id))
229 });
230
231 let limit = limit.unwrap_or(100) as usize;
233 if all_tasks.len() > limit {
234 all_tasks.truncate(limit);
235 }
236
237 if format == "json" {
238 println!("{}", serde_json::to_string_pretty(&all_tasks)?);
239 } else {
240 let status_str = statuses.join(", ");
242 println!(
243 "Tasks with status [{}]: {} found",
244 status_str,
245 all_tasks.len()
246 );
247 println!();
248 for task in &all_tasks {
249 let status_icon = match task.status.as_str() {
250 "todo" => "○",
251 "doing" => "●",
252 "done" => "✓",
253 _ => "?",
254 };
255 let parent_info = task
256 .parent_id
257 .map(|p| format!(" (parent: #{})", p))
258 .unwrap_or_default();
259 let priority_info = task
260 .priority
261 .map(|p| format!(" [P{}]", p))
262 .unwrap_or_default();
263 println!(
264 " {} #{} {}{}{}",
265 status_icon, task.id, task.name, parent_info, priority_info
266 );
267 if let Some(spec) = &task.spec {
268 if !spec.is_empty() {
269 let truncated = if spec.len() > 60 {
270 format!("{}...", &spec[..57])
271 } else {
272 spec.clone()
273 };
274 println!(" Spec: {}", truncated);
275 }
276 }
277 println!(" Owner: {}", task.owner);
278 if let Some(ts) = task.first_todo_at {
279 print!(" todo: {} ", ts.format("%m-%d %H:%M:%S"));
280 }
281 if let Some(ts) = task.first_doing_at {
282 print!("doing: {} ", ts.format("%m-%d %H:%M:%S"));
283 }
284 if let Some(ts) = task.first_done_at {
285 print!("done: {}", ts.format("%m-%d %H:%M:%S"));
286 }
287 if task.first_todo_at.is_some()
288 || task.first_doing_at.is_some()
289 || task.first_done_at.is_some()
290 {
291 println!();
292 }
293 }
294 }
295 return Ok(());
296 }
297
298 let search_mgr = SearchManager::new(&ctx.pool);
300
301 let results = search_mgr
302 .search(query, include_tasks, include_events, limit, offset, false)
303 .await?;
304
305 if format == "json" {
306 println!("{}", serde_json::to_string_pretty(&results)?);
307 } else {
308 use crate::db::models::SearchResult;
309
310 println!(
312 "Search: \"{}\" → {} tasks, {} events (limit: {}, offset: {})",
313 query, results.total_tasks, results.total_events, results.limit, results.offset
314 );
315 println!();
316
317 for result in &results.results {
318 match result {
319 SearchResult::Task {
320 task,
321 match_field,
322 match_snippet,
323 } => {
324 let status_icon = match task.status.as_str() {
325 "todo" => "○",
326 "doing" => "●",
327 "done" => "✓",
328 _ => "?",
329 };
330 let parent_info = task
331 .parent_id
332 .map(|p| format!(" (parent: #{})", p))
333 .unwrap_or_default();
334 let priority_info = task
335 .priority
336 .map(|p| format!(" [P{}]", p))
337 .unwrap_or_default();
338 println!(
339 " {} #{} {} [match: {}]{}{}",
340 status_icon, task.id, task.name, match_field, parent_info, priority_info
341 );
342 if let Some(spec) = &task.spec {
343 if !spec.is_empty() {
344 let truncated = if spec.len() > 60 {
345 format!("{}...", &spec[..57])
346 } else {
347 spec.clone()
348 };
349 println!(" Spec: {}", truncated);
350 }
351 }
352 if !match_snippet.is_empty() {
353 println!(" Snippet: {}", match_snippet);
354 }
355 println!(" Owner: {}", task.owner);
356 if let Some(ts) = task.first_todo_at {
357 print!(" todo: {} ", ts.format("%m-%d %H:%M:%S"));
358 }
359 if let Some(ts) = task.first_doing_at {
360 print!("doing: {} ", ts.format("%m-%d %H:%M:%S"));
361 }
362 if let Some(ts) = task.first_done_at {
363 print!("done: {}", ts.format("%m-%d %H:%M:%S"));
364 }
365 if task.first_todo_at.is_some()
366 || task.first_doing_at.is_some()
367 || task.first_done_at.is_some()
368 {
369 println!();
370 }
371 },
372 SearchResult::Event {
373 event,
374 task_chain,
375 match_snippet,
376 } => {
377 let icon = match event.log_type.as_str() {
378 "decision" => "💡",
379 "blocker" => "🚫",
380 "milestone" => "🎯",
381 _ => "📝",
382 };
383 println!(
384 " {} #{} [{}] (task #{}) {}",
385 icon,
386 event.id,
387 event.log_type,
388 event.task_id,
389 event.timestamp.format("%Y-%m-%d %H:%M:%S")
390 );
391 println!(" Message: {}", event.discussion_data);
392 if !match_snippet.is_empty() {
393 println!(" Snippet: {}", match_snippet);
394 }
395 if !task_chain.is_empty() {
396 let chain_str: Vec<String> = task_chain
397 .iter()
398 .map(|t| format!("#{} {}", t.id, t.name))
399 .collect();
400 println!(" Task chain: {}", chain_str.join(" → "));
401 }
402 },
403 }
404 }
405
406 if results.has_more {
407 println!();
408 println!(
409 " ... more results available (use --offset {})",
410 results.offset + results.limit
411 );
412 }
413 }
414 Ok(())
415}
416
417pub async fn handle_doctor_command() -> Result<()> {
418 use crate::cli_handlers::dashboard::{check_dashboard_health, DASHBOARD_PORT};
419
420 let db_path_info = ProjectContext::get_database_path_info();
422
423 println!("Database:");
425 if let Some(db_path) = &db_path_info.final_database_path {
426 println!(" {}", db_path);
427 } else {
428 println!(" Not found");
429 }
430 println!();
431
432 let dirs_with_db: Vec<&String> = db_path_info
434 .directories_checked
435 .iter()
436 .filter(|d| d.has_intent_engine)
437 .map(|d| &d.path)
438 .collect();
439
440 if !dirs_with_db.is_empty() {
441 println!("Ancestor directories with databases:");
442 for dir in dirs_with_db {
443 println!(" {}", dir);
444 }
445 } else {
446 println!("Ancestor directories with databases: None");
447 }
448 println!();
449
450 print!("Dashboard: ");
452 let dashboard_health = check_dashboard_health(DASHBOARD_PORT).await;
453 if dashboard_health {
454 println!("Running (http://127.0.0.1:{})", DASHBOARD_PORT);
455 } else {
456 println!("Not running (start with 'ie dashboard start')");
457 }
458
459 Ok(())
460}
461
462pub async fn handle_init_command(at: Option<String>, force: bool) -> Result<()> {
463 use serde_json::json;
464
465 let target_dir = if let Some(path) = &at {
467 let p = PathBuf::from(path);
468 if !p.exists() {
469 return Err(IntentError::InvalidInput(format!(
470 "Directory does not exist: {}",
471 path
472 )));
473 }
474 if !p.is_dir() {
475 return Err(IntentError::InvalidInput(format!(
476 "Path is not a directory: {}",
477 path
478 )));
479 }
480 p
481 } else {
482 std::env::current_dir().expect("Failed to get current directory")
484 };
485
486 let intent_dir = target_dir.join(".intent-engine");
487
488 if intent_dir.exists() && !force {
490 let error_msg = format!(
491 ".intent-engine already exists at {}\nUse --force to re-initialize",
492 intent_dir.display()
493 );
494 return Err(IntentError::InvalidInput(error_msg));
495 }
496
497 let ctx = ProjectContext::initialize_project_at(target_dir).await?;
499
500 let result = json!({
502 "success": true,
503 "root": ctx.root.display().to_string(),
504 "database_path": ctx.db_path.display().to_string(),
505 "message": "Intent-Engine initialized successfully"
506 });
507
508 println!("{}", serde_json::to_string_pretty(&result)?);
509 Ok(())
510}
511
512pub async fn handle_session_restore(
513 include_events: usize,
514 workspace: Option<String>,
515) -> Result<()> {
516 use crate::session_restore::SessionRestoreManager;
517
518 if let Some(ws_path) = workspace {
520 std::env::set_current_dir(&ws_path)?;
521 }
522
523 let ctx = match ProjectContext::load().await {
525 Ok(ctx) => ctx,
526 Err(_) => {
527 let result = crate::session_restore::SessionRestoreResult {
529 status: crate::session_restore::SessionStatus::Error,
530 workspace_path: std::env::current_dir()
531 .ok()
532 .and_then(|p| p.to_str().map(String::from)),
533 current_task: None,
534 parent_task: None,
535 siblings: None,
536 children: None,
537 recent_events: None,
538 suggested_commands: Some(vec![
539 "ie workspace init".to_string(),
540 "ie help".to_string(),
541 ]),
542 stats: None,
543 recommended_task: None,
544 top_pending_tasks: None,
545 error_type: Some(crate::session_restore::ErrorType::WorkspaceNotFound),
546 message: Some("No Intent-Engine workspace found in current directory".to_string()),
547 recovery_suggestion: Some(
548 "Run 'ie workspace init' to create a new workspace".to_string(),
549 ),
550 };
551 println!("{}", serde_json::to_string_pretty(&result)?);
552 return Ok(());
553 },
554 };
555
556 let restore_mgr = SessionRestoreManager::new(&ctx.pool);
557 let result = restore_mgr.restore(include_events).await?;
558
559 println!("{}", serde_json::to_string_pretty(&result)?);
560
561 Ok(())
562}
563
564pub async fn handle_setup(
565 target: Option<String>,
566 scope: &str,
567 force: bool,
568 config_path: Option<String>,
569) -> Result<()> {
570 use crate::setup::claude_code::ClaudeCodeSetup;
571 use crate::setup::{SetupModule, SetupOptions, SetupScope};
572
573 println!("Intent-Engine Unified Setup");
574 println!("============================\n");
575
576 let setup_scope: SetupScope = scope.parse()?;
578
579 let opts = SetupOptions {
581 scope: setup_scope,
582 force,
583 config_path: config_path.map(PathBuf::from),
584 };
585
586 let target_tool = if let Some(t) = target {
588 t
590 } else {
591 use crate::setup::interactive::SetupWizard;
593
594 let wizard = SetupWizard::new();
595 let result = wizard.run(&opts)?;
596
597 if result.success {
599 println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
600 println!("✅ {}", result.message);
601 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
602
603 if !result.files_modified.is_empty() {
604 println!("Files modified:");
605 for file in &result.files_modified {
606 println!(" - {}", file.display());
607 }
608 println!();
609 }
610
611 if let Some(test) = result.connectivity_test {
612 if test.passed {
613 println!("✓ Connectivity test: {}", test.details);
614 } else {
615 println!("✗ Connectivity test: {}", test.details);
616 }
617 println!();
618 }
619
620 println!("Next steps:");
621 println!(" - Restart Claude Code to load MCP server");
622 println!(" - Run 'ie doctor' to verify configuration");
623 println!(" - Try 'ie task add --name \"Test task\"'");
624 println!();
625 } else {
626 println!("\n{}", result.message);
627 }
628
629 return Ok(());
630 };
631
632 match target_tool.as_str() {
634 "claude-code" => {
635 let setup = ClaudeCodeSetup;
636 let result = setup.setup(&opts)?;
637
638 println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
639 println!("✅ {}", result.message);
640 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
641
642 println!("Files modified:");
643 for file in &result.files_modified {
644 println!(" - {}", file.display());
645 }
646
647 if let Some(conn_test) = result.connectivity_test {
648 println!("\nConnectivity test:");
649 if conn_test.passed {
650 println!(" ✅ {}", conn_test.details);
651 } else {
652 println!(" ⚠️ {}", conn_test.details);
653 }
654 }
655
656 println!("\nNext steps:");
657 println!(" 1. Restart Claude Code completely");
658 println!(" 2. Open a new session in a project directory");
659 println!(" 3. You should see Intent-Engine context restored");
660 println!("\nTo verify setup:");
661 println!(" ie setup --target claude-code --diagnose");
662
663 Ok(())
664 },
665 "gemini-cli" | "codex" => {
666 println!("⚠️ Target '{}' is not yet supported.", target_tool);
667 println!("Currently supported: claude-code");
668 Err(IntentError::InvalidInput(format!(
669 "Unsupported target: {}",
670 target_tool
671 )))
672 },
673 _ => Err(IntentError::InvalidInput(format!(
674 "Unknown target: {}. Available: claude-code, gemini-cli, codex",
675 target_tool
676 ))),
677 }
678}
679
680pub fn check_session_start_hook() -> serde_json::Value {
682 use crate::setup::common::get_home_dir;
683 use serde_json::json;
684
685 let home = match get_home_dir() {
686 Ok(h) => h,
687 Err(_) => {
688 return json!({
689 "check": "SessionStart Hook",
690 "status": "⚠ WARNING",
691 "details": {"error": "Unable to determine home directory"}
692 })
693 },
694 };
695
696 let user_hook = home.join(".claude/hooks/session-start.sh");
697 let user_settings = home.join(".claude/settings.json");
698
699 let script_exists = user_hook.exists();
700 let script_executable = script_exists && {
701 #[cfg(unix)]
702 {
703 use std::os::unix::fs::PermissionsExt;
704 std::fs::metadata(&user_hook)
705 .map(|m| m.permissions().mode() & 0o111 != 0)
706 .unwrap_or(false)
707 }
708 #[cfg(not(unix))]
709 {
710 true
711 }
712 };
713
714 let is_configured = if user_settings.exists() {
715 std::fs::read_to_string(&user_settings)
716 .ok()
717 .and_then(|content| serde_json::from_str::<serde_json::Value>(&content).ok())
718 .map(|settings| {
719 settings
720 .get("hooks")
721 .and_then(|h| h.get("SessionStart"))
722 .is_some()
723 })
724 .unwrap_or(false)
725 } else {
726 false
727 };
728
729 let is_active = script_exists && script_executable && is_configured;
730
731 if is_active {
732 json!({
733 "check": "SessionStart Hook",
734 "status": "✓ PASS",
735 "details": {
736 "script": user_hook.display().to_string(),
737 "configured": true,
738 "executable": true,
739 "message": "SessionStart hook is active"
740 }
741 })
742 } else if is_configured && !script_exists {
743 json!({
744 "check": "SessionStart Hook",
745 "status": "✗ FAIL",
746 "details": {
747 "configured": true,
748 "exists": false,
749 "message": "Hook configured but script file missing"
750 }
751 })
752 } else if script_exists && !script_executable {
753 json!({
754 "check": "SessionStart Hook",
755 "status": "✗ FAIL",
756 "details": {
757 "executable": false,
758 "message": "Script not executable. Run: chmod +x ~/.claude/hooks/session-start.sh"
759 }
760 })
761 } else {
762 json!({
763 "check": "SessionStart Hook",
764 "status": "⚠ WARNING",
765 "details": {
766 "configured": false,
767 "message": "Not configured. Run 'ie setup --target claude-code'",
768 "setup_command": "ie setup --target claude-code"
769 }
770 })
771 }
772}
773
774pub fn handle_logs_command(
775 mode: Option<String>,
776 level: Option<String>,
777 since: Option<String>,
778 until: Option<String>,
779 limit: Option<usize>,
780 follow: bool,
781 export: String,
782) -> Result<()> {
783 use crate::logs::{
784 follow_logs, format_entry_json, format_entry_text, parse_duration, query_logs, LogQuery,
785 };
786
787 let mut query = LogQuery {
789 mode,
790 level,
791 limit,
792 ..Default::default()
793 };
794
795 if let Some(since_str) = since {
796 query.since = parse_duration(&since_str);
797 if query.since.is_none() {
798 return Err(IntentError::InvalidInput(format!(
799 "Invalid duration format: {}. Use format like '1h', '24h', '7d'",
800 since_str
801 )));
802 }
803 }
804
805 if let Some(until_str) = until {
806 use chrono::DateTime;
807 match DateTime::parse_from_rfc3339(&until_str) {
808 Ok(dt) => query.until = Some(dt.with_timezone(&chrono::Utc)),
809 Err(e) => {
810 return Err(IntentError::InvalidInput(format!(
811 "Invalid timestamp format: {}. Error: {}",
812 until_str, e
813 )))
814 },
815 }
816 }
817
818 if follow {
820 return follow_logs(&query).map_err(IntentError::IoError);
821 }
822
823 let entries = query_logs(&query).map_err(IntentError::IoError)?;
825
826 if entries.is_empty() {
827 eprintln!("No log entries found matching the criteria");
828 return Ok(());
829 }
830
831 match export.as_str() {
833 "json" => {
834 println!("[");
835 for (i, entry) in entries.iter().enumerate() {
836 print!(" {}", format_entry_json(entry));
837 if i < entries.len() - 1 {
838 println!(",");
839 } else {
840 println!();
841 }
842 }
843 println!("]");
844 },
845 _ => {
846 for entry in entries {
847 println!("{}", format_entry_text(&entry));
848 }
849 },
850 }
851
852 Ok(())
853}