Skip to content

Commit 7e2df7a

Browse files
committed
feat(cortex-cli): implement 10 open PRs
This commit implements the following 10 open PRs for cortex-cli: 1. PR #155 - Prompt for tab completion setup on first run - Added completion_setup module for first-run completion detection - On first interactive run, prompts user to enable tab completion - Automatically detects shell (bash, zsh, fish, PowerShell, elvish) - Creates marker file to avoid repeated prompts 2. PR #153 - Emit valid JSONL with full event data in streaming mode - Already implemented in previous work 3. PR #151 - Add man page generation command - Added clap_mangen dependency - Added 'man' command with optional output directory - Generates roff-format man pages 4. PR #137 - Use consistent provider name casing in models output - Already using lowercase provider names (no changes needed) 5. PR #134 - Check actual write permission for current user in debug file - Added is_writable_by_current_user() helper function - Uses actual file open test instead of permission bits 6. PR #133 - Detect actual binary location from PATH for uninstall dry-run - Added 'which' dependency for PATH lookup - Updated collect_binary_locations() to use PATH search first 7. PR #130 - Show searched paths in debug ripgrep output - Added searched_paths field to RipgrepDebugOutput - Added get_path_directories() helper function - Shows PATH directories when ripgrep is not found 8. PR #129 - Output valid JSON on errors when --json flag is set - Updated run_servers() to handle MdnsBrowser errors as JSON - Move discovery banner inside conditional for non-JSON mode 9. PR #126 - Add batch export for sessions - Added --all (-a) flag for batch export mode - Added --output-dir option for batch exports - Each session exported to separate JSON file 10. PR #124 - Display feature descriptions in features list - Updated list_features() to use actual feature registry - Added Description column to features list output 11. PR #122 - Add debug system command for system information - Added System subcommand to debug CLI - Gathers OS, hardware, environment, and Cortex info - Supports JSON output for scripts/automation
1 parent beededb commit 7e2df7a

8 files changed

Lines changed: 904 additions & 108 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ toml_edit = "0.23"
242242
# CLI - Arguments
243243
clap = { version = "4", features = ["derive", "wrap_help", "env"] }
244244
clap_complete = "4"
245+
clap_mangen = "0.2"
245246

246247
# CLI - TUI
247248
ratatui = { version = "0.29", features = ["scrolling-regions", "unstable-backend-writer", "unstable-rendered-line-info", "unstable-widget-ref"] }

cortex-cli/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ cortex-update = { workspace = true }
3434

3535
clap = { workspace = true }
3636
clap_complete = { workspace = true }
37+
clap_mangen = { workspace = true }
3738
tokio = { workspace = true, features = ["full"] }
3839
anyhow = { workspace = true }
3940
tracing = { workspace = true }
@@ -61,3 +62,6 @@ scraper = "0.22"
6162

6263
# For mDNS service discovery
6364
hostname = { workspace = true }
65+
66+
# For finding binary location in PATH
67+
which = { workspace = true }

cortex-cli/src/completion_setup.rs

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
//! First-run shell completion setup module.
2+
//!
3+
//! This module handles prompting users to install shell completions on first run.
4+
//! It detects the user's shell, offers to install completions, and tracks whether
5+
//! the prompt has been shown to avoid repeated prompts.
6+
7+
use std::fs;
8+
use std::io::{self, IsTerminal, Write};
9+
use std::path::PathBuf;
10+
11+
use clap_complete::Shell;
12+
13+
/// Marker file name to track if completion setup has been offered.
14+
const COMPLETION_OFFERED_MARKER: &str = ".completion_offered";
15+
16+
/// Check if this is an interactive terminal.
17+
fn is_interactive_terminal() -> bool {
18+
io::stdin().is_terminal() && io::stdout().is_terminal()
19+
}
20+
21+
/// Get the cortex home directory.
22+
fn get_cortex_home() -> Option<PathBuf> {
23+
// Check CORTEX_HOME or CORTEX_CONFIG_DIR environment variables
24+
if let Ok(val) = std::env::var("CORTEX_CONFIG_DIR") {
25+
if !val.is_empty() {
26+
return Some(PathBuf::from(val));
27+
}
28+
}
29+
if let Ok(val) = std::env::var("CORTEX_HOME") {
30+
if !val.is_empty() {
31+
return Some(PathBuf::from(val));
32+
}
33+
}
34+
35+
// Default to ~/.cortex
36+
dirs::home_dir().map(|h| h.join(".cortex"))
37+
}
38+
39+
/// Check if completion setup has already been offered.
40+
fn completion_already_offered(cortex_home: &PathBuf) -> bool {
41+
cortex_home.join(COMPLETION_OFFERED_MARKER).exists()
42+
}
43+
44+
/// Mark that completion setup has been offered.
45+
fn mark_completion_offered(cortex_home: &PathBuf) -> io::Result<()> {
46+
// Ensure the directory exists
47+
fs::create_dir_all(cortex_home)?;
48+
49+
// Create the marker file
50+
let marker_path = cortex_home.join(COMPLETION_OFFERED_MARKER);
51+
fs::write(&marker_path, "completion setup offered\n")?;
52+
Ok(())
53+
}
54+
55+
/// Detect the user's shell from the SHELL environment variable.
56+
fn detect_shell() -> Option<Shell> {
57+
let shell_path = std::env::var("SHELL").ok()?;
58+
let shell_name = std::path::Path::new(&shell_path)
59+
.file_name()?
60+
.to_str()?
61+
.to_lowercase();
62+
63+
match shell_name.as_str() {
64+
"bash" => Some(Shell::Bash),
65+
"zsh" => Some(Shell::Zsh),
66+
"fish" => Some(Shell::Fish),
67+
"powershell" | "pwsh" => Some(Shell::PowerShell),
68+
"elvish" => Some(Shell::Elvish),
69+
_ => None,
70+
}
71+
}
72+
73+
/// Get the completion installation path for a given shell.
74+
fn get_completion_install_path(shell: Shell) -> Option<PathBuf> {
75+
let home = dirs::home_dir()?;
76+
77+
match shell {
78+
Shell::Bash => {
79+
// Try bash-completion directory first, then fall back to .bashrc
80+
let bash_completion = home.join(".local/share/bash-completion/completions");
81+
if bash_completion.parent().map_or(false, |p| p.exists()) {
82+
Some(bash_completion.join("cortex"))
83+
} else {
84+
Some(home.join(".bashrc"))
85+
}
86+
}
87+
Shell::Zsh => {
88+
// Check for common zsh completion directories
89+
let zsh_completions = home.join(".zsh/completions");
90+
if zsh_completions.exists() {
91+
Some(zsh_completions.join("_cortex"))
92+
} else {
93+
Some(home.join(".zshrc"))
94+
}
95+
}
96+
Shell::Fish => Some(home.join(".config/fish/completions/cortex.fish")),
97+
Shell::PowerShell => {
98+
// PowerShell profile location varies by platform
99+
#[cfg(windows)]
100+
{
101+
Some(home.join("Documents/PowerShell/Microsoft.PowerShell_profile.ps1"))
102+
}
103+
#[cfg(not(windows))]
104+
{
105+
Some(home.join(".config/powershell/Microsoft.PowerShell_profile.ps1"))
106+
}
107+
}
108+
Shell::Elvish => Some(home.join(".elvish/lib/cortex.elv")),
109+
_ => None,
110+
}
111+
}
112+
113+
/// Get the shell name in lowercase for command output.
114+
fn shell_name(shell: Shell) -> &'static str {
115+
match shell {
116+
Shell::Bash => "bash",
117+
Shell::Zsh => "zsh",
118+
Shell::Fish => "fish",
119+
Shell::PowerShell => "powershell",
120+
Shell::Elvish => "elvish",
121+
_ => "bash",
122+
}
123+
}
124+
125+
/// Install completions for the given shell.
126+
///
127+
/// For most shells, we add an eval command to the shell configuration file
128+
/// that dynamically loads completions. This approach is more robust as it
129+
/// doesn't require updating the completion script when the CLI changes.
130+
fn install_completions(shell: Shell) -> io::Result<()> {
131+
let install_path = get_completion_install_path(shell).ok_or_else(|| {
132+
io::Error::new(
133+
io::ErrorKind::NotFound,
134+
"Could not determine completion installation path",
135+
)
136+
})?;
137+
138+
// Create parent directory if needed
139+
if let Some(parent) = install_path.parent() {
140+
fs::create_dir_all(parent)?;
141+
}
142+
143+
let shell_cmd = shell_name(shell);
144+
145+
match shell {
146+
Shell::Bash => {
147+
// Append eval command to .bashrc
148+
let mut file = fs::OpenOptions::new()
149+
.create(true)
150+
.append(true)
151+
.open(&install_path)?;
152+
153+
writeln!(file)?;
154+
writeln!(file, "# Cortex CLI completions")?;
155+
writeln!(file, "eval \"$(cortex completion {})\"\n", shell_cmd)?;
156+
}
157+
Shell::Zsh => {
158+
// Append eval command to .zshrc
159+
let mut file = fs::OpenOptions::new()
160+
.create(true)
161+
.append(true)
162+
.open(&install_path)?;
163+
164+
writeln!(file)?;
165+
writeln!(file, "# Cortex CLI completions")?;
166+
writeln!(file, "eval \"$(cortex completion {})\"\n", shell_cmd)?;
167+
}
168+
Shell::Fish => {
169+
// Fish needs the script written to the completions directory
170+
// Generate it by running the command
171+
let output = std::process::Command::new("cortex")
172+
.args(["completion", shell_cmd])
173+
.output()?;
174+
175+
if output.status.success() {
176+
fs::write(&install_path, output.stdout)?;
177+
} else {
178+
return Err(io::Error::new(
179+
io::ErrorKind::Other,
180+
"Failed to generate fish completions",
181+
));
182+
}
183+
}
184+
Shell::PowerShell => {
185+
// Append to PowerShell profile
186+
let mut file = fs::OpenOptions::new()
187+
.create(true)
188+
.append(true)
189+
.open(&install_path)?;
190+
191+
writeln!(file)?;
192+
writeln!(file, "# Cortex CLI completions")?;
193+
writeln!(
194+
file,
195+
"cortex completion powershell | Out-String | Invoke-Expression\n"
196+
)?;
197+
}
198+
Shell::Elvish => {
199+
// Generate script by running the command
200+
let output = std::process::Command::new("cortex")
201+
.args(["completion", shell_cmd])
202+
.output()?;
203+
204+
if output.status.success() {
205+
fs::write(&install_path, output.stdout)?;
206+
} else {
207+
return Err(io::Error::new(
208+
io::ErrorKind::Other,
209+
"Failed to generate elvish completions",
210+
));
211+
}
212+
}
213+
_ => {
214+
return Err(io::Error::new(
215+
io::ErrorKind::Unsupported,
216+
"Unsupported shell for automatic installation",
217+
));
218+
}
219+
}
220+
221+
Ok(())
222+
}
223+
224+
/// Prompt the user to install shell completions on first run.
225+
///
226+
/// This function checks if:
227+
/// 1. We're running in an interactive terminal
228+
/// 2. Completion setup hasn't been offered before
229+
/// 3. We can detect the user's shell
230+
///
231+
/// If all conditions are met, it prompts the user and optionally installs completions.
232+
pub fn maybe_prompt_completion_setup() {
233+
// Only prompt in interactive terminals
234+
if !is_interactive_terminal() {
235+
return;
236+
}
237+
238+
// Get cortex home directory
239+
let cortex_home = match get_cortex_home() {
240+
Some(home) => home,
241+
None => return,
242+
};
243+
244+
// Check if we've already offered completion setup
245+
if completion_already_offered(&cortex_home) {
246+
return;
247+
}
248+
249+
// Detect the user's shell
250+
let shell = match detect_shell() {
251+
Some(s) => s,
252+
None => {
253+
// Mark as offered so we don't keep trying with unknown shells
254+
let _ = mark_completion_offered(&cortex_home);
255+
return;
256+
}
257+
};
258+
259+
let shell_str = shell_name(shell);
260+
261+
// Show the prompt
262+
println!();
263+
println!("Welcome to Cortex CLI!");
264+
println!();
265+
println!(
266+
"Would you like to enable tab completion for your shell ({shell_str})?",
267+
);
268+
println!("This will allow you to press TAB to complete commands and options.");
269+
println!();
270+
print!("Enable tab completion? [y/N] ");
271+
let _ = io::stdout().flush();
272+
273+
// Read user input
274+
let mut input = String::new();
275+
if io::stdin().read_line(&mut input).is_err() {
276+
let _ = mark_completion_offered(&cortex_home);
277+
return;
278+
}
279+
280+
let response = input.trim().to_lowercase();
281+
if response == "y" || response == "yes" {
282+
match install_completions(shell) {
283+
Ok(()) => {
284+
println!();
285+
println!("Tab completion has been enabled!");
286+
println!(
287+
"Please restart your shell or run 'source ~/.{}rc' to activate.",
288+
shell_str
289+
);
290+
println!();
291+
}
292+
Err(e) => {
293+
println!();
294+
println!("Could not automatically install completions: {e}");
295+
println!();
296+
println!("You can manually enable completions by running:");
297+
println!(" cortex completion {shell_str}");
298+
println!();
299+
println!("Then add the output to your shell configuration file.");
300+
println!();
301+
}
302+
}
303+
} else {
304+
println!();
305+
println!("No problem! You can enable tab completion later by running:");
306+
println!(" cortex completion {shell_str}");
307+
println!();
308+
}
309+
310+
// Mark as offered regardless of the user's choice
311+
let _ = mark_completion_offered(&cortex_home);
312+
}

0 commit comments

Comments
 (0)