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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""CLI-level wrappers for GitHub URL parsing with error handling.
|
|
2
|
+
|
|
3
|
+
This module provides CLI-friendly wrappers around the shared parsing functions
|
|
4
|
+
in erk_shared.github.parsing. These wrappers handle user input (not just URLs)
|
|
5
|
+
and raise SystemExit(1) with appropriate error messages for invalid input.
|
|
6
|
+
|
|
7
|
+
Two-layer architecture:
|
|
8
|
+
- erk_shared.github.parsing: Pure parsing functions, return None on failure (LBYL-friendly)
|
|
9
|
+
- src/erk/cli/github_parsing.py: CLI wrappers that raise SystemExit(1)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from erk_shared.github.parsing import (
|
|
15
|
+
parse_issue_number_from_url,
|
|
16
|
+
parse_pr_number_from_url,
|
|
17
|
+
)
|
|
18
|
+
from erk_shared.output.output import user_output
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_issue_identifier(identifier: str) -> int:
|
|
22
|
+
"""Parse issue number from plain number or GitHub issue URL.
|
|
23
|
+
|
|
24
|
+
This is a CLI-level function that handles user input.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
identifier: Plain number ("42") or GitHub issue URL
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Issue number as int
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
SystemExit: If identifier cannot be parsed
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
>>> parse_issue_identifier("42")
|
|
37
|
+
42
|
|
38
|
+
>>> parse_issue_identifier("https://github.com/owner/repo/issues/123")
|
|
39
|
+
123
|
|
40
|
+
"""
|
|
41
|
+
# Plain number (handles leading zeros like "0042" -> 42)
|
|
42
|
+
if identifier.isdigit():
|
|
43
|
+
return int(identifier)
|
|
44
|
+
|
|
45
|
+
# GitHub URL
|
|
46
|
+
issue_number = parse_issue_number_from_url(identifier)
|
|
47
|
+
if issue_number is not None:
|
|
48
|
+
return issue_number
|
|
49
|
+
|
|
50
|
+
user_output(
|
|
51
|
+
click.style("Error: ", fg="red")
|
|
52
|
+
+ f"Invalid issue number or URL: {identifier}\n\n"
|
|
53
|
+
+ "Expected formats:\n"
|
|
54
|
+
+ " • Plain number: 123\n"
|
|
55
|
+
+ " • GitHub URL: https://github.com/owner/repo/issues/456"
|
|
56
|
+
)
|
|
57
|
+
raise SystemExit(1)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def parse_pr_identifier(identifier: str) -> int:
|
|
61
|
+
"""Parse PR number from plain number or GitHub PR URL.
|
|
62
|
+
|
|
63
|
+
This is a CLI-level function that handles user input.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
identifier: Plain number ("42") or GitHub PR URL
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
PR number as int
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
SystemExit: If identifier cannot be parsed
|
|
73
|
+
|
|
74
|
+
Examples:
|
|
75
|
+
>>> parse_pr_identifier("42")
|
|
76
|
+
42
|
|
77
|
+
>>> parse_pr_identifier("https://github.com/owner/repo/pull/123")
|
|
78
|
+
123
|
|
79
|
+
"""
|
|
80
|
+
# Plain number (handles leading zeros like "0042" -> 42)
|
|
81
|
+
if identifier.isdigit():
|
|
82
|
+
return int(identifier)
|
|
83
|
+
|
|
84
|
+
# Try strict github.com /pull/ URL only
|
|
85
|
+
pr_number = parse_pr_number_from_url(identifier)
|
|
86
|
+
if pr_number is not None:
|
|
87
|
+
return pr_number
|
|
88
|
+
|
|
89
|
+
user_output(
|
|
90
|
+
click.style("Error: ", fg="red")
|
|
91
|
+
+ f"Invalid PR number or URL: {identifier}\n\n"
|
|
92
|
+
+ "Expected formats:\n"
|
|
93
|
+
+ " • Plain number: 123\n"
|
|
94
|
+
+ " • GitHub URL: https://github.com/owner/repo/pull/456"
|
|
95
|
+
)
|
|
96
|
+
raise SystemExit(1)
|
erk/cli/graphite.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Graphite integration for erk.
|
|
2
|
+
|
|
3
|
+
Graphite (https://graphite.com) is a stacked git workflow tool that allows developers
|
|
4
|
+
to manage dependent branches in linear stacks. This module provides utility functions
|
|
5
|
+
for working with worktree stacks.
|
|
6
|
+
|
|
7
|
+
For comprehensive gt mental model and command reference, see:
|
|
8
|
+
.agent/GT_MENTAL_MODEL.md
|
|
9
|
+
|
|
10
|
+
## What is Graphite?
|
|
11
|
+
|
|
12
|
+
Graphite organizes branches into "stacks" - linear chains of dependent branches built
|
|
13
|
+
on top of each other. For example:
|
|
14
|
+
|
|
15
|
+
main (trunk)
|
|
16
|
+
└─ feature/phase-1
|
|
17
|
+
└─ feature/phase-2
|
|
18
|
+
└─ feature/phase-3
|
|
19
|
+
|
|
20
|
+
Each branch in the stack depends on its parent, making it easy to work on multiple
|
|
21
|
+
related changes while keeping them in separate PRs.
|
|
22
|
+
|
|
23
|
+
## Graphite Abstraction
|
|
24
|
+
|
|
25
|
+
This module uses the Graphite abstraction to read Graphite cache data. Production
|
|
26
|
+
code should use ctx.graphite_ops methods directly instead of importing functions from
|
|
27
|
+
this module.
|
|
28
|
+
|
|
29
|
+
See erk.core.graphite_ops for the abstraction interface.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
from erk.core.context import ErkContext
|
|
35
|
+
from erk_shared.git.abc import WorktreeInfo
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def find_worktrees_containing_branch(
|
|
39
|
+
ctx: ErkContext,
|
|
40
|
+
repo_root: Path,
|
|
41
|
+
worktrees: list[WorktreeInfo],
|
|
42
|
+
target_branch: str,
|
|
43
|
+
) -> list[WorktreeInfo]:
|
|
44
|
+
"""Find all worktrees that have target_branch checked out (exact match only).
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
ctx: Erk context with git operations
|
|
48
|
+
repo_root: Path to the repository root
|
|
49
|
+
worktrees: List of all worktrees from list_worktrees()
|
|
50
|
+
target_branch: Branch name to search for
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List of WorktreeInfo objects where target_branch is checked out.
|
|
54
|
+
Empty list if no worktrees have the branch checked out.
|
|
55
|
+
|
|
56
|
+
Algorithm:
|
|
57
|
+
1. For each worktree:
|
|
58
|
+
a. Get the worktree's checked-out branch
|
|
59
|
+
b. Skip worktrees with detached HEAD (branch=None)
|
|
60
|
+
c. Check if worktree.branch == target_branch (exact string match)
|
|
61
|
+
d. If yes, add worktree to results
|
|
62
|
+
2. Return all matching worktrees
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
>>> worktrees = ctx.git_ops.list_worktrees(repo.root)
|
|
66
|
+
>>> matching = find_worktrees_containing_branch(ctx, repo.root, worktrees, "feature-2")
|
|
67
|
+
>>> print([wt.path for wt in matching])
|
|
68
|
+
[Path("/path/to/work/feature-work")]
|
|
69
|
+
"""
|
|
70
|
+
matching_worktrees: list[WorktreeInfo] = []
|
|
71
|
+
|
|
72
|
+
for wt in worktrees:
|
|
73
|
+
# Skip worktrees with detached HEAD
|
|
74
|
+
if wt.branch is None:
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
# Check if target_branch is exactly checked out in this worktree
|
|
78
|
+
if wt.branch == target_branch:
|
|
79
|
+
matching_worktrees.append(wt)
|
|
80
|
+
|
|
81
|
+
return matching_worktrees
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Custom Click command classes that require Graphite integration.
|
|
2
|
+
|
|
3
|
+
This module provides declarative command classes that automatically:
|
|
4
|
+
1. Check Graphite availability before command execution
|
|
5
|
+
2. Are hidden from help output when Graphite is unavailable
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
@click.command("list", cls=GraphiteCommand)
|
|
9
|
+
def list_stack(ctx: ErkContext) -> None:
|
|
10
|
+
# No need for Ensure.graphite_available(ctx) - handled by GraphiteCommand
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
@click.command("up", cls=GraphiteCommandWithHiddenOptions)
|
|
14
|
+
@script_option
|
|
15
|
+
def up_cmd(ctx: ErkContext, script: bool) -> None:
|
|
16
|
+
# Combines Graphite check with hidden options support
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
@click.group("stack", cls=GraphiteGroup)
|
|
20
|
+
def stack_group() -> None:
|
|
21
|
+
# Entire group hidden when Graphite unavailable
|
|
22
|
+
...
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
import click
|
|
28
|
+
|
|
29
|
+
from erk.cli.ensure import Ensure
|
|
30
|
+
from erk.cli.help_formatter import CommandWithHiddenOptions
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GraphiteCommand(click.Command):
|
|
34
|
+
"""Command that requires Graphite integration.
|
|
35
|
+
|
|
36
|
+
Automatically checks Graphite availability before command execution.
|
|
37
|
+
When Graphite is unavailable, this command is hidden from help output
|
|
38
|
+
but can still be invoked directly (failing with a helpful error message).
|
|
39
|
+
|
|
40
|
+
Use this class for commands that depend on Graphite functionality
|
|
41
|
+
but don't need hidden options support.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def invoke(self, ctx: click.Context) -> Any:
|
|
45
|
+
"""Invoke command after validating Graphite availability."""
|
|
46
|
+
if ctx.obj is not None:
|
|
47
|
+
Ensure.graphite_available(ctx.obj)
|
|
48
|
+
return super().invoke(ctx)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class GraphiteCommandWithHiddenOptions(CommandWithHiddenOptions):
|
|
52
|
+
"""GraphiteCommand + hidden options support.
|
|
53
|
+
|
|
54
|
+
Combines the Graphite availability check from GraphiteCommand
|
|
55
|
+
with the hidden options formatting from CommandWithHiddenOptions.
|
|
56
|
+
|
|
57
|
+
Use this class for commands that:
|
|
58
|
+
1. Require Graphite functionality
|
|
59
|
+
2. Have hidden options (like --script)
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def invoke(self, ctx: click.Context) -> Any:
|
|
63
|
+
"""Invoke command after validating Graphite availability."""
|
|
64
|
+
if ctx.obj is not None:
|
|
65
|
+
Ensure.graphite_available(ctx.obj)
|
|
66
|
+
return super().invoke(ctx)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class GraphiteGroup(click.Group):
|
|
70
|
+
"""Group that requires Graphite integration.
|
|
71
|
+
|
|
72
|
+
When used with cls=GraphiteGroup, the entire command group is hidden
|
|
73
|
+
from help output when Graphite is unavailable. Commands within the
|
|
74
|
+
group can still be invoked directly and will fail with helpful error
|
|
75
|
+
messages via their own GraphiteCommand classes.
|
|
76
|
+
|
|
77
|
+
The hiding logic is implemented in ErkCommandGroup.format_commands().
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
pass
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""Custom Click help formatter for organized command display."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any, TypeVar, cast
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from erk.cli.alias import get_aliases
|
|
10
|
+
from erk_shared.gateway.erk_installation.real import RealErkInstallation
|
|
11
|
+
from erk_shared.gateway.graphite.disabled import GraphiteDisabled
|
|
12
|
+
|
|
13
|
+
F = TypeVar("F", bound=Callable[..., object])
|
|
14
|
+
|
|
15
|
+
# Type names for Graphite-requiring commands (checked by string to avoid circular imports)
|
|
16
|
+
_GRAPHITE_COMMAND_TYPES = frozenset(
|
|
17
|
+
{
|
|
18
|
+
"GraphiteCommand",
|
|
19
|
+
"GraphiteCommandWithHiddenOptions",
|
|
20
|
+
"GraphiteGroup",
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _requires_graphite(cmd: click.Command) -> bool:
|
|
26
|
+
"""Check if a command requires Graphite integration.
|
|
27
|
+
|
|
28
|
+
Uses class name matching to avoid circular imports with graphite_command.py.
|
|
29
|
+
"""
|
|
30
|
+
return type(cmd).__name__ in _GRAPHITE_COMMAND_TYPES
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_show_hidden_from_context(ctx: click.Context) -> bool:
|
|
34
|
+
"""Check if hidden items should be shown based on config.
|
|
35
|
+
|
|
36
|
+
Checks ctx.obj.global_config if available (tests),
|
|
37
|
+
otherwise loads config from disk (direct CLI invocation).
|
|
38
|
+
"""
|
|
39
|
+
if ctx.obj is not None:
|
|
40
|
+
config = getattr(ctx.obj, "global_config", None)
|
|
41
|
+
if config is not None:
|
|
42
|
+
return bool(getattr(config, "show_hidden_commands", False))
|
|
43
|
+
# Fallback to loading from disk
|
|
44
|
+
installation = RealErkInstallation()
|
|
45
|
+
if installation.config_exists():
|
|
46
|
+
return installation.load_config().show_hidden_commands
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _set_param_hidden(param: click.Parameter, hidden: bool) -> None:
|
|
51
|
+
"""Set hidden attribute on Click parameter.
|
|
52
|
+
|
|
53
|
+
Click's Option class has a 'hidden' attribute, but Parameter (the base class)
|
|
54
|
+
doesn't expose it in type stubs. We use cast(Any, ...) since we've already
|
|
55
|
+
verified via getattr that this parameter has the 'hidden' attribute.
|
|
56
|
+
"""
|
|
57
|
+
cast(Any, param).hidden = hidden
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _set_ctx_show_hidden(ctx: click.Context, value: bool) -> None:
|
|
61
|
+
"""Set show_hidden attribute on Click context.
|
|
62
|
+
|
|
63
|
+
Click's Context allows dynamic attributes at runtime (documented API behavior).
|
|
64
|
+
We use cast(Any, ...) to bypass type stubs that don't include dynamic attrs.
|
|
65
|
+
"""
|
|
66
|
+
cast(Any, ctx).show_hidden = value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_graphite_available(ctx: click.Context) -> bool:
|
|
70
|
+
"""Check if Graphite is available for command visibility.
|
|
71
|
+
|
|
72
|
+
Checks ctx.obj.graphite if available (tests or after callback),
|
|
73
|
+
otherwise loads config from disk and checks gt binary (help before callback).
|
|
74
|
+
"""
|
|
75
|
+
if ctx.obj is not None:
|
|
76
|
+
return not isinstance(ctx.obj.graphite, GraphiteDisabled)
|
|
77
|
+
# Fallback to loading from disk (for help before callback runs)
|
|
78
|
+
installation = RealErkInstallation()
|
|
79
|
+
if installation.config_exists():
|
|
80
|
+
config = installation.load_config()
|
|
81
|
+
if config.use_graphite:
|
|
82
|
+
# Config says use Graphite - check if gt is installed
|
|
83
|
+
return shutil.which("gt") is not None
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class CommandWithHiddenOptions(click.Command):
|
|
88
|
+
"""Command that respects show_hidden_commands config for hidden options.
|
|
89
|
+
|
|
90
|
+
Use this class for any command with hidden options (like --script).
|
|
91
|
+
Hidden options are shown in a separate "Hidden Options" section when
|
|
92
|
+
show_hidden_commands is enabled in config.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def format_options(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
96
|
+
"""Format options, showing hidden ones if config allows."""
|
|
97
|
+
show_hidden = _get_show_hidden_from_context(ctx)
|
|
98
|
+
|
|
99
|
+
opts = []
|
|
100
|
+
hidden_opts = []
|
|
101
|
+
for param in self.get_params(ctx):
|
|
102
|
+
# Use getattr since only Option has 'hidden', not all Parameter types
|
|
103
|
+
is_hidden = getattr(param, "hidden", False)
|
|
104
|
+
|
|
105
|
+
if is_hidden:
|
|
106
|
+
if show_hidden:
|
|
107
|
+
# Temporarily unhide to get help record (Click returns None for hidden)
|
|
108
|
+
_set_param_hidden(param, hidden=False)
|
|
109
|
+
rv = param.get_help_record(ctx)
|
|
110
|
+
_set_param_hidden(param, hidden=True)
|
|
111
|
+
if rv is not None:
|
|
112
|
+
hidden_opts.append(rv)
|
|
113
|
+
else:
|
|
114
|
+
rv = param.get_help_record(ctx)
|
|
115
|
+
if rv is not None:
|
|
116
|
+
opts.append(rv)
|
|
117
|
+
|
|
118
|
+
if opts:
|
|
119
|
+
with formatter.section("Options"):
|
|
120
|
+
formatter.write_dl(opts)
|
|
121
|
+
|
|
122
|
+
if hidden_opts:
|
|
123
|
+
with formatter.section("Hidden Options"):
|
|
124
|
+
formatter.write_dl(hidden_opts)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def script_option(fn: F) -> F:
|
|
128
|
+
"""Decorator that adds --script option with proper settings.
|
|
129
|
+
|
|
130
|
+
Must be applied to a function decorated with @click.command(cls=CommandWithHiddenOptions).
|
|
131
|
+
The --script flag is hidden by default but visible when show_hidden_commands=True.
|
|
132
|
+
|
|
133
|
+
Example:
|
|
134
|
+
@click.command("up", cls=CommandWithHiddenOptions)
|
|
135
|
+
@script_option
|
|
136
|
+
def up_cmd(ctx: ErkContext, script: bool) -> None:
|
|
137
|
+
...
|
|
138
|
+
"""
|
|
139
|
+
return click.option(
|
|
140
|
+
"--script",
|
|
141
|
+
is_flag=True,
|
|
142
|
+
hidden=True,
|
|
143
|
+
help="Output shell script for integration. NOT a dry run.",
|
|
144
|
+
)(fn)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class ErkCommandGroup(click.Group):
|
|
148
|
+
"""Click Group that organizes commands into logical sections in help output.
|
|
149
|
+
|
|
150
|
+
Commands are organized into sections based on their usage patterns:
|
|
151
|
+
- Core Navigation: Primary workflow commands
|
|
152
|
+
- Command Groups: Organized subcommands
|
|
153
|
+
- Quick Access: Backward compatibility aliases
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
grouped: If True, organize commands into sections. If False, show flat list.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def __init__(self, grouped: bool = True, **kwargs: object) -> None:
|
|
160
|
+
super().__init__(**cast(dict[str, Any], kwargs))
|
|
161
|
+
self.grouped = grouped
|
|
162
|
+
|
|
163
|
+
def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
164
|
+
"""Format help output, setting show_hidden based on config first.
|
|
165
|
+
|
|
166
|
+
This hook runs after context creation but before format_commands,
|
|
167
|
+
allowing us to set ctx.show_hidden based on the global config.
|
|
168
|
+
"""
|
|
169
|
+
# Set show_hidden based on config before formatting help
|
|
170
|
+
self._set_show_hidden_from_context(ctx)
|
|
171
|
+
|
|
172
|
+
# Call parent to format help (which will call format_commands)
|
|
173
|
+
super().format_help(ctx, formatter)
|
|
174
|
+
|
|
175
|
+
def _set_show_hidden_from_context(self, ctx: click.Context) -> None:
|
|
176
|
+
"""Set ctx.show_hidden based on config.
|
|
177
|
+
|
|
178
|
+
Checks ctx.obj.global_config if available (tests),
|
|
179
|
+
otherwise loads config from disk (direct CLI invocation).
|
|
180
|
+
"""
|
|
181
|
+
# If ctx.obj is provided (tests or already-created context), use its config
|
|
182
|
+
if ctx.obj is not None:
|
|
183
|
+
config = getattr(ctx.obj, "global_config", None)
|
|
184
|
+
if config is not None and getattr(config, "show_hidden_commands", False):
|
|
185
|
+
_set_ctx_show_hidden(ctx, value=True)
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
# Otherwise try to load config directly from disk
|
|
189
|
+
installation = RealErkInstallation()
|
|
190
|
+
if installation.config_exists():
|
|
191
|
+
config = installation.load_config()
|
|
192
|
+
if config.show_hidden_commands:
|
|
193
|
+
_set_ctx_show_hidden(ctx, value=True)
|
|
194
|
+
|
|
195
|
+
def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
196
|
+
"""Format commands into organized sections or flat list."""
|
|
197
|
+
show_hidden = getattr(ctx, "show_hidden", False)
|
|
198
|
+
|
|
199
|
+
# Check if Graphite is available (for hiding Graphite-dependent commands)
|
|
200
|
+
graphite_available = _is_graphite_available(ctx)
|
|
201
|
+
|
|
202
|
+
commands = []
|
|
203
|
+
hidden_commands = []
|
|
204
|
+
# Build alias map: alias_name -> primary_name
|
|
205
|
+
alias_map: dict[str, str] = {}
|
|
206
|
+
|
|
207
|
+
for subcommand in self.list_commands(ctx):
|
|
208
|
+
cmd = self.get_command(ctx, subcommand)
|
|
209
|
+
if cmd is None:
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
# Build alias map from decorator-declared aliases
|
|
213
|
+
for alias_name in get_aliases(cmd):
|
|
214
|
+
alias_map[alias_name] = subcommand
|
|
215
|
+
|
|
216
|
+
# Commands are effectively hidden if:
|
|
217
|
+
# 1. They have hidden=True, OR
|
|
218
|
+
# 2. They require Graphite and Graphite is unavailable
|
|
219
|
+
effectively_hidden = cmd.hidden or (_requires_graphite(cmd) and not graphite_available)
|
|
220
|
+
|
|
221
|
+
if effectively_hidden:
|
|
222
|
+
if show_hidden:
|
|
223
|
+
hidden_commands.append((subcommand, cmd))
|
|
224
|
+
continue
|
|
225
|
+
commands.append((subcommand, cmd))
|
|
226
|
+
|
|
227
|
+
if not commands:
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
# Flat output mode - single "Commands:" section
|
|
231
|
+
if not self.grouped:
|
|
232
|
+
# Filter out aliases (they'll be shown with their primary command)
|
|
233
|
+
primary_commands = [(n, c) for n, c in commands if n not in alias_map]
|
|
234
|
+
with formatter.section("Commands"):
|
|
235
|
+
self._format_command_list(ctx, formatter, primary_commands)
|
|
236
|
+
|
|
237
|
+
if hidden_commands:
|
|
238
|
+
with formatter.section("Hidden"):
|
|
239
|
+
self._format_command_list(ctx, formatter, hidden_commands)
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
# Grouped output mode - organize into sections
|
|
243
|
+
# Define command organization (aliases now derived from decorator, not hardcoded)
|
|
244
|
+
top_level_commands = [
|
|
245
|
+
"checkout",
|
|
246
|
+
"dash",
|
|
247
|
+
"delete",
|
|
248
|
+
"doctor",
|
|
249
|
+
"down",
|
|
250
|
+
"implement",
|
|
251
|
+
"land",
|
|
252
|
+
"list",
|
|
253
|
+
"up",
|
|
254
|
+
"upgrade",
|
|
255
|
+
]
|
|
256
|
+
command_groups = [
|
|
257
|
+
"admin",
|
|
258
|
+
"artifact",
|
|
259
|
+
"branch",
|
|
260
|
+
"cc",
|
|
261
|
+
"completion",
|
|
262
|
+
"config",
|
|
263
|
+
"docs",
|
|
264
|
+
"hook",
|
|
265
|
+
"info",
|
|
266
|
+
"md",
|
|
267
|
+
"objective",
|
|
268
|
+
"plan",
|
|
269
|
+
"planner",
|
|
270
|
+
"pr",
|
|
271
|
+
"project",
|
|
272
|
+
"run",
|
|
273
|
+
"slot",
|
|
274
|
+
"stack",
|
|
275
|
+
"wt",
|
|
276
|
+
]
|
|
277
|
+
initialization = ["init"]
|
|
278
|
+
|
|
279
|
+
# Categorize commands
|
|
280
|
+
top_level_cmds = []
|
|
281
|
+
group_cmds = []
|
|
282
|
+
init_cmds = []
|
|
283
|
+
other_cmds = []
|
|
284
|
+
|
|
285
|
+
for name, cmd in commands:
|
|
286
|
+
# Skip aliases (they'll be shown with their primary command)
|
|
287
|
+
if name in alias_map:
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
if name in top_level_commands:
|
|
291
|
+
top_level_cmds.append((name, cmd))
|
|
292
|
+
elif name in command_groups:
|
|
293
|
+
group_cmds.append((name, cmd))
|
|
294
|
+
elif name in initialization:
|
|
295
|
+
init_cmds.append((name, cmd))
|
|
296
|
+
else:
|
|
297
|
+
# Other commands
|
|
298
|
+
other_cmds.append((name, cmd))
|
|
299
|
+
|
|
300
|
+
# Format sections
|
|
301
|
+
if top_level_cmds:
|
|
302
|
+
with formatter.section("Top-Level Commands"):
|
|
303
|
+
self._format_command_list(ctx, formatter, top_level_cmds)
|
|
304
|
+
|
|
305
|
+
if group_cmds:
|
|
306
|
+
with formatter.section("Command Groups"):
|
|
307
|
+
self._format_command_list(ctx, formatter, group_cmds)
|
|
308
|
+
|
|
309
|
+
if init_cmds:
|
|
310
|
+
with formatter.section("Initialization"):
|
|
311
|
+
self._format_command_list(ctx, formatter, init_cmds)
|
|
312
|
+
|
|
313
|
+
if other_cmds:
|
|
314
|
+
with formatter.section("Other"):
|
|
315
|
+
self._format_command_list(ctx, formatter, other_cmds)
|
|
316
|
+
|
|
317
|
+
if hidden_commands:
|
|
318
|
+
with formatter.section("Hidden"):
|
|
319
|
+
self._format_command_list(ctx, formatter, hidden_commands)
|
|
320
|
+
|
|
321
|
+
def _format_command_list(
|
|
322
|
+
self,
|
|
323
|
+
ctx: click.Context,
|
|
324
|
+
formatter: click.HelpFormatter,
|
|
325
|
+
commands: list[tuple[str, click.Command]],
|
|
326
|
+
) -> None:
|
|
327
|
+
"""Format a list of commands with their help text.
|
|
328
|
+
|
|
329
|
+
Commands with aliases (declared via @alias decorator) are displayed
|
|
330
|
+
as 'checkout (co)'.
|
|
331
|
+
"""
|
|
332
|
+
rows = []
|
|
333
|
+
for name, cmd in commands:
|
|
334
|
+
# Get aliases for this command and format display name
|
|
335
|
+
aliases = get_aliases(cmd)
|
|
336
|
+
if aliases:
|
|
337
|
+
display_name = f"{name} ({', '.join(aliases)})"
|
|
338
|
+
else:
|
|
339
|
+
display_name = name
|
|
340
|
+
|
|
341
|
+
help_text = cmd.get_short_help_str(limit=formatter.width)
|
|
342
|
+
rows.append((display_name, help_text))
|
|
343
|
+
|
|
344
|
+
if rows:
|
|
345
|
+
formatter.write_dl(rows)
|