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,148 @@
|
|
|
1
|
+
"""Create a new planner codespace."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from erk.core.context import ErkContext
|
|
9
|
+
from erk.core.planner.types import RegisteredPlanner
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _find_codespace_by_display_name(display_name: str) -> dict | None:
|
|
13
|
+
"""Find a codespace by its display name."""
|
|
14
|
+
# GH-API-AUDIT: REST - GET user/codespaces
|
|
15
|
+
result = subprocess.run(
|
|
16
|
+
["gh", "codespace", "list", "--json", "name,repository,displayName"],
|
|
17
|
+
capture_output=True,
|
|
18
|
+
text=True,
|
|
19
|
+
check=False,
|
|
20
|
+
)
|
|
21
|
+
if result.returncode != 0:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
content = result.stdout.strip()
|
|
25
|
+
if not content:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
codespaces = json.loads(content)
|
|
30
|
+
for cs in codespaces:
|
|
31
|
+
if cs.get("displayName") == display_name:
|
|
32
|
+
return cs
|
|
33
|
+
return None
|
|
34
|
+
except json.JSONDecodeError:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@click.command("create")
|
|
39
|
+
@click.argument("name", default="erk-planner-node")
|
|
40
|
+
@click.option(
|
|
41
|
+
"-r",
|
|
42
|
+
"--repo",
|
|
43
|
+
default=None,
|
|
44
|
+
help="Repository to create codespace from (owner/repo). Defaults to current repo.",
|
|
45
|
+
)
|
|
46
|
+
@click.option(
|
|
47
|
+
"-b",
|
|
48
|
+
"--branch",
|
|
49
|
+
default=None,
|
|
50
|
+
help="Branch to create codespace from. Defaults to default branch.",
|
|
51
|
+
)
|
|
52
|
+
@click.option(
|
|
53
|
+
"--run/--dry-run",
|
|
54
|
+
default=False,
|
|
55
|
+
help="Actually run the command (default: just print it).",
|
|
56
|
+
)
|
|
57
|
+
@click.pass_obj
|
|
58
|
+
def create_planner(
|
|
59
|
+
ctx: ErkContext,
|
|
60
|
+
name: str,
|
|
61
|
+
repo: str | None,
|
|
62
|
+
branch: str | None,
|
|
63
|
+
run: bool,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Create a new GitHub Codespace for use as a planner.
|
|
66
|
+
|
|
67
|
+
Creates a codespace with the right devcontainer configuration and
|
|
68
|
+
automatically registers it as a planner.
|
|
69
|
+
|
|
70
|
+
The codespace will be created with:
|
|
71
|
+
- GitHub CLI pre-installed
|
|
72
|
+
- Claude Code pre-installed
|
|
73
|
+
- uv and project dependencies
|
|
74
|
+
|
|
75
|
+
After creation, run 'erk planner configure NAME' to set up authentication.
|
|
76
|
+
"""
|
|
77
|
+
# Check if name already exists
|
|
78
|
+
existing = ctx.planner_registry.get(name)
|
|
79
|
+
if existing is not None:
|
|
80
|
+
click.echo(f"Error: A planner named '{name}' already exists.", err=True)
|
|
81
|
+
raise SystemExit(1)
|
|
82
|
+
|
|
83
|
+
# GH-API-AUDIT: REST - POST user/codespaces
|
|
84
|
+
cmd = ["gh", "codespace", "create"]
|
|
85
|
+
|
|
86
|
+
if repo:
|
|
87
|
+
cmd.extend(["--repo", repo])
|
|
88
|
+
|
|
89
|
+
if branch:
|
|
90
|
+
cmd.extend(["--branch", branch])
|
|
91
|
+
|
|
92
|
+
cmd.extend(["--display-name", name])
|
|
93
|
+
cmd.extend(["--devcontainer-path", ".devcontainer/devcontainer.json"])
|
|
94
|
+
|
|
95
|
+
if run:
|
|
96
|
+
click.echo(f"Creating codespace '{name}'...", err=True)
|
|
97
|
+
click.echo(f"Running: {' '.join(cmd)}", err=True)
|
|
98
|
+
click.echo("", err=True)
|
|
99
|
+
|
|
100
|
+
result = subprocess.run(cmd, check=False)
|
|
101
|
+
|
|
102
|
+
if result.returncode != 0:
|
|
103
|
+
click.echo(f"\nCodespace creation failed (exit code {result.returncode}).", err=True)
|
|
104
|
+
raise SystemExit(1)
|
|
105
|
+
|
|
106
|
+
# Find and register the created codespace
|
|
107
|
+
click.echo("", err=True)
|
|
108
|
+
click.echo("Looking up created codespace...", err=True)
|
|
109
|
+
|
|
110
|
+
codespace = _find_codespace_by_display_name(name)
|
|
111
|
+
if codespace is None:
|
|
112
|
+
click.echo(f"Warning: Could not find codespace '{name}' to register.", err=True)
|
|
113
|
+
click.echo(f"Run 'erk planner register {name}' manually.", err=True)
|
|
114
|
+
raise SystemExit(1)
|
|
115
|
+
|
|
116
|
+
gh_name = codespace.get("name", "")
|
|
117
|
+
repository = codespace.get("repository", "")
|
|
118
|
+
|
|
119
|
+
planner = RegisteredPlanner(
|
|
120
|
+
name=name,
|
|
121
|
+
gh_name=gh_name,
|
|
122
|
+
repository=repository,
|
|
123
|
+
configured=False,
|
|
124
|
+
registered_at=ctx.time.now(),
|
|
125
|
+
last_connected_at=None,
|
|
126
|
+
)
|
|
127
|
+
ctx.planner_registry.register(planner)
|
|
128
|
+
|
|
129
|
+
# Set as default if first planner
|
|
130
|
+
if len(ctx.planner_registry.list_planners()) == 1:
|
|
131
|
+
ctx.planner_registry.set_default(name)
|
|
132
|
+
click.echo(f"Registered planner '{name}' (set as default)", err=True)
|
|
133
|
+
else:
|
|
134
|
+
click.echo(f"Registered planner '{name}'", err=True)
|
|
135
|
+
|
|
136
|
+
click.echo("", err=True)
|
|
137
|
+
click.echo("Next step:", err=True)
|
|
138
|
+
click.echo(f" erk planner configure {name}", err=True)
|
|
139
|
+
else:
|
|
140
|
+
click.echo("Run this command to create the codespace:", err=True)
|
|
141
|
+
click.echo("", err=True)
|
|
142
|
+
click.echo(f" {' '.join(cmd)}", err=True)
|
|
143
|
+
click.echo("", err=True)
|
|
144
|
+
click.echo("Or run with --run to execute directly:", err=True)
|
|
145
|
+
click.echo(f" erk planner create {name} --run", err=True)
|
|
146
|
+
click.echo("", err=True)
|
|
147
|
+
click.echo("After creation, configure authentication:", err=True)
|
|
148
|
+
click.echo(f" erk planner configure {name}", err=True)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""List registered planner boxes."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from erk.core.context import ErkContext
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command("list")
|
|
11
|
+
@click.pass_obj
|
|
12
|
+
def list_planners(ctx: ErkContext) -> None:
|
|
13
|
+
"""List all registered planner boxes."""
|
|
14
|
+
planners = ctx.planner_registry.list_planners()
|
|
15
|
+
default_name = ctx.planner_registry.get_default_name()
|
|
16
|
+
|
|
17
|
+
if not planners:
|
|
18
|
+
click.echo("No planners registered.", err=True)
|
|
19
|
+
click.echo("\nUse 'erk planner register <name>' to register a codespace.", err=True)
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
# Create Rich table
|
|
23
|
+
table = Table(show_header=True, header_style="bold", box=None)
|
|
24
|
+
table.add_column("name", style="cyan", no_wrap=True)
|
|
25
|
+
table.add_column("repository", style="yellow", no_wrap=True)
|
|
26
|
+
table.add_column("configured", no_wrap=True)
|
|
27
|
+
table.add_column("last connected", no_wrap=True)
|
|
28
|
+
|
|
29
|
+
for planner in sorted(planners, key=lambda p: p.name):
|
|
30
|
+
# Name with default indicator
|
|
31
|
+
name_cell = planner.name
|
|
32
|
+
if planner.name == default_name:
|
|
33
|
+
name_cell = f"[cyan bold]{planner.name}[/cyan bold] (default)"
|
|
34
|
+
else:
|
|
35
|
+
name_cell = f"[cyan]{planner.name}[/cyan]"
|
|
36
|
+
|
|
37
|
+
# Configured status
|
|
38
|
+
configured_cell = "[green]yes[/green]" if planner.configured else "[yellow]no[/yellow]"
|
|
39
|
+
|
|
40
|
+
# Last connected
|
|
41
|
+
if planner.last_connected_at:
|
|
42
|
+
# Format as relative time or date
|
|
43
|
+
last_connected = planner.last_connected_at.strftime("%Y-%m-%d %H:%M")
|
|
44
|
+
else:
|
|
45
|
+
last_connected = "-"
|
|
46
|
+
|
|
47
|
+
table.add_row(name_cell, planner.repository, configured_cell, last_connected)
|
|
48
|
+
|
|
49
|
+
# Output table to stderr (consistent with erk conventions)
|
|
50
|
+
console = Console(stderr=True, force_terminal=True)
|
|
51
|
+
console.print(table)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Register an existing GitHub Codespace as a planner box."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from erk.cli.ensure import Ensure
|
|
9
|
+
from erk.core.context import ErkContext
|
|
10
|
+
from erk.core.planner.types import RegisteredPlanner
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _list_codespaces() -> list[dict]:
|
|
14
|
+
"""List available codespaces from GitHub.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
List of codespace dicts with name, repository, displayName fields
|
|
18
|
+
"""
|
|
19
|
+
# GH-API-AUDIT: REST - GET user/codespaces
|
|
20
|
+
result = subprocess.run(
|
|
21
|
+
["gh", "codespace", "list", "--json", "name,repository,displayName"],
|
|
22
|
+
capture_output=True,
|
|
23
|
+
text=True,
|
|
24
|
+
check=False,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if result.returncode != 0:
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
# JSON parsing requires exception handling for malformed data
|
|
31
|
+
content = result.stdout.strip()
|
|
32
|
+
if not content:
|
|
33
|
+
return []
|
|
34
|
+
try:
|
|
35
|
+
parsed = json.loads(content)
|
|
36
|
+
if isinstance(parsed, list):
|
|
37
|
+
return parsed
|
|
38
|
+
return []
|
|
39
|
+
except json.JSONDecodeError:
|
|
40
|
+
return []
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@click.command("register")
|
|
44
|
+
@click.argument("name")
|
|
45
|
+
@click.pass_obj
|
|
46
|
+
def register_planner(ctx: ErkContext, name: str) -> None:
|
|
47
|
+
"""Register an existing GitHub Codespace as a planner box.
|
|
48
|
+
|
|
49
|
+
Lists available codespaces and prompts you to select one.
|
|
50
|
+
The selected codespace will be registered under NAME.
|
|
51
|
+
"""
|
|
52
|
+
# Check if name already exists
|
|
53
|
+
existing = ctx.planner_registry.get(name)
|
|
54
|
+
if existing is not None:
|
|
55
|
+
click.echo(f"Error: A planner named '{name}' already exists.", err=True)
|
|
56
|
+
click.echo(f"Use 'erk planner unregister {name}' first to remove it.", err=True)
|
|
57
|
+
raise SystemExit(1)
|
|
58
|
+
|
|
59
|
+
# List available codespaces
|
|
60
|
+
click.echo("Fetching available codespaces...", err=True)
|
|
61
|
+
codespaces = _list_codespaces()
|
|
62
|
+
|
|
63
|
+
if not codespaces:
|
|
64
|
+
click.echo("No codespaces found.", err=True)
|
|
65
|
+
click.echo("\nCreate a codespace first, then run this command again.", err=True)
|
|
66
|
+
raise SystemExit(1)
|
|
67
|
+
|
|
68
|
+
# Display available codespaces
|
|
69
|
+
click.echo("\nAvailable codespaces:", err=True)
|
|
70
|
+
for i, cs in enumerate(codespaces, 1):
|
|
71
|
+
display_name = cs.get("displayName", cs.get("name", "unknown"))
|
|
72
|
+
repo = cs.get("repository", "unknown")
|
|
73
|
+
click.echo(f" {i}. {display_name} ({repo})", err=True)
|
|
74
|
+
|
|
75
|
+
# Prompt for selection
|
|
76
|
+
click.echo("", err=True)
|
|
77
|
+
selection = click.prompt(
|
|
78
|
+
"Select codespace number",
|
|
79
|
+
type=click.IntRange(1, len(codespaces)),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
selected = codespaces[selection - 1]
|
|
83
|
+
gh_name = Ensure.truthy(selected.get("name", ""), "Could not get codespace name.")
|
|
84
|
+
repository = selected.get("repository", "")
|
|
85
|
+
|
|
86
|
+
# Create and register the planner
|
|
87
|
+
planner = RegisteredPlanner(
|
|
88
|
+
name=name,
|
|
89
|
+
gh_name=gh_name,
|
|
90
|
+
repository=repository,
|
|
91
|
+
configured=False,
|
|
92
|
+
registered_at=ctx.time.now(),
|
|
93
|
+
last_connected_at=None,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
ctx.planner_registry.register(planner)
|
|
97
|
+
|
|
98
|
+
# If this is the first planner, set it as default
|
|
99
|
+
if len(ctx.planner_registry.list_planners()) == 1:
|
|
100
|
+
ctx.planner_registry.set_default(name)
|
|
101
|
+
click.echo(f"\nRegistered planner '{name}' (set as default)", err=True)
|
|
102
|
+
else:
|
|
103
|
+
click.echo(f"\nRegistered planner '{name}'", err=True)
|
|
104
|
+
|
|
105
|
+
click.echo(f"\nRun 'erk planner configure {name}' for initial setup.", err=True)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Set the default planner box."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from erk.cli.ensure import Ensure
|
|
6
|
+
from erk.core.context import ErkContext
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command("set-default")
|
|
10
|
+
@click.argument("name")
|
|
11
|
+
@click.pass_obj
|
|
12
|
+
def set_default_planner(ctx: ErkContext, name: str) -> None:
|
|
13
|
+
"""Set the default planner box.
|
|
14
|
+
|
|
15
|
+
The default planner is used when running 'erk planner' without arguments.
|
|
16
|
+
"""
|
|
17
|
+
_planner = Ensure.not_none(
|
|
18
|
+
ctx.planner_registry.get(name),
|
|
19
|
+
f"No planner named '{name}' found.\n\nUse 'erk planner list' to see registered planners.",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
ctx.planner_registry.set_default(name)
|
|
23
|
+
click.echo(f"Set '{name}' as the default planner.", err=True)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Unregister a planner box."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from erk.core.context import ErkContext
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.command("unregister")
|
|
9
|
+
@click.argument("name")
|
|
10
|
+
@click.option("--force", "-f", is_flag=True, help="Skip confirmation prompt")
|
|
11
|
+
@click.pass_obj
|
|
12
|
+
def unregister_planner(ctx: ErkContext, name: str, force: bool) -> None:
|
|
13
|
+
"""Remove a planner box from the registry.
|
|
14
|
+
|
|
15
|
+
This does not delete the codespace - it only removes the registration.
|
|
16
|
+
"""
|
|
17
|
+
planner = ctx.planner_registry.get(name)
|
|
18
|
+
if planner is None:
|
|
19
|
+
click.echo(f"Error: No planner named '{name}' found.", err=True)
|
|
20
|
+
click.echo("\nUse 'erk planner list' to see registered planners.", err=True)
|
|
21
|
+
raise SystemExit(1)
|
|
22
|
+
|
|
23
|
+
# Check if this is the default
|
|
24
|
+
is_default = ctx.planner_registry.get_default_name() == name
|
|
25
|
+
|
|
26
|
+
if not force:
|
|
27
|
+
msg = f"Unregister planner '{name}'?"
|
|
28
|
+
if is_default:
|
|
29
|
+
msg = f"Unregister planner '{name}' (currently the default)?"
|
|
30
|
+
if not click.confirm(msg):
|
|
31
|
+
click.echo("Cancelled.", err=True)
|
|
32
|
+
raise SystemExit(0)
|
|
33
|
+
|
|
34
|
+
ctx.planner_registry.unregister(name)
|
|
35
|
+
|
|
36
|
+
click.echo(f"Unregistered planner '{name}'.", err=True)
|
|
37
|
+
if is_default:
|
|
38
|
+
click.echo("Note: Default planner has been cleared.", err=True)
|
|
39
|
+
|
|
40
|
+
# Suggest setting a new default if there are other planners
|
|
41
|
+
remaining = ctx.planner_registry.list_planners()
|
|
42
|
+
if remaining and is_default:
|
|
43
|
+
click.echo("\nUse 'erk planner set-default <name>' to set a new default.", err=True)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""PR management commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from erk.cli.alias import register_with_aliases
|
|
6
|
+
from erk.cli.commands.pr.check_cmd import pr_check
|
|
7
|
+
from erk.cli.commands.pr.checkout_cmd import pr_checkout
|
|
8
|
+
from erk.cli.commands.pr.fix_conflicts_cmd import pr_fix_conflicts
|
|
9
|
+
from erk.cli.commands.pr.submit_cmd import pr_submit
|
|
10
|
+
from erk.cli.commands.pr.sync_cmd import pr_sync
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group("pr")
|
|
14
|
+
def pr_group() -> None:
|
|
15
|
+
"""Manage pull requests."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
pr_group.add_command(pr_check, name="check")
|
|
20
|
+
register_with_aliases(pr_group, pr_checkout)
|
|
21
|
+
pr_group.add_command(pr_fix_conflicts, name="fix-conflicts")
|
|
22
|
+
pr_group.add_command(pr_submit, name="submit")
|
|
23
|
+
pr_group.add_command(pr_sync, name="sync")
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Command to validate PR rules for the current branch."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from erk.cli.ensure import Ensure
|
|
6
|
+
from erk.core.context import ErkContext
|
|
7
|
+
from erk_shared.gateway.pr.submit import (
|
|
8
|
+
has_checkout_footer_for_pr,
|
|
9
|
+
has_issue_closing_reference,
|
|
10
|
+
)
|
|
11
|
+
from erk_shared.github.types import PRNotFound
|
|
12
|
+
from erk_shared.impl_folder import read_issue_reference, validate_issue_linkage
|
|
13
|
+
from erk_shared.output.output import user_output
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command("check")
|
|
17
|
+
@click.pass_obj
|
|
18
|
+
def pr_check(ctx: ErkContext) -> None:
|
|
19
|
+
"""Validate PR rules for the current branch.
|
|
20
|
+
|
|
21
|
+
Checks that the PR:
|
|
22
|
+
1. Has issue closing reference (Closes #N) when .impl/issue.json exists
|
|
23
|
+
2. Has the standard checkout command footer
|
|
24
|
+
"""
|
|
25
|
+
# Get current branch
|
|
26
|
+
branch = Ensure.not_none(
|
|
27
|
+
ctx.git.get_current_branch(ctx.cwd),
|
|
28
|
+
"Not on a branch (detached HEAD)",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Get repo root for GitHub operations
|
|
32
|
+
repo_root = ctx.git.get_repository_root(ctx.cwd)
|
|
33
|
+
|
|
34
|
+
# Get PR for branch
|
|
35
|
+
pr = ctx.github.get_pr_for_branch(repo_root, branch)
|
|
36
|
+
if isinstance(pr, PRNotFound):
|
|
37
|
+
user_output(
|
|
38
|
+
click.style("Error: ", fg="red") + f"No pull request found for branch '{branch}'"
|
|
39
|
+
)
|
|
40
|
+
raise SystemExit(1)
|
|
41
|
+
|
|
42
|
+
pr_number = pr.number
|
|
43
|
+
|
|
44
|
+
user_output(f"Checking PR #{pr_number} for branch {branch}...")
|
|
45
|
+
user_output("")
|
|
46
|
+
|
|
47
|
+
# Track validation results
|
|
48
|
+
checks: list[tuple[bool, str]] = []
|
|
49
|
+
|
|
50
|
+
pr_body = pr.body
|
|
51
|
+
|
|
52
|
+
# .impl always lives at worktree/repo root
|
|
53
|
+
impl_dir = repo_root / ".impl"
|
|
54
|
+
|
|
55
|
+
# Check 0: Branch/issue.json agreement
|
|
56
|
+
# This catches cases where branch name says "P42-..." but issue.json says #99
|
|
57
|
+
issue_number: int | None = None
|
|
58
|
+
try:
|
|
59
|
+
issue_number = validate_issue_linkage(impl_dir, branch)
|
|
60
|
+
if issue_number is not None:
|
|
61
|
+
checks.append((True, f"Branch name and .impl/issue.json agree (#{issue_number})"))
|
|
62
|
+
except ValueError as e:
|
|
63
|
+
checks.append((False, str(e)))
|
|
64
|
+
# Continue with other checks - use the issue from .impl/issue.json as fallback
|
|
65
|
+
issue_ref_fallback = read_issue_reference(impl_dir)
|
|
66
|
+
if issue_ref_fallback is not None:
|
|
67
|
+
issue_number = issue_ref_fallback.issue_number
|
|
68
|
+
|
|
69
|
+
# Check 1: Issue closing reference (if issue number is discoverable)
|
|
70
|
+
issue_ref = read_issue_reference(impl_dir)
|
|
71
|
+
|
|
72
|
+
if issue_ref is not None:
|
|
73
|
+
expected_issue_number = issue_ref.issue_number
|
|
74
|
+
plans_repo = ctx.local_config.plans_repo if ctx.local_config else None
|
|
75
|
+
if has_issue_closing_reference(pr_body, expected_issue_number, plans_repo):
|
|
76
|
+
# Format expected reference for display
|
|
77
|
+
if plans_repo is None:
|
|
78
|
+
ref_display = f"#{expected_issue_number}"
|
|
79
|
+
else:
|
|
80
|
+
ref_display = f"{plans_repo}#{expected_issue_number}"
|
|
81
|
+
msg = f"PR body contains issue closing reference (Closes {ref_display})"
|
|
82
|
+
checks.append((True, msg))
|
|
83
|
+
else:
|
|
84
|
+
if plans_repo is None:
|
|
85
|
+
expected = f"Closes #{expected_issue_number}"
|
|
86
|
+
else:
|
|
87
|
+
expected = f"Closes {plans_repo}#{expected_issue_number}"
|
|
88
|
+
msg = f"PR body missing issue closing reference (expected: {expected})"
|
|
89
|
+
checks.append((False, msg))
|
|
90
|
+
|
|
91
|
+
# Check 2: Checkout footer
|
|
92
|
+
if has_checkout_footer_for_pr(pr_body, pr_number):
|
|
93
|
+
checks.append((True, "PR body contains checkout footer"))
|
|
94
|
+
else:
|
|
95
|
+
checks.append((False, "PR body missing checkout footer"))
|
|
96
|
+
|
|
97
|
+
# Output results
|
|
98
|
+
for passed, description in checks:
|
|
99
|
+
status = click.style("[PASS]", fg="green") if passed else click.style("[FAIL]", fg="red")
|
|
100
|
+
user_output(f"{status} {description}")
|
|
101
|
+
|
|
102
|
+
user_output("")
|
|
103
|
+
|
|
104
|
+
# Determine overall result
|
|
105
|
+
failed_count = sum(1 for passed, _ in checks if not passed)
|
|
106
|
+
if failed_count == 0:
|
|
107
|
+
user_output(click.style("All checks passed", fg="green"))
|
|
108
|
+
raise SystemExit(0)
|
|
109
|
+
else:
|
|
110
|
+
check_word = "check" if failed_count == 1 else "checks"
|
|
111
|
+
user_output(click.style(f"{failed_count} {check_word} failed", fg="red"))
|
|
112
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Checkout a pull request into a worktree.
|
|
2
|
+
|
|
3
|
+
This command fetches PR code and creates a worktree for local review/testing.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from erk.cli.activation import render_activation_script
|
|
9
|
+
from erk.cli.alias import alias
|
|
10
|
+
from erk.cli.commands.pr.parse_pr_reference import parse_pr_reference
|
|
11
|
+
from erk.cli.core import worktree_path_for
|
|
12
|
+
from erk.cli.ensure import Ensure
|
|
13
|
+
from erk.cli.help_formatter import CommandWithHiddenOptions, script_option
|
|
14
|
+
from erk.core.context import ErkContext
|
|
15
|
+
from erk.core.repo_discovery import NoRepoSentinel, RepoContext
|
|
16
|
+
from erk_shared.github.types import PRNotFound
|
|
17
|
+
from erk_shared.output.output import user_output
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@alias("co")
|
|
21
|
+
@click.command("checkout", cls=CommandWithHiddenOptions)
|
|
22
|
+
@click.argument("pr_reference")
|
|
23
|
+
@script_option
|
|
24
|
+
@click.pass_obj
|
|
25
|
+
def pr_checkout(ctx: ErkContext, pr_reference: str, script: bool) -> None:
|
|
26
|
+
"""Checkout PR into a worktree for review.
|
|
27
|
+
|
|
28
|
+
PR_REFERENCE can be a plain number (123) or GitHub URL
|
|
29
|
+
(https://github.com/owner/repo/pull/123).
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
|
|
33
|
+
# Checkout by PR number
|
|
34
|
+
erk pr checkout 123
|
|
35
|
+
|
|
36
|
+
# Checkout by GitHub URL
|
|
37
|
+
erk pr checkout https://github.com/owner/repo/pull/123
|
|
38
|
+
"""
|
|
39
|
+
# Validate preconditions upfront (LBYL)
|
|
40
|
+
Ensure.gh_authenticated(ctx)
|
|
41
|
+
|
|
42
|
+
if isinstance(ctx.repo, NoRepoSentinel):
|
|
43
|
+
ctx.feedback.error("Not in a git repository")
|
|
44
|
+
raise SystemExit(1)
|
|
45
|
+
repo: RepoContext = ctx.repo
|
|
46
|
+
|
|
47
|
+
pr_number = parse_pr_reference(pr_reference)
|
|
48
|
+
|
|
49
|
+
# Get PR details from GitHub
|
|
50
|
+
ctx.feedback.info(f"Fetching PR #{pr_number}...")
|
|
51
|
+
pr = ctx.github.get_pr(repo.root, pr_number)
|
|
52
|
+
if isinstance(pr, PRNotFound):
|
|
53
|
+
ctx.feedback.error(
|
|
54
|
+
f"Could not find PR #{pr_number}\n\n"
|
|
55
|
+
"Check the PR number and ensure you're authenticated with gh CLI."
|
|
56
|
+
)
|
|
57
|
+
raise SystemExit(1)
|
|
58
|
+
|
|
59
|
+
# Warn for closed/merged PRs
|
|
60
|
+
if pr.state != "OPEN":
|
|
61
|
+
ctx.feedback.info(f"Warning: PR #{pr_number} is {pr.state}")
|
|
62
|
+
|
|
63
|
+
# Determine branch name strategy
|
|
64
|
+
# For cross-repository PRs (forks), use pr/<number> to avoid conflicts
|
|
65
|
+
# For same-repository PRs, use the actual branch name
|
|
66
|
+
if pr.is_cross_repository:
|
|
67
|
+
branch_name = f"pr/{pr_number}"
|
|
68
|
+
else:
|
|
69
|
+
branch_name = pr.head_ref_name
|
|
70
|
+
|
|
71
|
+
# Check if branch already exists in a worktree
|
|
72
|
+
existing_worktree = ctx.git.find_worktree_for_branch(repo.root, branch_name)
|
|
73
|
+
if existing_worktree is not None:
|
|
74
|
+
# Branch already exists in a worktree - activate it
|
|
75
|
+
if script:
|
|
76
|
+
activation_script = render_activation_script(
|
|
77
|
+
worktree_path=existing_worktree,
|
|
78
|
+
target_subpath=None,
|
|
79
|
+
post_cd_commands=None,
|
|
80
|
+
final_message=f'echo "Went to existing worktree for PR #{pr_number}"',
|
|
81
|
+
comment="work activate-script",
|
|
82
|
+
)
|
|
83
|
+
result = ctx.script_writer.write_activation_script(
|
|
84
|
+
activation_script,
|
|
85
|
+
command_name="pr-checkout",
|
|
86
|
+
comment=f"activate PR #{pr_number}",
|
|
87
|
+
)
|
|
88
|
+
result.output_for_shell_integration()
|
|
89
|
+
else:
|
|
90
|
+
styled_path = click.style(str(existing_worktree), fg="cyan", bold=True)
|
|
91
|
+
user_output(f"PR #{pr_number} already checked out at {styled_path}")
|
|
92
|
+
user_output("\nShell integration not detected. Run 'erk init --shell' to set up.")
|
|
93
|
+
user_output(f"Or use: source <(erk pr checkout {pr_reference} --script)")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
# For cross-repository PRs, always fetch via refs/pull/<n>/head
|
|
97
|
+
# For same-repo PRs, check if branch exists locally first
|
|
98
|
+
if pr.is_cross_repository:
|
|
99
|
+
# Fetch PR ref directly
|
|
100
|
+
ctx.git.fetch_pr_ref(repo.root, "origin", pr_number, branch_name)
|
|
101
|
+
else:
|
|
102
|
+
# Check if branch exists locally or on remote
|
|
103
|
+
local_branches = ctx.git.list_local_branches(repo.root)
|
|
104
|
+
if branch_name in local_branches:
|
|
105
|
+
# Branch already exists locally - just need to create worktree
|
|
106
|
+
pass
|
|
107
|
+
else:
|
|
108
|
+
# Check remote and fetch if needed
|
|
109
|
+
remote_branches = ctx.git.list_remote_branches(repo.root)
|
|
110
|
+
remote_ref = f"origin/{branch_name}"
|
|
111
|
+
if remote_ref in remote_branches:
|
|
112
|
+
ctx.git.fetch_branch(repo.root, "origin", branch_name)
|
|
113
|
+
ctx.git.create_tracking_branch(repo.root, branch_name, remote_ref)
|
|
114
|
+
else:
|
|
115
|
+
# Branch not on remote (maybe local-only PR?), fetch via PR ref
|
|
116
|
+
ctx.git.fetch_pr_ref(repo.root, "origin", pr_number, branch_name)
|
|
117
|
+
|
|
118
|
+
# Create worktree
|
|
119
|
+
worktree_path = worktree_path_for(repo.worktrees_dir, branch_name)
|
|
120
|
+
ctx.git.add_worktree(
|
|
121
|
+
repo.root,
|
|
122
|
+
worktree_path,
|
|
123
|
+
branch=branch_name,
|
|
124
|
+
ref=None,
|
|
125
|
+
create_branch=False,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# For stacked PRs (base is not trunk), rebase onto base branch
|
|
129
|
+
# This ensures git history includes the base branch as an ancestor,
|
|
130
|
+
# which `gt track` requires for proper stacking
|
|
131
|
+
trunk_branch = ctx.git.detect_trunk_branch(repo.root)
|
|
132
|
+
if pr.base_ref_name != trunk_branch and not pr.is_cross_repository:
|
|
133
|
+
ctx.feedback.info(f"Fetching base branch '{pr.base_ref_name}'...")
|
|
134
|
+
ctx.git.fetch_branch(repo.root, "origin", pr.base_ref_name)
|
|
135
|
+
|
|
136
|
+
ctx.feedback.info("Rebasing onto base branch...")
|
|
137
|
+
rebase_result = ctx.git.rebase_onto(worktree_path, f"origin/{pr.base_ref_name}")
|
|
138
|
+
|
|
139
|
+
if not rebase_result.success:
|
|
140
|
+
ctx.git.rebase_abort(worktree_path)
|
|
141
|
+
ctx.feedback.info(
|
|
142
|
+
f"Warning: Rebase had conflicts. Worktree created but needs manual rebase.\n"
|
|
143
|
+
f"Run: cd {worktree_path} && git rebase origin/{pr.base_ref_name}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Output based on mode
|
|
147
|
+
if script:
|
|
148
|
+
activation_script = render_activation_script(
|
|
149
|
+
worktree_path=worktree_path,
|
|
150
|
+
target_subpath=None,
|
|
151
|
+
post_cd_commands=None,
|
|
152
|
+
final_message=f'echo "Checked out PR #{pr_number} at $(pwd)"',
|
|
153
|
+
comment="work activate-script",
|
|
154
|
+
)
|
|
155
|
+
result = ctx.script_writer.write_activation_script(
|
|
156
|
+
activation_script,
|
|
157
|
+
command_name="pr-checkout",
|
|
158
|
+
comment=f"activate PR #{pr_number}",
|
|
159
|
+
)
|
|
160
|
+
result.output_for_shell_integration()
|
|
161
|
+
else:
|
|
162
|
+
styled_path = click.style(str(worktree_path), fg="cyan", bold=True)
|
|
163
|
+
user_output(f"Created worktree for PR #{pr_number} at {styled_path}")
|
|
164
|
+
user_output("\nShell integration not detected. Run 'erk init --shell' to set up.")
|
|
165
|
+
user_output(f"Or use: source <(erk pr checkout {pr_reference} --script)")
|