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,260 @@
|
|
|
1
|
+
"""Check artifact sync status."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from erk.artifacts.artifact_health import (
|
|
8
|
+
ArtifactStatus,
|
|
9
|
+
find_missing_artifacts,
|
|
10
|
+
find_orphaned_artifacts,
|
|
11
|
+
get_artifact_health,
|
|
12
|
+
is_erk_managed,
|
|
13
|
+
)
|
|
14
|
+
from erk.artifacts.discovery import discover_artifacts
|
|
15
|
+
from erk.artifacts.models import InstalledArtifact
|
|
16
|
+
from erk.artifacts.staleness import check_staleness
|
|
17
|
+
from erk.artifacts.state import load_artifact_state
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _display_orphan_warnings(orphans: dict[str, list[str]]) -> None:
|
|
21
|
+
"""Display orphan warnings with remediation commands."""
|
|
22
|
+
total_orphans = sum(len(files) for files in orphans.values())
|
|
23
|
+
click.echo(click.style("⚠️ ", fg="yellow") + f"Found {total_orphans} orphaned artifact(s)")
|
|
24
|
+
click.echo(" Orphaned files (not in current erk package):")
|
|
25
|
+
for folder, files in sorted(orphans.items()):
|
|
26
|
+
click.echo(f" {folder}/:")
|
|
27
|
+
for filename in sorted(files):
|
|
28
|
+
click.echo(f" - {filename}")
|
|
29
|
+
|
|
30
|
+
click.echo("")
|
|
31
|
+
click.echo(" To remove:")
|
|
32
|
+
for folder, files in sorted(orphans.items()):
|
|
33
|
+
for filename in sorted(files):
|
|
34
|
+
# Workflows are in .github/, not .claude/
|
|
35
|
+
if folder.startswith(".github"):
|
|
36
|
+
click.echo(f" rm {folder}/{filename}")
|
|
37
|
+
else:
|
|
38
|
+
click.echo(f" rm .claude/{folder}/{filename}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _display_missing_warnings(missing: dict[str, list[str]]) -> None:
|
|
42
|
+
"""Display missing artifact warnings."""
|
|
43
|
+
total_missing = sum(len(files) for files in missing.values())
|
|
44
|
+
click.echo(click.style("⚠️ ", fg="yellow") + f"Found {total_missing} missing artifact(s)")
|
|
45
|
+
click.echo(" Missing from project:")
|
|
46
|
+
for folder, files in sorted(missing.items()):
|
|
47
|
+
click.echo(f" {folder}:")
|
|
48
|
+
for filename in sorted(files):
|
|
49
|
+
click.echo(f" - {filename}")
|
|
50
|
+
click.echo("")
|
|
51
|
+
click.echo(" Run 'erk artifact sync' to install missing artifacts")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _format_artifact_path(artifact: InstalledArtifact) -> str:
|
|
55
|
+
"""Format artifact as a display path string."""
|
|
56
|
+
if artifact.artifact_type == "command":
|
|
57
|
+
# Commands can be namespaced (local:foo) or top-level (foo)
|
|
58
|
+
if ":" in artifact.name:
|
|
59
|
+
namespace, name = artifact.name.split(":", 1)
|
|
60
|
+
return f"commands/{namespace}/{name}.md"
|
|
61
|
+
return f"commands/{artifact.name}.md"
|
|
62
|
+
if artifact.artifact_type == "skill":
|
|
63
|
+
return f"skills/{artifact.name}"
|
|
64
|
+
if artifact.artifact_type == "agent":
|
|
65
|
+
return f"agents/{artifact.name}"
|
|
66
|
+
if artifact.artifact_type == "workflow":
|
|
67
|
+
return f".github/workflows/{artifact.name}.yml"
|
|
68
|
+
if artifact.artifact_type == "hook":
|
|
69
|
+
return f"hooks/{artifact.name} (settings.json)"
|
|
70
|
+
return artifact.name
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _display_installed_artifacts(project_dir: Path) -> None:
|
|
74
|
+
"""Display list of artifacts actually installed in project."""
|
|
75
|
+
artifacts = discover_artifacts(project_dir)
|
|
76
|
+
|
|
77
|
+
if not artifacts:
|
|
78
|
+
click.echo(" (no artifacts installed)")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
for artifact in artifacts:
|
|
82
|
+
suffix = "" if is_erk_managed(artifact) else " (unmanaged)"
|
|
83
|
+
click.echo(f" {_format_artifact_path(artifact)}{suffix}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _format_artifact_status(artifact: ArtifactStatus, show_hashes: bool) -> str:
|
|
87
|
+
"""Format artifact status for verbose output.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
artifact: The artifact status to format
|
|
91
|
+
show_hashes: If True, show hash comparison details
|
|
92
|
+
"""
|
|
93
|
+
if artifact.status == "up-to-date":
|
|
94
|
+
icon = click.style("✓", fg="green")
|
|
95
|
+
detail = f"{artifact.current_version} (up-to-date)"
|
|
96
|
+
elif artifact.status == "changed-upstream":
|
|
97
|
+
icon = click.style("⚠", fg="yellow")
|
|
98
|
+
if artifact.installed_version:
|
|
99
|
+
detail = f"{artifact.installed_version} → {artifact.current_version} (changed upstream)"
|
|
100
|
+
else:
|
|
101
|
+
detail = f"→ {artifact.current_version} (new in this version)"
|
|
102
|
+
elif artifact.status == "locally-modified":
|
|
103
|
+
icon = click.style("⚠", fg="yellow")
|
|
104
|
+
detail = f"{artifact.current_version} (locally modified)"
|
|
105
|
+
else: # not-installed
|
|
106
|
+
icon = click.style("✗", fg="red")
|
|
107
|
+
detail = "(not installed)"
|
|
108
|
+
|
|
109
|
+
lines = [f" {icon} {artifact.name}: {detail}"]
|
|
110
|
+
|
|
111
|
+
if show_hashes:
|
|
112
|
+
# Show state.toml values
|
|
113
|
+
if artifact.installed_version is not None and artifact.installed_hash is not None:
|
|
114
|
+
ver = artifact.installed_version
|
|
115
|
+
h = artifact.installed_hash
|
|
116
|
+
lines.append(f" state.toml: version={ver}, hash={h}")
|
|
117
|
+
else:
|
|
118
|
+
lines.append(" state.toml: (not tracked)")
|
|
119
|
+
|
|
120
|
+
# Show current source values
|
|
121
|
+
if artifact.current_hash is not None:
|
|
122
|
+
ver = artifact.current_version
|
|
123
|
+
h = artifact.current_hash
|
|
124
|
+
lines.append(f" source: version={ver}, hash={h}")
|
|
125
|
+
else:
|
|
126
|
+
lines.append(" source: (not installed)")
|
|
127
|
+
|
|
128
|
+
return "\n".join(lines)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _display_verbose_status(project_dir: Path, show_hashes: bool) -> bool:
|
|
132
|
+
"""Display per-artifact status breakdown.
|
|
133
|
+
|
|
134
|
+
Shows two sections:
|
|
135
|
+
1. Erk-managed artifacts with version tracking status
|
|
136
|
+
2. Project artifacts (local commands, custom skills, etc.)
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
project_dir: Path to the project root
|
|
140
|
+
show_hashes: If True, show hash comparison details for each artifact
|
|
141
|
+
|
|
142
|
+
Returns True if any erk-managed artifacts need attention (not up-to-date).
|
|
143
|
+
"""
|
|
144
|
+
state = load_artifact_state(project_dir)
|
|
145
|
+
saved_files = dict(state.files) if state else {}
|
|
146
|
+
|
|
147
|
+
health_result = get_artifact_health(project_dir, saved_files)
|
|
148
|
+
|
|
149
|
+
if health_result.skipped_reason is not None:
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
click.echo("")
|
|
153
|
+
click.echo("Erk-managed artifacts:")
|
|
154
|
+
|
|
155
|
+
has_issues = False
|
|
156
|
+
for artifact in health_result.artifacts:
|
|
157
|
+
click.echo(_format_artifact_status(artifact, show_hashes))
|
|
158
|
+
if artifact.status != "up-to-date":
|
|
159
|
+
has_issues = True
|
|
160
|
+
|
|
161
|
+
# Also show project-specific artifacts (non-erk-managed)
|
|
162
|
+
all_artifacts = discover_artifacts(project_dir)
|
|
163
|
+
project_artifacts = [a for a in all_artifacts if not is_erk_managed(a)]
|
|
164
|
+
|
|
165
|
+
if project_artifacts:
|
|
166
|
+
click.echo("")
|
|
167
|
+
click.echo("Project artifacts (unmanaged):")
|
|
168
|
+
for artifact in project_artifacts:
|
|
169
|
+
click.echo(f" {_format_artifact_path(artifact)}")
|
|
170
|
+
|
|
171
|
+
return has_issues
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@click.command("check")
|
|
175
|
+
@click.option(
|
|
176
|
+
"--verbose",
|
|
177
|
+
"-v",
|
|
178
|
+
count=True,
|
|
179
|
+
help="Show per-artifact status. Use -vv to also show hash comparisons.",
|
|
180
|
+
)
|
|
181
|
+
def check_cmd(verbose: int) -> None:
|
|
182
|
+
"""Check if artifacts are in sync with erk version.
|
|
183
|
+
|
|
184
|
+
Compares the version recorded in .erk/state.toml against
|
|
185
|
+
the currently installed erk package version. Also checks
|
|
186
|
+
for orphaned files that should be removed.
|
|
187
|
+
|
|
188
|
+
Examples:
|
|
189
|
+
|
|
190
|
+
\b
|
|
191
|
+
# Check sync status
|
|
192
|
+
erk artifact check
|
|
193
|
+
|
|
194
|
+
\b
|
|
195
|
+
# Show per-artifact breakdown
|
|
196
|
+
erk artifact check -v
|
|
197
|
+
|
|
198
|
+
\b
|
|
199
|
+
# Show hash comparisons (state.toml vs source)
|
|
200
|
+
erk artifact check -vv
|
|
201
|
+
"""
|
|
202
|
+
project_dir = Path.cwd()
|
|
203
|
+
|
|
204
|
+
staleness_result = check_staleness(project_dir)
|
|
205
|
+
orphan_result = find_orphaned_artifacts(project_dir)
|
|
206
|
+
missing_result = find_missing_artifacts(project_dir)
|
|
207
|
+
|
|
208
|
+
has_errors = False
|
|
209
|
+
show_per_artifact = verbose >= 1
|
|
210
|
+
show_hashes = verbose >= 2
|
|
211
|
+
|
|
212
|
+
# Check staleness
|
|
213
|
+
if staleness_result.reason == "erk-repo":
|
|
214
|
+
click.echo(click.style("✓ ", fg="green") + "Development mode (artifacts read from source)")
|
|
215
|
+
if not show_per_artifact:
|
|
216
|
+
_display_installed_artifacts(project_dir)
|
|
217
|
+
elif staleness_result.reason == "not-initialized":
|
|
218
|
+
click.echo(click.style("⚠️ ", fg="yellow") + "Artifacts not initialized")
|
|
219
|
+
click.echo(f" Current erk version: {staleness_result.current_version}")
|
|
220
|
+
click.echo(" Run 'erk artifact sync' to initialize")
|
|
221
|
+
has_errors = True
|
|
222
|
+
elif staleness_result.reason == "version-mismatch":
|
|
223
|
+
click.echo(click.style("⚠️ ", fg="yellow") + "Artifacts out of sync")
|
|
224
|
+
click.echo(f" Installed version: {staleness_result.installed_version}")
|
|
225
|
+
click.echo(f" Current erk version: {staleness_result.current_version}")
|
|
226
|
+
click.echo(" Run 'erk artifact sync' to update")
|
|
227
|
+
has_errors = True
|
|
228
|
+
else:
|
|
229
|
+
click.echo(
|
|
230
|
+
click.style("✓ ", fg="green")
|
|
231
|
+
+ f"Artifacts up to date (v{staleness_result.current_version})"
|
|
232
|
+
)
|
|
233
|
+
if not show_per_artifact:
|
|
234
|
+
_display_installed_artifacts(project_dir)
|
|
235
|
+
|
|
236
|
+
# Show verbose per-artifact breakdown if requested
|
|
237
|
+
if show_per_artifact and staleness_result.reason != "not-initialized":
|
|
238
|
+
verbose_has_issues = _display_verbose_status(project_dir, show_hashes)
|
|
239
|
+
# In dev mode (erk-repo), don't report issues - artifacts come from source
|
|
240
|
+
if verbose_has_issues and staleness_result.reason != "erk-repo":
|
|
241
|
+
has_errors = True
|
|
242
|
+
|
|
243
|
+
# Check for orphans (skip if erk-repo or no-claude-dir)
|
|
244
|
+
if orphan_result.skipped_reason is None:
|
|
245
|
+
if orphan_result.orphans:
|
|
246
|
+
_display_orphan_warnings(orphan_result.orphans)
|
|
247
|
+
has_errors = True
|
|
248
|
+
else:
|
|
249
|
+
click.echo(click.style("✓ ", fg="green") + "No orphaned artifacts")
|
|
250
|
+
|
|
251
|
+
# Check for missing artifacts (skip if erk-repo or no-claude-dir)
|
|
252
|
+
if missing_result.skipped_reason is None:
|
|
253
|
+
if missing_result.missing:
|
|
254
|
+
_display_missing_warnings(missing_result.missing)
|
|
255
|
+
has_errors = True
|
|
256
|
+
else:
|
|
257
|
+
click.echo(click.style("✓ ", fg="green") + "No missing artifacts")
|
|
258
|
+
|
|
259
|
+
if has_errors:
|
|
260
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Artifact command group for managing .claude/ artifacts."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from erk.cli.commands.artifact.check import check_cmd
|
|
6
|
+
from erk.cli.commands.artifact.list_cmd import list_cmd
|
|
7
|
+
from erk.cli.commands.artifact.show import show_cmd
|
|
8
|
+
from erk.cli.commands.artifact.sync_cmd import sync_cmd
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group(name="artifact")
|
|
12
|
+
def artifact_group() -> None:
|
|
13
|
+
"""Manage erk-managed artifacts.
|
|
14
|
+
|
|
15
|
+
Artifacts are Claude Code extensions like skills, commands, agents,
|
|
16
|
+
and workflows stored in your project's .claude/ and .github/ directories.
|
|
17
|
+
|
|
18
|
+
\b
|
|
19
|
+
Commands:
|
|
20
|
+
list List installed artifacts
|
|
21
|
+
show Display artifact content
|
|
22
|
+
sync Sync artifacts from erk package
|
|
23
|
+
check Check if artifacts are up to date
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Register subcommands
|
|
28
|
+
artifact_group.add_command(list_cmd)
|
|
29
|
+
artifact_group.add_command(show_cmd)
|
|
30
|
+
artifact_group.add_command(sync_cmd)
|
|
31
|
+
artifact_group.add_command(check_cmd)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""List artifacts installed in the project."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import cast
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from erk.artifacts.artifact_health import is_erk_managed
|
|
9
|
+
from erk.artifacts.discovery import discover_artifacts
|
|
10
|
+
from erk.artifacts.models import ArtifactType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command("list")
|
|
14
|
+
@click.option(
|
|
15
|
+
"--type",
|
|
16
|
+
"artifact_type",
|
|
17
|
+
type=click.Choice(["skill", "command", "agent", "workflow", "hook"]),
|
|
18
|
+
help="Filter by artifact type",
|
|
19
|
+
)
|
|
20
|
+
@click.option("--verbose", "-v", is_flag=True, help="Show additional details")
|
|
21
|
+
def list_cmd(artifact_type: str | None, verbose: bool) -> None:
|
|
22
|
+
"""List all artifacts in project.
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
|
|
26
|
+
\b
|
|
27
|
+
# List all artifacts
|
|
28
|
+
erk artifact list
|
|
29
|
+
|
|
30
|
+
\b
|
|
31
|
+
# List only skills
|
|
32
|
+
erk artifact list --type skill
|
|
33
|
+
|
|
34
|
+
\b
|
|
35
|
+
# List with details
|
|
36
|
+
erk artifact list --verbose
|
|
37
|
+
"""
|
|
38
|
+
project_dir = Path.cwd()
|
|
39
|
+
claude_dir = project_dir / ".claude"
|
|
40
|
+
if not claude_dir.exists():
|
|
41
|
+
click.echo("No .claude/ directory found in current directory", err=True)
|
|
42
|
+
raise SystemExit(1)
|
|
43
|
+
|
|
44
|
+
artifacts = discover_artifacts(project_dir)
|
|
45
|
+
|
|
46
|
+
# Filter by type if specified
|
|
47
|
+
if artifact_type is not None:
|
|
48
|
+
assert artifact_type in ("skill", "command", "agent", "workflow", "hook")
|
|
49
|
+
typed_filter = cast(ArtifactType, artifact_type)
|
|
50
|
+
artifacts = [a for a in artifacts if a.artifact_type == typed_filter]
|
|
51
|
+
|
|
52
|
+
if not artifacts:
|
|
53
|
+
if artifact_type:
|
|
54
|
+
click.echo(f"No {artifact_type} artifacts found")
|
|
55
|
+
else:
|
|
56
|
+
click.echo("No artifacts found")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
# Group by type for display
|
|
60
|
+
current_type: str | None = None
|
|
61
|
+
for artifact in artifacts:
|
|
62
|
+
if artifact.artifact_type != current_type:
|
|
63
|
+
if current_type is not None:
|
|
64
|
+
click.echo("") # Blank line between types
|
|
65
|
+
current_type = artifact.artifact_type
|
|
66
|
+
# Special headers for types with non-standard locations/display
|
|
67
|
+
if current_type == "workflow":
|
|
68
|
+
header = "Github Workflows (.github/workflows):"
|
|
69
|
+
elif current_type == "hook":
|
|
70
|
+
header = "Hooks (.claude/settings.json):"
|
|
71
|
+
else:
|
|
72
|
+
# Capitalize first letter only (e.g., "Commands:")
|
|
73
|
+
header = current_type.capitalize() + "s:"
|
|
74
|
+
click.echo(click.style(header, bold=True))
|
|
75
|
+
|
|
76
|
+
# Format badge based on management status
|
|
77
|
+
is_managed = is_erk_managed(artifact)
|
|
78
|
+
if is_managed:
|
|
79
|
+
badge = click.style(" [erk]", fg="cyan")
|
|
80
|
+
else:
|
|
81
|
+
badge = click.style(" [unmanaged]", fg="yellow")
|
|
82
|
+
|
|
83
|
+
if verbose:
|
|
84
|
+
click.echo(f" {artifact.name}{badge}")
|
|
85
|
+
click.echo(click.style(f" Path: {artifact.path}", dim=True))
|
|
86
|
+
if is_managed and artifact.content_hash:
|
|
87
|
+
click.echo(click.style(f" Hash: {artifact.content_hash}", dim=True))
|
|
88
|
+
else:
|
|
89
|
+
click.echo(f" {artifact.name}{badge}")
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Show artifact content."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import cast
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from erk.artifacts.discovery import get_artifact_by_name
|
|
9
|
+
from erk.artifacts.models import ArtifactType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command("show")
|
|
13
|
+
@click.argument("name")
|
|
14
|
+
@click.option(
|
|
15
|
+
"--type",
|
|
16
|
+
"artifact_type",
|
|
17
|
+
type=click.Choice(["skill", "command", "agent", "workflow"]),
|
|
18
|
+
help="Artifact type (optional, helps disambiguate)",
|
|
19
|
+
)
|
|
20
|
+
def show_cmd(name: str, artifact_type: str | None) -> None:
|
|
21
|
+
"""Display the content of an artifact.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
|
|
25
|
+
\b
|
|
26
|
+
# Show a skill
|
|
27
|
+
erk artifact show dignified-python
|
|
28
|
+
|
|
29
|
+
\b
|
|
30
|
+
# Show a command (use colon for namespaced commands)
|
|
31
|
+
erk artifact show erk:plan-implement
|
|
32
|
+
|
|
33
|
+
\b
|
|
34
|
+
# Disambiguate by type
|
|
35
|
+
erk artifact show my-artifact --type skill
|
|
36
|
+
"""
|
|
37
|
+
project_dir = Path.cwd()
|
|
38
|
+
claude_dir = project_dir / ".claude"
|
|
39
|
+
if not claude_dir.exists():
|
|
40
|
+
click.echo("No .claude/ directory found in current directory", err=True)
|
|
41
|
+
raise SystemExit(1)
|
|
42
|
+
|
|
43
|
+
type_filter: ArtifactType | None = None
|
|
44
|
+
if artifact_type is not None:
|
|
45
|
+
assert artifact_type in ("skill", "command", "agent", "workflow")
|
|
46
|
+
type_filter = cast(ArtifactType, artifact_type)
|
|
47
|
+
|
|
48
|
+
artifact = get_artifact_by_name(project_dir, name, type_filter)
|
|
49
|
+
|
|
50
|
+
if artifact is None:
|
|
51
|
+
click.echo(f"Artifact not found: {name}", err=True)
|
|
52
|
+
raise SystemExit(1)
|
|
53
|
+
|
|
54
|
+
# Display metadata
|
|
55
|
+
click.echo(click.style(f"# {artifact.name}", bold=True))
|
|
56
|
+
click.echo(click.style(f"Type: {artifact.artifact_type}", dim=True))
|
|
57
|
+
click.echo(click.style(f"Path: {artifact.path}", dim=True))
|
|
58
|
+
click.echo("")
|
|
59
|
+
|
|
60
|
+
# Display content
|
|
61
|
+
content = artifact.path.read_text(encoding="utf-8")
|
|
62
|
+
click.echo(content)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Sync artifacts from erk package to project."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from erk.artifacts.sync import sync_artifacts
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command("sync")
|
|
11
|
+
@click.option("-f", "--force", is_flag=True, help="Force sync even if up to date")
|
|
12
|
+
def sync_cmd(force: bool) -> None:
|
|
13
|
+
"""Sync artifacts from erk package to .claude/ directory.
|
|
14
|
+
|
|
15
|
+
Copies bundled artifacts (commands, skills, agents, docs) from the
|
|
16
|
+
installed erk package to the current project's .claude/ directory.
|
|
17
|
+
|
|
18
|
+
When running in the erk repo itself, this is a no-op since artifacts
|
|
19
|
+
are read directly from source.
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
|
|
23
|
+
\b
|
|
24
|
+
# Sync artifacts
|
|
25
|
+
erk artifact sync
|
|
26
|
+
|
|
27
|
+
\b
|
|
28
|
+
# Force re-sync even if up to date
|
|
29
|
+
erk artifact sync --force
|
|
30
|
+
"""
|
|
31
|
+
project_dir = Path.cwd()
|
|
32
|
+
|
|
33
|
+
result = sync_artifacts(project_dir, force)
|
|
34
|
+
|
|
35
|
+
if result.success:
|
|
36
|
+
click.echo(click.style("✓ ", fg="green") + result.message)
|
|
37
|
+
else:
|
|
38
|
+
click.echo(click.style("✗ ", fg="red") + result.message, err=True)
|
|
39
|
+
raise SystemExit(1)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Branch management commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from erk.cli.alias import alias, register_with_aliases
|
|
6
|
+
from erk.cli.commands.branch.assign_cmd import branch_assign
|
|
7
|
+
from erk.cli.commands.branch.checkout_cmd import branch_checkout
|
|
8
|
+
from erk.cli.commands.branch.create_cmd import branch_create
|
|
9
|
+
from erk.cli.commands.branch.list_cmd import branch_list
|
|
10
|
+
from erk.cli.commands.branch.unassign_cmd import branch_unassign
|
|
11
|
+
from erk.cli.help_formatter import ErkCommandGroup
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@alias("br")
|
|
15
|
+
@click.group("branch", cls=ErkCommandGroup, grouped=False)
|
|
16
|
+
def branch_group() -> None:
|
|
17
|
+
"""Manage branches."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Register subcommands
|
|
22
|
+
branch_group.add_command(branch_create)
|
|
23
|
+
branch_group.add_command(branch_assign)
|
|
24
|
+
branch_group.add_command(branch_unassign)
|
|
25
|
+
register_with_aliases(branch_group, branch_checkout)
|
|
26
|
+
register_with_aliases(branch_group, branch_list)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Branch assign command - assign an existing branch to a worktree slot."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from erk.cli.commands.slot.common import (
|
|
9
|
+
cleanup_worktree_artifacts,
|
|
10
|
+
find_branch_assignment,
|
|
11
|
+
find_next_available_slot,
|
|
12
|
+
generate_slot_name,
|
|
13
|
+
get_pool_size,
|
|
14
|
+
handle_pool_full_interactive,
|
|
15
|
+
)
|
|
16
|
+
from erk.cli.core import discover_repo_context
|
|
17
|
+
from erk.cli.ensure import Ensure
|
|
18
|
+
from erk.core.context import ErkContext
|
|
19
|
+
from erk.core.repo_discovery import ensure_erk_metadata_dir
|
|
20
|
+
from erk.core.worktree_pool import (
|
|
21
|
+
PoolState,
|
|
22
|
+
SlotAssignment,
|
|
23
|
+
load_pool_state,
|
|
24
|
+
save_pool_state,
|
|
25
|
+
)
|
|
26
|
+
from erk_shared.output.output import user_output
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@click.command("assign")
|
|
30
|
+
@click.argument("branch_name", metavar="BRANCH")
|
|
31
|
+
@click.option("-f", "--force", is_flag=True, help="Auto-unassign oldest branch if pool is full")
|
|
32
|
+
@click.pass_obj
|
|
33
|
+
def branch_assign(ctx: ErkContext, branch_name: str, force: bool) -> None:
|
|
34
|
+
"""Assign an EXISTING branch to an available pool slot.
|
|
35
|
+
|
|
36
|
+
BRANCH is the name of an existing git branch to assign to the pool.
|
|
37
|
+
|
|
38
|
+
The command will:
|
|
39
|
+
1. Verify the branch EXISTS (fails if it doesn't)
|
|
40
|
+
2. Find the next available slot in the pool
|
|
41
|
+
3. Create a worktree for that slot if needed
|
|
42
|
+
4. Assign the branch to the slot
|
|
43
|
+
5. Persist the assignment to pool.json
|
|
44
|
+
|
|
45
|
+
Use `erk br create` to create a NEW branch and assign it.
|
|
46
|
+
"""
|
|
47
|
+
repo = discover_repo_context(ctx, ctx.cwd)
|
|
48
|
+
ensure_erk_metadata_dir(repo)
|
|
49
|
+
|
|
50
|
+
# Get pool size from config or default
|
|
51
|
+
pool_size = get_pool_size(ctx)
|
|
52
|
+
|
|
53
|
+
# Load or create pool state
|
|
54
|
+
state = load_pool_state(repo.pool_json_path)
|
|
55
|
+
if state is None:
|
|
56
|
+
state = PoolState(
|
|
57
|
+
version="1.0",
|
|
58
|
+
pool_size=pool_size,
|
|
59
|
+
slots=(),
|
|
60
|
+
assignments=(),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Check if branch is already assigned
|
|
64
|
+
existing = find_branch_assignment(state, branch_name)
|
|
65
|
+
if existing is not None:
|
|
66
|
+
user_output(f"Error: Branch '{branch_name}' already assigned to {existing.slot_name}")
|
|
67
|
+
raise SystemExit(1) from None
|
|
68
|
+
|
|
69
|
+
# Check if branch exists - assign command requires EXISTING branch
|
|
70
|
+
local_branches = ctx.git.list_local_branches(repo.root)
|
|
71
|
+
if branch_name not in local_branches:
|
|
72
|
+
user_output(
|
|
73
|
+
f"Error: Branch '{branch_name}' does not exist.\n"
|
|
74
|
+
"Use `erk br create` to create a new branch."
|
|
75
|
+
)
|
|
76
|
+
raise SystemExit(1) from None
|
|
77
|
+
|
|
78
|
+
# Find next available slot
|
|
79
|
+
slot_num = find_next_available_slot(state, repo.worktrees_dir)
|
|
80
|
+
if slot_num is None:
|
|
81
|
+
# Pool is full - handle interactively or with --force
|
|
82
|
+
to_unassign = handle_pool_full_interactive(state, force, sys.stdin.isatty())
|
|
83
|
+
if to_unassign is None:
|
|
84
|
+
raise SystemExit(1) from None
|
|
85
|
+
|
|
86
|
+
# Remove the assignment from state
|
|
87
|
+
new_assignments = tuple(
|
|
88
|
+
a for a in state.assignments if a.slot_name != to_unassign.slot_name
|
|
89
|
+
)
|
|
90
|
+
state = PoolState(
|
|
91
|
+
version=state.version,
|
|
92
|
+
pool_size=state.pool_size,
|
|
93
|
+
slots=state.slots,
|
|
94
|
+
assignments=new_assignments,
|
|
95
|
+
)
|
|
96
|
+
save_pool_state(repo.pool_json_path, state)
|
|
97
|
+
user_output(
|
|
98
|
+
click.style("✓ ", fg="green")
|
|
99
|
+
+ f"Unassigned {click.style(to_unassign.branch_name, fg='yellow')} "
|
|
100
|
+
+ f"from {click.style(to_unassign.slot_name, fg='cyan')}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Use the slot we just unassigned (it has a worktree directory that can be reused)
|
|
104
|
+
slot_name = to_unassign.slot_name
|
|
105
|
+
worktree_path = to_unassign.worktree_path
|
|
106
|
+
else:
|
|
107
|
+
slot_name = generate_slot_name(slot_num)
|
|
108
|
+
worktree_path = repo.worktrees_dir / slot_name
|
|
109
|
+
|
|
110
|
+
# Create worktree if it doesn't exist
|
|
111
|
+
if not ctx.git.path_exists(worktree_path):
|
|
112
|
+
# Create directory for worktree
|
|
113
|
+
worktree_path.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
|
|
115
|
+
# Add worktree
|
|
116
|
+
ctx.git.add_worktree(
|
|
117
|
+
repo.root,
|
|
118
|
+
worktree_path,
|
|
119
|
+
branch=branch_name,
|
|
120
|
+
ref=None,
|
|
121
|
+
create_branch=False,
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
# Worktree exists - clean up stale artifacts and check out the branch
|
|
125
|
+
Ensure.invariant(
|
|
126
|
+
ctx.git.is_dir(worktree_path),
|
|
127
|
+
f"Expected {worktree_path} to be a directory",
|
|
128
|
+
)
|
|
129
|
+
cleanup_worktree_artifacts(worktree_path)
|
|
130
|
+
ctx.git.checkout_branch(worktree_path, branch_name)
|
|
131
|
+
|
|
132
|
+
# Create new assignment
|
|
133
|
+
now = datetime.now(UTC).isoformat()
|
|
134
|
+
new_assignment = SlotAssignment(
|
|
135
|
+
slot_name=slot_name,
|
|
136
|
+
branch_name=branch_name,
|
|
137
|
+
assigned_at=now,
|
|
138
|
+
worktree_path=worktree_path,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Update state with new assignment
|
|
142
|
+
new_state = PoolState(
|
|
143
|
+
version=state.version,
|
|
144
|
+
pool_size=state.pool_size,
|
|
145
|
+
slots=state.slots,
|
|
146
|
+
assignments=(*state.assignments, new_assignment),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Save state
|
|
150
|
+
save_pool_state(repo.pool_json_path, new_state)
|
|
151
|
+
|
|
152
|
+
user_output(click.style(f"✓ Assigned {branch_name} to {slot_name}", fg="green"))
|