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    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    // Print pagination info
163    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    // 1. Database Path Resolution
187    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    // 2. Database Health
195    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    // 3-5. New checks
232    checks.push(check_dashboard_status().await);
233    checks.push(check_mcp_connections().await);
234    checks.push(check_session_start_hook());
235
236    // Status summary
237    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    // Determine target directory
273    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        // Use current working directory
290        std::env::current_dir().expect("Failed to get current directory")
291    };
292
293    let intent_dir = target_dir.join(".intent-engine");
294
295    // Check if already exists
296    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    // Perform initialization
305    let ctx = ProjectContext::initialize_project_at(target_dir).await?;
306
307    // Success output
308    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 workspace path is specified, change to that directory
326    if let Some(ws_path) = workspace {
327        std::env::set_current_dir(&ws_path)?;
328    }
329
330    // Try to load project context
331    let ctx = match ProjectContext::load().await {
332        Ok(ctx) => ctx,
333        Err(_) => {
334            // Workspace not found
335            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    // Parse scope
384    let setup_scope: SetupScope = scope.parse()?;
385
386    // Build options
387    let opts = SetupOptions {
388        scope: setup_scope,
389        force,
390        config_path: config_path.map(PathBuf::from),
391    };
392
393    // Determine target (interactive if not specified)
394    let target_tool = if let Some(t) = target {
395        // Direct mode: target specified via CLI
396        t
397    } else {
398        // Interactive mode: launch wizard
399        use crate::setup::interactive::SetupWizard;
400
401        let wizard = SetupWizard::new();
402        let result = wizard.run(&opts)?;
403
404        // Print result and exit
405        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    // Setup mode
440    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
487/// Check SessionStart hook configuration and effectiveness
488pub 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    // Build query
597    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    // Handle follow mode
628    if follow {
629        return follow_logs(&query).map_err(IntentError::IoError);
630    }
631
632    // Query logs
633    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    // Display results
641    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}