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,332 @@
|
|
|
1
|
+
"""Fast local-only worktree listing command."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from erk.cli.alias import alias
|
|
10
|
+
from erk.cli.core import discover_repo_context
|
|
11
|
+
from erk.core.context import ErkContext
|
|
12
|
+
from erk.core.display_utils import format_relative_time, get_pr_status_emoji
|
|
13
|
+
from erk.core.repo_discovery import RepoContext
|
|
14
|
+
from erk.core.worktree_utils import find_current_worktree
|
|
15
|
+
from erk_shared.git.abc import BranchSyncInfo
|
|
16
|
+
from erk_shared.github.types import GitHubRepoId, PullRequestInfo
|
|
17
|
+
from erk_shared.impl_folder import get_impl_path, read_issue_reference
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_sync_status(ctx: ErkContext, worktree_path: Path, branch: str | None) -> str:
|
|
21
|
+
"""Get sync status description for a branch.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
ctx: Erk context with git operations
|
|
25
|
+
worktree_path: Path to the worktree (used for git commands)
|
|
26
|
+
branch: Branch name, or None if detached HEAD
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Sync status: "current", "3↑", "2↓", "3↑ 2↓", or "-"
|
|
30
|
+
"""
|
|
31
|
+
if branch is None:
|
|
32
|
+
return "-"
|
|
33
|
+
|
|
34
|
+
# Get tracking info - returns (0, 0) if no tracking branch
|
|
35
|
+
ahead, behind = ctx.git.get_ahead_behind(worktree_path, branch)
|
|
36
|
+
|
|
37
|
+
# Check if this is "no tracking branch" case vs "up to date"
|
|
38
|
+
# The git interface returns (0, 0) for both cases, so we check if there's a tracking branch
|
|
39
|
+
# For now, treat (0, 0) as "current" since it's the most common case
|
|
40
|
+
if ahead == 0 and behind == 0:
|
|
41
|
+
return "current"
|
|
42
|
+
|
|
43
|
+
parts = []
|
|
44
|
+
if ahead > 0:
|
|
45
|
+
parts.append(f"{ahead}↑")
|
|
46
|
+
if behind > 0:
|
|
47
|
+
parts.append(f"{behind}↓")
|
|
48
|
+
return " ".join(parts)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _format_sync_from_batch(all_sync: dict[str, BranchSyncInfo], branch: str | None) -> str:
|
|
52
|
+
"""Format sync status from batch-fetched data.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
all_sync: Dict mapping branch name to BranchSyncInfo
|
|
56
|
+
branch: Branch name, or None if detached HEAD
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Sync status: "current", "3↑", "2↓", "3↑ 2↓", or "-"
|
|
60
|
+
"""
|
|
61
|
+
if branch is None:
|
|
62
|
+
return "-"
|
|
63
|
+
|
|
64
|
+
info = all_sync.get(branch)
|
|
65
|
+
if info is None:
|
|
66
|
+
return "-"
|
|
67
|
+
|
|
68
|
+
if info.ahead == 0 and info.behind == 0:
|
|
69
|
+
return "current"
|
|
70
|
+
|
|
71
|
+
parts = []
|
|
72
|
+
if info.ahead > 0:
|
|
73
|
+
parts.append(f"{info.ahead}↑")
|
|
74
|
+
if info.behind > 0:
|
|
75
|
+
parts.append(f"{info.behind}↓")
|
|
76
|
+
return " ".join(parts)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _get_impl_issue(
|
|
80
|
+
ctx: ErkContext, worktree_path: Path, branch: str | None = None
|
|
81
|
+
) -> tuple[str | None, str | None]:
|
|
82
|
+
"""Get impl issue number and URL from local sources.
|
|
83
|
+
|
|
84
|
+
Checks .impl/issue.json first, then git config fallback.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
ctx: Erk context with git operations
|
|
88
|
+
worktree_path: Path to the worktree directory
|
|
89
|
+
branch: Optional branch name (avoids redundant git subprocess call if provided)
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Tuple of (issue number formatted as "#{number}", issue URL) or (None, None) if not found
|
|
93
|
+
"""
|
|
94
|
+
# Try .impl/issue.json first
|
|
95
|
+
impl_path = get_impl_path(worktree_path, git_ops=ctx.git)
|
|
96
|
+
if impl_path is not None:
|
|
97
|
+
# impl_path points to plan.md, get the parent .impl/ directory
|
|
98
|
+
issue_ref = read_issue_reference(impl_path.parent)
|
|
99
|
+
if issue_ref is not None:
|
|
100
|
+
return f"#{issue_ref.issue_number}", issue_ref.issue_url
|
|
101
|
+
|
|
102
|
+
# Fallback to git config (no URL available from git config)
|
|
103
|
+
# If branch not provided, fetch it (for backwards compatibility)
|
|
104
|
+
if branch is None:
|
|
105
|
+
branch = ctx.git.get_current_branch(worktree_path)
|
|
106
|
+
if branch is not None:
|
|
107
|
+
issue_num = ctx.git.get_branch_issue(worktree_path, branch)
|
|
108
|
+
if issue_num is not None:
|
|
109
|
+
return f"#{issue_num}", None
|
|
110
|
+
|
|
111
|
+
return None, None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _format_pr_cell(
|
|
115
|
+
pr: PullRequestInfo | None, *, use_graphite: bool, graphite_url: str | None
|
|
116
|
+
) -> str:
|
|
117
|
+
"""Format PR cell for Rich table: emoji + clickable #number or "-".
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
pr: Pull request info, or None if no PR
|
|
121
|
+
use_graphite: If True, use Graphite URL; if False, use GitHub URL
|
|
122
|
+
graphite_url: Graphite URL for the PR (None if unavailable)
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Formatted string for table cell with Rich link markup
|
|
126
|
+
"""
|
|
127
|
+
if pr is None:
|
|
128
|
+
return "-"
|
|
129
|
+
|
|
130
|
+
emoji = get_pr_status_emoji(pr)
|
|
131
|
+
pr_text = f"#{pr.number}"
|
|
132
|
+
|
|
133
|
+
# Determine which URL to use
|
|
134
|
+
url = graphite_url if use_graphite else pr.url
|
|
135
|
+
|
|
136
|
+
# Make PR number clickable if URL is available using Rich [link=...] markup
|
|
137
|
+
if url:
|
|
138
|
+
return f"{emoji} [link={url}]{pr_text}[/link]"
|
|
139
|
+
else:
|
|
140
|
+
return f"{emoji} {pr_text}"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _format_impl_cell(issue_text: str | None, issue_url: str | None) -> str:
|
|
144
|
+
"""Format impl issue cell for Rich table with optional link.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
issue_text: Issue number formatted as "#{number}", or None
|
|
148
|
+
issue_url: Issue URL for clickable link, or None
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Formatted string for table cell with Rich link markup
|
|
152
|
+
"""
|
|
153
|
+
if issue_text is None:
|
|
154
|
+
return "-"
|
|
155
|
+
|
|
156
|
+
if issue_url:
|
|
157
|
+
return f"[link={issue_url}]{issue_text}[/link]"
|
|
158
|
+
else:
|
|
159
|
+
return issue_text
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _format_last_commit_cell(
|
|
163
|
+
ctx: ErkContext, repo_root: Path, branch: str | None, trunk: str
|
|
164
|
+
) -> str:
|
|
165
|
+
"""Format last commit time cell for Rich table.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
ctx: Erk context with git operations
|
|
169
|
+
repo_root: Path to the repository root
|
|
170
|
+
branch: Branch name, or None if detached HEAD
|
|
171
|
+
trunk: Trunk branch name
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Relative time string (e.g., "2d ago") or "-" if no unique commits
|
|
175
|
+
"""
|
|
176
|
+
if branch is None or branch == trunk:
|
|
177
|
+
return "-"
|
|
178
|
+
timestamp = ctx.git.get_branch_last_commit_time(repo_root, branch, trunk)
|
|
179
|
+
if timestamp is None:
|
|
180
|
+
return "-"
|
|
181
|
+
relative_time = format_relative_time(timestamp)
|
|
182
|
+
return relative_time if relative_time else "-"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _list_worktrees(ctx: ErkContext, *, show_last_commit: bool = False) -> None:
|
|
186
|
+
"""List worktrees with fast local-only data.
|
|
187
|
+
|
|
188
|
+
Shows a Rich table with columns:
|
|
189
|
+
- worktree: Directory name with cwd indicator
|
|
190
|
+
- branch: Branch name or (=) if matches worktree name
|
|
191
|
+
- pr: PR emoji + number from Graphite cache
|
|
192
|
+
- sync: Ahead/behind status
|
|
193
|
+
- impl: Issue number from .impl/issue.json
|
|
194
|
+
"""
|
|
195
|
+
# Use ctx.repo if it's a valid RepoContext, otherwise discover
|
|
196
|
+
if isinstance(ctx.repo, RepoContext):
|
|
197
|
+
repo = ctx.repo
|
|
198
|
+
else:
|
|
199
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
200
|
+
|
|
201
|
+
current_dir = ctx.cwd
|
|
202
|
+
|
|
203
|
+
# Get worktree info
|
|
204
|
+
worktrees = ctx.git.list_worktrees(repo.root)
|
|
205
|
+
|
|
206
|
+
# Fetch all branch sync info in a single git call (batch operation for performance)
|
|
207
|
+
all_sync_info = ctx.git.get_all_branch_sync_info(repo.root)
|
|
208
|
+
|
|
209
|
+
# Determine which worktree the user is currently in
|
|
210
|
+
wt_info = find_current_worktree(worktrees, current_dir)
|
|
211
|
+
current_worktree_path = wt_info.path if wt_info is not None else None
|
|
212
|
+
|
|
213
|
+
# Fetch PR information from Graphite cache (graceful degradation)
|
|
214
|
+
prs: dict[str, PullRequestInfo] = {}
|
|
215
|
+
if ctx.global_config and ctx.global_config.show_pr_info:
|
|
216
|
+
graphite_prs = ctx.graphite.get_prs_from_graphite(ctx.git, repo.root)
|
|
217
|
+
if graphite_prs:
|
|
218
|
+
prs = graphite_prs
|
|
219
|
+
# If Graphite cache is missing, prs stays empty - graceful degradation
|
|
220
|
+
|
|
221
|
+
# Determine use_graphite for URL selection
|
|
222
|
+
use_graphite = ctx.global_config.use_graphite if ctx.global_config else False
|
|
223
|
+
|
|
224
|
+
# Get trunk branch once if showing last commit
|
|
225
|
+
trunk = ctx.git.detect_trunk_branch(repo.root) if show_last_commit else ""
|
|
226
|
+
|
|
227
|
+
# Create Rich table
|
|
228
|
+
table = Table(show_header=True, header_style="bold", box=None)
|
|
229
|
+
table.add_column("worktree", style="cyan", no_wrap=True)
|
|
230
|
+
table.add_column("branch", style="yellow", no_wrap=True)
|
|
231
|
+
table.add_column("pr", no_wrap=True)
|
|
232
|
+
table.add_column("sync", no_wrap=True)
|
|
233
|
+
if show_last_commit:
|
|
234
|
+
table.add_column("last", no_wrap=True)
|
|
235
|
+
table.add_column("impl", no_wrap=True)
|
|
236
|
+
|
|
237
|
+
# Build rows starting with root worktree
|
|
238
|
+
root_branch = None
|
|
239
|
+
for wt in worktrees:
|
|
240
|
+
if wt.path == repo.root:
|
|
241
|
+
root_branch = wt.branch
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
# Root worktree row
|
|
245
|
+
is_current_root = repo.root == current_worktree_path
|
|
246
|
+
root_name = "root"
|
|
247
|
+
if is_current_root:
|
|
248
|
+
root_name = "[green bold]root[/green bold] ← (cwd)"
|
|
249
|
+
else:
|
|
250
|
+
root_name = "[green bold]root[/green bold]"
|
|
251
|
+
|
|
252
|
+
root_branch_display = f"({root_branch})" if root_branch else "-"
|
|
253
|
+
root_pr = prs.get(root_branch) if root_branch else None
|
|
254
|
+
root_graphite_url = (
|
|
255
|
+
ctx.graphite.get_graphite_url(GitHubRepoId(root_pr.owner, root_pr.repo), root_pr.number)
|
|
256
|
+
if root_pr
|
|
257
|
+
else None
|
|
258
|
+
)
|
|
259
|
+
root_pr_cell = _format_pr_cell(
|
|
260
|
+
root_pr, use_graphite=use_graphite, graphite_url=root_graphite_url
|
|
261
|
+
)
|
|
262
|
+
root_sync = _format_sync_from_batch(all_sync_info, root_branch)
|
|
263
|
+
root_impl_text, root_impl_url = _get_impl_issue(ctx, repo.root, root_branch)
|
|
264
|
+
root_impl_cell = _format_impl_cell(root_impl_text, root_impl_url)
|
|
265
|
+
|
|
266
|
+
if show_last_commit:
|
|
267
|
+
root_last_cell = _format_last_commit_cell(ctx, repo.root, root_branch, trunk)
|
|
268
|
+
table.add_row(
|
|
269
|
+
root_name, root_branch_display, root_pr_cell, root_sync, root_last_cell, root_impl_cell
|
|
270
|
+
)
|
|
271
|
+
else:
|
|
272
|
+
table.add_row(root_name, root_branch_display, root_pr_cell, root_sync, root_impl_cell)
|
|
273
|
+
|
|
274
|
+
# Non-root worktrees, sorted by name
|
|
275
|
+
non_root_worktrees = [wt for wt in worktrees if wt.path != repo.root]
|
|
276
|
+
for wt in sorted(non_root_worktrees, key=lambda w: w.path.name):
|
|
277
|
+
name = wt.path.name
|
|
278
|
+
branch = wt.branch
|
|
279
|
+
is_current = wt.path == current_worktree_path
|
|
280
|
+
|
|
281
|
+
# Format name with cwd indicator if current
|
|
282
|
+
if is_current:
|
|
283
|
+
name_cell = f"[cyan bold]{name}[/cyan bold] ← (cwd)"
|
|
284
|
+
else:
|
|
285
|
+
name_cell = f"[cyan]{name}[/cyan]"
|
|
286
|
+
|
|
287
|
+
# Branch display: (=) if matches name, else (branch-name)
|
|
288
|
+
if branch is not None:
|
|
289
|
+
branch_display = "(=)" if name == branch else f"({branch})"
|
|
290
|
+
else:
|
|
291
|
+
branch_display = "-"
|
|
292
|
+
|
|
293
|
+
# PR info from Graphite cache
|
|
294
|
+
pr = prs.get(branch) if branch else None
|
|
295
|
+
graphite_url = None
|
|
296
|
+
if pr:
|
|
297
|
+
graphite_url = ctx.graphite.get_graphite_url(GitHubRepoId(pr.owner, pr.repo), pr.number)
|
|
298
|
+
pr_cell = _format_pr_cell(pr, use_graphite=use_graphite, graphite_url=graphite_url)
|
|
299
|
+
|
|
300
|
+
# Sync status
|
|
301
|
+
sync_cell = _format_sync_from_batch(all_sync_info, branch)
|
|
302
|
+
|
|
303
|
+
# Impl issue
|
|
304
|
+
impl_text, impl_url = _get_impl_issue(ctx, wt.path, branch)
|
|
305
|
+
impl_cell = _format_impl_cell(impl_text, impl_url)
|
|
306
|
+
|
|
307
|
+
if show_last_commit:
|
|
308
|
+
last_cell = _format_last_commit_cell(ctx, repo.root, branch, trunk)
|
|
309
|
+
table.add_row(name_cell, branch_display, pr_cell, sync_cell, last_cell, impl_cell)
|
|
310
|
+
else:
|
|
311
|
+
table.add_row(name_cell, branch_display, pr_cell, sync_cell, impl_cell)
|
|
312
|
+
|
|
313
|
+
# Output table to stderr (consistent with user_output convention)
|
|
314
|
+
console = Console(stderr=True, force_terminal=True)
|
|
315
|
+
console.print(table)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@alias("ls")
|
|
319
|
+
@click.command("list")
|
|
320
|
+
@click.pass_obj
|
|
321
|
+
def list_wt(ctx: ErkContext) -> None:
|
|
322
|
+
"""List worktrees with branch, PR, sync, and implementation info.
|
|
323
|
+
|
|
324
|
+
Shows a fast local-only table with:
|
|
325
|
+
- worktree: Directory name
|
|
326
|
+
- branch: Branch name (or = if matches worktree name)
|
|
327
|
+
- pr: PR status from Graphite cache
|
|
328
|
+
- sync: Ahead/behind status vs tracking branch
|
|
329
|
+
- last: Last commit time
|
|
330
|
+
- impl: Implementation issue number
|
|
331
|
+
"""
|
|
332
|
+
_list_worktrees(ctx, show_last_commit=True)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from erk.cli.commands.completions import complete_worktree_names
|
|
4
|
+
from erk.cli.commands.wt.create_cmd import make_env_content
|
|
5
|
+
from erk.cli.core import discover_repo_context, worktree_path_for
|
|
6
|
+
from erk.cli.ensure import Ensure
|
|
7
|
+
from erk.core.context import ErkContext, create_context
|
|
8
|
+
from erk.core.repo_discovery import ensure_erk_metadata_dir
|
|
9
|
+
from erk_shared.naming import sanitize_worktree_name
|
|
10
|
+
from erk_shared.output.output import user_output
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command("rename")
|
|
14
|
+
@click.argument("old_name", metavar="OLD_NAME", shell_complete=complete_worktree_names)
|
|
15
|
+
@click.argument("new_name", metavar="NEW_NAME")
|
|
16
|
+
@click.option(
|
|
17
|
+
"--dry-run",
|
|
18
|
+
is_flag=True,
|
|
19
|
+
# dry_run=False: Allow destructive operations by default
|
|
20
|
+
default=False,
|
|
21
|
+
help="Print what would be done without executing destructive operations.",
|
|
22
|
+
)
|
|
23
|
+
@click.pass_obj
|
|
24
|
+
def rename_wt(ctx: ErkContext, old_name: str, new_name: str, dry_run: bool) -> None:
|
|
25
|
+
"""Rename a worktree directory.
|
|
26
|
+
|
|
27
|
+
Renames the worktree directory and updates git metadata.
|
|
28
|
+
The .env file is regenerated with updated paths and name.
|
|
29
|
+
"""
|
|
30
|
+
# Create dry-run context if needed
|
|
31
|
+
if dry_run:
|
|
32
|
+
ctx = create_context(dry_run=True)
|
|
33
|
+
|
|
34
|
+
# Sanitize new name
|
|
35
|
+
sanitized_new_name = sanitize_worktree_name(new_name)
|
|
36
|
+
|
|
37
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
38
|
+
ensure_erk_metadata_dir(repo)
|
|
39
|
+
|
|
40
|
+
old_path = worktree_path_for(repo.worktrees_dir, old_name)
|
|
41
|
+
new_path = worktree_path_for(repo.worktrees_dir, sanitized_new_name)
|
|
42
|
+
|
|
43
|
+
# Validate old worktree exists
|
|
44
|
+
Ensure.path_exists(ctx, old_path, f"Worktree not found: {old_path}")
|
|
45
|
+
|
|
46
|
+
# Validate new path doesn't already exist
|
|
47
|
+
Ensure.invariant(not ctx.git.path_exists(new_path), f"Destination already exists: {new_path}")
|
|
48
|
+
|
|
49
|
+
# Move via git worktree move
|
|
50
|
+
ctx.git.move_worktree(repo.root, old_path, new_path)
|
|
51
|
+
|
|
52
|
+
# Regenerate .env file with updated paths and name
|
|
53
|
+
cfg = ctx.local_config
|
|
54
|
+
env_content = make_env_content(
|
|
55
|
+
cfg, worktree_path=new_path, repo_root=repo.root, name=sanitized_new_name
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Write .env file (dry-run vs real)
|
|
59
|
+
env_file = new_path / ".env"
|
|
60
|
+
if ctx.dry_run:
|
|
61
|
+
user_output(f"[DRY RUN] Would write .env file: {env_file}")
|
|
62
|
+
else:
|
|
63
|
+
env_file.write_text(env_content, encoding="utf-8")
|
|
64
|
+
|
|
65
|
+
user_output(f"Renamed worktree: {old_name} -> {sanitized_new_name}")
|
|
66
|
+
user_output(str(new_path))
|
erk/cli/config.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import tomllib
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
# Re-export LoadedConfig from erk_shared for backwards compatibility
|
|
6
|
+
from erk_shared.context.types import LoadedConfig as LoadedConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class ProjectConfig:
|
|
11
|
+
"""In-memory representation of `.erk/project.toml`.
|
|
12
|
+
|
|
13
|
+
Example project.toml:
|
|
14
|
+
# Optional: custom name (defaults to directory name)
|
|
15
|
+
# name = "dagster-open-platform"
|
|
16
|
+
|
|
17
|
+
[env]
|
|
18
|
+
# Project-specific env vars (merged with repo-level)
|
|
19
|
+
DAGSTER_HOME = "{project_root}"
|
|
20
|
+
|
|
21
|
+
[post_create]
|
|
22
|
+
# Runs AFTER repo-level commands, FROM project directory
|
|
23
|
+
shell = "bash"
|
|
24
|
+
commands = [
|
|
25
|
+
"source .venv/bin/activate",
|
|
26
|
+
]
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
name: str | None # Custom project name (None = use directory name)
|
|
30
|
+
env: dict[str, str]
|
|
31
|
+
post_create_commands: list[str]
|
|
32
|
+
post_create_shell: str | None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class LegacyConfigLocation:
|
|
37
|
+
"""Information about a detected legacy config location."""
|
|
38
|
+
|
|
39
|
+
path: Path
|
|
40
|
+
description: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _parse_config_file(cfg_path: Path) -> LoadedConfig:
|
|
44
|
+
"""Parse a config.toml file into a LoadedConfig.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
cfg_path: Path to the config.toml file (must exist)
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
LoadedConfig with parsed values
|
|
51
|
+
"""
|
|
52
|
+
data = tomllib.loads(cfg_path.read_text(encoding="utf-8"))
|
|
53
|
+
env = {str(k): str(v) for k, v in data.get("env", {}).items()}
|
|
54
|
+
post = data.get("post_create", {})
|
|
55
|
+
commands = [str(x) for x in post.get("commands", [])]
|
|
56
|
+
shell = post.get("shell")
|
|
57
|
+
if shell is not None:
|
|
58
|
+
shell = str(shell)
|
|
59
|
+
|
|
60
|
+
# Parse [plans] section
|
|
61
|
+
plans = data.get("plans", {})
|
|
62
|
+
plans_repo = plans.get("repo")
|
|
63
|
+
if plans_repo is not None:
|
|
64
|
+
plans_repo = str(plans_repo)
|
|
65
|
+
|
|
66
|
+
# Parse [pool] section
|
|
67
|
+
pool = data.get("pool", {})
|
|
68
|
+
pool_size = pool.get("max_slots")
|
|
69
|
+
if pool_size is not None:
|
|
70
|
+
pool_size = int(pool_size)
|
|
71
|
+
|
|
72
|
+
# Parse [pool.checkout] section
|
|
73
|
+
pool_checkout = pool.get("checkout", {})
|
|
74
|
+
pool_checkout_commands = [str(x) for x in pool_checkout.get("commands", [])]
|
|
75
|
+
pool_checkout_shell = pool_checkout.get("shell")
|
|
76
|
+
if pool_checkout_shell is not None:
|
|
77
|
+
pool_checkout_shell = str(pool_checkout_shell)
|
|
78
|
+
|
|
79
|
+
return LoadedConfig(
|
|
80
|
+
env=env,
|
|
81
|
+
post_create_commands=commands,
|
|
82
|
+
post_create_shell=shell,
|
|
83
|
+
plans_repo=plans_repo,
|
|
84
|
+
pool_size=pool_size,
|
|
85
|
+
pool_checkout_commands=pool_checkout_commands,
|
|
86
|
+
pool_checkout_shell=pool_checkout_shell,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def detect_legacy_config_locations(
|
|
91
|
+
repo_root: Path, legacy_metadata_dir: Path | None
|
|
92
|
+
) -> list[LegacyConfigLocation]:
|
|
93
|
+
"""Detect legacy config.toml files that should be migrated.
|
|
94
|
+
|
|
95
|
+
Legacy locations:
|
|
96
|
+
1. <repo-root>/config.toml (created by 'erk init --repo')
|
|
97
|
+
2. ~/.erk/repos/<repo>/config.toml (created by 'erk init' without --repo)
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
repo_root: Path to the repository root
|
|
101
|
+
legacy_metadata_dir: Path to ~/.erk/repos/<repo>/ directory (or None)
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List of detected legacy config locations
|
|
105
|
+
"""
|
|
106
|
+
legacy_locations: list[LegacyConfigLocation] = []
|
|
107
|
+
|
|
108
|
+
# Check for config at repo root (created by 'erk init --repo')
|
|
109
|
+
repo_root_config = repo_root / "config.toml"
|
|
110
|
+
if repo_root_config.exists():
|
|
111
|
+
legacy_locations.append(
|
|
112
|
+
LegacyConfigLocation(
|
|
113
|
+
path=repo_root_config,
|
|
114
|
+
description="repo root (created by 'erk init --repo')",
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Check for config in ~/.erk/repos/<repo>/ (created by 'erk init')
|
|
119
|
+
if legacy_metadata_dir is not None:
|
|
120
|
+
metadata_dir_config = legacy_metadata_dir / "config.toml"
|
|
121
|
+
if metadata_dir_config.exists():
|
|
122
|
+
legacy_locations.append(
|
|
123
|
+
LegacyConfigLocation(
|
|
124
|
+
path=metadata_dir_config,
|
|
125
|
+
description=f"~/.erk/repos/ metadata dir ({legacy_metadata_dir})",
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return legacy_locations
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def load_config(repo_root: Path) -> LoadedConfig:
|
|
133
|
+
"""Load config.toml for a repository.
|
|
134
|
+
|
|
135
|
+
Location: <repo-root>/.erk/config.toml
|
|
136
|
+
|
|
137
|
+
Example config:
|
|
138
|
+
[env]
|
|
139
|
+
DAGSTER_GIT_REPO_DIR = "{worktree_path}"
|
|
140
|
+
|
|
141
|
+
[post_create]
|
|
142
|
+
shell = "bash"
|
|
143
|
+
commands = [
|
|
144
|
+
"uv venv",
|
|
145
|
+
"uv run make dev_install",
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
Note: Legacy config locations (repo root, ~/.erk/repos/) are NOT supported here.
|
|
149
|
+
Run 'erk doctor' to detect legacy configs that need migration.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
repo_root: Path to the repository root
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
LoadedConfig with parsed values or defaults if no config found
|
|
156
|
+
"""
|
|
157
|
+
config_path = repo_root / ".erk" / "config.toml"
|
|
158
|
+
if config_path.exists():
|
|
159
|
+
return _parse_config_file(config_path)
|
|
160
|
+
|
|
161
|
+
# No config found
|
|
162
|
+
return LoadedConfig(
|
|
163
|
+
env={},
|
|
164
|
+
post_create_commands=[],
|
|
165
|
+
post_create_shell=None,
|
|
166
|
+
plans_repo=None,
|
|
167
|
+
pool_size=None,
|
|
168
|
+
pool_checkout_commands=[],
|
|
169
|
+
pool_checkout_shell=None,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def load_project_config(project_root: Path) -> ProjectConfig:
|
|
174
|
+
"""Load project.toml from the project's .erk directory.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
project_root: Path to the project root directory
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
ProjectConfig with parsed values, or defaults if file doesn't exist
|
|
181
|
+
"""
|
|
182
|
+
cfg_path = project_root / ".erk" / "project.toml"
|
|
183
|
+
if not cfg_path.exists():
|
|
184
|
+
return ProjectConfig(name=None, env={}, post_create_commands=[], post_create_shell=None)
|
|
185
|
+
|
|
186
|
+
data = tomllib.loads(cfg_path.read_text(encoding="utf-8"))
|
|
187
|
+
|
|
188
|
+
# Optional name field
|
|
189
|
+
name = data.get("name")
|
|
190
|
+
if name is not None:
|
|
191
|
+
name = str(name)
|
|
192
|
+
|
|
193
|
+
# Env vars
|
|
194
|
+
env = {str(k): str(v) for k, v in data.get("env", {}).items()}
|
|
195
|
+
|
|
196
|
+
# Post-create commands
|
|
197
|
+
post = data.get("post_create", {})
|
|
198
|
+
commands = [str(x) for x in post.get("commands", [])]
|
|
199
|
+
shell = post.get("shell")
|
|
200
|
+
if shell is not None:
|
|
201
|
+
shell = str(shell)
|
|
202
|
+
|
|
203
|
+
return ProjectConfig(name=name, env=env, post_create_commands=commands, post_create_shell=shell)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def merge_configs(repo_config: LoadedConfig, project_config: ProjectConfig) -> LoadedConfig:
|
|
207
|
+
"""Merge repo-level and project-level configs.
|
|
208
|
+
|
|
209
|
+
Merge rules:
|
|
210
|
+
- env: Project values override repo values (dict merge)
|
|
211
|
+
- post_create_commands: Repo commands run first, then project commands (list concat)
|
|
212
|
+
- post_create_shell: Project shell overrides repo shell if set
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
repo_config: Repository-level configuration
|
|
216
|
+
project_config: Project-level configuration
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Merged LoadedConfig
|
|
220
|
+
"""
|
|
221
|
+
# Merge env: project overrides repo
|
|
222
|
+
merged_env = {**repo_config.env, **project_config.env}
|
|
223
|
+
|
|
224
|
+
# Concat commands: repo first, then project
|
|
225
|
+
merged_commands = repo_config.post_create_commands + project_config.post_create_commands
|
|
226
|
+
|
|
227
|
+
# Shell: project overrides if set
|
|
228
|
+
merged_shell = (
|
|
229
|
+
project_config.post_create_shell
|
|
230
|
+
if project_config.post_create_shell is not None
|
|
231
|
+
else repo_config.post_create_shell
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return LoadedConfig(
|
|
235
|
+
env=merged_env,
|
|
236
|
+
post_create_commands=merged_commands,
|
|
237
|
+
post_create_shell=merged_shell,
|
|
238
|
+
plans_repo=repo_config.plans_repo,
|
|
239
|
+
pool_size=repo_config.pool_size, # Pool is repo-level only, no project override
|
|
240
|
+
pool_checkout_commands=repo_config.pool_checkout_commands,
|
|
241
|
+
pool_checkout_shell=repo_config.pool_checkout_shell,
|
|
242
|
+
)
|
erk/cli/constants.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Shared constants for erk CLI commands."""
|
|
2
|
+
|
|
3
|
+
# GitHub issue label for erk plans
|
|
4
|
+
ERK_PLAN_LABEL = "erk-plan"
|
|
5
|
+
|
|
6
|
+
# GitHub Actions workflow for remote implementation dispatch
|
|
7
|
+
DISPATCH_WORKFLOW_NAME = "erk-impl.yml"
|
|
8
|
+
DISPATCH_WORKFLOW_METADATA_NAME = "erk-impl"
|
|
9
|
+
|
|
10
|
+
# Workflow names that trigger the autofix workflow
|
|
11
|
+
# Must match the `name:` field in each .yml file (which should match filename without .yml)
|
|
12
|
+
AUTOFIX_TRIGGER_WORKFLOWS = frozenset(
|
|
13
|
+
{
|
|
14
|
+
"python-format",
|
|
15
|
+
"lint",
|
|
16
|
+
"docs-check",
|
|
17
|
+
"markdown-format",
|
|
18
|
+
}
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Documentation extraction tracking label
|
|
22
|
+
DOCS_EXTRACTED_LABEL = "docs-extracted"
|
|
23
|
+
DOCS_EXTRACTED_LABEL_DESCRIPTION = "Session logs analyzed for documentation improvements"
|
|
24
|
+
DOCS_EXTRACTED_LABEL_COLOR = "5319E7" # Purple
|
|
25
|
+
|
|
26
|
+
# Extraction plan label (for plans that extract documentation from sessions)
|
|
27
|
+
ERK_EXTRACTION_LABEL = "erk-extraction"
|
|
28
|
+
ERK_EXTRACTION_LABEL_DESCRIPTION = "Documentation extraction plan"
|
|
29
|
+
ERK_EXTRACTION_LABEL_COLOR = "D93F0B" # Orange-red
|