-
Notifications
You must be signed in to change notification settings - Fork 35
Expand file tree
/
Copy pathproject_resolver.py
More file actions
89 lines (71 loc) · 2.88 KB
/
project_resolver.py
File metadata and controls
89 lines (71 loc) · 2.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
from __future__ import annotations
import os
import subprocess
from functools import lru_cache
from pathlib import Path
__all__ = ["project_from_encoded_path", "resolve_project_name"]
def resolve_project_name(cwd: str | Path) -> str:
"""Resolve a cwd to its canonical project name, including git worktrees."""
if not str(cwd):
return "unknown"
path = Path(os.path.expanduser(str(cwd))).resolve(strict=False)
return _resolve_project_name(str(path))
@lru_cache(maxsize=256)
def _resolve_project_name(normalized_cwd: str) -> str:
fallback = Path(normalized_cwd).name or "unknown"
try:
result = subprocess.run(
["git", "-C", normalized_cwd, "worktree", "list", "--porcelain"],
capture_output=True,
check=False,
text=True,
# Force UTF-8 instead of the locale default: a .app launched via
# LaunchServices has no LANG set, so text=True would decode git's
# output as ASCII and crash on non-ASCII (e.g. Chinese) repo paths.
encoding="utf-8",
errors="replace",
timeout=3,
)
except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
return fallback
if result.returncode != 0 or result.stderr or not result.stdout:
return fallback
lines = result.stdout.splitlines()
first_line = lines[0] if lines else ""
prefix = "worktree "
if not first_line.startswith(prefix):
return fallback
main_path = first_line.removeprefix(prefix).strip()
if not main_path:
return fallback
return Path(main_path).name or fallback
def project_from_encoded_path(jsonl_path: Path, projects_dir: Path) -> str:
"""Decode a Claude Code project name from a sessions JSONL path under projects_dir."""
try:
project_dir = jsonl_path.relative_to(projects_dir).parts[0]
except (IndexError, ValueError):
return "unknown"
parts = [part for part in project_dir.split("-") if part]
if not parts:
return "unknown"
slash_candidate = Path(os.sep, *parts)
if slash_candidate.is_dir():
return slash_candidate.name or "unknown"
existing_project = _existing_encoded_project_path(parts)
if existing_project is not None:
return existing_project.name or "unknown"
fallback = project_dir.removeprefix("-")
return fallback or "unknown"
def _existing_encoded_project_path(parts: list[str]) -> Path | None:
def search(index: int, current: Path) -> Path | None:
for end in range(index + 1, len(parts) + 1):
candidate = current / "-".join(parts[index:end])
if not candidate.is_dir():
continue
if end == len(parts):
return candidate
result = search(end, candidate)
if result is not None:
return result
return None
return search(0, Path(os.sep))