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,998 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import shlex
|
|
3
|
+
import subprocess
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from erk.cli.config import LoadedConfig
|
|
10
|
+
from erk.cli.core import discover_repo_context, worktree_path_for
|
|
11
|
+
from erk.cli.ensure import Ensure
|
|
12
|
+
from erk.cli.github_parsing import parse_issue_identifier
|
|
13
|
+
from erk.cli.help_formatter import CommandWithHiddenOptions, script_option
|
|
14
|
+
from erk.cli.shell_utils import render_navigation_script
|
|
15
|
+
from erk.cli.subprocess_utils import run_with_error_reporting
|
|
16
|
+
from erk.core.context import ErkContext
|
|
17
|
+
from erk.core.repo_discovery import RepoContext, ensure_erk_metadata_dir
|
|
18
|
+
from erk_shared.impl_folder import create_impl_folder, get_impl_path, save_issue_reference
|
|
19
|
+
from erk_shared.issue_workflow import (
|
|
20
|
+
IssueBranchSetup,
|
|
21
|
+
IssueValidationFailed,
|
|
22
|
+
prepare_plan_for_worktree,
|
|
23
|
+
)
|
|
24
|
+
from erk_shared.naming import (
|
|
25
|
+
default_branch_for_worktree,
|
|
26
|
+
ensure_simple_worktree_name,
|
|
27
|
+
ensure_unique_worktree_name,
|
|
28
|
+
ensure_unique_worktree_name_with_date,
|
|
29
|
+
sanitize_worktree_name,
|
|
30
|
+
strip_plan_from_filename,
|
|
31
|
+
)
|
|
32
|
+
from erk_shared.output.output import user_output
|
|
33
|
+
from erk_shared.plan_store.types import Plan
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def run_post_worktree_setup(
|
|
37
|
+
ctx: ErkContext,
|
|
38
|
+
config: LoadedConfig,
|
|
39
|
+
worktree_path: Path,
|
|
40
|
+
repo_root: Path,
|
|
41
|
+
name: str,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Run post-worktree-creation setup: .env file and post-create commands.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
ctx: Erk context
|
|
47
|
+
config: Loaded local configuration
|
|
48
|
+
worktree_path: Path to the newly created worktree
|
|
49
|
+
repo_root: Path to repository root
|
|
50
|
+
name: Worktree name
|
|
51
|
+
"""
|
|
52
|
+
# Write .env file if template exists
|
|
53
|
+
env_content = make_env_content(
|
|
54
|
+
config, worktree_path=worktree_path, repo_root=repo_root, name=name
|
|
55
|
+
)
|
|
56
|
+
if env_content:
|
|
57
|
+
env_path = worktree_path / ".env"
|
|
58
|
+
env_path.write_text(env_content, encoding="utf-8")
|
|
59
|
+
|
|
60
|
+
# Run post-create commands
|
|
61
|
+
if config.post_create_commands:
|
|
62
|
+
run_commands_in_worktree(
|
|
63
|
+
ctx=ctx,
|
|
64
|
+
commands=config.post_create_commands,
|
|
65
|
+
worktree_path=worktree_path,
|
|
66
|
+
shell=config.post_create_shell,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def ensure_worktree_for_branch(
|
|
71
|
+
ctx: ErkContext,
|
|
72
|
+
repo: RepoContext,
|
|
73
|
+
branch: str,
|
|
74
|
+
*,
|
|
75
|
+
is_plan_derived: bool = False,
|
|
76
|
+
) -> tuple[Path, bool]:
|
|
77
|
+
"""Ensure worktree exists for branch, creating if necessary.
|
|
78
|
+
|
|
79
|
+
This function checks if a worktree already exists for the given branch.
|
|
80
|
+
If it does, validates branch match and returns path. If not, creates a new worktree
|
|
81
|
+
with config-driven post-create commands and .env generation.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
ctx: The Erk context with git operations
|
|
85
|
+
repo: Repository context with root and worktrees directory
|
|
86
|
+
branch: The branch name to ensure a worktree for
|
|
87
|
+
is_plan_derived: If True, use dated worktree names (for plan workflows).
|
|
88
|
+
If False, use simple names (for manual checkout).
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Tuple of (worktree_path, was_created)
|
|
92
|
+
- worktree_path: Path to the worktree directory
|
|
93
|
+
- was_created: True if worktree was newly created, False if it already existed
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
SystemExit: If branch doesn't exist, tracking branch creation fails,
|
|
97
|
+
or worktree name collision with different branch
|
|
98
|
+
"""
|
|
99
|
+
# Check if worktree already exists for this branch
|
|
100
|
+
existing_path = ctx.git.is_branch_checked_out(repo.root, branch)
|
|
101
|
+
if existing_path is not None:
|
|
102
|
+
return existing_path, False
|
|
103
|
+
|
|
104
|
+
# Get trunk branch for validation
|
|
105
|
+
trunk_branch = ctx.git.detect_trunk_branch(repo.root)
|
|
106
|
+
|
|
107
|
+
# Validate that we're not trying to create worktree for trunk branch
|
|
108
|
+
Ensure.invariant(
|
|
109
|
+
branch != trunk_branch,
|
|
110
|
+
f'Cannot create worktree for trunk branch "{trunk_branch}".\n'
|
|
111
|
+
f"The trunk branch should be checked out in the root worktree.\n"
|
|
112
|
+
f"To switch to {trunk_branch}, use:\n"
|
|
113
|
+
f" erk br co root",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Branch not checked out - need to create worktree
|
|
117
|
+
# First check if branch exists locally
|
|
118
|
+
local_branches = ctx.git.list_local_branches(repo.root)
|
|
119
|
+
|
|
120
|
+
if branch not in local_branches:
|
|
121
|
+
# Not a local branch - check if remote branch exists
|
|
122
|
+
remote_branches = ctx.git.list_remote_branches(repo.root)
|
|
123
|
+
remote_ref = f"origin/{branch}"
|
|
124
|
+
|
|
125
|
+
if remote_ref not in remote_branches:
|
|
126
|
+
# Branch doesn't exist locally or on origin
|
|
127
|
+
user_output(
|
|
128
|
+
f"Error: Branch '{branch}' does not exist.\n"
|
|
129
|
+
f"To create a new branch and worktree, run:\n"
|
|
130
|
+
f" erk wt create --branch {branch}"
|
|
131
|
+
)
|
|
132
|
+
raise SystemExit(1) from None
|
|
133
|
+
|
|
134
|
+
# Remote branch exists - create local tracking branch
|
|
135
|
+
user_output(f"Branch '{branch}' exists on origin, creating local tracking branch...")
|
|
136
|
+
try:
|
|
137
|
+
ctx.git.create_tracking_branch(repo.root, branch, remote_ref)
|
|
138
|
+
except subprocess.CalledProcessError as e:
|
|
139
|
+
user_output(
|
|
140
|
+
f"Error: Failed to create local tracking branch from {remote_ref}\n"
|
|
141
|
+
f"Details: {e.stderr}\n"
|
|
142
|
+
f"Suggested action:\n"
|
|
143
|
+
f" 1. Check git status and resolve any issues\n"
|
|
144
|
+
f" 2. Manually create branch: git branch --track {branch} {remote_ref}\n"
|
|
145
|
+
f" 3. Or use: erk wt create --branch {branch}"
|
|
146
|
+
)
|
|
147
|
+
raise SystemExit(1) from e
|
|
148
|
+
|
|
149
|
+
# Branch exists but not checked out - auto-create worktree
|
|
150
|
+
user_output(f"Branch '{branch}' not checked out, creating worktree...")
|
|
151
|
+
|
|
152
|
+
# Load local config for .env template and post-create commands
|
|
153
|
+
config = ctx.local_config if ctx.local_config is not None else LoadedConfig.test()
|
|
154
|
+
|
|
155
|
+
# Generate and ensure unique worktree name
|
|
156
|
+
name = sanitize_worktree_name(branch)
|
|
157
|
+
|
|
158
|
+
# Use appropriate naming strategy based on whether worktree is plan-derived
|
|
159
|
+
if is_plan_derived:
|
|
160
|
+
# Plan workflows need date suffixes to create multiple worktrees from same plan
|
|
161
|
+
name = ensure_unique_worktree_name_with_date(name, repo.worktrees_dir, ctx.git)
|
|
162
|
+
else:
|
|
163
|
+
# Manual checkouts use simple names for predictability
|
|
164
|
+
name = ensure_simple_worktree_name(name, repo.worktrees_dir, ctx.git)
|
|
165
|
+
|
|
166
|
+
# Calculate worktree path
|
|
167
|
+
wt_path = worktree_path_for(repo.worktrees_dir, name)
|
|
168
|
+
|
|
169
|
+
# Check for name collision with different branch (for non-plan checkouts)
|
|
170
|
+
if not is_plan_derived and ctx.git.path_exists(wt_path):
|
|
171
|
+
# Worktree exists - check what branch it has
|
|
172
|
+
worktrees = ctx.git.list_worktrees(repo.root)
|
|
173
|
+
for wt in worktrees:
|
|
174
|
+
if wt.path == wt_path:
|
|
175
|
+
if wt.branch != branch:
|
|
176
|
+
# Detached HEAD: provide specific guidance
|
|
177
|
+
if wt.branch is None:
|
|
178
|
+
user_output(
|
|
179
|
+
f"Error: Worktree '{name}' is in detached HEAD state "
|
|
180
|
+
f"(possibly mid-rebase).\n\n"
|
|
181
|
+
f"Cannot create new worktree for branch '{branch}' with same name.\n\n"
|
|
182
|
+
f"Options:\n"
|
|
183
|
+
f" 1. Resume work in existing worktree: erk wt co {name}\n"
|
|
184
|
+
f" 2. Complete or abort the rebase first, then try again\n"
|
|
185
|
+
f" 3. Use a different branch name"
|
|
186
|
+
)
|
|
187
|
+
raise SystemExit(1) from None
|
|
188
|
+
# Different branch: existing error handling
|
|
189
|
+
user_output(
|
|
190
|
+
f"Error: Worktree '{name}' already exists "
|
|
191
|
+
f"with different branch '{wt.branch}'.\n"
|
|
192
|
+
f"Cannot create worktree for branch '{branch}' with same name.\n"
|
|
193
|
+
f"Options:\n"
|
|
194
|
+
f" 1. Switch to existing worktree: erk wt co {name}\n"
|
|
195
|
+
f" 2. Use a different branch name"
|
|
196
|
+
)
|
|
197
|
+
raise SystemExit(1) from None
|
|
198
|
+
# Same branch - return existing path
|
|
199
|
+
return wt_path, False
|
|
200
|
+
# Path exists but not in worktree list (shouldn't happen, but handle gracefully)
|
|
201
|
+
user_output(
|
|
202
|
+
f"Error: Directory '{wt_path}' exists but is not a git worktree.\n"
|
|
203
|
+
f"Please remove or rename the directory and try again."
|
|
204
|
+
)
|
|
205
|
+
raise SystemExit(1) from None
|
|
206
|
+
|
|
207
|
+
# Create worktree from existing branch
|
|
208
|
+
add_worktree(
|
|
209
|
+
ctx,
|
|
210
|
+
repo.root,
|
|
211
|
+
wt_path,
|
|
212
|
+
branch=branch,
|
|
213
|
+
ref=None,
|
|
214
|
+
use_existing_branch=True,
|
|
215
|
+
use_graphite=False,
|
|
216
|
+
skip_remote_check=True,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
user_output(click.style(f"✓ Created worktree: {name}", fg="green"))
|
|
220
|
+
|
|
221
|
+
# Run post-worktree setup (.env and post-create commands)
|
|
222
|
+
run_post_worktree_setup(ctx, config, wt_path, repo.root, name)
|
|
223
|
+
|
|
224
|
+
return wt_path, True
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def add_worktree(
|
|
228
|
+
ctx: ErkContext,
|
|
229
|
+
repo_root: Path,
|
|
230
|
+
path: Path,
|
|
231
|
+
*,
|
|
232
|
+
branch: str | None,
|
|
233
|
+
ref: str | None,
|
|
234
|
+
use_existing_branch: bool,
|
|
235
|
+
use_graphite: bool,
|
|
236
|
+
skip_remote_check: bool,
|
|
237
|
+
) -> None:
|
|
238
|
+
"""Create a git worktree.
|
|
239
|
+
|
|
240
|
+
If `use_existing_branch` is True and `branch` is provided, checks out the existing branch
|
|
241
|
+
in the new worktree: `git worktree add <path> <branch>`.
|
|
242
|
+
|
|
243
|
+
If `use_existing_branch` is False and `branch` is provided, creates a new branch:
|
|
244
|
+
- With graphite: `gt create <branch>` followed by `git worktree add <path> <branch>`
|
|
245
|
+
- Without graphite: `git worktree add -b <branch> <path> <ref or HEAD>`
|
|
246
|
+
|
|
247
|
+
Otherwise, uses `git worktree add <path> <ref or HEAD>`.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
if branch and use_existing_branch:
|
|
251
|
+
# Validate branch is not already checked out
|
|
252
|
+
existing_path = ctx.git.is_branch_checked_out(repo_root, branch)
|
|
253
|
+
if existing_path:
|
|
254
|
+
user_output(
|
|
255
|
+
f"Error: Branch '{branch}' is already checked out at {existing_path}\n"
|
|
256
|
+
f"Git doesn't allow the same branch to be checked out in multiple worktrees.\n\n"
|
|
257
|
+
f"Options:\n"
|
|
258
|
+
f" • Use a different branch name\n"
|
|
259
|
+
f" • Create a new branch instead: erk create {path.name}\n"
|
|
260
|
+
f" • Switch to that worktree: erk br co {branch}",
|
|
261
|
+
)
|
|
262
|
+
raise SystemExit(1) from None
|
|
263
|
+
|
|
264
|
+
ctx.git.add_worktree(repo_root, path, branch=branch, ref=None, create_branch=False)
|
|
265
|
+
|
|
266
|
+
# Track existing branch with Graphite if enabled
|
|
267
|
+
if use_graphite and ref:
|
|
268
|
+
# Only track if not already tracked (idempotent)
|
|
269
|
+
all_branches = ctx.graphite.get_all_branches(ctx.git, repo_root)
|
|
270
|
+
if branch not in all_branches:
|
|
271
|
+
ctx.graphite.track_branch(repo_root, branch, ref)
|
|
272
|
+
elif branch:
|
|
273
|
+
# Check if branch name exists on remote origin (only when creating new branches)
|
|
274
|
+
if not skip_remote_check:
|
|
275
|
+
try:
|
|
276
|
+
remote_branches = ctx.git.list_remote_branches(repo_root)
|
|
277
|
+
remote_ref = f"origin/{branch}"
|
|
278
|
+
|
|
279
|
+
if remote_ref in remote_branches:
|
|
280
|
+
user_output(
|
|
281
|
+
click.style("Error: ", fg="red")
|
|
282
|
+
+ f"Branch '{branch}' already exists on remote 'origin'\n\n"
|
|
283
|
+
+ "A branch with this name is already pushed to the remote repository.\n"
|
|
284
|
+
+ "Please choose a different name for your new branch."
|
|
285
|
+
)
|
|
286
|
+
raise SystemExit(1) from None
|
|
287
|
+
except Exception as e:
|
|
288
|
+
# Remote unavailable or other error - proceed with warning
|
|
289
|
+
user_output(
|
|
290
|
+
click.style("Warning: ", fg="yellow")
|
|
291
|
+
+ f"Could not check remote branches: {e}\n"
|
|
292
|
+
+ "Proceeding with branch creation..."
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if use_graphite:
|
|
296
|
+
cwd = ctx.cwd
|
|
297
|
+
original_branch = ctx.git.get_current_branch(cwd)
|
|
298
|
+
if original_branch is None:
|
|
299
|
+
raise ValueError("Cannot create graphite branch from detached HEAD")
|
|
300
|
+
if ctx.git.has_staged_changes(repo_root):
|
|
301
|
+
user_output(
|
|
302
|
+
"Error: Staged changes detected. "
|
|
303
|
+
"Graphite cannot create a branch while staged changes are present.\n"
|
|
304
|
+
"`gt create --no-interactive` attempts to commit staged files but fails when "
|
|
305
|
+
"no commit message is provided.\n\n"
|
|
306
|
+
"Resolve the staged changes before running `erk create`:\n"
|
|
307
|
+
' • Commit them: git commit -m "message"\n'
|
|
308
|
+
" • Unstage them: git reset\n"
|
|
309
|
+
" • Stash them: git stash\n"
|
|
310
|
+
" • Disable Graphite: erk config set use_graphite false",
|
|
311
|
+
)
|
|
312
|
+
raise SystemExit(1) from None
|
|
313
|
+
run_with_error_reporting(
|
|
314
|
+
["gt", "create", "--no-interactive", branch],
|
|
315
|
+
cwd=cwd,
|
|
316
|
+
error_prefix=f"Failed to create Graphite branch '{branch}'",
|
|
317
|
+
troubleshooting=[
|
|
318
|
+
"Check if branch name is valid",
|
|
319
|
+
"Ensure Graphite is properly configured (gt repo init)",
|
|
320
|
+
f"Try creating the branch manually: gt create {branch}",
|
|
321
|
+
"Disable Graphite: erk config set use_graphite false",
|
|
322
|
+
],
|
|
323
|
+
)
|
|
324
|
+
ctx.git.checkout_branch(cwd, original_branch)
|
|
325
|
+
ctx.git.add_worktree(repo_root, path, branch=branch, ref=None, create_branch=False)
|
|
326
|
+
else:
|
|
327
|
+
ctx.git.add_worktree(repo_root, path, branch=branch, ref=ref, create_branch=True)
|
|
328
|
+
else:
|
|
329
|
+
ctx.git.add_worktree(repo_root, path, branch=None, ref=ref, create_branch=False)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def make_env_content(
|
|
333
|
+
cfg: LoadedConfig,
|
|
334
|
+
*,
|
|
335
|
+
worktree_path: Path,
|
|
336
|
+
repo_root: Path,
|
|
337
|
+
name: str,
|
|
338
|
+
) -> str:
|
|
339
|
+
"""Render .env content using config templates.
|
|
340
|
+
|
|
341
|
+
Substitution variables:
|
|
342
|
+
- {worktree_path} - Path to the worktree directory
|
|
343
|
+
- {repo_root} - Path to the git repository root
|
|
344
|
+
- {name} - Worktree name
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
variables: dict[str, str] = {
|
|
348
|
+
"worktree_path": str(worktree_path),
|
|
349
|
+
"repo_root": str(repo_root),
|
|
350
|
+
"name": name,
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
lines: list[str] = []
|
|
354
|
+
for key, template in cfg.env.items():
|
|
355
|
+
value = template.format(**variables)
|
|
356
|
+
# Quote value to be safe; dotenv parsers commonly accept quotes.
|
|
357
|
+
lines.append(f"{key}={quote_env_value(value)}")
|
|
358
|
+
|
|
359
|
+
# Always include these basics for convenience
|
|
360
|
+
lines.append(f"WORKTREE_PATH={quote_env_value(str(worktree_path))}")
|
|
361
|
+
lines.append(f"REPO_ROOT={quote_env_value(str(repo_root))}")
|
|
362
|
+
lines.append(f"WORKTREE_NAME={quote_env_value(name)}")
|
|
363
|
+
|
|
364
|
+
return "\n".join(lines) + "\n"
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def quote_env_value(value: str) -> str:
|
|
368
|
+
"""Return a quoted value suitable for .env files."""
|
|
369
|
+
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
|
370
|
+
return f'"{escaped}"'
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _create_json_response(
|
|
374
|
+
*,
|
|
375
|
+
worktree_name: str,
|
|
376
|
+
worktree_path: Path,
|
|
377
|
+
branch_name: str | None,
|
|
378
|
+
plan_file_path: Path | None,
|
|
379
|
+
status: str,
|
|
380
|
+
) -> str:
|
|
381
|
+
"""Generate JSON response for create command.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
worktree_name: Name of the worktree
|
|
385
|
+
worktree_path: Path to the worktree directory
|
|
386
|
+
branch_name: Git branch name (may be None if not available)
|
|
387
|
+
plan_file_path: Path to plan file if exists, None otherwise
|
|
388
|
+
status: Status string ("created" or "exists")
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
JSON string with worktree information
|
|
392
|
+
"""
|
|
393
|
+
return json.dumps(
|
|
394
|
+
{
|
|
395
|
+
"worktree_name": worktree_name,
|
|
396
|
+
"worktree_path": str(worktree_path),
|
|
397
|
+
"branch_name": branch_name,
|
|
398
|
+
"plan_file": str(plan_file_path) if plan_file_path else None,
|
|
399
|
+
"status": status,
|
|
400
|
+
}
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@click.command("create", cls=CommandWithHiddenOptions)
|
|
405
|
+
@click.argument("name", metavar="NAME", required=False)
|
|
406
|
+
@click.option(
|
|
407
|
+
"--branch",
|
|
408
|
+
"branch",
|
|
409
|
+
type=str,
|
|
410
|
+
help=("Branch name to create and check out in the worktree. Defaults to NAME if omitted."),
|
|
411
|
+
)
|
|
412
|
+
@click.option(
|
|
413
|
+
"--ref",
|
|
414
|
+
"ref",
|
|
415
|
+
type=str,
|
|
416
|
+
default=None,
|
|
417
|
+
help=("Git ref to base the worktree on (e.g. HEAD, origin/main). Defaults to HEAD if omitted."),
|
|
418
|
+
)
|
|
419
|
+
@click.option(
|
|
420
|
+
"--no-post",
|
|
421
|
+
is_flag=True,
|
|
422
|
+
help="Skip running post-create commands from config.toml.",
|
|
423
|
+
)
|
|
424
|
+
@click.option(
|
|
425
|
+
"--from-plan-file",
|
|
426
|
+
"from_plan_file",
|
|
427
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
428
|
+
help=(
|
|
429
|
+
"Path to a plan markdown file. Will derive worktree name from filename "
|
|
430
|
+
"and create .impl/ folder with plan.md in the worktree. "
|
|
431
|
+
"Worktree names are automatically suffixed with the current date (-YY-MM-DD) "
|
|
432
|
+
"and versioned if duplicates exist."
|
|
433
|
+
),
|
|
434
|
+
)
|
|
435
|
+
@click.option(
|
|
436
|
+
"--keep-plan-file",
|
|
437
|
+
is_flag=True,
|
|
438
|
+
help="Copy the plan file instead of moving it (requires --from-plan-file).",
|
|
439
|
+
)
|
|
440
|
+
@click.option(
|
|
441
|
+
"--from-plan",
|
|
442
|
+
"from_plan",
|
|
443
|
+
type=str,
|
|
444
|
+
help=(
|
|
445
|
+
"GitHub issue number or URL with erk-plan label. Fetches issue content "
|
|
446
|
+
"and creates worktree with .impl/ folder and .impl/issue.json metadata. "
|
|
447
|
+
"Worktree names are automatically suffixed with the current date (-YY-MM-DD) "
|
|
448
|
+
"and versioned if duplicates exist."
|
|
449
|
+
),
|
|
450
|
+
)
|
|
451
|
+
@click.option(
|
|
452
|
+
"--copy-plan",
|
|
453
|
+
is_flag=True,
|
|
454
|
+
default=False,
|
|
455
|
+
help=(
|
|
456
|
+
"Copy .impl directory from current worktree to new worktree. "
|
|
457
|
+
"Useful for multi-phase workflows where each phase builds on the previous plan. "
|
|
458
|
+
"Mutually exclusive with --from-plan."
|
|
459
|
+
),
|
|
460
|
+
)
|
|
461
|
+
@click.option(
|
|
462
|
+
"--from-current-branch",
|
|
463
|
+
is_flag=True,
|
|
464
|
+
help=(
|
|
465
|
+
"Move the current branch to the new worktree, then switch current worktree to --ref "
|
|
466
|
+
"(defaults to main/master). NAME defaults to current branch name."
|
|
467
|
+
),
|
|
468
|
+
)
|
|
469
|
+
@click.option(
|
|
470
|
+
"--from-branch",
|
|
471
|
+
"from_branch",
|
|
472
|
+
type=str,
|
|
473
|
+
default=None,
|
|
474
|
+
help=("Create worktree from an existing branch. NAME defaults to the branch name."),
|
|
475
|
+
)
|
|
476
|
+
@script_option
|
|
477
|
+
@click.option(
|
|
478
|
+
"--json",
|
|
479
|
+
"output_json",
|
|
480
|
+
is_flag=True,
|
|
481
|
+
help="Output JSON with worktree information instead of human-readable messages.",
|
|
482
|
+
)
|
|
483
|
+
@click.option(
|
|
484
|
+
"--stay",
|
|
485
|
+
is_flag=True,
|
|
486
|
+
help="Stay in current directory instead of switching to new worktree.",
|
|
487
|
+
)
|
|
488
|
+
@click.option(
|
|
489
|
+
"--skip-remote-check",
|
|
490
|
+
is_flag=True,
|
|
491
|
+
default=False,
|
|
492
|
+
help="Skip checking if branch exists on remote (for offline work)",
|
|
493
|
+
)
|
|
494
|
+
@click.pass_obj
|
|
495
|
+
def create_wt(
|
|
496
|
+
ctx: ErkContext,
|
|
497
|
+
name: str | None,
|
|
498
|
+
branch: str | None,
|
|
499
|
+
ref: str | None,
|
|
500
|
+
no_post: bool,
|
|
501
|
+
from_plan_file: Path | None,
|
|
502
|
+
keep_plan_file: bool,
|
|
503
|
+
from_plan: str | None,
|
|
504
|
+
copy_plan: bool,
|
|
505
|
+
from_current_branch: bool,
|
|
506
|
+
from_branch: str | None,
|
|
507
|
+
script: bool,
|
|
508
|
+
output_json: bool,
|
|
509
|
+
stay: bool,
|
|
510
|
+
skip_remote_check: bool,
|
|
511
|
+
) -> None:
|
|
512
|
+
"""Create a worktree and write a .env file.
|
|
513
|
+
|
|
514
|
+
Reads config.toml for env templates and post-create commands (if present).
|
|
515
|
+
If --from-plan-file is provided, derives name from the plan filename and creates
|
|
516
|
+
.impl/ folder in the worktree.
|
|
517
|
+
If --from-plan is provided, fetches the GitHub issue, validates the erk-plan label,
|
|
518
|
+
derives name from the issue title, and creates .impl/ folder with issue.json metadata.
|
|
519
|
+
If --from-current-branch is provided, moves the current branch to the new worktree.
|
|
520
|
+
If --from-branch is provided, creates a worktree from an existing branch.
|
|
521
|
+
|
|
522
|
+
By default, the command checks if a branch with the same name already exists on
|
|
523
|
+
the 'origin' remote. If a conflict is detected, the command fails with an error.
|
|
524
|
+
Use --skip-remote-check to bypass this validation for offline workflows.
|
|
525
|
+
"""
|
|
526
|
+
|
|
527
|
+
# Validate mutually exclusive options
|
|
528
|
+
flags_set = sum(
|
|
529
|
+
[
|
|
530
|
+
from_current_branch,
|
|
531
|
+
from_branch is not None,
|
|
532
|
+
from_plan_file is not None,
|
|
533
|
+
from_plan is not None,
|
|
534
|
+
]
|
|
535
|
+
)
|
|
536
|
+
Ensure.invariant(
|
|
537
|
+
flags_set <= 1,
|
|
538
|
+
"Cannot use multiple of: --from-current-branch, --from-branch, "
|
|
539
|
+
"--from-plan-file, --from-plan",
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
# Validate --json and --script are mutually exclusive
|
|
543
|
+
Ensure.invariant(not (output_json and script), "Cannot use both --json and --script")
|
|
544
|
+
|
|
545
|
+
# Validate --keep-plan-file requires --from-plan-file
|
|
546
|
+
Ensure.invariant(
|
|
547
|
+
not keep_plan_file or from_plan_file is not None,
|
|
548
|
+
"--keep-plan-file requires --from-plan-file",
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
# Validate --copy-plan and --from-plan-file/--from-plan are mutually exclusive
|
|
552
|
+
Ensure.invariant(
|
|
553
|
+
not (copy_plan and (from_plan_file is not None or from_plan is not None)),
|
|
554
|
+
"--copy-plan and --from-plan-file/--from-plan are mutually exclusive. "
|
|
555
|
+
"Use --copy-plan to copy from current worktree OR --from-plan-file <file> to use a plan "
|
|
556
|
+
"file OR --from-plan <number> to use a GitHub issue.",
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
# Note: --copy-plan validation is deferred until after repo discovery
|
|
560
|
+
# to ensure we check for .impl at the worktree root, not ctx.cwd
|
|
561
|
+
|
|
562
|
+
# Initialize variables used in conditional blocks (for type checking)
|
|
563
|
+
issue_number_parsed: int | None = None
|
|
564
|
+
plan: Plan | None = None
|
|
565
|
+
|
|
566
|
+
# Handle --from-current-branch flag
|
|
567
|
+
if from_current_branch:
|
|
568
|
+
# Get the current branch
|
|
569
|
+
current_branch = Ensure.not_none(
|
|
570
|
+
ctx.git.get_current_branch(ctx.cwd), "HEAD is detached (not on a branch)"
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
# Set branch to current branch and derive name if not provided
|
|
574
|
+
Ensure.invariant(
|
|
575
|
+
not branch, "Cannot specify --branch with --from-current-branch (uses current branch)."
|
|
576
|
+
)
|
|
577
|
+
branch = current_branch
|
|
578
|
+
|
|
579
|
+
if not name:
|
|
580
|
+
name = sanitize_worktree_name(current_branch)
|
|
581
|
+
|
|
582
|
+
# Handle --from-branch flag
|
|
583
|
+
elif from_branch:
|
|
584
|
+
Ensure.invariant(
|
|
585
|
+
not branch, "Cannot specify --branch with --from-branch (uses the specified branch)."
|
|
586
|
+
)
|
|
587
|
+
branch = from_branch
|
|
588
|
+
|
|
589
|
+
if not name:
|
|
590
|
+
name = sanitize_worktree_name(from_branch)
|
|
591
|
+
|
|
592
|
+
# Handle --from-plan-file flag
|
|
593
|
+
elif from_plan_file:
|
|
594
|
+
Ensure.invariant(
|
|
595
|
+
not name, "Cannot specify both NAME and --from-plan-file. Use one or the other."
|
|
596
|
+
)
|
|
597
|
+
# Derive name from plan filename (strip extension)
|
|
598
|
+
plan_stem = from_plan_file.stem # filename without extension
|
|
599
|
+
cleaned_stem = strip_plan_from_filename(plan_stem)
|
|
600
|
+
base_name = sanitize_worktree_name(cleaned_stem)
|
|
601
|
+
# Note: Apply ensure_unique_worktree_name() and truncation after getting erks_dir
|
|
602
|
+
name = base_name
|
|
603
|
+
|
|
604
|
+
# Handle --from-plan flag (GitHub issue)
|
|
605
|
+
elif from_plan:
|
|
606
|
+
Ensure.invariant(
|
|
607
|
+
not name, "Cannot specify both NAME and --from-plan. Use one or the other."
|
|
608
|
+
)
|
|
609
|
+
# Parse issue number from URL or plain number - raises click.ClickException if invalid
|
|
610
|
+
issue_number_parsed = parse_issue_identifier(from_plan)
|
|
611
|
+
# Note: name will be derived from issue title after fetching
|
|
612
|
+
# Defer fetch until after repo discovery below
|
|
613
|
+
name = None # Will be set after fetching issue
|
|
614
|
+
|
|
615
|
+
# Regular create (no special flags)
|
|
616
|
+
else:
|
|
617
|
+
# Allow --branch alone to derive name from branch
|
|
618
|
+
if not name and branch:
|
|
619
|
+
name = sanitize_worktree_name(branch)
|
|
620
|
+
elif not name:
|
|
621
|
+
user_output(
|
|
622
|
+
"Must provide NAME or --from-plan-file or --from-branch "
|
|
623
|
+
"or --from-current-branch or --from-plan or --branch option."
|
|
624
|
+
)
|
|
625
|
+
raise SystemExit(1) from None
|
|
626
|
+
|
|
627
|
+
# Track if name came from plan file (will need unique naming with date suffix)
|
|
628
|
+
is_plan_derived = from_plan_file is not None
|
|
629
|
+
|
|
630
|
+
# Discover repo context (needed for all paths)
|
|
631
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
632
|
+
ensure_erk_metadata_dir(repo)
|
|
633
|
+
|
|
634
|
+
# Validate .impl directory exists if --copy-plan is used (now that we have repo.root)
|
|
635
|
+
# .impl always lives at worktree/repo root
|
|
636
|
+
if copy_plan:
|
|
637
|
+
impl_source_check = repo.root / ".impl"
|
|
638
|
+
Ensure.path_is_dir(
|
|
639
|
+
ctx,
|
|
640
|
+
impl_source_check,
|
|
641
|
+
f"No .impl directory found at {repo.root}. "
|
|
642
|
+
"Use 'erk create --from-plan-file <file>' to create a worktree with a plan.",
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
# Track linked branch name and setup for issue-based worktrees
|
|
646
|
+
linked_branch_name: str | None = None
|
|
647
|
+
setup: IssueBranchSetup | None = None
|
|
648
|
+
|
|
649
|
+
# Handle issue fetching after repo discovery
|
|
650
|
+
if from_plan:
|
|
651
|
+
# Type narrowing: issue_number_parsed must be set if from_plan is True
|
|
652
|
+
assert issue_number_parsed is not None, (
|
|
653
|
+
"issue_number_parsed must be set when from_plan is True"
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
# Fetch plan using plan_store (composed from issues layer)
|
|
657
|
+
try:
|
|
658
|
+
plan = ctx.plan_store.get_plan(repo.root, str(issue_number_parsed))
|
|
659
|
+
except RuntimeError as e:
|
|
660
|
+
user_output(
|
|
661
|
+
click.style("Error: ", fg="red")
|
|
662
|
+
+ f"Failed to fetch issue #{issue_number_parsed}\n"
|
|
663
|
+
+ f"Details: {e}\n\n"
|
|
664
|
+
+ "Troubleshooting:\n"
|
|
665
|
+
+ " • Verify issue number is correct\n"
|
|
666
|
+
+ " • Check repository access: gh auth status\n"
|
|
667
|
+
+ f" • Try viewing manually: gh issue view {issue_number_parsed}"
|
|
668
|
+
)
|
|
669
|
+
raise SystemExit(1) from e
|
|
670
|
+
|
|
671
|
+
# Prepare and validate using shared helper (returns union type)
|
|
672
|
+
trunk_branch = ctx.git.detect_trunk_branch(repo.root)
|
|
673
|
+
result = prepare_plan_for_worktree(plan, ctx.time.now())
|
|
674
|
+
|
|
675
|
+
if isinstance(result, IssueValidationFailed):
|
|
676
|
+
user_output(click.style("Error: ", fg="red") + result.message)
|
|
677
|
+
raise SystemExit(1) from None
|
|
678
|
+
|
|
679
|
+
setup = result
|
|
680
|
+
for warning in setup.warnings:
|
|
681
|
+
user_output(click.style("Warning: ", fg="yellow") + warning)
|
|
682
|
+
|
|
683
|
+
# Create branch directly via git
|
|
684
|
+
ctx.git.create_branch(repo.root, setup.branch_name, trunk_branch)
|
|
685
|
+
user_output(f"Created branch: {setup.branch_name}")
|
|
686
|
+
|
|
687
|
+
# Track linked branch name for add_worktree call
|
|
688
|
+
linked_branch_name = setup.branch_name
|
|
689
|
+
|
|
690
|
+
# Use the branch name for the worktree name
|
|
691
|
+
name = setup.worktree_name
|
|
692
|
+
|
|
693
|
+
# At this point, name should always be set
|
|
694
|
+
assert name is not None, "name must be set by now"
|
|
695
|
+
|
|
696
|
+
# Sanitize the name to ensure consistency (truncate to 31 chars, normalize)
|
|
697
|
+
# This applies to user-provided names as well as derived names
|
|
698
|
+
# Note: sanitize_worktree_name is idempotent - preserves timestamp suffixes
|
|
699
|
+
if not is_plan_derived:
|
|
700
|
+
name = sanitize_worktree_name(name)
|
|
701
|
+
|
|
702
|
+
# Validate that name is not a reserved word
|
|
703
|
+
Ensure.invariant(
|
|
704
|
+
name.lower() != "root", '"root" is a reserved name and cannot be used for a worktree.'
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
cfg = ctx.local_config
|
|
708
|
+
trunk_branch = ctx.git.detect_trunk_branch(repo.root)
|
|
709
|
+
|
|
710
|
+
# Validate that name is not trunk branch (should use root worktree)
|
|
711
|
+
if name == trunk_branch:
|
|
712
|
+
user_output(
|
|
713
|
+
f'Error: "{name}" cannot be used as a worktree name.\n'
|
|
714
|
+
f"To switch to the {name} branch in the root repository, use:\n"
|
|
715
|
+
f" erk br co root",
|
|
716
|
+
)
|
|
717
|
+
raise SystemExit(1) from None
|
|
718
|
+
|
|
719
|
+
# Apply date prefix and uniqueness for plan-derived names
|
|
720
|
+
if is_plan_derived:
|
|
721
|
+
name = ensure_unique_worktree_name(name, repo.worktrees_dir, ctx.git)
|
|
722
|
+
|
|
723
|
+
wt_path = worktree_path_for(repo.worktrees_dir, name)
|
|
724
|
+
|
|
725
|
+
if ctx.git.path_exists(wt_path):
|
|
726
|
+
if output_json:
|
|
727
|
+
# For JSON output, emit a status: "exists" response with available info
|
|
728
|
+
existing_branch = ctx.git.get_current_branch(wt_path)
|
|
729
|
+
plan_path = get_impl_path(wt_path, git_ops=ctx.git)
|
|
730
|
+
json_response = _create_json_response(
|
|
731
|
+
worktree_name=name,
|
|
732
|
+
worktree_path=wt_path,
|
|
733
|
+
branch_name=existing_branch,
|
|
734
|
+
plan_file_path=plan_path,
|
|
735
|
+
status="exists",
|
|
736
|
+
)
|
|
737
|
+
user_output(json_response)
|
|
738
|
+
raise SystemExit(1) from None
|
|
739
|
+
else:
|
|
740
|
+
user_output(f"Worktree path already exists: {wt_path}")
|
|
741
|
+
raise SystemExit(1) from None
|
|
742
|
+
|
|
743
|
+
# Handle from-current-branch logic: switch current worktree first
|
|
744
|
+
to_branch = None
|
|
745
|
+
if from_current_branch:
|
|
746
|
+
current_branch = Ensure.not_none(
|
|
747
|
+
ctx.git.get_current_branch(ctx.cwd), "Unable to determine current branch"
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
# Determine preferred branch to checkout (prioritize Graphite parent)
|
|
751
|
+
parent_branch = (
|
|
752
|
+
ctx.graphite.get_parent_branch(ctx.git, repo.root, current_branch)
|
|
753
|
+
if current_branch
|
|
754
|
+
else None
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
if parent_branch:
|
|
758
|
+
# Prefer Graphite parent branch
|
|
759
|
+
to_branch = parent_branch
|
|
760
|
+
elif ref:
|
|
761
|
+
# Use ref if provided
|
|
762
|
+
to_branch = ref
|
|
763
|
+
else:
|
|
764
|
+
# Fall back to default branch (main/master)
|
|
765
|
+
to_branch = ctx.git.detect_trunk_branch(repo.root)
|
|
766
|
+
|
|
767
|
+
# Check for edge case: can't move main to worktree then switch to main
|
|
768
|
+
Ensure.invariant(
|
|
769
|
+
current_branch != to_branch,
|
|
770
|
+
f"Cannot use --from-current-branch when on '{current_branch}'.\n"
|
|
771
|
+
f"The current branch cannot be moved to a worktree and then checked out again.\n\n"
|
|
772
|
+
f"Alternatives:\n"
|
|
773
|
+
f" • Create a new branch: erk create {name}\n"
|
|
774
|
+
f" • Switch to a feature branch first, then use --from-current-branch\n"
|
|
775
|
+
f" • Use --from-branch to create from a different existing branch",
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
# Check if target branch is available (not checked out in another worktree)
|
|
779
|
+
checkout_path = ctx.git.is_branch_checked_out(repo.root, to_branch)
|
|
780
|
+
if checkout_path is not None:
|
|
781
|
+
# Target branch is in use, fall back to detached HEAD
|
|
782
|
+
ctx.git.checkout_detached(ctx.cwd, current_branch)
|
|
783
|
+
else:
|
|
784
|
+
# Target branch is available, checkout normally
|
|
785
|
+
ctx.git.checkout_branch(ctx.cwd, to_branch)
|
|
786
|
+
|
|
787
|
+
# Create worktree with existing branch
|
|
788
|
+
add_worktree(
|
|
789
|
+
ctx,
|
|
790
|
+
repo.root,
|
|
791
|
+
wt_path,
|
|
792
|
+
branch=branch,
|
|
793
|
+
ref=None,
|
|
794
|
+
use_existing_branch=True,
|
|
795
|
+
use_graphite=False,
|
|
796
|
+
skip_remote_check=skip_remote_check,
|
|
797
|
+
)
|
|
798
|
+
elif from_branch:
|
|
799
|
+
# Validate that we're not trying to create worktree for trunk branch
|
|
800
|
+
if branch == trunk_branch:
|
|
801
|
+
user_output(
|
|
802
|
+
f'Error: Cannot create worktree for trunk branch "{trunk_branch}".\n'
|
|
803
|
+
f"The trunk branch should be checked out in the root worktree.\n"
|
|
804
|
+
f"To switch to {trunk_branch}, use:\n"
|
|
805
|
+
f" erk br co root"
|
|
806
|
+
)
|
|
807
|
+
raise SystemExit(1) from None
|
|
808
|
+
|
|
809
|
+
# Create worktree with existing branch
|
|
810
|
+
add_worktree(
|
|
811
|
+
ctx,
|
|
812
|
+
repo.root,
|
|
813
|
+
wt_path,
|
|
814
|
+
branch=branch,
|
|
815
|
+
ref=None,
|
|
816
|
+
use_existing_branch=True,
|
|
817
|
+
use_graphite=False,
|
|
818
|
+
skip_remote_check=skip_remote_check,
|
|
819
|
+
)
|
|
820
|
+
elif linked_branch_name:
|
|
821
|
+
# Issue-based worktree: use the branch created for this issue
|
|
822
|
+
use_graphite = ctx.global_config.use_graphite if ctx.global_config else False
|
|
823
|
+
add_worktree(
|
|
824
|
+
ctx,
|
|
825
|
+
repo.root,
|
|
826
|
+
wt_path,
|
|
827
|
+
branch=linked_branch_name,
|
|
828
|
+
ref=trunk_branch, # Needed for Graphite tracking
|
|
829
|
+
use_existing_branch=True,
|
|
830
|
+
use_graphite=use_graphite, # Respect global config
|
|
831
|
+
skip_remote_check=skip_remote_check,
|
|
832
|
+
)
|
|
833
|
+
else:
|
|
834
|
+
# Create worktree via git. If no branch provided, derive a sensible default.
|
|
835
|
+
if branch is None:
|
|
836
|
+
branch = default_branch_for_worktree(name)
|
|
837
|
+
|
|
838
|
+
# Get graphite setting from global config
|
|
839
|
+
use_graphite = ctx.global_config.use_graphite if ctx.global_config else False
|
|
840
|
+
add_worktree(
|
|
841
|
+
ctx,
|
|
842
|
+
repo.root,
|
|
843
|
+
wt_path,
|
|
844
|
+
branch=branch,
|
|
845
|
+
ref=ref,
|
|
846
|
+
use_graphite=use_graphite,
|
|
847
|
+
use_existing_branch=False,
|
|
848
|
+
skip_remote_check=skip_remote_check,
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
# Write .env based on config
|
|
852
|
+
env_content = make_env_content(
|
|
853
|
+
cfg,
|
|
854
|
+
worktree_path=wt_path,
|
|
855
|
+
repo_root=repo.root,
|
|
856
|
+
name=name,
|
|
857
|
+
)
|
|
858
|
+
(wt_path / ".env").write_text(env_content, encoding="utf-8")
|
|
859
|
+
|
|
860
|
+
# Create impl folder if plan file provided
|
|
861
|
+
# Track impl folder destination: set to .impl/ path only if
|
|
862
|
+
# --from-plan-file or --from-plan was provided
|
|
863
|
+
impl_folder_destination: Path | None = None
|
|
864
|
+
if from_plan_file:
|
|
865
|
+
# Read plan content from source file
|
|
866
|
+
plan_content = from_plan_file.read_text(encoding="utf-8")
|
|
867
|
+
|
|
868
|
+
# Create .impl/ folder in new worktree
|
|
869
|
+
# Use overwrite=False since fresh worktree should not have .impl/
|
|
870
|
+
impl_folder_destination = create_impl_folder(wt_path, plan_content, overwrite=False)
|
|
871
|
+
|
|
872
|
+
# Handle --keep-plan-file flag
|
|
873
|
+
if keep_plan_file:
|
|
874
|
+
if not script and not output_json:
|
|
875
|
+
user_output(f"Copied plan to {impl_folder_destination}")
|
|
876
|
+
else:
|
|
877
|
+
from_plan_file.unlink() # Remove source file
|
|
878
|
+
if not script and not output_json:
|
|
879
|
+
user_output(f"Moved plan to {impl_folder_destination}")
|
|
880
|
+
|
|
881
|
+
# Create impl folder if GitHub issue provided
|
|
882
|
+
if from_plan:
|
|
883
|
+
# Type narrowing: setup must be set if from_plan is True
|
|
884
|
+
assert setup is not None, "setup must be set when from_plan is True"
|
|
885
|
+
|
|
886
|
+
# Create .impl/ folder in new worktree
|
|
887
|
+
# Use overwrite=False since fresh worktree should not have .impl/
|
|
888
|
+
impl_folder_destination = create_impl_folder(wt_path, setup.plan_content, overwrite=False)
|
|
889
|
+
|
|
890
|
+
# Create .impl/issue.json metadata using shared helper
|
|
891
|
+
save_issue_reference(
|
|
892
|
+
wt_path / ".impl",
|
|
893
|
+
setup.issue_number,
|
|
894
|
+
setup.issue_url,
|
|
895
|
+
setup.issue_title,
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
if not script and not output_json:
|
|
899
|
+
user_output(f"Created worktree from issue #{setup.issue_number}: {setup.issue_title}")
|
|
900
|
+
|
|
901
|
+
# Copy .impl directory if --copy-plan flag is set
|
|
902
|
+
if copy_plan:
|
|
903
|
+
import shutil
|
|
904
|
+
|
|
905
|
+
# .impl always lives at worktree/repo root
|
|
906
|
+
impl_source = repo.root / ".impl"
|
|
907
|
+
impl_dest = wt_path / ".impl"
|
|
908
|
+
|
|
909
|
+
# Copy entire directory
|
|
910
|
+
shutil.copytree(impl_source, impl_dest)
|
|
911
|
+
|
|
912
|
+
# Set impl_folder_destination for JSON response
|
|
913
|
+
impl_folder_destination = impl_dest
|
|
914
|
+
|
|
915
|
+
if not script and not output_json:
|
|
916
|
+
user_output(
|
|
917
|
+
" "
|
|
918
|
+
+ click.style("✓", fg="green")
|
|
919
|
+
+ f" Copied .impl from {click.style(str(repo.root), fg='yellow')}"
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
# Post-create commands (suppress output if JSON mode)
|
|
923
|
+
if not no_post and cfg.post_create_commands:
|
|
924
|
+
if not output_json:
|
|
925
|
+
user_output("Running post-create commands...")
|
|
926
|
+
run_commands_in_worktree(
|
|
927
|
+
ctx=ctx,
|
|
928
|
+
commands=cfg.post_create_commands,
|
|
929
|
+
worktree_path=wt_path,
|
|
930
|
+
shell=cfg.post_create_shell,
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
if script and not stay:
|
|
934
|
+
script_content = render_navigation_script(
|
|
935
|
+
wt_path,
|
|
936
|
+
repo.root,
|
|
937
|
+
comment="cd to new worktree",
|
|
938
|
+
success_message="✓ Went to new worktree.",
|
|
939
|
+
)
|
|
940
|
+
result = ctx.script_writer.write_activation_script(
|
|
941
|
+
script_content,
|
|
942
|
+
command_name="create",
|
|
943
|
+
comment=f"cd to {name}",
|
|
944
|
+
)
|
|
945
|
+
result.output_for_shell_integration()
|
|
946
|
+
elif output_json:
|
|
947
|
+
# Output JSON with worktree information
|
|
948
|
+
json_response = _create_json_response(
|
|
949
|
+
worktree_name=name,
|
|
950
|
+
worktree_path=wt_path,
|
|
951
|
+
branch_name=branch,
|
|
952
|
+
plan_file_path=impl_folder_destination,
|
|
953
|
+
status="created",
|
|
954
|
+
)
|
|
955
|
+
user_output(json_response)
|
|
956
|
+
elif stay:
|
|
957
|
+
# User explicitly opted out of navigation
|
|
958
|
+
user_output(f"Created worktree at {wt_path} checked out at branch '{branch}'")
|
|
959
|
+
else:
|
|
960
|
+
# Shell integration not detected - provide setup instructions
|
|
961
|
+
user_output(f"Created worktree at {wt_path} checked out at branch '{branch}'")
|
|
962
|
+
user_output("\nShell integration not detected. Run 'erk init --shell' to set up.")
|
|
963
|
+
user_output("Or use: source <(erk wt create --from-current-branch --script)")
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def run_commands_in_worktree(
|
|
967
|
+
*,
|
|
968
|
+
ctx: ErkContext,
|
|
969
|
+
commands: Iterable[str],
|
|
970
|
+
worktree_path: Path,
|
|
971
|
+
shell: str | None,
|
|
972
|
+
) -> None:
|
|
973
|
+
"""Run commands serially in the worktree directory.
|
|
974
|
+
|
|
975
|
+
Each command is executed in its own subprocess. If `shell` is provided, commands
|
|
976
|
+
run through that shell (e.g., "bash -lc <cmd>"). Otherwise, commands are tokenized
|
|
977
|
+
via `shlex.split` and run directly.
|
|
978
|
+
|
|
979
|
+
Args:
|
|
980
|
+
ctx: Erk context
|
|
981
|
+
commands: Iterable of commands to run
|
|
982
|
+
worktree_path: Path to worktree where commands should run
|
|
983
|
+
shell: Optional shell to use for command execution
|
|
984
|
+
"""
|
|
985
|
+
|
|
986
|
+
for cmd in commands:
|
|
987
|
+
# Output per-command diagnostic
|
|
988
|
+
ctx.feedback.info(f"Running: {cmd}")
|
|
989
|
+
cmd_list = [shell, "-lc", cmd] if shell else shlex.split(cmd)
|
|
990
|
+
run_with_error_reporting(
|
|
991
|
+
cmd_list,
|
|
992
|
+
cwd=worktree_path,
|
|
993
|
+
error_prefix="Post-create command failed",
|
|
994
|
+
troubleshooting=[
|
|
995
|
+
"The worktree was created successfully, but a post-create command failed",
|
|
996
|
+
"You can still use the worktree or re-run the command manually",
|
|
997
|
+
],
|
|
998
|
+
)
|