mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
301 lines
10 KiB
Python
301 lines
10 KiB
Python
"""Path mapping utilities for source paths and index paths.
|
|
|
|
This module provides bidirectional mapping between source code directories
|
|
and their corresponding index storage locations.
|
|
|
|
Storage Structure:
|
|
~/.codexlens/
|
|
├── registry.db # Global mapping table
|
|
└── indexes/
|
|
└── D/
|
|
└── Claude_dms3/
|
|
├── _index.db # Root directory index
|
|
└── src/
|
|
└── _index.db # src/ directory index
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import platform
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
|
|
def _get_configured_index_root() -> Path:
|
|
"""Get the index root from environment or config file.
|
|
|
|
Priority order:
|
|
1. CODEXLENS_INDEX_DIR environment variable
|
|
2. index_dir from ~/.codexlens/config.json
|
|
3. Default: ~/.codexlens/indexes
|
|
"""
|
|
env_override = os.getenv("CODEXLENS_INDEX_DIR")
|
|
if env_override:
|
|
return Path(env_override).expanduser().resolve()
|
|
|
|
config_file = Path.home() / ".codexlens" / "config.json"
|
|
if config_file.exists():
|
|
try:
|
|
cfg = json.loads(config_file.read_text(encoding="utf-8"))
|
|
if "index_dir" in cfg:
|
|
return Path(cfg["index_dir"]).expanduser().resolve()
|
|
except (json.JSONDecodeError, OSError):
|
|
pass
|
|
|
|
return Path.home() / ".codexlens" / "indexes"
|
|
|
|
|
|
class PathMapper:
|
|
"""Bidirectional mapping tool for source paths ↔ index paths.
|
|
|
|
Handles cross-platform path normalization and conversion between
|
|
source code directories and their index storage locations.
|
|
|
|
Attributes:
|
|
DEFAULT_INDEX_ROOT: Default root directory for all indexes
|
|
INDEX_DB_NAME: Standard name for index database files
|
|
index_root: Configured index root directory
|
|
"""
|
|
|
|
DEFAULT_INDEX_ROOT = _get_configured_index_root()
|
|
INDEX_DB_NAME = "_index.db"
|
|
|
|
def __init__(self, index_root: Optional[Path] = None):
|
|
"""Initialize PathMapper with optional custom index root.
|
|
|
|
Args:
|
|
index_root: Custom index root directory. If None, uses DEFAULT_INDEX_ROOT.
|
|
"""
|
|
self.index_root = (index_root or self.DEFAULT_INDEX_ROOT).resolve()
|
|
|
|
def source_to_index_dir(self, source_path: Path) -> Path:
|
|
"""Convert source directory to its index directory path.
|
|
|
|
Maps a source code directory to where its index data should be stored.
|
|
The mapping preserves the directory structure but normalizes paths
|
|
for cross-platform compatibility.
|
|
|
|
Args:
|
|
source_path: Source directory path to map
|
|
|
|
Returns:
|
|
Index directory path under index_root
|
|
|
|
Examples:
|
|
>>> mapper = PathMapper()
|
|
>>> mapper.source_to_index_dir(Path("D:/Claude_dms3/src"))
|
|
PosixPath('/home/user/.codexlens/indexes/D/Claude_dms3/src')
|
|
|
|
>>> mapper.source_to_index_dir(Path("/home/user/project"))
|
|
PosixPath('/home/user/.codexlens/indexes/home/user/project')
|
|
"""
|
|
source_path = source_path.resolve()
|
|
normalized = self.normalize_path(source_path)
|
|
return self.index_root / normalized
|
|
|
|
def source_to_index_db(self, source_path: Path) -> Path:
|
|
"""Convert source directory to its index database file path.
|
|
|
|
Maps a source directory to the full path of its index database file,
|
|
including the standard INDEX_DB_NAME.
|
|
|
|
Args:
|
|
source_path: Source directory path to map
|
|
|
|
Returns:
|
|
Full path to the index database file
|
|
|
|
Examples:
|
|
>>> mapper = PathMapper()
|
|
>>> mapper.source_to_index_db(Path("D:/Claude_dms3/src"))
|
|
PosixPath('/home/user/.codexlens/indexes/D/Claude_dms3/src/_index.db')
|
|
"""
|
|
index_dir = self.source_to_index_dir(source_path)
|
|
return index_dir / self.INDEX_DB_NAME
|
|
|
|
def index_to_source(self, index_path: Path) -> Path:
|
|
"""Convert index path back to original source path.
|
|
|
|
Performs reverse mapping from an index storage location to the
|
|
original source directory. Handles both directory paths and
|
|
database file paths.
|
|
|
|
Args:
|
|
index_path: Index directory or database file path
|
|
|
|
Returns:
|
|
Original source directory path
|
|
|
|
Raises:
|
|
ValueError: If index_path is not under index_root
|
|
|
|
Examples:
|
|
>>> mapper = PathMapper()
|
|
>>> mapper.index_to_source(
|
|
... Path("~/.codexlens/indexes/D/Claude_dms3/src/_index.db")
|
|
... )
|
|
WindowsPath('D:/Claude_dms3/src')
|
|
|
|
>>> mapper.index_to_source(
|
|
... Path("~/.codexlens/indexes/D/Claude_dms3/src")
|
|
... )
|
|
WindowsPath('D:/Claude_dms3/src')
|
|
"""
|
|
index_path = index_path.resolve()
|
|
|
|
# Remove _index.db if present
|
|
if index_path.name == self.INDEX_DB_NAME:
|
|
index_path = index_path.parent
|
|
|
|
# Verify path is under index_root
|
|
try:
|
|
relative = index_path.relative_to(self.index_root)
|
|
except ValueError:
|
|
raise ValueError(
|
|
f"Index path {index_path} is not under index root {self.index_root}"
|
|
)
|
|
|
|
# Convert normalized path back to source path
|
|
normalized_str = str(relative).replace("\\", "/")
|
|
return self.denormalize_path(normalized_str)
|
|
|
|
def get_project_root(self, source_path: Path) -> Path:
|
|
"""Find the project root directory (topmost indexed directory).
|
|
|
|
Walks up the directory tree to find the highest-level directory
|
|
that has an index database.
|
|
|
|
Args:
|
|
source_path: Source directory to start from
|
|
|
|
Returns:
|
|
Project root directory path. Returns source_path itself if
|
|
no parent index is found.
|
|
|
|
Examples:
|
|
>>> mapper = PathMapper()
|
|
>>> mapper.get_project_root(Path("D:/Claude_dms3/src/codexlens"))
|
|
WindowsPath('D:/Claude_dms3')
|
|
"""
|
|
source_path = source_path.resolve()
|
|
current = source_path
|
|
project_root = source_path
|
|
|
|
# Walk up the tree
|
|
while current.parent != current: # Stop at filesystem root
|
|
parent_index_db = self.source_to_index_db(current.parent)
|
|
if parent_index_db.exists():
|
|
project_root = current.parent
|
|
current = current.parent
|
|
else:
|
|
break
|
|
|
|
return project_root
|
|
|
|
def get_relative_depth(self, source_path: Path, project_root: Path) -> int:
|
|
"""Calculate directory depth relative to project root.
|
|
|
|
Args:
|
|
source_path: Target directory path
|
|
project_root: Project root directory path
|
|
|
|
Returns:
|
|
Number of directory levels from project_root to source_path
|
|
|
|
Raises:
|
|
ValueError: If source_path is not under project_root
|
|
|
|
Examples:
|
|
>>> mapper = PathMapper()
|
|
>>> mapper.get_relative_depth(
|
|
... Path("D:/Claude_dms3/src/codexlens"),
|
|
... Path("D:/Claude_dms3")
|
|
... )
|
|
2
|
|
"""
|
|
source_path = source_path.resolve()
|
|
project_root = project_root.resolve()
|
|
|
|
try:
|
|
relative = source_path.relative_to(project_root)
|
|
# Count path components
|
|
return len(relative.parts)
|
|
except ValueError:
|
|
raise ValueError(
|
|
f"Source path {source_path} is not under project root {project_root}"
|
|
)
|
|
|
|
def normalize_path(self, path: Path) -> str:
|
|
"""Normalize path to cross-platform storage format.
|
|
|
|
Converts OS-specific paths to a standardized format for storage:
|
|
- Windows: Removes drive colons (D: → D)
|
|
- Unix: Removes leading slash
|
|
- Uses forward slashes throughout
|
|
|
|
Args:
|
|
path: Path to normalize
|
|
|
|
Returns:
|
|
Normalized path string
|
|
|
|
Examples:
|
|
>>> mapper = PathMapper()
|
|
>>> mapper.normalize_path(Path("D:/path/to/dir"))
|
|
'D/path/to/dir'
|
|
|
|
>>> mapper.normalize_path(Path("/home/user/path"))
|
|
'home/user/path'
|
|
"""
|
|
path = path.resolve()
|
|
path_str = str(path)
|
|
|
|
# Handle Windows paths with drive letters
|
|
if platform.system() == "Windows" and len(path.parts) > 0:
|
|
# Convert D:\path\to\dir → D/path/to/dir
|
|
drive = path.parts[0].replace(":", "") # D: → D
|
|
rest = Path(*path.parts[1:]) if len(path.parts) > 1 else Path()
|
|
normalized = f"{drive}/{rest}".replace("\\", "/")
|
|
return normalized.rstrip("/")
|
|
|
|
# Handle Unix paths
|
|
# /home/user/path → home/user/path
|
|
return path_str.lstrip("/").replace("\\", "/")
|
|
|
|
def denormalize_path(self, normalized: str) -> Path:
|
|
"""Convert normalized path back to OS-specific path.
|
|
|
|
Reverses the normalization process to restore OS-native path format:
|
|
- Windows: Adds drive colons (D → D:)
|
|
- Unix: Adds leading slash
|
|
|
|
Args:
|
|
normalized: Normalized path string
|
|
|
|
Returns:
|
|
OS-specific Path object
|
|
|
|
Examples:
|
|
>>> mapper = PathMapper()
|
|
>>> mapper.denormalize_path("D/path/to/dir") # On Windows
|
|
WindowsPath('D:/path/to/dir')
|
|
|
|
>>> mapper.denormalize_path("home/user/path") # On Unix
|
|
PosixPath('/home/user/path')
|
|
"""
|
|
parts = normalized.split("/")
|
|
|
|
# Handle Windows paths
|
|
if platform.system() == "Windows" and len(parts) > 0:
|
|
# Check if first part is a drive letter
|
|
if len(parts[0]) == 1 and parts[0].isalpha():
|
|
# D/path/to/dir → D:/path/to/dir
|
|
drive = f"{parts[0]}:"
|
|
if len(parts) > 1:
|
|
return Path(drive) / Path(*parts[1:])
|
|
return Path(drive)
|
|
|
|
# Handle Unix paths or relative paths
|
|
# home/user/path → /home/user/path
|
|
return Path("/") / Path(*parts)
|