intent_engine/cli_handlers/
other.rs

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    // Handle backward compatibility: --set flag takes precedence
21    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    // Handle subcommands
35    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            // Default: display current task in JSON format
58            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            // Determine the target task ID
103            let target_task_id = if let Some(id) = task_id {
104                // Use the provided task_id
105                id
106            } else {
107                // Fall back to current_task_id
108                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    // 1. Database Path Resolution
171    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    // 2. Database Health
179    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    // 3-5. New checks
216    checks.push(check_dashboard_status().await);
217    checks.push(check_mcp_connections().await);
218    checks.push(check_session_start_hook());
219
220    // Status summary
221    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    // Determine target directory
257    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        // Use current working directory
274        std::env::current_dir().expect("Failed to get current directory")
275    };
276
277    let intent_dir = target_dir.join(".intent-engine");
278
279    // Check if already exists
280    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    // Perform initialization
289    let ctx = ProjectContext::initialize_project_at(target_dir).await?;
290
291    // Success output
292    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 workspace path is specified, change to that directory
310    if let Some(ws_path) = workspace {
311        std::env::set_current_dir(&ws_path)?;
312    }
313
314    // Try to load project context
315    let ctx = match ProjectContext::load().await {
316        Ok(ctx) => ctx,
317        Err(_) => {
318            // Workspace not found
319            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    // Parse scope
366    let setup_scope: SetupScope = scope.parse()?;
367
368    // Build options
369    let opts = SetupOptions {
370        scope: setup_scope,
371        force,
372        config_path: config_path.map(PathBuf::from),
373    };
374
375    // Determine target (interactive if not specified)
376    let target_tool = if let Some(t) = target {
377        // Direct mode: target specified via CLI
378        t
379    } else {
380        // Interactive mode: launch wizard
381        use crate::setup::interactive::SetupWizard;
382
383        let wizard = SetupWizard::new();
384        let result = wizard.run(&opts)?;
385
386        // Print result and exit
387        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    // Setup mode
422    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
469/// Check SessionStart hook configuration and effectiveness
470pub 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    // Build query
579    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    // Handle follow mode
610    if follow {
611        return follow_logs(&query).map_err(IntentError::IoError);
612    }
613
614    // Query logs
615    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    // Display results
623    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}