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) -> Result<()> {
152 use crate::search::SearchManager;
153
154 let ctx = ProjectContext::load_or_init().await?;
155 let search_mgr = SearchManager::new(&ctx.pool);
156
157 let results = search_mgr
158 .unified_search(query, include_tasks, include_events, limit)
159 .await?;
160
161 println!("{}", serde_json::to_string_pretty(&results)?);
162 Ok(())
163}
164
165pub async fn handle_doctor_command() -> Result<()> {
166 use serde_json::json;
167
168 let mut checks = vec![];
169
170 let db_path_info = ProjectContext::get_database_path_info();
172 checks.push(json!({
173 "check": "Database Path Resolution",
174 "status": "✓ INFO",
175 "details": db_path_info
176 }));
177
178 match ProjectContext::load_or_init().await {
180 Ok(ctx) => {
181 match sqlx::query(sql_constants::COUNT_TASKS_TOTAL)
182 .fetch_one(&ctx.pool)
183 .await
184 {
185 Ok(row) => {
186 let count: i64 = row.try_get(0).unwrap_or(0);
187 checks.push(json!({
188 "check": "Database Health",
189 "status": "✓ PASS",
190 "details": {
191 "connected": true,
192 "tasks_count": count,
193 "message": format!("Database operational with {} tasks", count)
194 }
195 }));
196 },
197 Err(e) => {
198 checks.push(json!({
199 "check": "Database Health",
200 "status": "✗ FAIL",
201 "details": {"error": format!("Query failed: {}", e)}
202 }));
203 },
204 }
205 },
206 Err(e) => {
207 checks.push(json!({
208 "check": "Database Health",
209 "status": "✗ FAIL",
210 "details": {"error": format!("Failed to load database: {}", e)}
211 }));
212 },
213 }
214
215 checks.push(check_dashboard_status().await);
217 checks.push(check_mcp_connections().await);
218 checks.push(check_session_start_hook());
219
220 let has_failures = checks
222 .iter()
223 .any(|c| c["status"].as_str().unwrap_or("").contains("✗ FAIL"));
224 let has_warnings = checks
225 .iter()
226 .any(|c| c["status"].as_str().unwrap_or("").contains("⚠ WARNING"));
227
228 let summary = if has_failures {
229 "✗ Critical issues detected"
230 } else if has_warnings {
231 "⚠ Some optional features need attention"
232 } else {
233 "✓ All systems operational"
234 };
235
236 let result = json!({
237 "summary": summary,
238 "overall_status": if has_failures { "unhealthy" }
239 else if has_warnings { "warnings" }
240 else { "healthy" },
241 "checks": checks
242 });
243
244 println!("{}", serde_json::to_string_pretty(&result)?);
245
246 if has_failures {
247 std::process::exit(1);
248 }
249
250 Ok(())
251}
252
253pub async fn handle_init_command(at: Option<String>, force: bool) -> Result<()> {
254 use serde_json::json;
255
256 let target_dir = if let Some(path) = &at {
258 let p = PathBuf::from(path);
259 if !p.exists() {
260 return Err(IntentError::InvalidInput(format!(
261 "Directory does not exist: {}",
262 path
263 )));
264 }
265 if !p.is_dir() {
266 return Err(IntentError::InvalidInput(format!(
267 "Path is not a directory: {}",
268 path
269 )));
270 }
271 p
272 } else {
273 std::env::current_dir().expect("Failed to get current directory")
275 };
276
277 let intent_dir = target_dir.join(".intent-engine");
278
279 if intent_dir.exists() && !force {
281 let error_msg = format!(
282 ".intent-engine already exists at {}\nUse --force to re-initialize",
283 intent_dir.display()
284 );
285 return Err(IntentError::InvalidInput(error_msg));
286 }
287
288 let ctx = ProjectContext::initialize_project_at(target_dir).await?;
290
291 let result = json!({
293 "success": true,
294 "root": ctx.root.display().to_string(),
295 "database_path": ctx.db_path.display().to_string(),
296 "message": "Intent-Engine initialized successfully"
297 });
298
299 println!("{}", serde_json::to_string_pretty(&result)?);
300 Ok(())
301}
302
303pub async fn handle_session_restore(
304 include_events: usize,
305 workspace: Option<String>,
306) -> Result<()> {
307 use crate::session_restore::SessionRestoreManager;
308
309 if let Some(ws_path) = workspace {
311 std::env::set_current_dir(&ws_path)?;
312 }
313
314 let ctx = match ProjectContext::load().await {
316 Ok(ctx) => ctx,
317 Err(_) => {
318 let result = crate::session_restore::SessionRestoreResult {
320 status: crate::session_restore::SessionStatus::Error,
321 workspace_path: std::env::current_dir()
322 .ok()
323 .and_then(|p| p.to_str().map(String::from)),
324 current_task: None,
325 parent_task: None,
326 siblings: None,
327 children: None,
328 recent_events: None,
329 suggested_commands: Some(vec![
330 "ie workspace init".to_string(),
331 "ie help".to_string(),
332 ]),
333 stats: None,
334 error_type: Some(crate::session_restore::ErrorType::WorkspaceNotFound),
335 message: Some("No Intent-Engine workspace found in current directory".to_string()),
336 recovery_suggestion: Some(
337 "Run 'ie workspace init' to create a new workspace".to_string(),
338 ),
339 };
340 println!("{}", serde_json::to_string_pretty(&result)?);
341 return Ok(());
342 },
343 };
344
345 let restore_mgr = SessionRestoreManager::new(&ctx.pool);
346 let result = restore_mgr.restore(include_events).await?;
347
348 println!("{}", serde_json::to_string_pretty(&result)?);
349
350 Ok(())
351}
352
353pub async fn handle_setup(
354 target: Option<String>,
355 scope: &str,
356 force: bool,
357 config_path: Option<String>,
358) -> Result<()> {
359 use crate::setup::claude_code::ClaudeCodeSetup;
360 use crate::setup::{SetupModule, SetupOptions, SetupScope};
361
362 println!("Intent-Engine Unified Setup");
363 println!("============================\n");
364
365 let setup_scope: SetupScope = scope.parse()?;
367
368 let opts = SetupOptions {
370 scope: setup_scope,
371 force,
372 config_path: config_path.map(PathBuf::from),
373 };
374
375 let target_tool = if let Some(t) = target {
377 t
379 } else {
380 use crate::setup::interactive::SetupWizard;
382
383 let wizard = SetupWizard::new();
384 let result = wizard.run(&opts)?;
385
386 if result.success {
388 println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
389 println!("✅ {}", result.message);
390 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
391
392 if !result.files_modified.is_empty() {
393 println!("Files modified:");
394 for file in &result.files_modified {
395 println!(" - {}", file.display());
396 }
397 println!();
398 }
399
400 if let Some(test) = result.connectivity_test {
401 if test.passed {
402 println!("✓ Connectivity test: {}", test.details);
403 } else {
404 println!("✗ Connectivity test: {}", test.details);
405 }
406 println!();
407 }
408
409 println!("Next steps:");
410 println!(" - Restart Claude Code to load MCP server");
411 println!(" - Run 'ie doctor' to verify configuration");
412 println!(" - Try 'ie task add --name \"Test task\"'");
413 println!();
414 } else {
415 println!("\n{}", result.message);
416 }
417
418 return Ok(());
419 };
420
421 match target_tool.as_str() {
423 "claude-code" => {
424 let setup = ClaudeCodeSetup;
425 let result = setup.setup(&opts)?;
426
427 println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
428 println!("✅ {}", result.message);
429 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
430
431 println!("Files modified:");
432 for file in &result.files_modified {
433 println!(" - {}", file.display());
434 }
435
436 if let Some(conn_test) = result.connectivity_test {
437 println!("\nConnectivity test:");
438 if conn_test.passed {
439 println!(" ✅ {}", conn_test.details);
440 } else {
441 println!(" ⚠️ {}", conn_test.details);
442 }
443 }
444
445 println!("\nNext steps:");
446 println!(" 1. Restart Claude Code completely");
447 println!(" 2. Open a new session in a project directory");
448 println!(" 3. You should see Intent-Engine context restored");
449 println!("\nTo verify setup:");
450 println!(" ie setup --target claude-code --diagnose");
451
452 Ok(())
453 },
454 "gemini-cli" | "codex" => {
455 println!("⚠️ Target '{}' is not yet supported.", target_tool);
456 println!("Currently supported: claude-code");
457 Err(IntentError::InvalidInput(format!(
458 "Unsupported target: {}",
459 target_tool
460 )))
461 },
462 _ => Err(IntentError::InvalidInput(format!(
463 "Unknown target: {}. Available: claude-code, gemini-cli, codex",
464 target_tool
465 ))),
466 }
467}
468
469pub fn check_session_start_hook() -> serde_json::Value {
471 use crate::setup::common::get_home_dir;
472 use serde_json::json;
473
474 let home = match get_home_dir() {
475 Ok(h) => h,
476 Err(_) => {
477 return json!({
478 "check": "SessionStart Hook",
479 "status": "⚠ WARNING",
480 "details": {"error": "Unable to determine home directory"}
481 })
482 },
483 };
484
485 let user_hook = home.join(".claude/hooks/session-start.sh");
486 let user_settings = home.join(".claude/settings.json");
487
488 let script_exists = user_hook.exists();
489 let script_executable = if script_exists {
490 #[cfg(unix)]
491 {
492 use std::os::unix::fs::PermissionsExt;
493 std::fs::metadata(&user_hook)
494 .map(|m| m.permissions().mode() & 0o111 != 0)
495 .unwrap_or(false)
496 }
497 #[cfg(not(unix))]
498 {
499 true
500 }
501 } else {
502 false
503 };
504
505 let is_configured = if user_settings.exists() {
506 std::fs::read_to_string(&user_settings)
507 .ok()
508 .and_then(|content| serde_json::from_str::<serde_json::Value>(&content).ok())
509 .map(|settings| {
510 settings
511 .get("hooks")
512 .and_then(|h| h.get("SessionStart"))
513 .is_some()
514 })
515 .unwrap_or(false)
516 } else {
517 false
518 };
519
520 let is_active = script_exists && script_executable && is_configured;
521
522 if is_active {
523 json!({
524 "check": "SessionStart Hook",
525 "status": "✓ PASS",
526 "details": {
527 "script": user_hook.display().to_string(),
528 "configured": true,
529 "executable": true,
530 "message": "SessionStart hook is active"
531 }
532 })
533 } else if is_configured && !script_exists {
534 json!({
535 "check": "SessionStart Hook",
536 "status": "✗ FAIL",
537 "details": {
538 "configured": true,
539 "exists": false,
540 "message": "Hook configured but script file missing"
541 }
542 })
543 } else if script_exists && !script_executable {
544 json!({
545 "check": "SessionStart Hook",
546 "status": "✗ FAIL",
547 "details": {
548 "executable": false,
549 "message": "Script not executable. Run: chmod +x ~/.claude/hooks/session-start.sh"
550 }
551 })
552 } else {
553 json!({
554 "check": "SessionStart Hook",
555 "status": "⚠ WARNING",
556 "details": {
557 "configured": false,
558 "message": "Not configured. Run 'ie setup --target claude-code'",
559 "setup_command": "ie setup --target claude-code"
560 }
561 })
562 }
563}
564
565pub fn handle_logs_command(
566 mode: Option<String>,
567 level: Option<String>,
568 since: Option<String>,
569 until: Option<String>,
570 limit: Option<usize>,
571 follow: bool,
572 export: String,
573) -> Result<()> {
574 use crate::logs::{
575 follow_logs, format_entry_json, format_entry_text, parse_duration, query_logs, LogQuery,
576 };
577
578 let mut query = LogQuery {
580 mode,
581 level,
582 limit,
583 ..Default::default()
584 };
585
586 if let Some(since_str) = since {
587 query.since = parse_duration(&since_str);
588 if query.since.is_none() {
589 return Err(IntentError::InvalidInput(format!(
590 "Invalid duration format: {}. Use format like '1h', '24h', '7d'",
591 since_str
592 )));
593 }
594 }
595
596 if let Some(until_str) = until {
597 use chrono::DateTime;
598 match DateTime::parse_from_rfc3339(&until_str) {
599 Ok(dt) => query.until = Some(dt.with_timezone(&chrono::Utc)),
600 Err(e) => {
601 return Err(IntentError::InvalidInput(format!(
602 "Invalid timestamp format: {}. Error: {}",
603 until_str, e
604 )))
605 },
606 }
607 }
608
609 if follow {
611 return follow_logs(&query).map_err(IntentError::IoError);
612 }
613
614 let entries = query_logs(&query).map_err(IntentError::IoError)?;
616
617 if entries.is_empty() {
618 eprintln!("No log entries found matching the criteria");
619 return Ok(());
620 }
621
622 match export.as_str() {
624 "json" => {
625 println!("[");
626 for (i, entry) in entries.iter().enumerate() {
627 print!(" {}", format_entry_json(entry));
628 if i < entries.len() - 1 {
629 println!(",");
630 } else {
631 println!();
632 }
633 }
634 println!("]");
635 },
636 _ => {
637 for entry in entries {
638 println!("{}", format_entry_text(&entry));
639 }
640 },
641 }
642
643 Ok(())
644}