1use crate::cli::{CurrentAction, EventCommands};
2use crate::cli_handlers::read_stdin;
3use crate::cli_handlers::{check_dashboard_status, check_mcp_connections};
4use crate::error::{IntentError, Result};
5use crate::events::EventManager;
6use crate::project::ProjectContext;
7use crate::report::ReportManager;
8use crate::sql_constants;
9use crate::workspace::WorkspaceManager;
10use sqlx::Row;
11use std::path::PathBuf;
12
13pub async fn handle_current_command(
14 set: Option<i64>,
15 command: Option<CurrentAction>,
16) -> Result<()> {
17 let ctx = ProjectContext::load().await?;
18 let workspace_mgr = WorkspaceManager::new(&ctx.pool);
19
20 if let Some(task_id) = set {
22 eprintln!("⚠️ Warning: 'ie current --set' is a low-level atomic command.");
23 eprintln!(
24 " For normal use, prefer 'ie task start {}' which ensures data consistency.",
25 task_id
26 );
27 eprintln!();
28 let response = workspace_mgr.set_current_task(task_id).await?;
29 println!("✓ Switched to task #{}", task_id);
30 println!("{}", serde_json::to_string_pretty(&response)?);
31 return Ok(());
32 }
33
34 match command {
36 Some(CurrentAction::Set { task_id }) => {
37 eprintln!("⚠️ Warning: 'ie current set' is a low-level atomic command.");
38 eprintln!(
39 " For normal use, prefer 'ie task start {}' which ensures data consistency.",
40 task_id
41 );
42 eprintln!();
43 let response = workspace_mgr.set_current_task(task_id).await?;
44 println!("✓ Switched to task #{}", task_id);
45 println!("{}", serde_json::to_string_pretty(&response)?);
46 },
47 Some(CurrentAction::Clear) => {
48 eprintln!("⚠️ Warning: 'ie current clear' is a low-level atomic command.");
49 eprintln!(" For normal use, prefer 'ie task done' or 'ie task switch' which ensures data consistency.");
50 eprintln!();
51 sqlx::query("DELETE FROM workspace_state WHERE key = 'current_task_id'")
52 .execute(&ctx.pool)
53 .await?;
54 println!("✓ Current task cleared");
55 },
56 None => {
57 let response = workspace_mgr.get_current_task().await?;
59 println!("{}", serde_json::to_string_pretty(&response)?);
60 },
61 }
62
63 Ok(())
64}
65
66pub async fn handle_report_command(
67 since: Option<String>,
68 status: Option<String>,
69 filter_name: Option<String>,
70 filter_spec: Option<String>,
71 summary_only: bool,
72) -> Result<()> {
73 let ctx = ProjectContext::load().await?;
74 let report_mgr = ReportManager::new(&ctx.pool);
75
76 let report = report_mgr
77 .generate_report(since, status, filter_name, filter_spec, summary_only)
78 .await?;
79 println!("{}", serde_json::to_string_pretty(&report)?);
80
81 Ok(())
82}
83
84pub async fn handle_event_command(cmd: EventCommands) -> Result<()> {
85 match cmd {
86 EventCommands::Add {
87 task_id,
88 log_type,
89 data_stdin,
90 } => {
91 let ctx = ProjectContext::load_or_init().await?;
92 let event_mgr = EventManager::new(&ctx.pool);
93
94 let data = if data_stdin {
95 read_stdin()?
96 } else {
97 return Err(IntentError::InvalidInput(
98 "--data-stdin is required".to_string(),
99 ));
100 };
101
102 let target_task_id = if let Some(id) = task_id {
104 id
106 } else {
107 let current_task_id: Option<String> = sqlx::query_scalar(
109 "SELECT value FROM workspace_state WHERE key = 'current_task_id'",
110 )
111 .fetch_optional(&ctx.pool)
112 .await?;
113
114 current_task_id
115 .and_then(|s| s.parse::<i64>().ok())
116 .ok_or_else(|| IntentError::InvalidInput(
117 "No current task is set and --task-id was not provided. Use 'current --set <ID>' to set a task first.".to_string(),
118 ))?
119 };
120
121 let event = event_mgr
122 .add_event(target_task_id, &log_type, &data)
123 .await?;
124 println!("{}", serde_json::to_string_pretty(&event)?);
125 },
126
127 EventCommands::List {
128 task_id,
129 limit,
130 log_type,
131 since,
132 } => {
133 let ctx = ProjectContext::load().await?;
134 let event_mgr = EventManager::new(&ctx.pool);
135
136 let events = event_mgr
137 .list_events(task_id, limit, log_type, since)
138 .await?;
139 println!("{}", serde_json::to_string_pretty(&events)?);
140 },
141 }
142
143 Ok(())
144}
145
146pub async fn handle_search_command(
147 query: &str,
148 include_tasks: bool,
149 include_events: bool,
150 limit: Option<i64>,
151 offset: Option<i64>,
152) -> Result<()> {
153 use crate::search::SearchManager;
154
155 let ctx = ProjectContext::load_or_init().await?;
156 let search_mgr = SearchManager::new(&ctx.pool);
157
158 let results = search_mgr
159 .search(query, include_tasks, include_events, limit, offset, false)
160 .await?;
161
162 eprintln!(
164 "Found {} tasks, {} events (showing {} results)",
165 results.total_tasks,
166 results.total_events,
167 results.results.len()
168 );
169
170 if results.has_more {
171 eprintln!(
172 "Use --offset {} to see more results",
173 results.offset + results.limit
174 );
175 }
176
177 println!("{}", serde_json::to_string_pretty(&results.results)?);
178 Ok(())
179}
180
181pub async fn handle_doctor_command() -> Result<()> {
182 use serde_json::json;
183
184 let mut checks = vec![];
185
186 let db_path_info = ProjectContext::get_database_path_info();
188 checks.push(json!({
189 "check": "Database Path Resolution",
190 "status": "✓ INFO",
191 "details": db_path_info
192 }));
193
194 match ProjectContext::load_or_init().await {
196 Ok(ctx) => {
197 match sqlx::query(sql_constants::COUNT_TASKS_TOTAL)
198 .fetch_one(&ctx.pool)
199 .await
200 {
201 Ok(row) => {
202 let count: i64 = row.try_get(0).unwrap_or(0);
203 checks.push(json!({
204 "check": "Database Health",
205 "status": "✓ PASS",
206 "details": {
207 "connected": true,
208 "tasks_count": count,
209 "message": format!("Database operational with {} tasks", count)
210 }
211 }));
212 },
213 Err(e) => {
214 checks.push(json!({
215 "check": "Database Health",
216 "status": "✗ FAIL",
217 "details": {"error": format!("Query failed: {}", e)}
218 }));
219 },
220 }
221 },
222 Err(e) => {
223 checks.push(json!({
224 "check": "Database Health",
225 "status": "✗ FAIL",
226 "details": {"error": format!("Failed to load database: {}", e)}
227 }));
228 },
229 }
230
231 checks.push(check_dashboard_status().await);
233 checks.push(check_mcp_connections().await);
234 checks.push(check_session_start_hook());
235
236 let has_failures = checks
238 .iter()
239 .any(|c| c["status"].as_str().unwrap_or("").contains("✗ FAIL"));
240 let has_warnings = checks
241 .iter()
242 .any(|c| c["status"].as_str().unwrap_or("").contains("⚠ WARNING"));
243
244 let summary = if has_failures {
245 "✗ Critical issues detected"
246 } else if has_warnings {
247 "⚠ Some optional features need attention"
248 } else {
249 "✓ All systems operational"
250 };
251
252 let result = json!({
253 "summary": summary,
254 "overall_status": if has_failures { "unhealthy" }
255 else if has_warnings { "warnings" }
256 else { "healthy" },
257 "checks": checks
258 });
259
260 println!("{}", serde_json::to_string_pretty(&result)?);
261
262 if has_failures {
263 std::process::exit(1);
264 }
265
266 Ok(())
267}
268
269pub async fn handle_init_command(at: Option<String>, force: bool) -> Result<()> {
270 use serde_json::json;
271
272 let target_dir = if let Some(path) = &at {
274 let p = PathBuf::from(path);
275 if !p.exists() {
276 return Err(IntentError::InvalidInput(format!(
277 "Directory does not exist: {}",
278 path
279 )));
280 }
281 if !p.is_dir() {
282 return Err(IntentError::InvalidInput(format!(
283 "Path is not a directory: {}",
284 path
285 )));
286 }
287 p
288 } else {
289 std::env::current_dir().expect("Failed to get current directory")
291 };
292
293 let intent_dir = target_dir.join(".intent-engine");
294
295 if intent_dir.exists() && !force {
297 let error_msg = format!(
298 ".intent-engine already exists at {}\nUse --force to re-initialize",
299 intent_dir.display()
300 );
301 return Err(IntentError::InvalidInput(error_msg));
302 }
303
304 let ctx = ProjectContext::initialize_project_at(target_dir).await?;
306
307 let result = json!({
309 "success": true,
310 "root": ctx.root.display().to_string(),
311 "database_path": ctx.db_path.display().to_string(),
312 "message": "Intent-Engine initialized successfully"
313 });
314
315 println!("{}", serde_json::to_string_pretty(&result)?);
316 Ok(())
317}
318
319pub async fn handle_session_restore(
320 include_events: usize,
321 workspace: Option<String>,
322) -> Result<()> {
323 use crate::session_restore::SessionRestoreManager;
324
325 if let Some(ws_path) = workspace {
327 std::env::set_current_dir(&ws_path)?;
328 }
329
330 let ctx = match ProjectContext::load().await {
332 Ok(ctx) => ctx,
333 Err(_) => {
334 let result = crate::session_restore::SessionRestoreResult {
336 status: crate::session_restore::SessionStatus::Error,
337 workspace_path: std::env::current_dir()
338 .ok()
339 .and_then(|p| p.to_str().map(String::from)),
340 current_task: None,
341 parent_task: None,
342 siblings: None,
343 children: None,
344 recent_events: None,
345 suggested_commands: Some(vec![
346 "ie workspace init".to_string(),
347 "ie help".to_string(),
348 ]),
349 stats: None,
350 recommended_task: None,
351 top_pending_tasks: None,
352 error_type: Some(crate::session_restore::ErrorType::WorkspaceNotFound),
353 message: Some("No Intent-Engine workspace found in current directory".to_string()),
354 recovery_suggestion: Some(
355 "Run 'ie workspace init' to create a new workspace".to_string(),
356 ),
357 };
358 println!("{}", serde_json::to_string_pretty(&result)?);
359 return Ok(());
360 },
361 };
362
363 let restore_mgr = SessionRestoreManager::new(&ctx.pool);
364 let result = restore_mgr.restore(include_events).await?;
365
366 println!("{}", serde_json::to_string_pretty(&result)?);
367
368 Ok(())
369}
370
371pub async fn handle_setup(
372 target: Option<String>,
373 scope: &str,
374 force: bool,
375 config_path: Option<String>,
376) -> Result<()> {
377 use crate::setup::claude_code::ClaudeCodeSetup;
378 use crate::setup::{SetupModule, SetupOptions, SetupScope};
379
380 println!("Intent-Engine Unified Setup");
381 println!("============================\n");
382
383 let setup_scope: SetupScope = scope.parse()?;
385
386 let opts = SetupOptions {
388 scope: setup_scope,
389 force,
390 config_path: config_path.map(PathBuf::from),
391 };
392
393 let target_tool = if let Some(t) = target {
395 t
397 } else {
398 use crate::setup::interactive::SetupWizard;
400
401 let wizard = SetupWizard::new();
402 let result = wizard.run(&opts)?;
403
404 if result.success {
406 println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
407 println!("✅ {}", result.message);
408 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
409
410 if !result.files_modified.is_empty() {
411 println!("Files modified:");
412 for file in &result.files_modified {
413 println!(" - {}", file.display());
414 }
415 println!();
416 }
417
418 if let Some(test) = result.connectivity_test {
419 if test.passed {
420 println!("✓ Connectivity test: {}", test.details);
421 } else {
422 println!("✗ Connectivity test: {}", test.details);
423 }
424 println!();
425 }
426
427 println!("Next steps:");
428 println!(" - Restart Claude Code to load MCP server");
429 println!(" - Run 'ie doctor' to verify configuration");
430 println!(" - Try 'ie task add --name \"Test task\"'");
431 println!();
432 } else {
433 println!("\n{}", result.message);
434 }
435
436 return Ok(());
437 };
438
439 match target_tool.as_str() {
441 "claude-code" => {
442 let setup = ClaudeCodeSetup;
443 let result = setup.setup(&opts)?;
444
445 println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
446 println!("✅ {}", result.message);
447 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
448
449 println!("Files modified:");
450 for file in &result.files_modified {
451 println!(" - {}", file.display());
452 }
453
454 if let Some(conn_test) = result.connectivity_test {
455 println!("\nConnectivity test:");
456 if conn_test.passed {
457 println!(" ✅ {}", conn_test.details);
458 } else {
459 println!(" ⚠️ {}", conn_test.details);
460 }
461 }
462
463 println!("\nNext steps:");
464 println!(" 1. Restart Claude Code completely");
465 println!(" 2. Open a new session in a project directory");
466 println!(" 3. You should see Intent-Engine context restored");
467 println!("\nTo verify setup:");
468 println!(" ie setup --target claude-code --diagnose");
469
470 Ok(())
471 },
472 "gemini-cli" | "codex" => {
473 println!("⚠️ Target '{}' is not yet supported.", target_tool);
474 println!("Currently supported: claude-code");
475 Err(IntentError::InvalidInput(format!(
476 "Unsupported target: {}",
477 target_tool
478 )))
479 },
480 _ => Err(IntentError::InvalidInput(format!(
481 "Unknown target: {}. Available: claude-code, gemini-cli, codex",
482 target_tool
483 ))),
484 }
485}
486
487pub fn check_session_start_hook() -> serde_json::Value {
489 use crate::setup::common::get_home_dir;
490 use serde_json::json;
491
492 let home = match get_home_dir() {
493 Ok(h) => h,
494 Err(_) => {
495 return json!({
496 "check": "SessionStart Hook",
497 "status": "⚠ WARNING",
498 "details": {"error": "Unable to determine home directory"}
499 })
500 },
501 };
502
503 let user_hook = home.join(".claude/hooks/session-start.sh");
504 let user_settings = home.join(".claude/settings.json");
505
506 let script_exists = user_hook.exists();
507 let script_executable = if script_exists {
508 #[cfg(unix)]
509 {
510 use std::os::unix::fs::PermissionsExt;
511 std::fs::metadata(&user_hook)
512 .map(|m| m.permissions().mode() & 0o111 != 0)
513 .unwrap_or(false)
514 }
515 #[cfg(not(unix))]
516 {
517 true
518 }
519 } else {
520 false
521 };
522
523 let is_configured = if user_settings.exists() {
524 std::fs::read_to_string(&user_settings)
525 .ok()
526 .and_then(|content| serde_json::from_str::<serde_json::Value>(&content).ok())
527 .map(|settings| {
528 settings
529 .get("hooks")
530 .and_then(|h| h.get("SessionStart"))
531 .is_some()
532 })
533 .unwrap_or(false)
534 } else {
535 false
536 };
537
538 let is_active = script_exists && script_executable && is_configured;
539
540 if is_active {
541 json!({
542 "check": "SessionStart Hook",
543 "status": "✓ PASS",
544 "details": {
545 "script": user_hook.display().to_string(),
546 "configured": true,
547 "executable": true,
548 "message": "SessionStart hook is active"
549 }
550 })
551 } else if is_configured && !script_exists {
552 json!({
553 "check": "SessionStart Hook",
554 "status": "✗ FAIL",
555 "details": {
556 "configured": true,
557 "exists": false,
558 "message": "Hook configured but script file missing"
559 }
560 })
561 } else if script_exists && !script_executable {
562 json!({
563 "check": "SessionStart Hook",
564 "status": "✗ FAIL",
565 "details": {
566 "executable": false,
567 "message": "Script not executable. Run: chmod +x ~/.claude/hooks/session-start.sh"
568 }
569 })
570 } else {
571 json!({
572 "check": "SessionStart Hook",
573 "status": "⚠ WARNING",
574 "details": {
575 "configured": false,
576 "message": "Not configured. Run 'ie setup --target claude-code'",
577 "setup_command": "ie setup --target claude-code"
578 }
579 })
580 }
581}
582
583pub fn handle_logs_command(
584 mode: Option<String>,
585 level: Option<String>,
586 since: Option<String>,
587 until: Option<String>,
588 limit: Option<usize>,
589 follow: bool,
590 export: String,
591) -> Result<()> {
592 use crate::logs::{
593 follow_logs, format_entry_json, format_entry_text, parse_duration, query_logs, LogQuery,
594 };
595
596 let mut query = LogQuery {
598 mode,
599 level,
600 limit,
601 ..Default::default()
602 };
603
604 if let Some(since_str) = since {
605 query.since = parse_duration(&since_str);
606 if query.since.is_none() {
607 return Err(IntentError::InvalidInput(format!(
608 "Invalid duration format: {}. Use format like '1h', '24h', '7d'",
609 since_str
610 )));
611 }
612 }
613
614 if let Some(until_str) = until {
615 use chrono::DateTime;
616 match DateTime::parse_from_rfc3339(&until_str) {
617 Ok(dt) => query.until = Some(dt.with_timezone(&chrono::Utc)),
618 Err(e) => {
619 return Err(IntentError::InvalidInput(format!(
620 "Invalid timestamp format: {}. Error: {}",
621 until_str, e
622 )))
623 },
624 }
625 }
626
627 if follow {
629 return follow_logs(&query).map_err(IntentError::IoError);
630 }
631
632 let entries = query_logs(&query).map_err(IntentError::IoError)?;
634
635 if entries.is_empty() {
636 eprintln!("No log entries found matching the criteria");
637 return Ok(());
638 }
639
640 match export.as_str() {
642 "json" => {
643 println!("[");
644 for (i, entry) in entries.iter().enumerate() {
645 print!(" {}", format_entry_json(entry));
646 if i < entries.len() - 1 {
647 println!(",");
648 } else {
649 println!();
650 }
651 }
652 println!("]");
653 },
654 _ => {
655 for entry in entries {
656 println!("{}", format_entry_text(&entry));
657 }
658 },
659 }
660
661 Ok(())
662}