Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions scripts/coding_discovery_tools/coding_tool_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@
from .macos.junie.mcp_config_extractor import MacOSJunieMCPConfigExtractor
from .macos.junie.junie_rules_extractor import MacOSJunieRulesExtractor

# Windows - Junie
from .windows.junie.junie import WindowsJunieDetector
from .windows.junie.mcp_config_extractor import WindowsJunieMCPConfigExtractor
from .windows.junie.junie_rules_extractor import WindowsJunieRulesExtractor

# Windows - JetBrains
from .windows.jetbrains.jetbrains import WindowsJetBrainsDetector
from .windows.jetbrains.mcp_config_extractor import WindowsJetBrainsMCPConfigExtractor
Expand Down Expand Up @@ -569,6 +574,8 @@ def create_junie_detector(os_name: Optional[str] = None) -> Optional[BaseToolDet

if os_name == "Darwin":
return MacOSJunieDetector()
elif os_name == "Windows":
return WindowsJunieDetector()
else:
return None

Expand Down Expand Up @@ -1299,6 +1306,8 @@ def create(os_name: Optional[str] = None) -> Optional[BaseMCPConfigExtractor]:

if os_name == "Darwin":
return MacOSJunieMCPConfigExtractor()
elif os_name == "Windows":
return WindowsJunieMCPConfigExtractor()
else:
return None

Expand All @@ -1322,6 +1331,8 @@ def create(os_name: Optional[str] = None) -> Optional[BaseJunieRulesExtractor]:

if os_name == "Darwin":
return MacOSJunieRulesExtractor()
elif os_name == "Windows":
return WindowsJunieRulesExtractor()
else:
return None

Expand Down
9 changes: 9 additions & 0 deletions scripts/coding_discovery_tools/windows/junie/__init__.py
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']
92 changes: 92 additions & 0 deletions scripts/coding_discovery_tools/windows/junie/junie.py
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 scripts/coding_discovery_tools/windows/junie/junie_rules_extractor.py
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
Comment thread
AakashVelusamy marked this conversation as resolved.

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}")
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
Comment thread
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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ def _detect_rule_scope(rule_file: Path) -> str:
Path.home() so that scope detection works correctly when running
as admin via MDM (where Path.home() may not match the actual user).
"""
config_dir_names = {".cursor", ".claude", ".windsurf", ".antigravity", ".roo", ".cline", ".clinerules", ".kilocode", ".gemini"}
config_dir_names = {".cursor", ".claude", ".windsurf", ".antigravity", ".roo", ".cline", ".clinerules", ".kilocode", ".gemini", ".junie"}
try:
parts = rule_file.resolve().parts
# On Windows: ('C:\\', 'Users', '<username>', '.<config_dir>', ...)
Expand Down
Loading