Skip to main content

resq_cli/commands/
explore.rs

1/*
2 * Copyright 2026 ResQ
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! ResQ Explore commands — Unified TUI launcher.
18
19use anyhow::{Context, Result};
20use clap::Parser;
21use std::process::Command;
22
23/// Arguments for the 'explore' (perf-monitor) command
24#[derive(Parser, Debug)]
25pub struct ExploreArgs {
26    /// Service URL to monitor
27    #[arg(default_value = "http://localhost:3000/admin/status")]
28    pub url: String,
29    /// Refresh rate in milliseconds
30    #[arg(long, default_value_t = 500)]
31    pub refresh_ms: u64,
32}
33
34/// Arguments for the 'logs' (log-viewer) command
35#[derive(Parser, Debug)]
36pub struct LogsArgs {
37    /// Log source: "docker" or "file"
38    #[arg(long, default_value = "docker")]
39    pub source: String,
40    /// Filter to a specific service name
41    #[arg(long)]
42    pub service: Option<String>,
43}
44
45/// Arguments for the 'health' (health-checker) command
46#[derive(Parser, Debug)]
47pub struct HealthArgs {
48    /// Poll interval in seconds
49    #[arg(short, long, default_value_t = 5)]
50    pub interval: u64,
51}
52
53/// Arguments for the 'deploy' (deploy-cli) command
54#[derive(Parser, Debug)]
55pub struct DeployArgs {
56    /// Target environment: dev, staging, prod
57    #[arg(long, default_value = "dev")]
58    pub env: String,
59    /// Use Kubernetes instead of Docker Compose
60    #[arg(long)]
61    pub k8s: bool,
62}
63
64/// Arguments for the 'clean' (cleanup) command
65#[derive(Parser, Debug)]
66pub struct CleanArgs {
67    /// Preview what would be deleted without removing anything
68    #[arg(long, default_value_t = false)]
69    pub dry_run: bool,
70}
71
72/// Arguments for the 'asm' (bin-explorer) command
73#[derive(Parser, Debug)]
74pub struct AsmArgs {
75    /// Analyze a single binary path
76    #[arg(long, conflicts_with = "dir")]
77    pub file: Option<String>,
78    /// Analyze binaries under a directory
79    #[arg(long, conflicts_with = "file")]
80    pub dir: Option<String>,
81    /// Recursively traverse directory in batch mode
82    #[arg(long, default_value_t = false)]
83    pub recursive: bool,
84    /// Optional suffix filter in batch mode (e.g. .so, .o)
85    #[arg(long)]
86    pub ext: Option<String>,
87    /// Optional bin-explorer config TOML path
88    #[arg(long)]
89    pub config: Option<String>,
90    /// Disable cache reads/writes
91    #[arg(long, default_value_t = false)]
92    pub no_cache: bool,
93    /// Force cache rebuild
94    #[arg(long, default_value_t = false)]
95    pub rebuild_cache: bool,
96    /// Disable disassembly and only collect metadata
97    #[arg(long, default_value_t = false)]
98    pub no_disasm: bool,
99    /// Maximum functions to disassemble per binary
100    #[arg(long)]
101    pub max_functions: Option<usize>,
102    /// Force interactive TUI mode
103    #[arg(long, default_value_t = false)]
104    pub tui: bool,
105    /// Use non-interactive plain output
106    #[arg(long, default_value_t = false)]
107    pub plain: bool,
108    /// Emit JSON report output
109    #[arg(long, default_value_t = false)]
110    pub json: bool,
111}
112
113/// Run 'perf-monitor' (Perf-Explorer)
114pub async fn run_explore(args: ExploreArgs) -> Result<()> {
115    run_tool(
116        "perf-monitor",
117        &[&args.url, "--refresh-ms", &args.refresh_ms.to_string()],
118    )
119}
120
121/// Run 'log-viewer' (Log-Explorer)
122pub async fn run_logs(args: LogsArgs) -> Result<()> {
123    let mut cmd_args = vec!["--source", &args.source];
124    if let Some(ref s) = args.service {
125        cmd_args.push("--service");
126        cmd_args.push(s);
127    }
128    run_tool("log-viewer", &cmd_args)
129}
130
131/// Run 'health-checker' (Health-Explorer)
132pub async fn run_health(args: HealthArgs) -> Result<()> {
133    run_tool(
134        "health-checker",
135        &["--interval", &args.interval.to_string()],
136    )
137}
138
139/// Run 'deploy-cli' (Deploy-Explorer)
140pub async fn run_deploy(args: DeployArgs) -> Result<()> {
141    let mut cmd_args = vec!["--env", &args.env];
142    if args.k8s {
143        cmd_args.push("--k8s");
144    }
145    run_tool("deploy-cli", &cmd_args)
146}
147
148/// Run 'cleanup' (Cleanup-Explorer)
149pub async fn run_clean(args: CleanArgs) -> Result<()> {
150    let mut cmd_args = Vec::new();
151    if args.dry_run {
152        cmd_args.push("--dry-run");
153    }
154    run_tool("cleanup", &cmd_args)
155}
156
157/// Run 'bin-explorer' (Asm-Explorer)
158pub async fn run_asm(args: AsmArgs) -> Result<()> {
159    let mut cmd_args = Vec::new();
160    if let Some(ref file) = args.file {
161        cmd_args.push("--file");
162        cmd_args.push(file);
163    }
164    if let Some(ref dir) = args.dir {
165        cmd_args.push("--dir");
166        cmd_args.push(dir);
167    }
168    if args.recursive {
169        cmd_args.push("--recursive");
170    }
171    if let Some(ref ext) = args.ext {
172        cmd_args.push("--ext");
173        cmd_args.push(ext);
174    }
175    if let Some(ref config) = args.config {
176        cmd_args.push("--config");
177        cmd_args.push(config);
178    }
179    if args.no_cache {
180        cmd_args.push("--no-cache");
181    }
182    if args.rebuild_cache {
183        cmd_args.push("--rebuild-cache");
184    }
185    if args.no_disasm {
186        cmd_args.push("--no-disasm");
187    }
188    let max_functions = args.max_functions.map(|v| v.to_string());
189    if let Some(ref max_functions) = max_functions {
190        cmd_args.push("--max-functions");
191        cmd_args.push(max_functions);
192    }
193    if args.tui {
194        cmd_args.push("--tui");
195    }
196    if args.plain {
197        cmd_args.push("--plain");
198    }
199    if args.json {
200        cmd_args.push("--json");
201    }
202    run_tool("bin-explorer", &cmd_args)
203}
204
205fn run_tool(name: &str, args: &[&str]) -> Result<()> {
206    // We assume the tool is built and available via 'cargo run -p'
207    // or eventually as a pre-built binary in the path.
208    // For now, using 'cargo run -p' ensures we always run the latest version in dev.
209
210    let mut child = Command::new("cargo")
211        .arg("run")
212        .arg("-q")
213        .arg("-p")
214        .arg(name)
215        .arg("--")
216        .args(args)
217        .spawn()
218        .with_context(|| format!("Failed to launch tool: {name}"))?;
219
220    let status = child.wait().context("Tool crashed or was interrupted")?;
221    if !status.success() {
222        // We don't necessarily want to exit the main CLI with error if the TUI was just closed
223    }
224    Ok(())
225}