|
24 | 24 | import sys |
25 | 25 | import zipfile |
26 | 26 | from datetime import datetime |
27 | | -from typing import cast |
| 27 | +from typing import NoReturn, cast |
28 | 28 |
|
29 | 29 | # Allow running from repo root or scripts/ directory |
30 | 30 | SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) |
@@ -182,11 +182,7 @@ def cmd_list(args): |
182 | 182 | base_dir = getattr(args, "base_dir", None) or get_claude_projects_dir() |
183 | 183 | project_filter = getattr(args, "project", None) |
184 | 184 |
|
185 | | - if not os.path.isdir(base_dir): |
186 | | - _die( |
187 | | - ErrorCode.INTERNAL_ERROR, |
188 | | - detail=f"Claude Code projects directory not found: {base_dir}", |
189 | | - ) |
| 185 | + _require_projects_dir(base_dir) |
190 | 186 |
|
191 | 187 | projects = list_projects(base_dir) |
192 | 188 | if project_filter: |
@@ -249,11 +245,7 @@ def cmd_stats(args): |
249 | 245 | session_id = getattr(args, "session", None) |
250 | 246 | fmt = getattr(args, "format", "text") or "text" |
251 | 247 |
|
252 | | - if not os.path.isdir(base_dir): |
253 | | - _die( |
254 | | - ErrorCode.INTERNAL_ERROR, |
255 | | - detail=f"Claude Code projects directory not found: {base_dir}", |
256 | | - ) |
| 248 | + _require_projects_dir(base_dir) |
257 | 249 |
|
258 | 250 | if session_id: |
259 | 251 | _session_stats(session_id, base_dir, fmt) |
@@ -459,11 +451,7 @@ def cmd_export(args): |
459 | 451 | session_filter = getattr(args, "session", None) |
460 | 452 | exclusion_rules_path = getattr(args, "exclude_rules", None) |
461 | 453 |
|
462 | | - if not os.path.isdir(base_dir): |
463 | | - _die( |
464 | | - ErrorCode.INTERNAL_ERROR, |
465 | | - detail=f"Claude Code projects directory not found: {base_dir}", |
466 | | - ) |
| 454 | + _require_projects_dir(base_dir) |
467 | 455 |
|
468 | 456 | rules = load_rules(resolve_exclusion_rules_path(exclusion_rules_path)) |
469 | 457 |
|
@@ -725,7 +713,7 @@ def _find_session(session_id: str, base_dir: str) -> str | None: |
725 | 713 | return matches[0]["path"] |
726 | 714 | if len(matches) > 1: |
727 | 715 | _die( |
728 | | - ErrorCode.SESSION_NOT_FOUND, |
| 716 | + ErrorCode.SESSION_AMBIGUOUS_PREFIX, |
729 | 717 | detail=( |
730 | 718 | f"Ambiguous prefix '{session_id}' matches {len(matches)} sessions:\n" |
731 | 719 | + "\n".join(f" {m['id']}" for m in matches) |
@@ -774,14 +762,22 @@ def _save_state(sessions: dict, count: int, out_dir: str): |
774 | 762 | atomic_write_export_state(disk, STATE_FILE) |
775 | 763 |
|
776 | 764 |
|
| 765 | +def _require_projects_dir(base_dir: str) -> None: |
| 766 | + if not os.path.isdir(base_dir): |
| 767 | + _die( |
| 768 | + ErrorCode.PROJECTS_DIR_NOT_FOUND, |
| 769 | + detail=f"Claude Code projects directory not found: {base_dir}", |
| 770 | + ) |
| 771 | + |
| 772 | + |
777 | 773 | def _cli_die_message(code: ErrorCode) -> str: |
778 | 774 | """Stable CLI stderr label (not export-scoped).""" |
779 | 775 | if code == ErrorCode.INTERNAL_ERROR: |
780 | 776 | return "Command failed" |
781 | 777 | return failure_message_for_code(code) |
782 | 778 |
|
783 | 779 |
|
784 | | -def _die(code: ErrorCode, *, detail: str | None = None) -> None: |
| 780 | +def _die(code: ErrorCode, *, detail: str | None = None) -> NoReturn: |
785 | 781 | print(f"Error: {code.value} — {_cli_die_message(code)}", file=sys.stderr) |
786 | 782 | if detail: |
787 | 783 | print(detail, file=sys.stderr) |
|
0 commit comments