77from typing import Optional , Dict , List
88
99from ...coding_tool_base import BaseToolDetector
10+ from ...macos .jetbrains .jetbrains import MacOSJetBrainsDetector
1011from ...macos_extraction_helpers import is_running_as_root
12+ from ...user_tool_detector import find_junie_binary_for_user
1113
1214logger = logging .getLogger (__name__ )
1315
@@ -56,22 +58,74 @@ def get_version(self) -> Optional[str]:
5658 def _detect_junie_for_user (self , user_home : Path ) -> Optional [Dict ]:
5759 """
5860 Detect Junie installation for a specific user.
61+
62+ Gates on a real install signal — the Junie CLI binary OR the Junie
63+ plugin in a JetBrains IDE — not on the ``~/.junie`` directory, which is
64+ user-authored guidelines residue that survives uninstall. ``~/.junie``
65+ is still used as the version source.
5966 """
60- junie_dir = user_home / self .JUNIE_DIR_NAME
67+ junie_bin = find_junie_binary_for_user (user_home )
68+ install_path : Optional [str ] = junie_bin
69+
70+ if not install_path :
71+ install_path = self ._has_junie_jetbrains_plugin (user_home )
6172
62- if not junie_dir . exists () or not junie_dir . is_dir () :
73+ if not install_path :
6374 return None
6475
65- logger .debug (f"Found Junie directory at: { junie_dir } " )
76+ logger .debug (f"Detected Junie install signal at: { install_path } " )
6677
67- version = self ._get_version_from_config (junie_dir )
78+ version = self ._get_version_from_config (user_home / self . JUNIE_DIR_NAME )
6879
6980 return {
7081 "name" : self .tool_name ,
7182 "version" : version or "Unknown" ,
72- "install_path" : str ( junie_dir )
83+ "install_path" : install_path
7384 }
7485
86+ def _has_junie_jetbrains_plugin (self , user_home : Path ) -> Optional [str ]:
87+ """Return an install_path if the Junie plugin is present in a JetBrains
88+ IDE belonging to ``user_home``, else None.
89+
90+ Scoping matters: ``MacOSJetBrainsDetector.detect()`` ignores ``user_home``
91+ and, under root, scans every user under ``/Users``. Calling it here would
92+ attribute another user's Junie plugin to whichever user is currently
93+ being scanned (cross-user false positive). Instead we drive the
94+ detector's per-user config-dir scan for ``user_home`` only, then enrich
95+ with plugins. The JetBrains detector itself is never modified — only its
96+ existing per-user methods are reused read-only.
97+ """
98+ try :
99+ jetbrains_detector = MacOSJetBrainsDetector ()
100+ scoped_ides = jetbrains_detector ._scan_jetbrains_config_dir (user_home )
101+ except (PermissionError , OSError ) as e :
102+ logger .debug (f"JetBrains scan for Junie failed under { user_home } : { e } " )
103+ return None
104+
105+ for ide in scoped_ides :
106+ config_path = ide .get ("config_path" )
107+ if not self ._path_under_user_home (config_path , user_home ):
108+ continue
109+ try :
110+ plugins = jetbrains_detector ._get_plugins (config_path )
111+ except (PermissionError , OSError ) as e :
112+ logger .debug (f"Plugin scan for Junie failed under { config_path } : { e } " )
113+ continue
114+ for plugin_name in plugins :
115+ if "junie" in plugin_name .lower ():
116+ return config_path
117+ return None
118+
119+ @staticmethod
120+ def _path_under_user_home (config_path : Optional [str ], user_home : Path ) -> bool :
121+ """True if ``config_path`` is inside ``user_home`` (strict scoping guard)."""
122+ if not config_path :
123+ return False
124+ try :
125+ return Path (config_path ).resolve ().is_relative_to (user_home .resolve ())
126+ except (OSError , ValueError ):
127+ return False
128+
75129 def _get_version_from_config (self , junie_dir : Path ) -> Optional [str ]:
76130 """
77131 Try to extract Junie version from configuration files.
@@ -87,7 +141,7 @@ def _get_version_from_config(self, junie_dir: Path) -> Optional[str]:
87141 import json
88142 with open (config_file , 'r' , encoding = 'utf-8' ) as f :
89143 data = json .load (f )
90- if 'version' in data :
144+ if isinstance ( data , dict ) and isinstance ( data . get ( 'version' ), str ) :
91145 return data ['version' ]
92146 except (json .JSONDecodeError , OSError , PermissionError ) as e :
93147 logger .debug (f"Could not read config file { config_file } : { e } " )
0 commit comments