From 9043c2ed39fd2e1101b9ca6bd798e090715d0b86 Mon Sep 17 00:00:00 2001 From: Aidan Cunniffe Date: Sun, 15 Mar 2026 14:16:17 -0400 Subject: [PATCH 1/5] codex was reporting installed even when missing --- src/mdm/agents/codex.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mdm/agents/codex.rs b/src/mdm/agents/codex.rs index 1d55b24b0..510a35a4e 100644 --- a/src/mdm/agents/codex.rs +++ b/src/mdm/agents/codex.rs @@ -139,7 +139,7 @@ impl HookInstaller for CodexInstaller { let config_path = Self::config_path(); if !config_path.exists() { return Ok(HookCheckResult { - tool_installed: true, + tool_installed: has_binary, hooks_installed: false, hooks_up_to_date: false, }); From 630a2d77297e957c13d26812e28a5b532e2c8c1b Mon Sep 17 00:00:00 2001 From: Aidan Cunniffe Date: Sun, 15 Mar 2026 15:37:00 -0400 Subject: [PATCH 2/5] Revert "codex was reporting installed even when missing" This reverts commit 9043c2ed39fd2e1101b9ca6bd798e090715d0b86. --- src/mdm/agents/codex.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mdm/agents/codex.rs b/src/mdm/agents/codex.rs index 510a35a4e..1d55b24b0 100644 --- a/src/mdm/agents/codex.rs +++ b/src/mdm/agents/codex.rs @@ -139,7 +139,7 @@ impl HookInstaller for CodexInstaller { let config_path = Self::config_path(); if !config_path.exists() { return Ok(HookCheckResult { - tool_installed: has_binary, + tool_installed: true, hooks_installed: false, hooks_up_to_date: false, }); From 392208da35384299ec5cb1ef27f1e1cacb2f707a Mon Sep 17 00:00:00 2001 From: Aidan Cunniffe Date: Sun, 15 Mar 2026 15:49:10 -0400 Subject: [PATCH 3/5] incorrect reporting on codex, claude and cursor installation --- src/commands/install_hooks.rs | 19 ++++++++++--------- src/mdm/skills_installer.rs | 31 +++++++++++++++++++------------ 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/commands/install_hooks.rs b/src/commands/install_hooks.rs index ccdbad1ad..5e1dc01b2 100644 --- a/src/commands/install_hooks.rs +++ b/src/commands/install_hooks.rs @@ -7,7 +7,7 @@ use crate::mdm::hook_installer::HookInstallerParams; use crate::mdm::skills_installer; use crate::mdm::spinner::{Spinner, print_diff}; use crate::mdm::utils::{get_current_binary_path, git_shim_path}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; /// Installation status for a tool #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -169,14 +169,6 @@ async fn async_run_install( // Track detailed results for metrics (tool_id, result) let mut detailed_results: Vec<(String, InstallResult)> = Vec::new(); - // Install skills first (these are global, not per-agent) - // Skills are always nuked and reinstalled fresh (silently) - if let Ok(result) = skills_installer::install_skills(dry_run, verbose) - && result.changed - { - has_changes = true; - } - // Ensure git symlinks for Fork compatibility if let Err(e) = crate::mdm::ensure_git_symlinks() { eprintln!("Warning: Failed to create git symlinks: {}", e); @@ -186,6 +178,7 @@ async fn async_run_install( println!("\n\x1b[1mCoding Agents\x1b[0m"); let installers = get_all_installers(); + let mut installed_tools: HashSet = HashSet::new(); for installer in installers { let name = installer.name(); @@ -200,6 +193,7 @@ async fn async_run_install( continue; } + installed_tools.insert(id.to_string()); any_checked = true; // Install/update hooks (only for tools that use config file hooks) @@ -310,6 +304,13 @@ async fn async_run_install( } } + // Install skills for detected agents only + if let Ok(result) = skills_installer::install_skills(dry_run, verbose, &installed_tools) + && result.changed + { + has_changes = true; + } + if !any_checked { println!("No compatible coding agents detected. Nothing to install."); } diff --git a/src/mdm/skills_installer.rs b/src/mdm/skills_installer.rs index 051009b7f..5effde535 100644 --- a/src/mdm/skills_installer.rs +++ b/src/mdm/skills_installer.rs @@ -1,6 +1,7 @@ use crate::config::skills_dir_path; use crate::error::GitAiError; use crate::mdm::utils::write_atomic; +use std::collections::HashSet; use std::fs; use std::path::PathBuf; @@ -126,7 +127,7 @@ fn remove_skill_link(link_path: &PathBuf) -> Result<(), GitAiError> { /// Then links each skill to: /// - ~/.agents/skills/{skill-name} (symlink on Unix, copy on Windows) /// - ~/.claude/skills/{skill-name} (symlink on Unix, copy on Windows) -pub fn install_skills(dry_run: bool, _verbose: bool) -> Result { +pub fn install_skills(dry_run: bool, _verbose: bool, installed_tools: &HashSet) -> Result { let skills_base = skills_dir_path().ok_or_else(|| { GitAiError::Generic("Could not determine skills directory path".to_string()) })?; @@ -166,18 +167,22 @@ pub fn install_skills(dry_run: bool, _verbose: bool) -> Result ~/.git-ai/skills/{skill-name} - if let Some(claude_dir) = claude_skills_dir() { - let claude_link = claude_dir.join(skill.name); - if let Err(e) = link_skill_dir(&skill_dir, &claude_link) { - eprintln!("Warning: Failed to link skill at {:?}: {}", claude_link, e); + if installed_tools.contains("claude-code") { + if let Some(claude_dir) = claude_skills_dir() { + let claude_link = claude_dir.join(skill.name); + if let Err(e) = link_skill_dir(&skill_dir, &claude_link) { + eprintln!("Warning: Failed to link skill at {:?}: {}", claude_link, e); + } } } // ~/.cursor/skills/{skill-name} -> ~/.git-ai/skills/{skill-name} - if let Some(cursor_dir) = cursor_skills_dir() { - let cursor_link = cursor_dir.join(skill.name); - if let Err(e) = link_skill_dir(&skill_dir, &cursor_link) { - eprintln!("Warning: Failed to link skill at {:?}: {}", cursor_link, e); + if installed_tools.contains("cursor") { + if let Some(cursor_dir) = cursor_skills_dir() { + let cursor_link = cursor_dir.join(skill.name); + if let Err(e) = link_skill_dir(&skill_dir, &cursor_link) { + eprintln!("Warning: Failed to link skill at {:?}: {}", cursor_link, e); + } } } } @@ -439,14 +444,16 @@ mod tests { // and don't race with other tests that mutate HOME (e.g. codex tests). with_temp_home(|_home| { let skills_base = skills_dir_path().unwrap(); + let all_tools: HashSet = + ["claude-code", "cursor"].iter().map(|s| s.to_string()).collect(); // Dry run should not create anything - let dry_result = install_skills(true, false).unwrap(); + let dry_result = install_skills(true, false, &all_tools).unwrap(); assert!(dry_result.changed); assert_eq!(dry_result.installed_count, EMBEDDED_SKILLS.len()); // Install creates skill files with correct content - let result = install_skills(false, false).unwrap(); + let result = install_skills(false, false, &all_tools).unwrap(); assert!(result.changed); assert_eq!(result.installed_count, EMBEDDED_SKILLS.len()); assert!(skills_base.exists()); @@ -458,7 +465,7 @@ mod tests { } // Install again is idempotent - let result2 = install_skills(false, false).unwrap(); + let result2 = install_skills(false, false, &all_tools).unwrap(); assert!(result2.changed); for skill in EMBEDDED_SKILLS { let skill_md = skills_base.join(skill.name).join("SKILL.md"); From c23274812792ef35462efd711398f5ed1222bdeb Mon Sep 17 00:00:00 2001 From: Aidan Cunniffe Date: Mon, 16 Mar 2026 15:39:02 -0400 Subject: [PATCH 4/5] demo-commit --- src/utils.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/utils.rs b/src/utils.rs index da660606f..eff5551ef 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -210,6 +210,39 @@ pub fn is_in_background_agent() -> bool { }) } +/// Detect the current shell name and version. +/// +/// Returns a tuple of `(shell_name, version)` where `shell_name` is the +/// basename of the shell (e.g. "zsh", "bash", "fish") and `version` is +/// the output of ` --version` trimmed to a single line. +/// +/// Returns `None` if the shell cannot be determined. +pub fn shell_name_and_version() -> Option<(String, String)> { + // Aidan wrote this... + let shell_path = std::env::var("SHELL").ok()?; + let shell_name = std::path::Path::new(&shell_path) + .file_name()? + .to_str()? + .to_string(); + + let version_output = Command::new(&shell_path) + .arg("--version") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .ok()?; + + let raw = if version_output.status.success() { + String::from_utf8_lossy(&version_output.stdout) + } else { + String::from_utf8_lossy(&version_output.stderr) + }; + + let version = raw.lines().next().unwrap_or("").trim().to_string(); + + Some((shell_name, version)) +} + /// A cross-platform exclusive file lock. /// /// Holds an exclusive advisory lock (Unix) or exclusive-access file handle (Windows) From 128b58302a7a77d5aa044a7b0f12793ddf7a125b Mon Sep 17 00:00:00 2001 From: Aidan Cunniffe Date: Thu, 19 Mar 2026 12:06:20 -0700 Subject: [PATCH 5/5] fix fmt --- src/mdm/skills_installer.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/mdm/skills_installer.rs b/src/mdm/skills_installer.rs index 5effde535..871ea0bfb 100644 --- a/src/mdm/skills_installer.rs +++ b/src/mdm/skills_installer.rs @@ -127,7 +127,11 @@ fn remove_skill_link(link_path: &PathBuf) -> Result<(), GitAiError> { /// Then links each skill to: /// - ~/.agents/skills/{skill-name} (symlink on Unix, copy on Windows) /// - ~/.claude/skills/{skill-name} (symlink on Unix, copy on Windows) -pub fn install_skills(dry_run: bool, _verbose: bool, installed_tools: &HashSet) -> Result { +pub fn install_skills( + dry_run: bool, + _verbose: bool, + installed_tools: &HashSet, +) -> Result { let skills_base = skills_dir_path().ok_or_else(|| { GitAiError::Generic("Could not determine skills directory path".to_string()) })?; @@ -444,8 +448,10 @@ mod tests { // and don't race with other tests that mutate HOME (e.g. codex tests). with_temp_home(|_home| { let skills_base = skills_dir_path().unwrap(); - let all_tools: HashSet = - ["claude-code", "cursor"].iter().map(|s| s.to_string()).collect(); + let all_tools: HashSet = ["claude-code", "cursor"] + .iter() + .map(|s| s.to_string()) + .collect(); // Dry run should not create anything let dry_result = install_skills(true, false, &all_tools).unwrap();