erk 0.4.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- erk/__init__.py +12 -0
- erk/__main__.py +6 -0
- erk/agent_docs/__init__.py +5 -0
- erk/agent_docs/models.py +123 -0
- erk/agent_docs/operations.py +666 -0
- erk/artifacts/__init__.py +5 -0
- erk/artifacts/artifact_health.py +623 -0
- erk/artifacts/detection.py +16 -0
- erk/artifacts/discovery.py +343 -0
- erk/artifacts/models.py +63 -0
- erk/artifacts/staleness.py +56 -0
- erk/artifacts/state.py +100 -0
- erk/artifacts/sync.py +624 -0
- erk/cli/__init__.py +0 -0
- erk/cli/activation.py +132 -0
- erk/cli/alias.py +53 -0
- erk/cli/cli.py +221 -0
- erk/cli/commands/__init__.py +0 -0
- erk/cli/commands/admin.py +153 -0
- erk/cli/commands/artifact/__init__.py +1 -0
- erk/cli/commands/artifact/check.py +260 -0
- erk/cli/commands/artifact/group.py +31 -0
- erk/cli/commands/artifact/list_cmd.py +89 -0
- erk/cli/commands/artifact/show.py +62 -0
- erk/cli/commands/artifact/sync_cmd.py +39 -0
- erk/cli/commands/branch/__init__.py +26 -0
- erk/cli/commands/branch/assign_cmd.py +152 -0
- erk/cli/commands/branch/checkout_cmd.py +357 -0
- erk/cli/commands/branch/create_cmd.py +161 -0
- erk/cli/commands/branch/list_cmd.py +82 -0
- erk/cli/commands/branch/unassign_cmd.py +197 -0
- erk/cli/commands/cc/__init__.py +15 -0
- erk/cli/commands/cc/jsonl_cmd.py +20 -0
- erk/cli/commands/cc/session/AGENTS.md +30 -0
- erk/cli/commands/cc/session/CLAUDE.md +1 -0
- erk/cli/commands/cc/session/__init__.py +15 -0
- erk/cli/commands/cc/session/list_cmd.py +167 -0
- erk/cli/commands/cc/session/show_cmd.py +175 -0
- erk/cli/commands/completion.py +89 -0
- erk/cli/commands/completions.py +165 -0
- erk/cli/commands/config.py +327 -0
- erk/cli/commands/docs/__init__.py +1 -0
- erk/cli/commands/docs/group.py +16 -0
- erk/cli/commands/docs/sync.py +121 -0
- erk/cli/commands/docs/validate.py +102 -0
- erk/cli/commands/doctor.py +243 -0
- erk/cli/commands/down.py +171 -0
- erk/cli/commands/exec/__init__.py +1 -0
- erk/cli/commands/exec/group.py +164 -0
- erk/cli/commands/exec/scripts/AGENTS.md +79 -0
- erk/cli/commands/exec/scripts/CLAUDE.md +1 -0
- erk/cli/commands/exec/scripts/__init__.py +5 -0
- erk/cli/commands/exec/scripts/add_reaction_to_comment.py +69 -0
- erk/cli/commands/exec/scripts/add_remote_execution_note.py +68 -0
- erk/cli/commands/exec/scripts/check_impl.py +152 -0
- erk/cli/commands/exec/scripts/ci_update_pr_body.py +294 -0
- erk/cli/commands/exec/scripts/create_extraction_branch.py +138 -0
- erk/cli/commands/exec/scripts/create_extraction_plan.py +242 -0
- erk/cli/commands/exec/scripts/create_issue_from_session.py +103 -0
- erk/cli/commands/exec/scripts/create_plan_from_context.py +103 -0
- erk/cli/commands/exec/scripts/create_worker_impl_from_issue.py +93 -0
- erk/cli/commands/exec/scripts/detect_trunk_branch.py +121 -0
- erk/cli/commands/exec/scripts/exit_plan_mode_hook.py +777 -0
- erk/cli/commands/exec/scripts/extract_latest_plan.py +49 -0
- erk/cli/commands/exec/scripts/extract_session_from_issue.py +150 -0
- erk/cli/commands/exec/scripts/find_project_dir.py +214 -0
- erk/cli/commands/exec/scripts/generate_pr_summary.py +112 -0
- erk/cli/commands/exec/scripts/get_closing_text.py +98 -0
- erk/cli/commands/exec/scripts/get_embedded_prompt.py +62 -0
- erk/cli/commands/exec/scripts/get_plan_metadata.py +95 -0
- erk/cli/commands/exec/scripts/get_pr_body_footer.py +70 -0
- erk/cli/commands/exec/scripts/get_pr_discussion_comments.py +149 -0
- erk/cli/commands/exec/scripts/get_pr_review_comments.py +155 -0
- erk/cli/commands/exec/scripts/impl_init.py +158 -0
- erk/cli/commands/exec/scripts/impl_signal.py +375 -0
- erk/cli/commands/exec/scripts/impl_verify.py +49 -0
- erk/cli/commands/exec/scripts/issue_title_to_filename.py +34 -0
- erk/cli/commands/exec/scripts/list_sessions.py +296 -0
- erk/cli/commands/exec/scripts/mark_impl_ended.py +188 -0
- erk/cli/commands/exec/scripts/mark_impl_started.py +188 -0
- erk/cli/commands/exec/scripts/marker.py +163 -0
- erk/cli/commands/exec/scripts/objective_save_to_issue.py +109 -0
- erk/cli/commands/exec/scripts/plan_save_to_issue.py +269 -0
- erk/cli/commands/exec/scripts/plan_update_issue.py +147 -0
- erk/cli/commands/exec/scripts/post_extraction_comment.py +237 -0
- erk/cli/commands/exec/scripts/post_or_update_pr_summary.py +133 -0
- erk/cli/commands/exec/scripts/post_pr_inline_comment.py +143 -0
- erk/cli/commands/exec/scripts/post_workflow_started_comment.py +168 -0
- erk/cli/commands/exec/scripts/preprocess_session.py +777 -0
- erk/cli/commands/exec/scripts/quick_submit.py +32 -0
- erk/cli/commands/exec/scripts/rebase_with_conflict_resolution.py +260 -0
- erk/cli/commands/exec/scripts/reply_to_discussion_comment.py +173 -0
- erk/cli/commands/exec/scripts/resolve_review_thread.py +170 -0
- erk/cli/commands/exec/scripts/session_id_injector_hook.py +52 -0
- erk/cli/commands/exec/scripts/setup_impl_from_issue.py +159 -0
- erk/cli/commands/exec/scripts/slot_objective.py +102 -0
- erk/cli/commands/exec/scripts/tripwires_reminder_hook.py +20 -0
- erk/cli/commands/exec/scripts/update_dispatch_info.py +116 -0
- erk/cli/commands/exec/scripts/user_prompt_hook.py +113 -0
- erk/cli/commands/exec/scripts/validate_plan_content.py +98 -0
- erk/cli/commands/exec/scripts/wrap_plan_in_metadata_block.py +34 -0
- erk/cli/commands/implement.py +695 -0
- erk/cli/commands/implement_shared.py +649 -0
- erk/cli/commands/info/__init__.py +14 -0
- erk/cli/commands/info/release_notes_cmd.py +128 -0
- erk/cli/commands/init.py +801 -0
- erk/cli/commands/land_cmd.py +690 -0
- erk/cli/commands/log_cmd.py +137 -0
- erk/cli/commands/md/__init__.py +5 -0
- erk/cli/commands/md/check.py +118 -0
- erk/cli/commands/md/group.py +14 -0
- erk/cli/commands/navigation_helpers.py +430 -0
- erk/cli/commands/objective/__init__.py +16 -0
- erk/cli/commands/objective/list_cmd.py +47 -0
- erk/cli/commands/objective_helpers.py +132 -0
- erk/cli/commands/plan/__init__.py +32 -0
- erk/cli/commands/plan/check_cmd.py +174 -0
- erk/cli/commands/plan/close_cmd.py +69 -0
- erk/cli/commands/plan/create_cmd.py +120 -0
- erk/cli/commands/plan/docs/__init__.py +18 -0
- erk/cli/commands/plan/docs/extract_cmd.py +53 -0
- erk/cli/commands/plan/docs/unextract_cmd.py +38 -0
- erk/cli/commands/plan/docs/unextracted_cmd.py +72 -0
- erk/cli/commands/plan/extraction/__init__.py +16 -0
- erk/cli/commands/plan/extraction/complete_cmd.py +101 -0
- erk/cli/commands/plan/extraction/create_raw_cmd.py +63 -0
- erk/cli/commands/plan/get.py +71 -0
- erk/cli/commands/plan/list_cmd.py +754 -0
- erk/cli/commands/plan/log_cmd.py +440 -0
- erk/cli/commands/plan/start_cmd.py +459 -0
- erk/cli/commands/planner/__init__.py +40 -0
- erk/cli/commands/planner/configure_cmd.py +73 -0
- erk/cli/commands/planner/connect_cmd.py +96 -0
- erk/cli/commands/planner/create_cmd.py +148 -0
- erk/cli/commands/planner/list_cmd.py +51 -0
- erk/cli/commands/planner/register_cmd.py +105 -0
- erk/cli/commands/planner/set_default_cmd.py +23 -0
- erk/cli/commands/planner/unregister_cmd.py +43 -0
- erk/cli/commands/pr/__init__.py +23 -0
- erk/cli/commands/pr/check_cmd.py +112 -0
- erk/cli/commands/pr/checkout_cmd.py +165 -0
- erk/cli/commands/pr/fix_conflicts_cmd.py +82 -0
- erk/cli/commands/pr/parse_pr_reference.py +10 -0
- erk/cli/commands/pr/submit_cmd.py +360 -0
- erk/cli/commands/pr/sync_cmd.py +181 -0
- erk/cli/commands/prepare_cwd_recovery.py +60 -0
- erk/cli/commands/project/__init__.py +16 -0
- erk/cli/commands/project/init_cmd.py +91 -0
- erk/cli/commands/run/__init__.py +17 -0
- erk/cli/commands/run/list_cmd.py +189 -0
- erk/cli/commands/run/logs_cmd.py +54 -0
- erk/cli/commands/run/shared.py +19 -0
- erk/cli/commands/shell_integration.py +29 -0
- erk/cli/commands/slot/__init__.py +23 -0
- erk/cli/commands/slot/check_cmd.py +277 -0
- erk/cli/commands/slot/common.py +314 -0
- erk/cli/commands/slot/init_pool_cmd.py +157 -0
- erk/cli/commands/slot/list_cmd.py +228 -0
- erk/cli/commands/slot/repair_cmd.py +190 -0
- erk/cli/commands/stack/__init__.py +23 -0
- erk/cli/commands/stack/consolidate_cmd.py +470 -0
- erk/cli/commands/stack/list_cmd.py +79 -0
- erk/cli/commands/stack/move_cmd.py +309 -0
- erk/cli/commands/stack/split_old/README.md +64 -0
- erk/cli/commands/stack/split_old/__init__.py +5 -0
- erk/cli/commands/stack/split_old/command.py +233 -0
- erk/cli/commands/stack/split_old/display.py +116 -0
- erk/cli/commands/stack/split_old/plan.py +216 -0
- erk/cli/commands/status.py +58 -0
- erk/cli/commands/submit.py +768 -0
- erk/cli/commands/up.py +154 -0
- erk/cli/commands/upgrade.py +82 -0
- erk/cli/commands/wt/__init__.py +29 -0
- erk/cli/commands/wt/checkout_cmd.py +110 -0
- erk/cli/commands/wt/create_cmd.py +998 -0
- erk/cli/commands/wt/current_cmd.py +35 -0
- erk/cli/commands/wt/delete_cmd.py +573 -0
- erk/cli/commands/wt/list_cmd.py +332 -0
- erk/cli/commands/wt/rename_cmd.py +66 -0
- erk/cli/config.py +242 -0
- erk/cli/constants.py +29 -0
- erk/cli/core.py +65 -0
- erk/cli/debug.py +9 -0
- erk/cli/ensure-conversion-tasks.md +288 -0
- erk/cli/ensure.py +628 -0
- erk/cli/github_parsing.py +96 -0
- erk/cli/graphite.py +81 -0
- erk/cli/graphite_command.py +80 -0
- erk/cli/help_formatter.py +345 -0
- erk/cli/output.py +361 -0
- erk/cli/presets/dagster.toml +12 -0
- erk/cli/presets/generic.toml +12 -0
- erk/cli/prompt_hooks_templates/README.md +68 -0
- erk/cli/script_output.py +32 -0
- erk/cli/shell_integration/bash_wrapper.sh +32 -0
- erk/cli/shell_integration/fish_wrapper.fish +39 -0
- erk/cli/shell_integration/handler.py +338 -0
- erk/cli/shell_integration/zsh_wrapper.sh +32 -0
- erk/cli/shell_utils.py +171 -0
- erk/cli/subprocess_utils.py +92 -0
- erk/cli/uvx_detection.py +59 -0
- erk/core/__init__.py +0 -0
- erk/core/claude_executor.py +511 -0
- erk/core/claude_settings.py +317 -0
- erk/core/command_log.py +406 -0
- erk/core/commit_message_generator.py +234 -0
- erk/core/completion.py +10 -0
- erk/core/consolidation_utils.py +177 -0
- erk/core/context.py +570 -0
- erk/core/display/__init__.py +4 -0
- erk/core/display/abc.py +24 -0
- erk/core/display/real.py +30 -0
- erk/core/display_utils.py +526 -0
- erk/core/file_utils.py +87 -0
- erk/core/health_checks.py +1315 -0
- erk/core/health_checks_dogfooder/__init__.py +85 -0
- erk/core/health_checks_dogfooder/deprecated_dot_agent_config.py +64 -0
- erk/core/health_checks_dogfooder/legacy_claude_docs.py +69 -0
- erk/core/health_checks_dogfooder/legacy_config_locations.py +122 -0
- erk/core/health_checks_dogfooder/legacy_erk_docs_agent.py +61 -0
- erk/core/health_checks_dogfooder/legacy_erk_kits_folder.py +60 -0
- erk/core/health_checks_dogfooder/legacy_hook_settings.py +104 -0
- erk/core/health_checks_dogfooder/legacy_kit_yaml.py +78 -0
- erk/core/health_checks_dogfooder/legacy_kits_toml.py +43 -0
- erk/core/health_checks_dogfooder/outdated_erk_skill.py +43 -0
- erk/core/implementation_queue/__init__.py +1 -0
- erk/core/implementation_queue/github/__init__.py +8 -0
- erk/core/implementation_queue/github/abc.py +7 -0
- erk/core/implementation_queue/github/noop.py +38 -0
- erk/core/implementation_queue/github/printing.py +43 -0
- erk/core/implementation_queue/github/real.py +119 -0
- erk/core/init_utils.py +227 -0
- erk/core/output_filter.py +338 -0
- erk/core/plan_store/__init__.py +6 -0
- erk/core/planner/__init__.py +1 -0
- erk/core/planner/registry_abc.py +8 -0
- erk/core/planner/registry_fake.py +129 -0
- erk/core/planner/registry_real.py +195 -0
- erk/core/planner/types.py +7 -0
- erk/core/pr_utils.py +30 -0
- erk/core/release_notes.py +263 -0
- erk/core/repo_discovery.py +126 -0
- erk/core/script_writer.py +41 -0
- erk/core/services/__init__.py +1 -0
- erk/core/services/plan_list_service.py +94 -0
- erk/core/shell.py +51 -0
- erk/core/user_feedback.py +11 -0
- erk/core/version_check.py +55 -0
- erk/core/workflow_display.py +75 -0
- erk/core/worktree_pool.py +190 -0
- erk/core/worktree_utils.py +300 -0
- erk/data/CHANGELOG.md +438 -0
- erk/data/__init__.py +1 -0
- erk/data/claude/agents/devrun.md +180 -0
- erk/data/claude/commands/erk/__init__.py +0 -0
- erk/data/claude/commands/erk/create-extraction-plan.md +360 -0
- erk/data/claude/commands/erk/fix-conflicts.md +25 -0
- erk/data/claude/commands/erk/git-pr-push.md +345 -0
- erk/data/claude/commands/erk/implement-stacked-plan.md +96 -0
- erk/data/claude/commands/erk/land.md +193 -0
- erk/data/claude/commands/erk/objective-create.md +370 -0
- erk/data/claude/commands/erk/objective-list.md +34 -0
- erk/data/claude/commands/erk/objective-next-plan.md +220 -0
- erk/data/claude/commands/erk/objective-update-with-landed-pr.md +216 -0
- erk/data/claude/commands/erk/plan-implement.md +202 -0
- erk/data/claude/commands/erk/plan-save.md +45 -0
- erk/data/claude/commands/erk/plan-submit.md +39 -0
- erk/data/claude/commands/erk/pr-address.md +367 -0
- erk/data/claude/commands/erk/pr-submit.md +58 -0
- erk/data/claude/skills/dignified-python/SKILL.md +48 -0
- erk/data/claude/skills/dignified-python/cli-patterns.md +155 -0
- erk/data/claude/skills/dignified-python/dignified-python-core.md +1190 -0
- erk/data/claude/skills/dignified-python/subprocess.md +99 -0
- erk/data/claude/skills/dignified-python/versions/python-3.10.md +517 -0
- erk/data/claude/skills/dignified-python/versions/python-3.11.md +536 -0
- erk/data/claude/skills/dignified-python/versions/python-3.12.md +662 -0
- erk/data/claude/skills/dignified-python/versions/python-3.13.md +653 -0
- erk/data/claude/skills/erk-diff-analysis/SKILL.md +27 -0
- erk/data/claude/skills/erk-diff-analysis/references/commit-message-prompt.md +78 -0
- erk/data/claude/skills/learned-docs/SKILL.md +362 -0
- erk/data/github/actions/setup-claude-erk/action.yml +11 -0
- erk/data/github/prompts/dignified-python-review.md +125 -0
- erk/data/github/workflows/dignified-python-review.yml +61 -0
- erk/data/github/workflows/erk-impl.yml +251 -0
- erk/hooks/__init__.py +1 -0
- erk/hooks/decorators.py +319 -0
- erk/status/__init__.py +8 -0
- erk/status/collectors/__init__.py +9 -0
- erk/status/collectors/base.py +52 -0
- erk/status/collectors/git.py +76 -0
- erk/status/collectors/github.py +81 -0
- erk/status/collectors/graphite.py +80 -0
- erk/status/collectors/impl.py +145 -0
- erk/status/models/__init__.py +4 -0
- erk/status/models/status_data.py +404 -0
- erk/status/orchestrator.py +169 -0
- erk/status/renderers/__init__.py +5 -0
- erk/status/renderers/simple.py +322 -0
- erk/tui/AGENTS.md +193 -0
- erk/tui/CLAUDE.md +1 -0
- erk/tui/__init__.py +1 -0
- erk/tui/app.py +1404 -0
- erk/tui/commands/__init__.py +1 -0
- erk/tui/commands/executor.py +66 -0
- erk/tui/commands/provider.py +165 -0
- erk/tui/commands/real_executor.py +63 -0
- erk/tui/commands/registry.py +121 -0
- erk/tui/commands/types.py +36 -0
- erk/tui/data/__init__.py +1 -0
- erk/tui/data/provider.py +492 -0
- erk/tui/data/types.py +104 -0
- erk/tui/filtering/__init__.py +1 -0
- erk/tui/filtering/logic.py +43 -0
- erk/tui/filtering/types.py +55 -0
- erk/tui/jsonl_viewer/__init__.py +1 -0
- erk/tui/jsonl_viewer/app.py +61 -0
- erk/tui/jsonl_viewer/models.py +208 -0
- erk/tui/jsonl_viewer/widgets.py +204 -0
- erk/tui/sorting/__init__.py +6 -0
- erk/tui/sorting/logic.py +55 -0
- erk/tui/sorting/types.py +68 -0
- erk/tui/styles/dash.tcss +95 -0
- erk/tui/widgets/__init__.py +1 -0
- erk/tui/widgets/command_output.py +112 -0
- erk/tui/widgets/plan_table.py +276 -0
- erk/tui/widgets/status_bar.py +116 -0
- erk-0.4.5.dist-info/METADATA +376 -0
- erk-0.4.5.dist-info/RECORD +331 -0
- erk-0.4.5.dist-info/WHEEL +4 -0
- erk-0.4.5.dist-info/entry_points.txt +2 -0
- erk-0.4.5.dist-info/licenses/LICENSE.md +3 -0
erk/cli/ensure.py
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
"""CLI error handling utilities with styled output.
|
|
2
|
+
|
|
3
|
+
This module provides the Ensure class for asserting invariants in CLI commands
|
|
4
|
+
with consistent, user-friendly error messages. All errors use red "Error:" prefix
|
|
5
|
+
for visual consistency.
|
|
6
|
+
|
|
7
|
+
Domain-Specific Methods:
|
|
8
|
+
- Git state validations (branch checks, worktree existence, clean state)
|
|
9
|
+
- Configuration validations (required fields, format checks)
|
|
10
|
+
- Argument validations (count, type, range)
|
|
11
|
+
- File/path validations (readable, writable, not hidden)
|
|
12
|
+
- String/collection validations (non-empty, non-null)
|
|
13
|
+
- External tool validations (gh CLI installed)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import shutil
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
21
|
+
|
|
22
|
+
import click
|
|
23
|
+
|
|
24
|
+
from erk_shared.gateway.graphite.disabled import GraphiteDisabled, GraphiteDisabledError
|
|
25
|
+
from erk_shared.github.types import PRDetails
|
|
26
|
+
from erk_shared.non_ideal_state import (
|
|
27
|
+
BranchDetectionFailed,
|
|
28
|
+
GitHubAPIFailed,
|
|
29
|
+
NonIdealState,
|
|
30
|
+
NoPRForBranch,
|
|
31
|
+
PRNotFoundError,
|
|
32
|
+
SessionNotFound,
|
|
33
|
+
)
|
|
34
|
+
from erk_shared.output.output import user_output
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from erk.core.context import ErkContext
|
|
38
|
+
|
|
39
|
+
T = TypeVar("T")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Ensure:
|
|
43
|
+
"""Helper class for asserting invariants with consistent error handling."""
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def invariant(condition: bool, error_message: str) -> None:
|
|
47
|
+
"""Ensure condition is true, otherwise output styled error and exit.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
condition: Boolean condition to check
|
|
51
|
+
error_message: Error message to display if condition is false.
|
|
52
|
+
"Error: " prefix will be added automatically in red.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
SystemExit: If condition is false (with exit code 1)
|
|
56
|
+
"""
|
|
57
|
+
if not condition:
|
|
58
|
+
user_output(click.style("Error: ", fg="red") + error_message)
|
|
59
|
+
raise SystemExit(1)
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def truthy(value: T, error_message: str) -> T:
|
|
63
|
+
"""Ensure value is truthy, otherwise output styled error and exit.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
value: Value to check for truthiness
|
|
67
|
+
error_message: Error message to display if value is falsy.
|
|
68
|
+
"Error: " prefix will be added automatically in red.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The value unchanged if truthy
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
SystemExit: If value is falsy (with exit code 1)
|
|
75
|
+
"""
|
|
76
|
+
if not value:
|
|
77
|
+
user_output(click.style("Error: ", fg="red") + error_message)
|
|
78
|
+
raise SystemExit(1)
|
|
79
|
+
return value
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def not_none(value: T | None, error_message: str) -> T:
|
|
83
|
+
"""Ensure value is not None, otherwise output styled error and exit.
|
|
84
|
+
|
|
85
|
+
This method provides type narrowing: it takes `T | None` and returns `T`,
|
|
86
|
+
allowing the type checker to understand the value cannot be None after
|
|
87
|
+
this call.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
value: Value to check for None
|
|
91
|
+
error_message: Error message to display if value is None.
|
|
92
|
+
"Error: " prefix will be added automatically in red.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
The value unchanged if not None (with narrowed type T)
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
SystemExit: If value is None (with exit code 1)
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
>>> # Type narrowing in action
|
|
102
|
+
>>> path: Path | None = get_worktree_path()
|
|
103
|
+
>>> safe_path: Path = Ensure.not_none(path, "Worktree path not found")
|
|
104
|
+
>>> # safe_path is now guaranteed to be Path, not Path | None
|
|
105
|
+
"""
|
|
106
|
+
if value is None:
|
|
107
|
+
user_output(click.style("Error: ", fg="red") + error_message)
|
|
108
|
+
raise SystemExit(1)
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def path_exists(
|
|
113
|
+
ctx: ErkContext,
|
|
114
|
+
path: Path,
|
|
115
|
+
error_message: str | None = None,
|
|
116
|
+
) -> None:
|
|
117
|
+
"""Ensure path exists, otherwise output styled error and exit.
|
|
118
|
+
|
|
119
|
+
This method is designed for validating git-managed paths (worktrees, repos).
|
|
120
|
+
It checks path existence before any operations that would fail on missing paths.
|
|
121
|
+
|
|
122
|
+
Supports both real filesystem paths and sentinel paths used in tests by using
|
|
123
|
+
ctx.git.path_exists, which works with both real paths and test sentinels.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
ctx: Application context with git integration for path checking
|
|
127
|
+
path: Path to check for existence
|
|
128
|
+
error_message: Optional custom error message. If not provided,
|
|
129
|
+
uses default "Path not found: {path}".
|
|
130
|
+
"Error: " prefix will be added automatically in red.
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
SystemExit: If path does not exist (with exit code 1)
|
|
134
|
+
|
|
135
|
+
Example:
|
|
136
|
+
>>> # Basic usage with default error message
|
|
137
|
+
>>> Ensure.path_exists(ctx, config_path)
|
|
138
|
+
>>>
|
|
139
|
+
>>> # With custom error message
|
|
140
|
+
>>> Ensure.path_exists(ctx, wt_path, f"Worktree not found: {wt_path}")
|
|
141
|
+
"""
|
|
142
|
+
if not ctx.git.path_exists(path):
|
|
143
|
+
if error_message is None:
|
|
144
|
+
error_message = f"Path not found: {path}"
|
|
145
|
+
user_output(click.style("Error: ", fg="red") + error_message)
|
|
146
|
+
raise SystemExit(1)
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def not_empty(value: str | list | dict | None, error_message: str) -> None:
|
|
150
|
+
"""Ensure value is not empty (non-empty string, list, dict), otherwise exit.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
value: Value to check for emptiness
|
|
154
|
+
error_message: Error message to display if value is empty.
|
|
155
|
+
"Error: " prefix will be added automatically in red.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
SystemExit: If value is None, empty string, empty list, or empty dict
|
|
159
|
+
|
|
160
|
+
Example:
|
|
161
|
+
>>> Ensure.not_empty(name, "Worktree name cannot be empty")
|
|
162
|
+
>>> Ensure.not_empty(args, "No arguments provided - specify at least one branch")
|
|
163
|
+
"""
|
|
164
|
+
if not value:
|
|
165
|
+
user_output(click.style("Error: ", fg="red") + error_message)
|
|
166
|
+
raise SystemExit(1)
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def git_worktree_exists(ctx: ErkContext, wt_path: Path, name: str | None = None) -> None:
|
|
170
|
+
"""Ensure worktree exists at path, otherwise output styled error and exit.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
ctx: Application context with git integration
|
|
174
|
+
wt_path: Path where worktree should exist
|
|
175
|
+
name: Optional worktree name for friendlier error message
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
SystemExit: If worktree does not exist
|
|
179
|
+
|
|
180
|
+
Example:
|
|
181
|
+
>>> Ensure.git_worktree_exists(ctx, wt_path, "feature-123")
|
|
182
|
+
>>> Ensure.git_worktree_exists(ctx, wt_path) # Uses path in error
|
|
183
|
+
"""
|
|
184
|
+
if name:
|
|
185
|
+
error_message = f"Worktree '{name}' does not exist"
|
|
186
|
+
else:
|
|
187
|
+
error_message = f"Worktree not found: {wt_path}"
|
|
188
|
+
Ensure.path_exists(ctx, wt_path, error_message)
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def git_branch_exists(ctx: ErkContext, repo_root: Path, branch: str) -> None:
|
|
192
|
+
"""Ensure git branch exists, otherwise output styled error and exit.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
ctx: Application context with git integration
|
|
196
|
+
repo_root: Repository root path
|
|
197
|
+
branch: Branch name to check
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
SystemExit: If branch does not exist
|
|
201
|
+
|
|
202
|
+
Example:
|
|
203
|
+
>>> Ensure.git_branch_exists(ctx, repo.root, "feature-branch")
|
|
204
|
+
"""
|
|
205
|
+
local_branches = ctx.git.list_local_branches(repo_root)
|
|
206
|
+
if branch not in local_branches:
|
|
207
|
+
user_output(
|
|
208
|
+
click.style("Error: ", fg="red")
|
|
209
|
+
+ f"Branch '{branch}' does not exist - Create it first or check the name"
|
|
210
|
+
)
|
|
211
|
+
raise SystemExit(1)
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def in_git_worktree(ctx: ErkContext, current_path: Path | None) -> None:
|
|
215
|
+
"""Ensure currently in a git worktree, otherwise output styled error and exit.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
ctx: Application context (for error handling)
|
|
219
|
+
current_path: Path to check (typically ctx.cwd or result of get_worktree_path)
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
SystemExit: If not in a git worktree
|
|
223
|
+
|
|
224
|
+
Example:
|
|
225
|
+
>>> current_wt = ctx.git.get_worktree_path(repo.root, ctx.cwd)
|
|
226
|
+
>>> Ensure.in_git_worktree(ctx, current_wt)
|
|
227
|
+
"""
|
|
228
|
+
if current_path is None:
|
|
229
|
+
user_output(
|
|
230
|
+
click.style("Error: ", fg="red")
|
|
231
|
+
+ "Not in a git worktree - Run this command from within a worktree directory"
|
|
232
|
+
)
|
|
233
|
+
raise SystemExit(1)
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def argument_count(
|
|
237
|
+
args: tuple[Any, ...] | list[Any],
|
|
238
|
+
expected: int,
|
|
239
|
+
error_message: str | None = None,
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Ensure argument count matches expected, otherwise output styled error and exit.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
args: Arguments tuple or list to check
|
|
245
|
+
expected: Expected number of arguments
|
|
246
|
+
error_message: Optional custom error message
|
|
247
|
+
|
|
248
|
+
Raises:
|
|
249
|
+
SystemExit: If argument count does not match expected
|
|
250
|
+
|
|
251
|
+
Example:
|
|
252
|
+
>>> Ensure.argument_count(args, 1, "Expected exactly 1 branch name")
|
|
253
|
+
>>> Ensure.argument_count(args, 0, "This command takes no arguments")
|
|
254
|
+
"""
|
|
255
|
+
if len(args) != expected:
|
|
256
|
+
if error_message is None:
|
|
257
|
+
if expected == 0:
|
|
258
|
+
error_message = f"Expected no arguments, got {len(args)}"
|
|
259
|
+
elif expected == 1:
|
|
260
|
+
error_message = f"Expected 1 argument, got {len(args)}"
|
|
261
|
+
else:
|
|
262
|
+
error_message = f"Expected {expected} arguments, got {len(args)}"
|
|
263
|
+
user_output(click.style("Error: ", fg="red") + error_message)
|
|
264
|
+
raise SystemExit(1)
|
|
265
|
+
|
|
266
|
+
@staticmethod
|
|
267
|
+
def config_field_set(
|
|
268
|
+
config: Any,
|
|
269
|
+
field_name: str,
|
|
270
|
+
error_message: str | None = None,
|
|
271
|
+
) -> None:
|
|
272
|
+
"""Ensure configuration field is set, otherwise output styled error and exit.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
config: Configuration object (must have __getattr__ or __getitem__)
|
|
276
|
+
field_name: Name of the field to check
|
|
277
|
+
error_message: Optional custom error message
|
|
278
|
+
|
|
279
|
+
Raises:
|
|
280
|
+
SystemExit: If field is not set (None or missing)
|
|
281
|
+
|
|
282
|
+
Example:
|
|
283
|
+
>>> Ensure.config_field_set(
|
|
284
|
+
... ctx.local_config,
|
|
285
|
+
... "github_token",
|
|
286
|
+
... "GitHub token not configured - Run 'erk config set github_token <token>'"
|
|
287
|
+
... )
|
|
288
|
+
"""
|
|
289
|
+
try:
|
|
290
|
+
value = getattr(config, field_name, None)
|
|
291
|
+
except AttributeError:
|
|
292
|
+
try:
|
|
293
|
+
value = config[field_name] if hasattr(config, "__getitem__") else None
|
|
294
|
+
except (KeyError, TypeError):
|
|
295
|
+
value = None
|
|
296
|
+
|
|
297
|
+
if value is None:
|
|
298
|
+
if error_message is None:
|
|
299
|
+
error_message = (
|
|
300
|
+
f"Required configuration '{field_name}' not set - "
|
|
301
|
+
f"Run 'erk config set {field_name} <value>'"
|
|
302
|
+
)
|
|
303
|
+
user_output(click.style("Error: ", fg="red") + error_message)
|
|
304
|
+
raise SystemExit(1)
|
|
305
|
+
|
|
306
|
+
@staticmethod
|
|
307
|
+
def path_is_dir(ctx: ErkContext, path: Path, error_message: str | None = None) -> None:
|
|
308
|
+
"""Ensure path exists and is a directory, otherwise output styled error and exit.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
ctx: Application context with git integration
|
|
312
|
+
path: Path to check
|
|
313
|
+
error_message: Optional custom error message
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
SystemExit: If path doesn't exist or is not a directory
|
|
317
|
+
|
|
318
|
+
Example:
|
|
319
|
+
>>> Ensure.path_is_dir(ctx, repo.worktrees_dir, "Worktrees directory not found")
|
|
320
|
+
"""
|
|
321
|
+
Ensure.path_exists(ctx, path, error_message)
|
|
322
|
+
if not path.is_dir():
|
|
323
|
+
if error_message is None:
|
|
324
|
+
error_message = f"Path is not a directory: {path}"
|
|
325
|
+
user_output(click.style("Error: ", fg="red") + error_message)
|
|
326
|
+
raise SystemExit(1)
|
|
327
|
+
|
|
328
|
+
@staticmethod
|
|
329
|
+
def path_not_exists(ctx: ErkContext, path: Path, error_message: str) -> None:
|
|
330
|
+
"""Ensure path does NOT exist, otherwise output styled error and exit.
|
|
331
|
+
|
|
332
|
+
Inverse of path_exists - used when creating new resources that must not collide.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
ctx: Application context with git integration
|
|
336
|
+
path: Path to check should not exist
|
|
337
|
+
error_message: Error message to display if path exists
|
|
338
|
+
|
|
339
|
+
Raises:
|
|
340
|
+
SystemExit: If path already exists
|
|
341
|
+
|
|
342
|
+
Example:
|
|
343
|
+
>>> Ensure.path_not_exists(
|
|
344
|
+
... ctx,
|
|
345
|
+
... new_path,
|
|
346
|
+
... f"Destination already exists: {new_path} - "
|
|
347
|
+
... f"Choose a different name or delete the existing path"
|
|
348
|
+
... )
|
|
349
|
+
"""
|
|
350
|
+
if ctx.git.path_exists(path):
|
|
351
|
+
user_output(click.style("Error: ", fg="red") + error_message)
|
|
352
|
+
raise SystemExit(1)
|
|
353
|
+
|
|
354
|
+
@staticmethod
|
|
355
|
+
def gh_installed() -> None:
|
|
356
|
+
"""Ensure GitHub CLI (gh) is installed and available on PATH.
|
|
357
|
+
|
|
358
|
+
Uses shutil.which to check for gh availability, which is the LBYL
|
|
359
|
+
approach to validating external tool availability before use.
|
|
360
|
+
|
|
361
|
+
Raises:
|
|
362
|
+
SystemExit: If gh CLI is not found on PATH
|
|
363
|
+
|
|
364
|
+
Example:
|
|
365
|
+
>>> Ensure.gh_installed()
|
|
366
|
+
>>> # Now safe to call gh commands
|
|
367
|
+
>>> pr_info = ctx.github.get_pr_checkout_info(repo.root, pr_number)
|
|
368
|
+
"""
|
|
369
|
+
if shutil.which("gh") is None:
|
|
370
|
+
user_output(
|
|
371
|
+
click.style("Error: ", fg="red")
|
|
372
|
+
+ "GitHub CLI (gh) is not installed\n\n"
|
|
373
|
+
+ "Install it from: https://cli.github.com/\n"
|
|
374
|
+
+ "Then authenticate with: gh auth login"
|
|
375
|
+
)
|
|
376
|
+
raise SystemExit(1)
|
|
377
|
+
|
|
378
|
+
@staticmethod
|
|
379
|
+
def gt_installed() -> None:
|
|
380
|
+
"""Ensure Graphite CLI (gt) is installed and available on PATH.
|
|
381
|
+
|
|
382
|
+
Uses shutil.which to check for gt availability, which is the LBYL
|
|
383
|
+
approach to validating external tool availability before use.
|
|
384
|
+
|
|
385
|
+
Raises:
|
|
386
|
+
SystemExit: If gt CLI is not found on PATH
|
|
387
|
+
|
|
388
|
+
Example:
|
|
389
|
+
>>> Ensure.gt_installed()
|
|
390
|
+
>>> # Now safe to call gt commands
|
|
391
|
+
>>> ctx.graphite.submit_stack(repo.root)
|
|
392
|
+
"""
|
|
393
|
+
if shutil.which("gt") is None:
|
|
394
|
+
user_output(
|
|
395
|
+
click.style("Error: ", fg="red")
|
|
396
|
+
+ "Graphite CLI (gt) is not installed\n\n"
|
|
397
|
+
+ "Install it from: https://withgraphite.com/docs/getting-started\n"
|
|
398
|
+
+ "Or use: npm install -g @withgraphite/graphite-cli"
|
|
399
|
+
)
|
|
400
|
+
raise SystemExit(1)
|
|
401
|
+
|
|
402
|
+
@staticmethod
|
|
403
|
+
def graphite_available(ctx: ErkContext) -> None:
|
|
404
|
+
"""Ensure Graphite integration is available (enabled and installed).
|
|
405
|
+
|
|
406
|
+
Checks if ctx.graphite is a GraphiteDisabled sentinel, and if so,
|
|
407
|
+
outputs a helpful error message based on why Graphite is unavailable
|
|
408
|
+
(config disabled vs not installed).
|
|
409
|
+
|
|
410
|
+
This is the LBYL check for commands that require Graphite functionality.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
ctx: Application context with graphite integration
|
|
414
|
+
|
|
415
|
+
Raises:
|
|
416
|
+
SystemExit: If Graphite is disabled or not installed
|
|
417
|
+
|
|
418
|
+
Example:
|
|
419
|
+
>>> Ensure.graphite_available(ctx)
|
|
420
|
+
>>> # Now safe to use Graphite operations
|
|
421
|
+
>>> ctx.graphite.get_parent_branch(ctx.git, repo.root, branch)
|
|
422
|
+
"""
|
|
423
|
+
if isinstance(ctx.graphite, GraphiteDisabled):
|
|
424
|
+
error = GraphiteDisabledError(ctx.graphite.reason)
|
|
425
|
+
user_output(click.style("Error: ", fg="red") + str(error))
|
|
426
|
+
raise SystemExit(1)
|
|
427
|
+
|
|
428
|
+
@staticmethod
|
|
429
|
+
def claude_installed() -> None:
|
|
430
|
+
"""Ensure Claude CLI is installed and available on PATH.
|
|
431
|
+
|
|
432
|
+
Uses shutil.which to check for claude availability, which is the LBYL
|
|
433
|
+
approach to validating external tool availability before use.
|
|
434
|
+
|
|
435
|
+
Raises:
|
|
436
|
+
SystemExit: If claude CLI is not found on PATH
|
|
437
|
+
|
|
438
|
+
Example:
|
|
439
|
+
>>> Ensure.claude_installed()
|
|
440
|
+
>>> # Now safe to call claude commands
|
|
441
|
+
>>> ctx.shell.run_claude_extraction_plan(cwd)
|
|
442
|
+
"""
|
|
443
|
+
if shutil.which("claude") is None:
|
|
444
|
+
user_output(
|
|
445
|
+
click.style("Error: ", fg="red")
|
|
446
|
+
+ "Claude CLI is not installed\n\n"
|
|
447
|
+
+ "Install it from: https://claude.ai/download\n"
|
|
448
|
+
+ "Or skip extraction with: erk pr land --no-extract"
|
|
449
|
+
)
|
|
450
|
+
raise SystemExit(1)
|
|
451
|
+
|
|
452
|
+
@staticmethod
|
|
453
|
+
def gt_authenticated(ctx: ErkContext) -> None:
|
|
454
|
+
"""Ensure Graphite CLI (gt) is authenticated.
|
|
455
|
+
|
|
456
|
+
Uses LBYL pattern to check gt authentication status before operations
|
|
457
|
+
that require it (like gt submit).
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
ctx: Application context with graphite integration
|
|
461
|
+
|
|
462
|
+
Raises:
|
|
463
|
+
SystemExit: If gt is not authenticated
|
|
464
|
+
|
|
465
|
+
Example:
|
|
466
|
+
>>> Ensure.gt_authenticated(ctx)
|
|
467
|
+
>>> # Now safe to call gt submit
|
|
468
|
+
>>> ctx.graphite.submit_branch(repo.root, branch_name, quiet=True)
|
|
469
|
+
"""
|
|
470
|
+
is_authenticated, username, _ = ctx.graphite.check_auth_status()
|
|
471
|
+
|
|
472
|
+
if not is_authenticated:
|
|
473
|
+
user_output(
|
|
474
|
+
click.style("Error: ", fg="red")
|
|
475
|
+
+ "Graphite CLI (gt) is not authenticated\n\n"
|
|
476
|
+
+ "Authenticate with: gt auth\n\n"
|
|
477
|
+
+ "This is required before submitting branches or creating PRs."
|
|
478
|
+
)
|
|
479
|
+
raise SystemExit(1)
|
|
480
|
+
|
|
481
|
+
@staticmethod
|
|
482
|
+
def gh_authenticated(ctx: ErkContext) -> None:
|
|
483
|
+
"""Ensure GitHub CLI (gh) is installed and authenticated.
|
|
484
|
+
|
|
485
|
+
Uses LBYL pattern to check gh installation and authentication status
|
|
486
|
+
before operations that require it. This is the canonical check for
|
|
487
|
+
GitHub CLI readiness - callers should use this single method rather
|
|
488
|
+
than calling gh_installed() separately.
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
ctx: Application context with github integration
|
|
492
|
+
|
|
493
|
+
Raises:
|
|
494
|
+
SystemExit: If gh is not installed or not authenticated
|
|
495
|
+
|
|
496
|
+
Example:
|
|
497
|
+
>>> Ensure.gh_authenticated(ctx)
|
|
498
|
+
>>> # Now safe to call gh commands
|
|
499
|
+
>>> pr_info = ctx.github.get_pr_status(repo.root, branch)
|
|
500
|
+
"""
|
|
501
|
+
Ensure.gh_installed()
|
|
502
|
+
is_authenticated, username, _ = ctx.github.check_auth_status()
|
|
503
|
+
|
|
504
|
+
if not is_authenticated:
|
|
505
|
+
user_output(
|
|
506
|
+
click.style("Error: ", fg="red")
|
|
507
|
+
+ "GitHub CLI (gh) is not authenticated\n\n"
|
|
508
|
+
+ "Authenticate with: gh auth login\n\n"
|
|
509
|
+
+ "This is required before submitting branches or creating PRs."
|
|
510
|
+
)
|
|
511
|
+
raise SystemExit(1)
|
|
512
|
+
|
|
513
|
+
@staticmethod
|
|
514
|
+
def ideal_state(result: T | NonIdealState) -> T:
|
|
515
|
+
"""Ensure result is not a NonIdealState, otherwise exit with error.
|
|
516
|
+
|
|
517
|
+
This method provides type narrowing: it takes `T | NonIdealState` and
|
|
518
|
+
returns `T`, allowing the type checker to understand the value cannot
|
|
519
|
+
be a NonIdealState after this call.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
result: Value that may be a NonIdealState
|
|
523
|
+
|
|
524
|
+
Returns:
|
|
525
|
+
The value unchanged if not NonIdealState (with narrowed type T)
|
|
526
|
+
|
|
527
|
+
Raises:
|
|
528
|
+
SystemExit: If result is NonIdealState (with exit code 1)
|
|
529
|
+
|
|
530
|
+
Example:
|
|
531
|
+
>>> from erk_shared.non_ideal_state import GitHubChecks
|
|
532
|
+
>>> branch = Ensure.ideal_state(GitHubChecks.branch(raw_branch))
|
|
533
|
+
>>> # branch is now guaranteed to be str, not str | BranchDetectionFailed
|
|
534
|
+
"""
|
|
535
|
+
if isinstance(result, NonIdealState):
|
|
536
|
+
user_output(click.style("Error: ", fg="red") + result.message)
|
|
537
|
+
raise SystemExit(1)
|
|
538
|
+
return result
|
|
539
|
+
|
|
540
|
+
@staticmethod
|
|
541
|
+
def branch(result: str | BranchDetectionFailed) -> str:
|
|
542
|
+
"""Ensure branch detection succeeded.
|
|
543
|
+
|
|
544
|
+
Args:
|
|
545
|
+
result: Branch name or BranchDetectionFailed
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
The branch name
|
|
549
|
+
|
|
550
|
+
Raises:
|
|
551
|
+
SystemExit: If detection failed (with exit code 1)
|
|
552
|
+
"""
|
|
553
|
+
if isinstance(result, BranchDetectionFailed):
|
|
554
|
+
user_output(click.style("Error: ", fg="red") + result.message)
|
|
555
|
+
raise SystemExit(1)
|
|
556
|
+
return result
|
|
557
|
+
|
|
558
|
+
@staticmethod
|
|
559
|
+
def pr(result: PRDetails | NoPRForBranch | PRNotFoundError) -> PRDetails:
|
|
560
|
+
"""Ensure PR lookup succeeded.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
result: PRDetails or a not-found error
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
The PRDetails
|
|
567
|
+
|
|
568
|
+
Raises:
|
|
569
|
+
SystemExit: If PR not found (with exit code 1)
|
|
570
|
+
"""
|
|
571
|
+
if isinstance(result, (NoPRForBranch, PRNotFoundError)):
|
|
572
|
+
user_output(click.style("Error: ", fg="red") + result.message)
|
|
573
|
+
raise SystemExit(1)
|
|
574
|
+
return result
|
|
575
|
+
|
|
576
|
+
@staticmethod
|
|
577
|
+
def comments(result: list | GitHubAPIFailed) -> list:
|
|
578
|
+
"""Ensure comments fetch succeeded.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
result: List of comments or GitHubAPIFailed
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
The list of comments
|
|
585
|
+
|
|
586
|
+
Raises:
|
|
587
|
+
SystemExit: If API call failed (with exit code 1)
|
|
588
|
+
"""
|
|
589
|
+
if isinstance(result, GitHubAPIFailed):
|
|
590
|
+
user_output(click.style("Error: ", fg="red") + result.message)
|
|
591
|
+
raise SystemExit(1)
|
|
592
|
+
return result
|
|
593
|
+
|
|
594
|
+
@staticmethod
|
|
595
|
+
def void_op(result: None | GitHubAPIFailed) -> None:
|
|
596
|
+
"""Ensure void operation succeeded.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
result: None (success) or GitHubAPIFailed
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
None
|
|
603
|
+
|
|
604
|
+
Raises:
|
|
605
|
+
SystemExit: If API call failed (with exit code 1)
|
|
606
|
+
"""
|
|
607
|
+
if isinstance(result, GitHubAPIFailed):
|
|
608
|
+
user_output(click.style("Error: ", fg="red") + result.message)
|
|
609
|
+
raise SystemExit(1)
|
|
610
|
+
return result
|
|
611
|
+
|
|
612
|
+
@staticmethod
|
|
613
|
+
def session(result: T | SessionNotFound) -> T:
|
|
614
|
+
"""Ensure session lookup succeeded.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
result: Session or SessionNotFound sentinel
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
The Session
|
|
621
|
+
|
|
622
|
+
Raises:
|
|
623
|
+
SystemExit: If session not found (with exit code 1)
|
|
624
|
+
"""
|
|
625
|
+
if isinstance(result, SessionNotFound):
|
|
626
|
+
user_output(click.style("Error: ", fg="red") + result.message)
|
|
627
|
+
raise SystemExit(1)
|
|
628
|
+
return result
|