-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Junie support for Windows #137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
9bb9427
feat: Junie support for Windows
AakashVelusamy 3b13bd8
fix: address Greptile review on Windows Junie
AakashVelusamy 7e25ce7
fix: Junie MCP parent_levels 2 -> 3 so path resolves to home
AakashVelusamy a2f18e3
test: add 45 unit tests for macOS and Windows Junie extraction
AakashVelusamy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| """ | ||
| Junie detection and extraction for Windows. | ||
| """ | ||
|
|
||
| from .junie import WindowsJunieDetector | ||
| from .mcp_config_extractor import WindowsJunieMCPConfigExtractor | ||
| from .junie_rules_extractor import WindowsJunieRulesExtractor | ||
|
|
||
| __all__ = ['WindowsJunieDetector', 'WindowsJunieMCPConfigExtractor', 'WindowsJunieRulesExtractor'] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| """ | ||
| Junie detection for Windows. | ||
|
|
||
| Junie is JetBrains' AI coding agent. On Windows it stores its config in a | ||
| user-level ``.junie`` directory (``%USERPROFILE%\\.junie``), the same layout | ||
| used on macOS/Linux. When running as administrator we scan every user's | ||
| profile under ``C:\\Users``; otherwise just the current user's home. | ||
| """ | ||
|
|
||
| import json | ||
| import logging | ||
| from pathlib import Path | ||
| from typing import Optional, Dict, List | ||
|
|
||
| from ...coding_tool_base import BaseToolDetector | ||
| from ...windows_extraction_helpers import scan_windows_user_directories | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class WindowsJunieDetector(BaseToolDetector): | ||
| """Detector for Junie installations on Windows systems.""" | ||
|
|
||
| JUNIE_DIR_NAME = ".junie" | ||
|
|
||
| @property | ||
| def tool_name(self) -> str: | ||
| """Return the name of the tool being detected.""" | ||
| return "Junie" | ||
|
|
||
| def detect(self) -> Optional[Dict]: | ||
| """Detect Junie installation on Windows. | ||
|
|
||
| Uses the shared scan_windows_user_directories helper for consistent | ||
| admin/non-admin branching and system-account exclusion, returning the | ||
| first user's installation found. | ||
| """ | ||
| found: List[Dict] = [] | ||
|
|
||
| def check_user(user_home: Path) -> None: | ||
| if found: | ||
| return | ||
| result = self._detect_junie_for_user(user_home) | ||
| if result: | ||
| found.append(result) | ||
|
|
||
| scan_windows_user_directories(check_user) | ||
| return found[0] if found else None | ||
|
|
||
| def get_version(self) -> Optional[str]: | ||
| """Extract Junie version.""" | ||
| result = self.detect() | ||
| if result: | ||
| return result.get('version') | ||
| return None | ||
|
|
||
| def _detect_junie_for_user(self, user_home: Path) -> Optional[Dict]: | ||
| """Detect Junie installation for a specific user.""" | ||
| junie_dir = user_home / self.JUNIE_DIR_NAME | ||
|
|
||
| if not junie_dir.exists() or not junie_dir.is_dir(): | ||
| return None | ||
|
|
||
| logger.debug(f"Found Junie directory at: {junie_dir}") | ||
|
|
||
| version = self._get_version_from_config(junie_dir) | ||
|
|
||
| return { | ||
| "name": self.tool_name, | ||
| "version": version or "Unknown", | ||
| "install_path": str(junie_dir) | ||
| } | ||
|
|
||
| def _get_version_from_config(self, junie_dir: Path) -> Optional[str]: | ||
| """Try to extract Junie version from configuration files.""" | ||
| config_files = [ | ||
| junie_dir / "config.json", | ||
| junie_dir / "settings.json", | ||
| ] | ||
|
|
||
| for config_file in config_files: | ||
| try: | ||
| if config_file.exists(): | ||
| with open(config_file, 'r', encoding='utf-8') as f: | ||
| data = json.load(f) | ||
| if 'version' in data: | ||
| return data['version'] | ||
| except (json.JSONDecodeError, OSError, PermissionError) as e: | ||
| logger.debug(f"Could not read config file {config_file}: {e}") | ||
| continue | ||
|
|
||
| return None |
144 changes: 144 additions & 0 deletions
144
scripts/coding_discovery_tools/windows/junie/junie_rules_extractor.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| """ | ||
| Junie rules extraction for Windows systems. | ||
|
|
||
| Extracts Junie rules from .md files: | ||
| - Global rules: %USERPROFILE%\\.junie\\*.md | ||
| - Project-level rules: <project>\\.junie\\*.md | ||
| """ | ||
|
|
||
| import logging | ||
| from pathlib import Path | ||
| from typing import List, Dict | ||
|
|
||
| from ...coding_tool_base import BaseJunieRulesExtractor | ||
| from ...constants import MAX_SEARCH_DEPTH | ||
| from ...windows_extraction_helpers import ( | ||
| add_rule_to_project, | ||
| build_project_list, | ||
| extract_single_rule_file, | ||
| is_user_level_tool_dir, | ||
| scan_windows_user_directories, | ||
| should_skip_path, | ||
| get_windows_system_directories, | ||
| ) | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| JUNIE_DIR_NAME = ".junie" | ||
|
|
||
|
|
||
| def find_junie_project_root(rule_file: Path) -> Path: | ||
| """ | ||
| Find the project root for a Junie rule file. | ||
|
|
||
| - Rules in project\\.junie\\*.md -> parent of .junie is project root | ||
| - Global rules in ~\\.junie\\*.md -> home directory is project root | ||
| """ | ||
| parent = rule_file.parent | ||
|
|
||
| if parent.name == JUNIE_DIR_NAME: | ||
| return parent.parent | ||
|
|
||
| return parent | ||
|
|
||
|
|
||
| class WindowsJunieRulesExtractor(BaseJunieRulesExtractor): | ||
| """Extractor for Junie rules on Windows systems.""" | ||
|
|
||
| def extract_all_junie_rules(self) -> List[Dict]: | ||
| """Extract all Junie rules from all projects on Windows.""" | ||
| projects_by_root: Dict[str, List[Dict]] = {} | ||
|
|
||
| self._extract_global_rules(projects_by_root) | ||
|
|
||
| root_path = Path(Path.home().anchor) # e.g. "C:\\" | ||
| logger.info(f"Searching for Junie rules from root: {root_path}") | ||
| self._extract_project_level_rules(root_path, projects_by_root) | ||
|
|
||
| return build_project_list(projects_by_root) | ||
|
|
||
| def _extract_global_rules(self, projects_by_root: Dict[str, List[Dict]]) -> None: | ||
| """Extract global Junie rules from ~\\.junie\\, scanning all users when admin. | ||
|
|
||
| Uses the shared scan_windows_user_directories helper, which centralises | ||
| the admin/non-admin branching, excludes system/default accounts | ||
| (Public, Default, etc.), and handles PermissionError. | ||
| """ | ||
| def extract_for_user(user_home: Path) -> None: | ||
| junie_dir = user_home / JUNIE_DIR_NAME | ||
| if not junie_dir.exists() or not junie_dir.is_dir(): | ||
| return | ||
| try: | ||
| for md_file in junie_dir.glob("*.md"): | ||
| if md_file.is_file() and not should_skip_path(md_file, set()): | ||
| rule_info = extract_single_rule_file( | ||
| md_file, find_junie_project_root, scope="user" | ||
| ) | ||
| if rule_info: | ||
| project_root = rule_info.get('project_root') | ||
| if project_root: | ||
| add_rule_to_project(rule_info, project_root, projects_by_root) | ||
| except Exception as e: | ||
| logger.debug(f"Error extracting global Junie rules for {user_home}: {e}") | ||
|
|
||
| scan_windows_user_directories(extract_for_user) | ||
|
|
||
| def _extract_project_level_rules(self, root_path: Path, projects_by_root: Dict[str, List[Dict]]) -> None: | ||
| """Walk the drive recursively for project-level .junie directories.""" | ||
| self._walk_for_junie_dirs(root_path, root_path, projects_by_root, current_depth=0) | ||
|
|
||
| def _walk_for_junie_dirs( | ||
| self, | ||
| root_path: Path, | ||
| current_dir: Path, | ||
| projects_by_root: Dict[str, List[Dict]], | ||
| current_depth: int = 0, | ||
| ) -> None: | ||
| """Recursively walk directory tree looking for .junie directories.""" | ||
| if current_depth > MAX_SEARCH_DEPTH: | ||
| return | ||
|
|
||
| try: | ||
| system_dirs = get_windows_system_directories() | ||
| for item in current_dir.iterdir(): | ||
| try: | ||
| if should_skip_path(item, system_dirs): | ||
| continue | ||
|
|
||
| try: | ||
| depth = len(item.relative_to(root_path).parts) | ||
| if depth > MAX_SEARCH_DEPTH: | ||
| continue | ||
| except ValueError: | ||
| continue | ||
|
|
||
| if item.is_dir(): | ||
| if item.name == JUNIE_DIR_NAME: | ||
| # Skip user-level ~\.junie — handled by _extract_global_rules. | ||
| if is_user_level_tool_dir(item): | ||
| continue | ||
| self._extract_junie_dir_rules(item, projects_by_root) | ||
| else: | ||
| self._walk_for_junie_dirs(root_path, item, projects_by_root, current_depth + 1) | ||
| except (PermissionError, OSError): | ||
| continue | ||
| except Exception as e: | ||
| logger.debug(f"Error processing {item}: {e}") | ||
| continue | ||
| except (PermissionError, OSError): | ||
| pass | ||
| except Exception as e: | ||
| logger.debug(f"Error walking {current_dir}: {e}") | ||
|
|
||
| def _extract_junie_dir_rules(self, junie_dir: Path, projects_by_root: Dict[str, List[Dict]]) -> None: | ||
| """Extract all .md files from a project-level .junie directory.""" | ||
| try: | ||
| for md_file in junie_dir.glob("*.md"): | ||
| if md_file.is_file(): | ||
| rule_info = extract_single_rule_file(md_file, find_junie_project_root) | ||
| if rule_info: | ||
| project_root = rule_info.get('project_root') | ||
| if project_root: | ||
| add_rule_to_project(rule_info, project_root, projects_by_root) | ||
| except Exception as e: | ||
| logger.debug(f"Error extracting rules from {junie_dir}: {e}") | ||
57 changes: 57 additions & 0 deletions
57
scripts/coding_discovery_tools/windows/junie/mcp_config_extractor.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| """ | ||
| MCP config extraction for Junie on Windows systems. | ||
| """ | ||
|
|
||
| import logging | ||
| from pathlib import Path | ||
| from typing import Optional, Dict, List | ||
|
|
||
| from ...coding_tool_base import BaseMCPConfigExtractor | ||
| from ...mcp_extraction_helpers import read_global_mcp_config | ||
| from ...windows_extraction_helpers import scan_windows_user_directories | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| _TOOL_NAME = "Junie" | ||
| _PARENT_LEVELS = 2 | ||
|
AakashVelusamy marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| class WindowsJunieMCPConfigExtractor(BaseMCPConfigExtractor): | ||
| """Extractor for Junie MCP config on Windows systems.""" | ||
|
|
||
| JUNIE_DIR_NAME = ".junie" | ||
| MCP_CONFIG_SUBPATH = Path("mcp") / "mcp.json" | ||
|
|
||
| def extract_mcp_config(self) -> Optional[Dict]: | ||
| """Extract Junie MCP configuration on Windows.""" | ||
| projects = self._extract_global_configs() | ||
| return {"projects": projects} if projects else None | ||
|
|
||
| def _extract_global_configs(self) -> List[Dict]: | ||
| """Extract global MCP configs from ~\\.junie\\mcp\\mcp.json for each user. | ||
|
|
||
| Uses the shared scan_windows_user_directories helper for consistent | ||
| admin/non-admin branching and system-account exclusion. | ||
| """ | ||
| configs: List[Dict] = [] | ||
|
|
||
| def collect_for_user(user_home: Path) -> None: | ||
| config = self._extract_config_for_user(user_home) | ||
| if config: | ||
| configs.append(config) | ||
|
|
||
| scan_windows_user_directories(collect_for_user) | ||
| return configs | ||
|
|
||
| def _extract_config_for_user(self, user_home: Path) -> Optional[Dict]: | ||
| """Extract MCP config for a specific user.""" | ||
| config_path = user_home / self.JUNIE_DIR_NAME / self.MCP_CONFIG_SUBPATH | ||
|
|
||
| if not config_path.exists(): | ||
| return None | ||
|
|
||
| return read_global_mcp_config( | ||
| config_path, | ||
| tool_name=_TOOL_NAME, | ||
| parent_levels=_PARENT_LEVELS, | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.