Skip to content

Commit ea87c48

Browse files
echobtfactorydroid
andauthored
feat(cortex-cli): implement 10 additional open PR features (#185) (#185)
This commit implements the following open PRs for cortex-cli: 1. PR #117 - Validate --days parameter range (1-3650) - Added value_parser to validate days is within 1-3650 range - Provides clear error messages for invalid values 2. PR #120 - Add pricing information to models --json output - Added input_cost_per_million and output_cost_per_million fields - Populated pricing for all cloud-hosted models - Local Ollama models show null (no API cost) 3. PR #121 - Add --compact flag for minified JSON config output - Added --compact flag to config command (requires --json) - Outputs single-line JSON instead of pretty-printed 4. PR #123 - Add explanation for Native field in agent show output - Changed from 'Native: true/false' to 'Native: yes/no (explanation)' - Shows 'built-in agent bundled with Cortex' or 'user-defined agent' 5. PR #125 - Add --diff flag to debug config command - Compares local project config with global config - Shows lines unique to each config and unified diff - Supports JSON output format 6. PR #128 - Display helpful error when running in non-TTY context - Checks for terminal support before starting TUI - Provides guidance to use 'cortex run <prompt>' for non-interactive use 7. PR #131 - Add help text explaining -- separator for mcp add command - Added after_help with examples showing correct usage - Explains the -- separator requirement for stdio servers 8. PR #132 - Defer success message until config write succeeds - Success messages now only print after std::fs::write() succeeds - Prevents misleading output when config write fails 9. PR #119 - Add OAuth authentication status column to mcp list output - Added 'Auth' column showing authentication status for HTTP servers - Stdio servers display 'N/A' (OAuth only applies to HTTP) 10. PR #116 - Return non-zero exit code when github status detects missing workflow - Now exits with code 1 if workflow is not installed - Works for both human-readable and JSON output modes Co-authored-by: Droid Agent <droid@factory.ai>
1 parent 8fc0902 commit ea87c48

7 files changed

Lines changed: 339 additions & 28 deletions

File tree

cortex-cli/src/agent_cmd.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -810,7 +810,15 @@ async fn run_show(args: ShowArgs) -> Result<()> {
810810

811811
println!("Mode: {}", agent.mode);
812812
println!("Source: {}", agent.source);
813-
println!("Native: {}", agent.native);
813+
println!(
814+
"Native: {} ({})",
815+
if agent.native { "yes" } else { "no" },
816+
if agent.native {
817+
"built-in agent bundled with Cortex"
818+
} else {
819+
"user-defined agent"
820+
}
821+
);
814822
println!("Hidden: {}", agent.hidden);
815823
println!(
816824
"Can Delegate: {} ({})",

cortex-cli/src/debug_cmd.rs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ pub struct ConfigArgs {
7575
/// Show environment variables related to Cortex.
7676
#[arg(long)]
7777
pub env: bool,
78+
79+
/// Show diff between local project config and global config.
80+
#[arg(long)]
81+
pub diff: bool,
7882
}
7983

8084
/// Config debug output.
@@ -223,9 +227,176 @@ async fn run_config(args: ConfigArgs) -> Result<()> {
223227
}
224228
}
225229

230+
// Handle --diff flag: compare local and global configs
231+
if args.diff {
232+
println!();
233+
println!("Config Diff (Global vs Local)");
234+
println!("{}", "=".repeat(50));
235+
236+
let global_path = config.fabric_home.join("config.toml");
237+
let local_path = std::env::current_dir()
238+
.ok()
239+
.map(|d| d.join(".cortex/config.toml"));
240+
241+
let global_content = if global_path.exists() {
242+
std::fs::read_to_string(&global_path).ok()
243+
} else {
244+
None
245+
};
246+
247+
let local_content = local_path.as_ref().and_then(|p| {
248+
if p.exists() {
249+
std::fs::read_to_string(p).ok()
250+
} else {
251+
None
252+
}
253+
});
254+
255+
match (global_content.as_ref(), local_content.as_ref()) {
256+
(None, None) => {
257+
println!(" No config files found.");
258+
}
259+
(Some(global), None) => {
260+
println!(" Only global config exists.");
261+
if args.json {
262+
let diff_output = serde_json::json!({
263+
"global_only": true,
264+
"local_only": false,
265+
"differences": [],
266+
});
267+
println!("{}", serde_json::to_string_pretty(&diff_output)?);
268+
}
269+
}
270+
(None, Some(local)) => {
271+
println!(" Only local config exists.");
272+
if args.json {
273+
let diff_output = serde_json::json!({
274+
"global_only": false,
275+
"local_only": true,
276+
"differences": [],
277+
});
278+
println!("{}", serde_json::to_string_pretty(&diff_output)?);
279+
}
280+
}
281+
(Some(global), Some(local)) => {
282+
if global == local {
283+
println!(" Configs are identical.");
284+
} else {
285+
let diff = compute_config_diff(global, local);
286+
if args.json {
287+
println!("{}", serde_json::to_string_pretty(&diff)?);
288+
} else {
289+
println!();
290+
if !diff.only_in_global.is_empty() {
291+
println!("Lines only in global config:");
292+
for line in &diff.only_in_global {
293+
println!(" - {}", line);
294+
}
295+
println!();
296+
}
297+
if !diff.only_in_local.is_empty() {
298+
println!("Lines only in local config:");
299+
for line in &diff.only_in_local {
300+
println!(" + {}", line);
301+
}
302+
println!();
303+
}
304+
if !diff.unified_diff.is_empty() {
305+
println!("Unified diff:");
306+
println!("{}", diff.unified_diff);
307+
}
308+
}
309+
}
310+
}
311+
}
312+
}
313+
226314
Ok(())
227315
}
228316

317+
/// Result of comparing two config files.
318+
#[derive(Debug, Serialize)]
319+
struct ConfigDiff {
320+
only_in_global: Vec<String>,
321+
only_in_local: Vec<String>,
322+
unified_diff: String,
323+
}
324+
325+
/// Compute diff between two config file contents.
326+
fn compute_config_diff(global: &str, local: &str) -> ConfigDiff {
327+
use std::collections::HashSet;
328+
329+
let global_lines: HashSet<&str> = global.lines().filter(|l| !l.trim().is_empty()).collect();
330+
let local_lines: HashSet<&str> = local.lines().filter(|l| !l.trim().is_empty()).collect();
331+
332+
let only_in_global: Vec<String> = global_lines
333+
.difference(&local_lines)
334+
.map(|s| s.to_string())
335+
.collect();
336+
let only_in_local: Vec<String> = local_lines
337+
.difference(&global_lines)
338+
.map(|s| s.to_string())
339+
.collect();
340+
341+
// Generate a simple unified diff
342+
let unified_diff = generate_unified_diff(global, local);
343+
344+
ConfigDiff {
345+
only_in_global,
346+
only_in_local,
347+
unified_diff,
348+
}
349+
}
350+
351+
/// Generate a simple unified diff output.
352+
fn generate_unified_diff(old_content: &str, new_content: &str) -> String {
353+
let old_lines: Vec<&str> = old_content.lines().collect();
354+
let new_lines: Vec<&str> = new_content.lines().collect();
355+
356+
let mut diff_output = String::new();
357+
diff_output.push_str("--- global/config.toml\n");
358+
diff_output.push_str("+++ local/config.toml\n");
359+
360+
// Simple line-by-line comparison (not a proper LCS diff, but useful for config files)
361+
let max_lines = old_lines.len().max(new_lines.len());
362+
let mut has_changes = false;
363+
364+
for i in 0..max_lines {
365+
let old_line = old_lines.get(i).copied();
366+
let new_line = new_lines.get(i).copied();
367+
368+
match (old_line, new_line) {
369+
(Some(o), Some(n)) if o == n => {
370+
// Lines are the same, show context
371+
diff_output.push_str(&format!(" {}\n", o));
372+
}
373+
(Some(o), Some(n)) => {
374+
// Lines differ
375+
has_changes = true;
376+
diff_output.push_str(&format!("-{}\n", o));
377+
diff_output.push_str(&format!("+{}\n", n));
378+
}
379+
(Some(o), None) => {
380+
// Line only in old
381+
has_changes = true;
382+
diff_output.push_str(&format!("-{}\n", o));
383+
}
384+
(None, Some(n)) => {
385+
// Line only in new
386+
has_changes = true;
387+
diff_output.push_str(&format!("+{}\n", n));
388+
}
389+
(None, None) => break,
390+
}
391+
}
392+
393+
if !has_changes {
394+
String::new()
395+
} else {
396+
diff_output
397+
}
398+
}
399+
229400
// =============================================================================
230401
// File subcommand
231402
// =============================================================================

cortex-cli/src/github_cmd.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,10 @@ async fn run_status(args: StatusArgs) -> Result<()> {
568568
if args.json {
569569
let json = serde_json::to_string_pretty(&status)?;
570570
println!("{}", json);
571+
// Return non-zero exit code if workflow is not installed
572+
if !status.workflow_installed {
573+
std::process::exit(1);
574+
}
571575
return Ok(());
572576
}
573577

@@ -578,13 +582,13 @@ async fn run_status(args: StatusArgs) -> Result<()> {
578582
if !status.is_git_repo {
579583
println!("⚠️ Not a git repository");
580584
println!(" Run this command from a git repository root.");
581-
return Ok(());
585+
std::process::exit(1);
582586
}
583587

584588
if !status.github_dir_exists {
585589
println!("❌ .github directory not found");
586590
println!(" Run `cortex github install` to set up GitHub Actions.");
587-
return Ok(());
591+
std::process::exit(1);
588592
}
589593

590594
if status.workflow_installed {
@@ -600,6 +604,7 @@ async fn run_status(args: StatusArgs) -> Result<()> {
600604
} else {
601605
println!("❌ Cortex workflow not found");
602606
println!(" Run `cortex github install` to set up GitHub Actions.");
607+
std::process::exit(1);
603608
}
604609

605610
Ok(())

cortex-cli/src/main.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,10 @@ struct ConfigCommand {
443443
#[arg(long)]
444444
json: bool,
445445

446+
/// Output minified/compact JSON (only valid with --json)
447+
#[arg(long, requires = "json")]
448+
compact: bool,
449+
446450
/// Edit configuration interactively
447451
#[arg(long)]
448452
edit: bool,
@@ -667,6 +671,16 @@ async fn main() -> Result<()> {
667671

668672
async fn run_tui(initial_prompt: Option<String>, args: &InteractiveArgs) -> Result<()> {
669673
use cortex_common::resolve_model_alias;
674+
use std::io::IsTerminal;
675+
676+
// Check if we're running in a terminal - TUI requires a TTY
677+
if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
678+
eprintln!("Error: Interactive mode requires a terminal.");
679+
eprintln!();
680+
eprintln!("Use 'cortex run <prompt>' for non-interactive execution,");
681+
eprintln!("or 'cortex --help' for usage information.");
682+
std::process::exit(1);
683+
}
670684

671685
let mut config = cortex_engine::Config::default();
672686

@@ -1179,7 +1193,11 @@ async fn show_config(config_cli: ConfigCommand) -> Result<()> {
11791193
"cwd": config.cwd,
11801194
"fabric_home": config.fabric_home,
11811195
});
1182-
println!("{}", serde_json::to_string_pretty(&json)?);
1196+
if config_cli.compact {
1197+
println!("{}", serde_json::to_string(&json)?);
1198+
} else {
1199+
println!("{}", serde_json::to_string_pretty(&json)?);
1200+
}
11831201
} else if config_cli.edit {
11841202
let config_path = config.fabric_home.join("config.toml");
11851203
let editor = get_default_editor();

0 commit comments

Comments
 (0)