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
erk/artifacts/sync.py
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
"""Sync artifacts from erk package to project's .claude/ directory."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from functools import cache
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from erk.artifacts.detection import is_in_erk_repo
|
|
10
|
+
from erk.artifacts.discovery import _compute_directory_hash, _compute_file_hash, _compute_hook_hash
|
|
11
|
+
from erk.artifacts.models import ArtifactFileState, ArtifactState
|
|
12
|
+
from erk.artifacts.state import save_artifact_state
|
|
13
|
+
from erk.core.claude_settings import (
|
|
14
|
+
ERK_EXIT_PLAN_HOOK_COMMAND,
|
|
15
|
+
ERK_USER_PROMPT_HOOK_COMMAND,
|
|
16
|
+
add_erk_hooks,
|
|
17
|
+
has_exit_plan_hook,
|
|
18
|
+
has_user_prompt_hook,
|
|
19
|
+
)
|
|
20
|
+
from erk.core.release_notes import get_current_version
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class SyncResult:
|
|
25
|
+
"""Result of artifact sync operation."""
|
|
26
|
+
|
|
27
|
+
success: bool
|
|
28
|
+
artifacts_installed: int
|
|
29
|
+
message: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@cache
|
|
33
|
+
def _get_erk_package_dir() -> Path:
|
|
34
|
+
"""Get the erk package directory (where erk/__init__.py lives)."""
|
|
35
|
+
# __file__ is .../erk/artifacts/sync.py, so parent.parent is erk/
|
|
36
|
+
return Path(__file__).parent.parent
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _is_editable_install() -> bool:
|
|
40
|
+
"""Check if erk is installed in editable mode.
|
|
41
|
+
|
|
42
|
+
Editable: erk package is in src/ layout (e.g., .../src/erk/)
|
|
43
|
+
Wheel: erk package is in site-packages (e.g., .../site-packages/erk/)
|
|
44
|
+
"""
|
|
45
|
+
return "site-packages" not in str(_get_erk_package_dir().resolve())
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@cache
|
|
49
|
+
def get_bundled_claude_dir() -> Path:
|
|
50
|
+
"""Get path to bundled .claude/ directory in installed erk package.
|
|
51
|
+
|
|
52
|
+
For wheel installs: .claude/ is bundled as package data at erk/data/claude/
|
|
53
|
+
via pyproject.toml force-include.
|
|
54
|
+
|
|
55
|
+
For editable installs: .claude/ is at the erk repo root (no wheel is built,
|
|
56
|
+
so erk/data/ doesn't exist).
|
|
57
|
+
"""
|
|
58
|
+
erk_package_dir = _get_erk_package_dir()
|
|
59
|
+
|
|
60
|
+
if _is_editable_install():
|
|
61
|
+
# Editable: erk package is at src/erk/, repo root is ../..
|
|
62
|
+
erk_repo_root = erk_package_dir.parent.parent
|
|
63
|
+
return erk_repo_root / ".claude"
|
|
64
|
+
|
|
65
|
+
# Wheel install: data is bundled at erk/data/claude/
|
|
66
|
+
return erk_package_dir / "data" / "claude"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@cache
|
|
70
|
+
def get_bundled_github_dir() -> Path:
|
|
71
|
+
"""Get path to bundled .github/ directory in installed erk package.
|
|
72
|
+
|
|
73
|
+
For wheel installs: .github/ is bundled as package data at erk/data/github/
|
|
74
|
+
via pyproject.toml force-include.
|
|
75
|
+
|
|
76
|
+
For editable installs: .github/ is at the erk repo root.
|
|
77
|
+
"""
|
|
78
|
+
erk_package_dir = _get_erk_package_dir()
|
|
79
|
+
|
|
80
|
+
if _is_editable_install():
|
|
81
|
+
# Editable: erk package is at src/erk/, repo root is ../..
|
|
82
|
+
erk_repo_root = erk_package_dir.parent.parent
|
|
83
|
+
return erk_repo_root / ".github"
|
|
84
|
+
|
|
85
|
+
# Wheel install: data is bundled at erk/data/github/
|
|
86
|
+
return erk_package_dir / "data" / "github"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _copy_directory_contents(source_dir: Path, target_dir: Path) -> int:
|
|
90
|
+
"""Copy directory contents recursively, returning count of files copied."""
|
|
91
|
+
if not source_dir.exists():
|
|
92
|
+
return 0
|
|
93
|
+
|
|
94
|
+
count = 0
|
|
95
|
+
for source_path in source_dir.rglob("*"):
|
|
96
|
+
if source_path.is_file():
|
|
97
|
+
relative = source_path.relative_to(source_dir)
|
|
98
|
+
target_path = target_dir / relative
|
|
99
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
shutil.copy2(source_path, target_path)
|
|
101
|
+
count += 1
|
|
102
|
+
return count
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True)
|
|
106
|
+
class SyncedArtifact:
|
|
107
|
+
"""Represents an artifact that was synced, with its computed hash."""
|
|
108
|
+
|
|
109
|
+
key: str # e.g. "skills/dignified-python", "commands/erk/plan-implement"
|
|
110
|
+
hash: str
|
|
111
|
+
file_count: int
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _sync_directory_artifacts(
|
|
115
|
+
source_dir: Path, target_dir: Path, names: frozenset[str], key_prefix: str
|
|
116
|
+
) -> tuple[int, list[SyncedArtifact]]:
|
|
117
|
+
"""Sync directory-based artifacts (skills) to project.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
source_dir: Parent directory containing source artifacts (e.g., bundled/skills/)
|
|
121
|
+
target_dir: Parent directory for target artifacts (e.g., project/.claude/skills/)
|
|
122
|
+
names: Set of artifact names to sync (e.g., BUNDLED_SKILLS)
|
|
123
|
+
key_prefix: Prefix for artifact keys (e.g., "skills")
|
|
124
|
+
|
|
125
|
+
Returns tuple of (file_count, synced_artifacts).
|
|
126
|
+
"""
|
|
127
|
+
if not source_dir.exists():
|
|
128
|
+
return 0, []
|
|
129
|
+
|
|
130
|
+
copied = 0
|
|
131
|
+
synced: list[SyncedArtifact] = []
|
|
132
|
+
for name in sorted(names):
|
|
133
|
+
source = source_dir / name
|
|
134
|
+
if source.exists():
|
|
135
|
+
target = target_dir / name
|
|
136
|
+
count = _copy_directory_contents(source, target)
|
|
137
|
+
copied += count
|
|
138
|
+
synced.append(
|
|
139
|
+
SyncedArtifact(
|
|
140
|
+
key=f"{key_prefix}/{name}",
|
|
141
|
+
hash=_compute_directory_hash(target),
|
|
142
|
+
file_count=count,
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
return copied, synced
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _sync_agent_artifacts(
|
|
149
|
+
source_dir: Path, target_dir: Path, names: frozenset[str]
|
|
150
|
+
) -> tuple[int, list[SyncedArtifact]]:
|
|
151
|
+
"""Sync agent artifacts to project (supports both directory-based and single-file).
|
|
152
|
+
|
|
153
|
+
Key format depends on structure:
|
|
154
|
+
- Directory: agents/{name} (like skills)
|
|
155
|
+
- Single-file: agents/{name}.md (like commands)
|
|
156
|
+
|
|
157
|
+
Returns tuple of (file_count, synced_artifacts).
|
|
158
|
+
"""
|
|
159
|
+
if not source_dir.exists():
|
|
160
|
+
return 0, []
|
|
161
|
+
|
|
162
|
+
copied = 0
|
|
163
|
+
synced: list[SyncedArtifact] = []
|
|
164
|
+
for name in sorted(names):
|
|
165
|
+
source_dir_path = source_dir / name
|
|
166
|
+
source_file_path = source_dir / f"{name}.md"
|
|
167
|
+
|
|
168
|
+
# Directory-based takes precedence, then single-file
|
|
169
|
+
if source_dir_path.exists() and source_dir_path.is_dir():
|
|
170
|
+
target = target_dir / name
|
|
171
|
+
count = _copy_directory_contents(source_dir_path, target)
|
|
172
|
+
copied += count
|
|
173
|
+
synced.append(
|
|
174
|
+
SyncedArtifact(
|
|
175
|
+
key=f"agents/{name}",
|
|
176
|
+
hash=_compute_directory_hash(target),
|
|
177
|
+
file_count=count,
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
elif source_file_path.exists() and source_file_path.is_file():
|
|
181
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
182
|
+
target_file = target_dir / f"{name}.md"
|
|
183
|
+
shutil.copy2(source_file_path, target_file)
|
|
184
|
+
copied += 1
|
|
185
|
+
synced.append(
|
|
186
|
+
SyncedArtifact(
|
|
187
|
+
key=f"agents/{name}.md",
|
|
188
|
+
hash=_compute_file_hash(target_file),
|
|
189
|
+
file_count=1,
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
return copied, synced
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _sync_commands(
|
|
196
|
+
source_commands_dir: Path, target_commands_dir: Path
|
|
197
|
+
) -> tuple[int, list[SyncedArtifact]]:
|
|
198
|
+
"""Sync bundled commands to project. Only syncs erk namespace.
|
|
199
|
+
|
|
200
|
+
Returns tuple of (file_count, synced_artifacts).
|
|
201
|
+
Each command is tracked individually.
|
|
202
|
+
"""
|
|
203
|
+
if not source_commands_dir.exists():
|
|
204
|
+
return 0, []
|
|
205
|
+
|
|
206
|
+
source = source_commands_dir / "erk"
|
|
207
|
+
if not source.exists():
|
|
208
|
+
return 0, []
|
|
209
|
+
|
|
210
|
+
target = target_commands_dir / "erk"
|
|
211
|
+
count = _copy_directory_contents(source, target)
|
|
212
|
+
|
|
213
|
+
# Track each command file individually
|
|
214
|
+
synced: list[SyncedArtifact] = []
|
|
215
|
+
if target.exists():
|
|
216
|
+
for cmd_file in target.glob("*.md"):
|
|
217
|
+
synced.append(
|
|
218
|
+
SyncedArtifact(
|
|
219
|
+
key=f"commands/erk/{cmd_file.name}",
|
|
220
|
+
hash=_compute_file_hash(cmd_file),
|
|
221
|
+
file_count=1,
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return count, synced
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _sync_workflows(
|
|
229
|
+
bundled_github_dir: Path, target_workflows_dir: Path
|
|
230
|
+
) -> tuple[int, list[SyncedArtifact]]:
|
|
231
|
+
"""Sync erk-managed workflows to project's .github/workflows/ directory.
|
|
232
|
+
|
|
233
|
+
Only syncs files listed in BUNDLED_WORKFLOWS registry.
|
|
234
|
+
Returns tuple of (file_count, synced_artifacts).
|
|
235
|
+
"""
|
|
236
|
+
# Inline import: artifact_health.py imports get_bundled_*_dir from this module
|
|
237
|
+
from erk.artifacts.artifact_health import BUNDLED_WORKFLOWS
|
|
238
|
+
|
|
239
|
+
source_workflows_dir = bundled_github_dir / "workflows"
|
|
240
|
+
if not source_workflows_dir.exists():
|
|
241
|
+
return 0, []
|
|
242
|
+
|
|
243
|
+
count = 0
|
|
244
|
+
synced: list[SyncedArtifact] = []
|
|
245
|
+
for workflow_name in sorted(BUNDLED_WORKFLOWS):
|
|
246
|
+
source_path = source_workflows_dir / workflow_name
|
|
247
|
+
if source_path.exists():
|
|
248
|
+
target_workflows_dir.mkdir(parents=True, exist_ok=True)
|
|
249
|
+
target_path = target_workflows_dir / workflow_name
|
|
250
|
+
shutil.copy2(source_path, target_path)
|
|
251
|
+
count += 1
|
|
252
|
+
synced.append(
|
|
253
|
+
SyncedArtifact(
|
|
254
|
+
key=f"workflows/{workflow_name}",
|
|
255
|
+
hash=_compute_file_hash(target_path),
|
|
256
|
+
file_count=1,
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
return count, synced
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _sync_actions(
|
|
263
|
+
bundled_github_dir: Path, target_actions_dir: Path
|
|
264
|
+
) -> tuple[int, list[SyncedArtifact]]:
|
|
265
|
+
"""Sync erk-managed actions to project's .github/actions/ directory.
|
|
266
|
+
|
|
267
|
+
Only syncs directories listed in BUNDLED_ACTIONS registry.
|
|
268
|
+
Returns tuple of (file_count, synced_artifacts).
|
|
269
|
+
"""
|
|
270
|
+
# Inline import: artifact_health.py imports get_bundled_*_dir from this module
|
|
271
|
+
from erk.artifacts.artifact_health import BUNDLED_ACTIONS
|
|
272
|
+
|
|
273
|
+
source_actions_dir = bundled_github_dir / "actions"
|
|
274
|
+
if not source_actions_dir.exists():
|
|
275
|
+
return 0, []
|
|
276
|
+
|
|
277
|
+
count = 0
|
|
278
|
+
synced: list[SyncedArtifact] = []
|
|
279
|
+
for action_name in sorted(BUNDLED_ACTIONS):
|
|
280
|
+
source_path = source_actions_dir / action_name
|
|
281
|
+
if source_path.exists() and source_path.is_dir():
|
|
282
|
+
target_path = target_actions_dir / action_name
|
|
283
|
+
file_count = _copy_directory_contents(source_path, target_path)
|
|
284
|
+
count += file_count
|
|
285
|
+
synced.append(
|
|
286
|
+
SyncedArtifact(
|
|
287
|
+
key=f"actions/{action_name}",
|
|
288
|
+
hash=_compute_directory_hash(target_path),
|
|
289
|
+
file_count=file_count,
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
return count, synced
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _sync_hooks(target_claude_dir: Path) -> tuple[int, list[SyncedArtifact]]:
|
|
296
|
+
"""Sync erk-managed hooks to project's .claude/settings.json.
|
|
297
|
+
|
|
298
|
+
Hooks are configuration entries, not files. This adds missing hooks
|
|
299
|
+
to settings.json using the existing add_erk_hooks() function.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Tuple of (hooks_added, synced_artifacts)
|
|
303
|
+
"""
|
|
304
|
+
settings_path = target_claude_dir / "settings.json"
|
|
305
|
+
|
|
306
|
+
# Read existing settings or start with empty
|
|
307
|
+
if settings_path.exists():
|
|
308
|
+
content = settings_path.read_text(encoding="utf-8")
|
|
309
|
+
settings = json.loads(content)
|
|
310
|
+
else:
|
|
311
|
+
settings = {}
|
|
312
|
+
|
|
313
|
+
# Count hooks before adding
|
|
314
|
+
had_user_prompt = has_user_prompt_hook(settings)
|
|
315
|
+
had_exit_plan = has_exit_plan_hook(settings)
|
|
316
|
+
|
|
317
|
+
# Add missing hooks
|
|
318
|
+
updated_settings = add_erk_hooks(settings)
|
|
319
|
+
|
|
320
|
+
# Write updated settings
|
|
321
|
+
target_claude_dir.mkdir(parents=True, exist_ok=True)
|
|
322
|
+
settings_path.write_text(json.dumps(updated_settings, indent=2), encoding="utf-8")
|
|
323
|
+
|
|
324
|
+
# Count how many hooks were newly added
|
|
325
|
+
added = 0
|
|
326
|
+
if not had_user_prompt:
|
|
327
|
+
added += 1
|
|
328
|
+
if not had_exit_plan:
|
|
329
|
+
added += 1
|
|
330
|
+
|
|
331
|
+
# ALWAYS record state for installed hooks (not just newly added)
|
|
332
|
+
# This ensures hooks from older erk versions get tracked in state.toml
|
|
333
|
+
synced: list[SyncedArtifact] = []
|
|
334
|
+
if has_user_prompt_hook(updated_settings):
|
|
335
|
+
synced.append(
|
|
336
|
+
SyncedArtifact(
|
|
337
|
+
key="hooks/user-prompt-hook",
|
|
338
|
+
hash=_compute_hook_hash(ERK_USER_PROMPT_HOOK_COMMAND),
|
|
339
|
+
file_count=1,
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
if has_exit_plan_hook(updated_settings):
|
|
343
|
+
synced.append(
|
|
344
|
+
SyncedArtifact(
|
|
345
|
+
key="hooks/exit-plan-mode-hook",
|
|
346
|
+
hash=_compute_hook_hash(ERK_EXIT_PLAN_HOOK_COMMAND),
|
|
347
|
+
file_count=1,
|
|
348
|
+
)
|
|
349
|
+
)
|
|
350
|
+
return added, synced
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _hash_directory_artifacts(
|
|
354
|
+
parent_dir: Path, names: frozenset[str], key_prefix: str
|
|
355
|
+
) -> list[SyncedArtifact]:
|
|
356
|
+
"""Compute hashes for directory-based artifacts without copying."""
|
|
357
|
+
if not parent_dir.exists():
|
|
358
|
+
return []
|
|
359
|
+
|
|
360
|
+
artifacts: list[SyncedArtifact] = []
|
|
361
|
+
for name in sorted(names):
|
|
362
|
+
artifact_dir = parent_dir / name
|
|
363
|
+
if artifact_dir.exists():
|
|
364
|
+
artifacts.append(
|
|
365
|
+
SyncedArtifact(
|
|
366
|
+
key=f"{key_prefix}/{name}",
|
|
367
|
+
hash=_compute_directory_hash(artifact_dir),
|
|
368
|
+
file_count=sum(1 for f in artifact_dir.rglob("*") if f.is_file()),
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
return artifacts
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _hash_agent_artifacts(agents_dir: Path, names: frozenset[str]) -> list[SyncedArtifact]:
|
|
375
|
+
"""Compute hashes for agents (supports both directory-based and single-file).
|
|
376
|
+
|
|
377
|
+
Key format depends on structure:
|
|
378
|
+
- Directory: agents/{name} (like skills)
|
|
379
|
+
- Single-file: agents/{name}.md (like commands)
|
|
380
|
+
"""
|
|
381
|
+
if not agents_dir.exists():
|
|
382
|
+
return []
|
|
383
|
+
|
|
384
|
+
artifacts: list[SyncedArtifact] = []
|
|
385
|
+
for name in sorted(names):
|
|
386
|
+
dir_path = agents_dir / name
|
|
387
|
+
file_path = agents_dir / f"{name}.md"
|
|
388
|
+
|
|
389
|
+
# Directory-based takes precedence, then single-file
|
|
390
|
+
if dir_path.exists() and dir_path.is_dir():
|
|
391
|
+
artifacts.append(
|
|
392
|
+
SyncedArtifact(
|
|
393
|
+
key=f"agents/{name}",
|
|
394
|
+
hash=_compute_directory_hash(dir_path),
|
|
395
|
+
file_count=sum(1 for f in dir_path.rglob("*") if f.is_file()),
|
|
396
|
+
)
|
|
397
|
+
)
|
|
398
|
+
elif file_path.exists() and file_path.is_file():
|
|
399
|
+
artifacts.append(
|
|
400
|
+
SyncedArtifact(
|
|
401
|
+
key=f"agents/{name}.md",
|
|
402
|
+
hash=_compute_file_hash(file_path),
|
|
403
|
+
file_count=1,
|
|
404
|
+
)
|
|
405
|
+
)
|
|
406
|
+
return artifacts
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _compute_source_artifact_state(project_dir: Path) -> list[SyncedArtifact]:
|
|
410
|
+
"""Compute artifact state from source (for erk repo dogfooding).
|
|
411
|
+
|
|
412
|
+
Instead of copying files, just compute hashes from the source artifacts.
|
|
413
|
+
"""
|
|
414
|
+
from erk.artifacts.artifact_health import (
|
|
415
|
+
BUNDLED_ACTIONS,
|
|
416
|
+
BUNDLED_AGENTS,
|
|
417
|
+
BUNDLED_SKILLS,
|
|
418
|
+
BUNDLED_WORKFLOWS,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
bundled_claude_dir = get_bundled_claude_dir()
|
|
422
|
+
bundled_github_dir = get_bundled_github_dir()
|
|
423
|
+
artifacts: list[SyncedArtifact] = []
|
|
424
|
+
|
|
425
|
+
# Hash directory-based skills
|
|
426
|
+
skills_dir = bundled_claude_dir / "skills"
|
|
427
|
+
artifacts.extend(_hash_directory_artifacts(skills_dir, BUNDLED_SKILLS, "skills"))
|
|
428
|
+
|
|
429
|
+
# Hash agents (supports both directory-based and single-file)
|
|
430
|
+
artifacts.extend(_hash_agent_artifacts(bundled_claude_dir / "agents", BUNDLED_AGENTS))
|
|
431
|
+
|
|
432
|
+
# Hash commands from source
|
|
433
|
+
commands_dir = bundled_claude_dir / "commands" / "erk"
|
|
434
|
+
if commands_dir.exists():
|
|
435
|
+
for cmd_file in sorted(commands_dir.glob("*.md")):
|
|
436
|
+
artifacts.append(
|
|
437
|
+
SyncedArtifact(
|
|
438
|
+
key=f"commands/erk/{cmd_file.name}",
|
|
439
|
+
hash=_compute_file_hash(cmd_file),
|
|
440
|
+
file_count=1,
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# Hash workflows from source
|
|
445
|
+
workflows_dir = bundled_github_dir / "workflows"
|
|
446
|
+
if workflows_dir.exists():
|
|
447
|
+
for workflow_name in sorted(BUNDLED_WORKFLOWS):
|
|
448
|
+
workflow_file = workflows_dir / workflow_name
|
|
449
|
+
if workflow_file.exists():
|
|
450
|
+
artifacts.append(
|
|
451
|
+
SyncedArtifact(
|
|
452
|
+
key=f"workflows/{workflow_name}",
|
|
453
|
+
hash=_compute_file_hash(workflow_file),
|
|
454
|
+
file_count=1,
|
|
455
|
+
)
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
# Hash actions from source
|
|
459
|
+
actions_dir = bundled_github_dir / "actions"
|
|
460
|
+
artifacts.extend(_hash_directory_artifacts(actions_dir, BUNDLED_ACTIONS, "actions"))
|
|
461
|
+
|
|
462
|
+
# Hash hooks (check if installed in settings.json)
|
|
463
|
+
settings_path = project_dir / ".claude" / "settings.json"
|
|
464
|
+
if settings_path.exists():
|
|
465
|
+
content = settings_path.read_text(encoding="utf-8")
|
|
466
|
+
settings = json.loads(content)
|
|
467
|
+
hook_checks = [
|
|
468
|
+
("hooks/user-prompt-hook", has_user_prompt_hook, ERK_USER_PROMPT_HOOK_COMMAND),
|
|
469
|
+
("hooks/exit-plan-mode-hook", has_exit_plan_hook, ERK_EXIT_PLAN_HOOK_COMMAND),
|
|
470
|
+
]
|
|
471
|
+
for key, check_fn, command in hook_checks:
|
|
472
|
+
if check_fn(settings):
|
|
473
|
+
artifacts.append(
|
|
474
|
+
SyncedArtifact(key=key, hash=_compute_hook_hash(command), file_count=1)
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
return artifacts
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def sync_dignified_review(project_dir: Path) -> SyncResult:
|
|
481
|
+
"""Sync dignified-review feature artifacts to project.
|
|
482
|
+
|
|
483
|
+
Installs opt-in artifacts for the dignified-review workflow:
|
|
484
|
+
- dignified-python skill (.claude/skills/dignified-python/)
|
|
485
|
+
- dignified-python-review.yml workflow (.github/workflows/)
|
|
486
|
+
- dignified-python-review.md prompt (.github/prompts/)
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
project_dir: Project root directory
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
SyncResult indicating success/failure and count of files installed.
|
|
493
|
+
"""
|
|
494
|
+
bundled_claude_dir = get_bundled_claude_dir()
|
|
495
|
+
bundled_github_dir = get_bundled_github_dir()
|
|
496
|
+
|
|
497
|
+
target_claude_dir = project_dir / ".claude"
|
|
498
|
+
target_github_dir = project_dir / ".github"
|
|
499
|
+
|
|
500
|
+
total_copied = 0
|
|
501
|
+
|
|
502
|
+
# 1. Sync dignified-python skill
|
|
503
|
+
skill_src = bundled_claude_dir / "skills" / "dignified-python"
|
|
504
|
+
if skill_src.exists():
|
|
505
|
+
skill_dst = target_claude_dir / "skills" / "dignified-python"
|
|
506
|
+
count = _copy_directory_contents(skill_src, skill_dst)
|
|
507
|
+
total_copied += count
|
|
508
|
+
|
|
509
|
+
# 2. Sync dignified-python-review.yml workflow
|
|
510
|
+
workflow_src = bundled_github_dir / "workflows" / "dignified-python-review.yml"
|
|
511
|
+
if workflow_src.exists():
|
|
512
|
+
workflow_dst = target_github_dir / "workflows" / "dignified-python-review.yml"
|
|
513
|
+
workflow_dst.parent.mkdir(parents=True, exist_ok=True)
|
|
514
|
+
shutil.copy2(workflow_src, workflow_dst)
|
|
515
|
+
total_copied += 1
|
|
516
|
+
|
|
517
|
+
# 3. Sync dignified-python-review.md prompt
|
|
518
|
+
prompt_src = bundled_github_dir / "prompts" / "dignified-python-review.md"
|
|
519
|
+
if prompt_src.exists():
|
|
520
|
+
prompt_dst = target_github_dir / "prompts" / "dignified-python-review.md"
|
|
521
|
+
prompt_dst.parent.mkdir(parents=True, exist_ok=True)
|
|
522
|
+
shutil.copy2(prompt_src, prompt_dst)
|
|
523
|
+
total_copied += 1
|
|
524
|
+
|
|
525
|
+
return SyncResult(
|
|
526
|
+
success=True,
|
|
527
|
+
artifacts_installed=total_copied,
|
|
528
|
+
message=f"Installed dignified-review ({total_copied} files)",
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def sync_artifacts(project_dir: Path, force: bool) -> SyncResult:
|
|
533
|
+
"""Sync artifacts from erk package to project's .claude/ and .github/ directories.
|
|
534
|
+
|
|
535
|
+
When running in the erk repo itself, skips file copying but still computes
|
|
536
|
+
and saves state for dogfooding.
|
|
537
|
+
"""
|
|
538
|
+
# Inline import: artifact_health.py imports get_bundled_*_dir from this module
|
|
539
|
+
from erk.artifacts.artifact_health import BUNDLED_AGENTS, BUNDLED_SKILLS
|
|
540
|
+
|
|
541
|
+
# In erk repo: skip copying but still save state for dogfooding
|
|
542
|
+
if is_in_erk_repo(project_dir):
|
|
543
|
+
all_synced = _compute_source_artifact_state(project_dir)
|
|
544
|
+
current_version = get_current_version()
|
|
545
|
+
files: dict[str, ArtifactFileState] = {}
|
|
546
|
+
for artifact in all_synced:
|
|
547
|
+
files[artifact.key] = ArtifactFileState(
|
|
548
|
+
version=current_version,
|
|
549
|
+
hash=artifact.hash,
|
|
550
|
+
)
|
|
551
|
+
save_artifact_state(project_dir, ArtifactState(version=current_version, files=files))
|
|
552
|
+
return SyncResult(
|
|
553
|
+
success=True,
|
|
554
|
+
artifacts_installed=0,
|
|
555
|
+
message="Development mode: state.toml updated (artifacts read from source)",
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
bundled_claude_dir = get_bundled_claude_dir()
|
|
559
|
+
if not bundled_claude_dir.exists():
|
|
560
|
+
return SyncResult(
|
|
561
|
+
success=False,
|
|
562
|
+
artifacts_installed=0,
|
|
563
|
+
message=f"Bundled .claude/ not found at {bundled_claude_dir}",
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
target_claude_dir = project_dir / ".claude"
|
|
567
|
+
target_claude_dir.mkdir(parents=True, exist_ok=True)
|
|
568
|
+
|
|
569
|
+
total_copied = 0
|
|
570
|
+
all_synced: list[SyncedArtifact] = []
|
|
571
|
+
|
|
572
|
+
# Sync directory-based skills
|
|
573
|
+
count, synced = _sync_directory_artifacts(
|
|
574
|
+
bundled_claude_dir / "skills", target_claude_dir / "skills", BUNDLED_SKILLS, "skills"
|
|
575
|
+
)
|
|
576
|
+
total_copied += count
|
|
577
|
+
all_synced.extend(synced)
|
|
578
|
+
|
|
579
|
+
# Sync agents (supports both directory-based and single-file)
|
|
580
|
+
count, synced = _sync_agent_artifacts(
|
|
581
|
+
bundled_claude_dir / "agents", target_claude_dir / "agents", BUNDLED_AGENTS
|
|
582
|
+
)
|
|
583
|
+
total_copied += count
|
|
584
|
+
all_synced.extend(synced)
|
|
585
|
+
|
|
586
|
+
count, synced = _sync_commands(bundled_claude_dir / "commands", target_claude_dir / "commands")
|
|
587
|
+
total_copied += count
|
|
588
|
+
all_synced.extend(synced)
|
|
589
|
+
|
|
590
|
+
# Sync workflows and actions from .github/
|
|
591
|
+
bundled_github_dir = get_bundled_github_dir()
|
|
592
|
+
if bundled_github_dir.exists():
|
|
593
|
+
target_workflows_dir = project_dir / ".github" / "workflows"
|
|
594
|
+
count, synced = _sync_workflows(bundled_github_dir, target_workflows_dir)
|
|
595
|
+
total_copied += count
|
|
596
|
+
all_synced.extend(synced)
|
|
597
|
+
|
|
598
|
+
target_actions_dir = project_dir / ".github" / "actions"
|
|
599
|
+
count, synced = _sync_actions(bundled_github_dir, target_actions_dir)
|
|
600
|
+
total_copied += count
|
|
601
|
+
all_synced.extend(synced)
|
|
602
|
+
|
|
603
|
+
# Sync hooks to settings.json
|
|
604
|
+
count, synced = _sync_hooks(target_claude_dir)
|
|
605
|
+
total_copied += count
|
|
606
|
+
all_synced.extend(synced)
|
|
607
|
+
|
|
608
|
+
# Build per-artifact state from synced artifacts
|
|
609
|
+
current_version = get_current_version()
|
|
610
|
+
files: dict[str, ArtifactFileState] = {}
|
|
611
|
+
for artifact in all_synced:
|
|
612
|
+
files[artifact.key] = ArtifactFileState(
|
|
613
|
+
version=current_version,
|
|
614
|
+
hash=artifact.hash,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
# Save state with current version and per-artifact state
|
|
618
|
+
save_artifact_state(project_dir, ArtifactState(version=current_version, files=files))
|
|
619
|
+
|
|
620
|
+
return SyncResult(
|
|
621
|
+
success=True,
|
|
622
|
+
artifacts_installed=total_copied,
|
|
623
|
+
message=f"Synced {total_copied} artifact files",
|
|
624
|
+
)
|
erk/cli/__init__.py
ADDED
|
File without changes
|