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/cli/commands/init.py
ADDED
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from erk.artifacts.sync import sync_artifacts
|
|
8
|
+
from erk.cli.core import discover_repo_context
|
|
9
|
+
from erk.core.claude_settings import (
|
|
10
|
+
ERK_PERMISSION,
|
|
11
|
+
NoBackupCreated,
|
|
12
|
+
StatuslineNotConfigured,
|
|
13
|
+
add_erk_hooks,
|
|
14
|
+
add_erk_permission,
|
|
15
|
+
add_erk_statusline,
|
|
16
|
+
get_erk_statusline_command,
|
|
17
|
+
get_repo_claude_settings_path,
|
|
18
|
+
get_statusline_config,
|
|
19
|
+
has_erk_permission,
|
|
20
|
+
has_erk_statusline,
|
|
21
|
+
has_exit_plan_hook,
|
|
22
|
+
has_user_prompt_hook,
|
|
23
|
+
read_claude_settings,
|
|
24
|
+
write_claude_settings,
|
|
25
|
+
)
|
|
26
|
+
from erk.core.context import ErkContext
|
|
27
|
+
from erk.core.init_utils import (
|
|
28
|
+
add_gitignore_entry,
|
|
29
|
+
discover_presets,
|
|
30
|
+
get_shell_wrapper_content,
|
|
31
|
+
has_shell_integration_in_rc,
|
|
32
|
+
is_repo_erk_ified,
|
|
33
|
+
is_repo_named,
|
|
34
|
+
render_config_template,
|
|
35
|
+
)
|
|
36
|
+
from erk.core.release_notes import get_current_version
|
|
37
|
+
from erk.core.repo_discovery import (
|
|
38
|
+
NoRepoSentinel,
|
|
39
|
+
discover_repo_or_sentinel,
|
|
40
|
+
ensure_erk_metadata_dir,
|
|
41
|
+
)
|
|
42
|
+
from erk.core.shell import Shell
|
|
43
|
+
from erk_shared.context.types import GlobalConfig
|
|
44
|
+
from erk_shared.extraction.claude_installation import RealClaudeInstallation
|
|
45
|
+
from erk_shared.git.real import RealGit
|
|
46
|
+
from erk_shared.github.issues.abc import GitHubIssues
|
|
47
|
+
from erk_shared.github.issues.real import RealGitHubIssues
|
|
48
|
+
from erk_shared.github.plan_issues import get_erk_label_definitions
|
|
49
|
+
from erk_shared.output.output import user_confirm, user_output
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def detect_graphite(shell_ops: Shell) -> bool:
|
|
53
|
+
"""Detect if Graphite (gt) is installed and available in PATH."""
|
|
54
|
+
return shell_ops.get_installed_tool_path("gt") is not None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def create_and_save_global_config(
|
|
58
|
+
ctx: ErkContext,
|
|
59
|
+
erk_root: Path,
|
|
60
|
+
shell_setup_complete: bool,
|
|
61
|
+
) -> GlobalConfig:
|
|
62
|
+
"""Create and save global config, returning the created config."""
|
|
63
|
+
use_graphite = detect_graphite(ctx.shell)
|
|
64
|
+
config = GlobalConfig(
|
|
65
|
+
erk_root=erk_root,
|
|
66
|
+
use_graphite=use_graphite,
|
|
67
|
+
shell_setup_complete=shell_setup_complete,
|
|
68
|
+
show_pr_info=True,
|
|
69
|
+
github_planning=True,
|
|
70
|
+
)
|
|
71
|
+
ctx.erk_installation.save_config(config)
|
|
72
|
+
return config
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _add_gitignore_entry_with_prompt(
|
|
76
|
+
content: str, entry: str, prompt_message: str
|
|
77
|
+
) -> tuple[str, bool]:
|
|
78
|
+
"""Add an entry to gitignore content if not present and user confirms.
|
|
79
|
+
|
|
80
|
+
This wrapper adds user interaction to the pure add_gitignore_entry function.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
content: Current gitignore content
|
|
84
|
+
entry: Entry to add (e.g., ".env")
|
|
85
|
+
prompt_message: Message to show user when confirming
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Tuple of (updated_content, was_modified)
|
|
89
|
+
"""
|
|
90
|
+
# Entry already present
|
|
91
|
+
if entry in content:
|
|
92
|
+
return (content, False)
|
|
93
|
+
|
|
94
|
+
# User declined
|
|
95
|
+
if not click.confirm(prompt_message, default=True):
|
|
96
|
+
return (content, False)
|
|
97
|
+
|
|
98
|
+
# Use pure function to add entry
|
|
99
|
+
new_content = add_gitignore_entry(content, entry)
|
|
100
|
+
return (new_content, True)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _create_prompt_hooks_directory(repo_root: Path) -> None:
|
|
104
|
+
"""Create .erk/prompt-hooks/ directory and install README.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
repo_root: Path to the repository root
|
|
108
|
+
"""
|
|
109
|
+
prompt_hooks_dir = repo_root / ".erk" / "prompt-hooks"
|
|
110
|
+
prompt_hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
|
|
112
|
+
# Install README template
|
|
113
|
+
template_path = Path(__file__).parent.parent / "prompt_hooks_templates" / "README.md"
|
|
114
|
+
readme_path = prompt_hooks_dir / "README.md"
|
|
115
|
+
|
|
116
|
+
if template_path.exists():
|
|
117
|
+
readme_content = template_path.read_text(encoding="utf-8")
|
|
118
|
+
readme_path.write_text(readme_content, encoding="utf-8")
|
|
119
|
+
user_output(click.style("✓", fg="green") + " Created prompt hooks directory")
|
|
120
|
+
user_output(" See .erk/prompt-hooks/README.md for available hooks")
|
|
121
|
+
else:
|
|
122
|
+
# Fallback: create directory but warn about missing template
|
|
123
|
+
user_output(
|
|
124
|
+
click.style("⚠️", fg="yellow") + " Created .erk/prompt-hooks/ (template not found)"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _run_gitignore_prompts(repo_root: Path) -> None:
|
|
129
|
+
"""Run interactive prompts for .gitignore entries.
|
|
130
|
+
|
|
131
|
+
Offers to add .env, .erk/scratch/, and .impl/ to .gitignore.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
repo_root: Path to the repository root
|
|
135
|
+
"""
|
|
136
|
+
gitignore_path = repo_root / ".gitignore"
|
|
137
|
+
if not gitignore_path.exists():
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
gitignore_content = gitignore_path.read_text(encoding="utf-8")
|
|
141
|
+
|
|
142
|
+
# Add .env
|
|
143
|
+
gitignore_content, env_added = _add_gitignore_entry_with_prompt(
|
|
144
|
+
gitignore_content,
|
|
145
|
+
".env",
|
|
146
|
+
"Add .env to .gitignore?",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Add .erk/scratch/
|
|
150
|
+
gitignore_content, scratch_added = _add_gitignore_entry_with_prompt(
|
|
151
|
+
gitignore_content,
|
|
152
|
+
".erk/scratch/",
|
|
153
|
+
"Add .erk/scratch/ to .gitignore (session-specific working files)?",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Add .impl/
|
|
157
|
+
gitignore_content, impl_added = _add_gitignore_entry_with_prompt(
|
|
158
|
+
gitignore_content,
|
|
159
|
+
".impl/",
|
|
160
|
+
"Add .impl/ to .gitignore (temporary implementation plans)?",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Write if any entry was modified
|
|
164
|
+
if env_added or scratch_added or impl_added:
|
|
165
|
+
gitignore_path.write_text(gitignore_content, encoding="utf-8")
|
|
166
|
+
user_output(f"Updated {gitignore_path}")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def print_shell_setup_instructions(
|
|
170
|
+
shell: str, rc_file: Path, completion_line: str, wrapper_content: str
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Print formatted shell integration setup instructions for manual installation.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
shell: The shell type (e.g., "zsh", "bash", "fish")
|
|
176
|
+
rc_file: Path to the shell's rc file (e.g., ~/.zshrc)
|
|
177
|
+
completion_line: The completion command to add (e.g., "source <(erk completion zsh)")
|
|
178
|
+
wrapper_content: The full wrapper function content to add
|
|
179
|
+
"""
|
|
180
|
+
user_output("\n" + "━" * 60)
|
|
181
|
+
user_output("Shell Integration Setup")
|
|
182
|
+
user_output("━" * 60)
|
|
183
|
+
user_output(f"\nDetected shell: {shell} ({rc_file})")
|
|
184
|
+
user_output("\nAdd the following to your rc file:\n")
|
|
185
|
+
user_output("# Erk completion")
|
|
186
|
+
user_output(f"{completion_line}\n")
|
|
187
|
+
user_output("# Erk shell integration")
|
|
188
|
+
user_output(wrapper_content)
|
|
189
|
+
user_output("\nThen reload your shell:")
|
|
190
|
+
user_output(f" source {rc_file}")
|
|
191
|
+
user_output("━" * 60)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def perform_shell_setup(shell_ops: Shell) -> bool:
|
|
195
|
+
"""Print shell integration setup instructions for manual installation.
|
|
196
|
+
|
|
197
|
+
Returns True if instructions were printed, False if setup was skipped.
|
|
198
|
+
"""
|
|
199
|
+
shell_info = shell_ops.detect_shell()
|
|
200
|
+
if not shell_info:
|
|
201
|
+
user_output("Unable to detect shell. Skipping shell integration setup.")
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
shell, rc_file = shell_info
|
|
205
|
+
|
|
206
|
+
# Resolve symlinks to show the real file path in instructions
|
|
207
|
+
if rc_file.exists():
|
|
208
|
+
rc_file = rc_file.resolve()
|
|
209
|
+
|
|
210
|
+
user_output(f"\nDetected shell: {shell}")
|
|
211
|
+
user_output("Shell integration provides:")
|
|
212
|
+
user_output(" - Tab completion for erk commands")
|
|
213
|
+
user_output(" - Automatic worktree activation on 'erk br co'")
|
|
214
|
+
|
|
215
|
+
if not click.confirm("\nShow shell integration setup instructions?", default=True):
|
|
216
|
+
user_output("Skipping shell integration. You can run 'erk init --shell' later.")
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
# Generate the instructions
|
|
220
|
+
completion_line = f"source <(erk completion {shell})"
|
|
221
|
+
shell_integration_dir = Path(__file__).parent.parent / "shell_integration"
|
|
222
|
+
wrapper_content = get_shell_wrapper_content(shell_integration_dir, shell)
|
|
223
|
+
|
|
224
|
+
# Print the formatted instructions
|
|
225
|
+
print_shell_setup_instructions(shell, rc_file, completion_line, wrapper_content)
|
|
226
|
+
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _get_presets_dir() -> Path:
|
|
231
|
+
"""Get the path to the presets directory."""
|
|
232
|
+
return Path(__file__).parent.parent / "presets"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def offer_claude_permission_setup(repo_root: Path) -> Path | NoBackupCreated:
|
|
236
|
+
"""Offer to add erk permission to repo's Claude Code settings.
|
|
237
|
+
|
|
238
|
+
This checks if the repo's .claude/settings.json exists and whether the erk
|
|
239
|
+
permission is already configured. If the file exists but permission is missing,
|
|
240
|
+
it prompts the user to add it.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
repo_root: Path to the repository root
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Path to backup file if one was created, NoBackupCreated sentinel otherwise.
|
|
247
|
+
"""
|
|
248
|
+
settings_path = get_repo_claude_settings_path(repo_root)
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
settings = read_claude_settings(settings_path)
|
|
252
|
+
except json.JSONDecodeError as e:
|
|
253
|
+
warning = click.style("⚠️ Warning: ", fg="yellow")
|
|
254
|
+
user_output(warning + "Invalid JSON in .claude/settings.json")
|
|
255
|
+
user_output(f" {e}")
|
|
256
|
+
return NoBackupCreated()
|
|
257
|
+
|
|
258
|
+
# No settings file - skip silently (repo may not have Claude settings)
|
|
259
|
+
if settings is None:
|
|
260
|
+
return NoBackupCreated()
|
|
261
|
+
|
|
262
|
+
# Permission already exists - skip silently
|
|
263
|
+
if has_erk_permission(settings):
|
|
264
|
+
return NoBackupCreated()
|
|
265
|
+
|
|
266
|
+
# Offer to add permission
|
|
267
|
+
user_output("\nClaude settings found. The erk permission allows Claude to run")
|
|
268
|
+
user_output("erk commands without prompting for approval each time.")
|
|
269
|
+
|
|
270
|
+
if not user_confirm(f"Add {ERK_PERMISSION} to .claude/settings.json?", default=True):
|
|
271
|
+
user_output("Skipped. You can add the permission manually to .claude/settings.json")
|
|
272
|
+
return NoBackupCreated()
|
|
273
|
+
|
|
274
|
+
# Add permission
|
|
275
|
+
new_settings = add_erk_permission(settings)
|
|
276
|
+
|
|
277
|
+
# Confirm before overwriting
|
|
278
|
+
user_output(f"\nThis will update: {settings_path}")
|
|
279
|
+
if not user_confirm("Proceed with writing changes?", default=False):
|
|
280
|
+
user_output("Skipped. No changes made to settings.json")
|
|
281
|
+
return NoBackupCreated()
|
|
282
|
+
|
|
283
|
+
backup_result = write_claude_settings(settings_path, new_settings)
|
|
284
|
+
user_output(click.style("✓", fg="green") + f" Added {ERK_PERMISSION} to {settings_path}")
|
|
285
|
+
|
|
286
|
+
# If backup was created, inform user (deletion offered at end of init)
|
|
287
|
+
if not isinstance(backup_result, NoBackupCreated):
|
|
288
|
+
user_output(f"\n📁 Backup created: {backup_result}")
|
|
289
|
+
user_output(f" To restore: cp {backup_result} {settings_path}")
|
|
290
|
+
|
|
291
|
+
return backup_result
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def offer_backup_cleanup(backup_path: Path) -> None:
|
|
295
|
+
"""Offer to delete a backup file.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
backup_path: Path to the backup file to potentially delete
|
|
299
|
+
"""
|
|
300
|
+
if click.confirm("Delete backup?", default=True):
|
|
301
|
+
backup_path.unlink()
|
|
302
|
+
user_output(click.style("✓", fg="green") + " Backup deleted")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def offer_claude_hook_setup(repo_root: Path) -> None:
|
|
306
|
+
"""Offer to add erk hooks to repo's Claude Code settings.
|
|
307
|
+
|
|
308
|
+
This checks if the repo's .claude/settings.json exists and whether the erk
|
|
309
|
+
hooks are already configured. If the file exists but hooks are missing,
|
|
310
|
+
it prompts the user to add them.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
repo_root: Path to the repository root
|
|
314
|
+
"""
|
|
315
|
+
settings_path = get_repo_claude_settings_path(repo_root)
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
settings = read_claude_settings(settings_path)
|
|
319
|
+
except json.JSONDecodeError as e:
|
|
320
|
+
warning = click.style("⚠️ Warning: ", fg="yellow")
|
|
321
|
+
user_output(warning + "Invalid JSON in .claude/settings.json")
|
|
322
|
+
user_output(f" {e}")
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
# No settings file - will create one
|
|
326
|
+
creating_new_file = settings is None
|
|
327
|
+
if creating_new_file:
|
|
328
|
+
settings = {}
|
|
329
|
+
user_output(f"\nNo .claude/settings.json found. Will create: {settings_path}")
|
|
330
|
+
|
|
331
|
+
assert settings is not None # Type narrowing: set to {} if was None above
|
|
332
|
+
if has_user_prompt_hook(settings) and has_exit_plan_hook(settings):
|
|
333
|
+
user_output(click.style("✓", fg="green") + " Hooks already configured")
|
|
334
|
+
return
|
|
335
|
+
|
|
336
|
+
# Explain what hooks do
|
|
337
|
+
user_output("\nErk uses Claude Code hooks for session management and plan tracking.")
|
|
338
|
+
|
|
339
|
+
if not user_confirm("Add erk hooks to .claude/settings.json?", default=False):
|
|
340
|
+
user_output("Skipped. You can add hooks later with: erk init --hooks")
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
new_settings = add_erk_hooks(settings)
|
|
344
|
+
write_claude_settings(settings_path, new_settings)
|
|
345
|
+
user_output(click.style("✓", fg="green") + " Added erk hooks")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def create_plans_repo_labels(
|
|
349
|
+
repo_root: Path,
|
|
350
|
+
plans_repo: str,
|
|
351
|
+
github_issues: GitHubIssues,
|
|
352
|
+
) -> str | None:
|
|
353
|
+
"""Create erk labels in the target issues repository.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
repo_root: Path to the working repository root (used for gh CLI context)
|
|
357
|
+
plans_repo: Target repository in "owner/repo" format
|
|
358
|
+
github_issues: GitHubIssues interface for label operations
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
None on success, error message string on failure
|
|
362
|
+
"""
|
|
363
|
+
labels = get_erk_label_definitions()
|
|
364
|
+
|
|
365
|
+
for label in labels:
|
|
366
|
+
github_issues.ensure_label_exists(
|
|
367
|
+
repo_root=repo_root,
|
|
368
|
+
label=label.name,
|
|
369
|
+
description=label.description,
|
|
370
|
+
color=label.color,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
return None
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def offer_plans_repo_label_setup(repo_root: Path, plans_repo: str) -> None:
|
|
377
|
+
"""Offer to set up erk labels in the target issues repository.
|
|
378
|
+
|
|
379
|
+
When a plans_repo is configured, issues are created in a separate repository
|
|
380
|
+
from the working repository. This function ensures all required erk labels
|
|
381
|
+
(erk-plan, erk-extraction, erk-objective) exist in that target repository.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
repo_root: Path to the working repository root (used for gh CLI context)
|
|
385
|
+
plans_repo: Target repository in "owner/repo" format
|
|
386
|
+
"""
|
|
387
|
+
user_output(f"\nPlans repo configured: {plans_repo}")
|
|
388
|
+
user_output("Erk uses labels (erk-plan, erk-extraction, erk-objective) to organize issues.")
|
|
389
|
+
|
|
390
|
+
if not user_confirm(f"Set up erk labels in {plans_repo}?", default=True):
|
|
391
|
+
user_output("Skipped. You can set up labels later with: erk doctor --fix")
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
github_issues = RealGitHubIssues(target_repo=plans_repo)
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
create_plans_repo_labels(repo_root, plans_repo, github_issues)
|
|
398
|
+
user_output(click.style("✓", fg="green") + f" Labels configured in {plans_repo}")
|
|
399
|
+
except RuntimeError as e:
|
|
400
|
+
warning = click.style("⚠️ Warning: ", fg="yellow")
|
|
401
|
+
user_output(warning + f"Failed to set up labels in {plans_repo}")
|
|
402
|
+
user_output(f" {e}")
|
|
403
|
+
user_output(" You can try again with: erk doctor --fix")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def perform_statusline_setup(settings_path: Path | None) -> bool:
|
|
407
|
+
"""Configure erk-statusline in global Claude Code settings.
|
|
408
|
+
|
|
409
|
+
Reads ~/.claude/settings.json, adds statusLine configuration if not present
|
|
410
|
+
or different, and writes back. Handles edge cases:
|
|
411
|
+
- File doesn't exist: creates it with just statusLine config
|
|
412
|
+
- Already configured with same command: skips
|
|
413
|
+
- Different statusLine command: warns and prompts to overwrite
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
settings_path: Path to settings.json. If None, uses ~/.claude/settings.json.
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
True if status line was configured, False otherwise.
|
|
420
|
+
"""
|
|
421
|
+
if settings_path is None:
|
|
422
|
+
# Use RealClaudeInstallation directly since this runs before ErkContext exists
|
|
423
|
+
installation = RealClaudeInstallation()
|
|
424
|
+
settings_path = installation.get_settings_path()
|
|
425
|
+
|
|
426
|
+
user_output("\n Configuring Claude Code status line...")
|
|
427
|
+
|
|
428
|
+
# Read existing settings (or None if file doesn't exist)
|
|
429
|
+
try:
|
|
430
|
+
settings = read_claude_settings(settings_path)
|
|
431
|
+
except json.JSONDecodeError as e:
|
|
432
|
+
warning = click.style("⚠️ Warning: ", fg="yellow")
|
|
433
|
+
user_output(warning + "Invalid JSON in ~/.claude/settings.json")
|
|
434
|
+
user_output(f" {e}")
|
|
435
|
+
return False
|
|
436
|
+
|
|
437
|
+
# No settings file - will create one
|
|
438
|
+
if settings is None:
|
|
439
|
+
settings = {}
|
|
440
|
+
user_output(f" Creating: {settings_path}")
|
|
441
|
+
|
|
442
|
+
# Check current statusline config
|
|
443
|
+
current_config = get_statusline_config(settings)
|
|
444
|
+
|
|
445
|
+
# Already configured with erk-statusline
|
|
446
|
+
if has_erk_statusline(settings):
|
|
447
|
+
user_output(click.style(" ✓", fg="green") + " Statusline already configured")
|
|
448
|
+
return True
|
|
449
|
+
|
|
450
|
+
# Different statusline configured - warn and prompt
|
|
451
|
+
if not isinstance(current_config, StatuslineNotConfigured):
|
|
452
|
+
user_output(f"\n Existing statusLine found: {current_config.command}")
|
|
453
|
+
if not user_confirm(f" Replace with {get_erk_statusline_command()}?", default=False):
|
|
454
|
+
user_output(" Skipped. Keeping existing statusLine configuration.")
|
|
455
|
+
return False
|
|
456
|
+
|
|
457
|
+
# Add statusline config
|
|
458
|
+
new_settings = add_erk_statusline(settings)
|
|
459
|
+
write_claude_settings(settings_path, new_settings)
|
|
460
|
+
statusline_msg = " Status line configured in ~/.claude/settings.json"
|
|
461
|
+
user_output(click.style(" ✓", fg="green") + statusline_msg)
|
|
462
|
+
user_output(" Note: Install erk-statusline with: uv tool install erk-statusline")
|
|
463
|
+
|
|
464
|
+
return True
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
@click.command("init")
|
|
468
|
+
@click.option("-f", "--force", is_flag=True, help="Overwrite existing repo config if present.")
|
|
469
|
+
@click.option(
|
|
470
|
+
"--preset",
|
|
471
|
+
type=str,
|
|
472
|
+
default="auto",
|
|
473
|
+
help=(
|
|
474
|
+
"Config template to use. 'auto' detects preset based on repo characteristics. "
|
|
475
|
+
f"Available: auto, {', '.join(discover_presets(_get_presets_dir()))}."
|
|
476
|
+
),
|
|
477
|
+
)
|
|
478
|
+
@click.option(
|
|
479
|
+
"--list-presets",
|
|
480
|
+
is_flag=True,
|
|
481
|
+
help="List available presets and exit.",
|
|
482
|
+
)
|
|
483
|
+
@click.option(
|
|
484
|
+
"--shell",
|
|
485
|
+
is_flag=True,
|
|
486
|
+
help="Show shell integration setup instructions (completion + auto-activation wrapper).",
|
|
487
|
+
)
|
|
488
|
+
@click.option(
|
|
489
|
+
"--hooks",
|
|
490
|
+
"hooks_only",
|
|
491
|
+
is_flag=True,
|
|
492
|
+
help="Only set up Claude Code hooks.",
|
|
493
|
+
)
|
|
494
|
+
@click.option(
|
|
495
|
+
"--statusline",
|
|
496
|
+
"statusline_only",
|
|
497
|
+
is_flag=True,
|
|
498
|
+
help="Only configure erk-statusline in Claude Code.",
|
|
499
|
+
)
|
|
500
|
+
@click.option(
|
|
501
|
+
"--no-interactive",
|
|
502
|
+
"no_interactive",
|
|
503
|
+
is_flag=True,
|
|
504
|
+
help="Skip all interactive prompts (gitignore, permissions, hooks, shell setup).",
|
|
505
|
+
)
|
|
506
|
+
@click.option(
|
|
507
|
+
"--with-dignified-review",
|
|
508
|
+
"with_dignified_review",
|
|
509
|
+
is_flag=True,
|
|
510
|
+
help="Install dignified-python skill and review workflow.",
|
|
511
|
+
)
|
|
512
|
+
@click.pass_obj
|
|
513
|
+
def init_cmd(
|
|
514
|
+
ctx: ErkContext,
|
|
515
|
+
force: bool,
|
|
516
|
+
preset: str,
|
|
517
|
+
list_presets: bool,
|
|
518
|
+
shell: bool,
|
|
519
|
+
hooks_only: bool,
|
|
520
|
+
statusline_only: bool,
|
|
521
|
+
no_interactive: bool,
|
|
522
|
+
with_dignified_review: bool,
|
|
523
|
+
) -> None:
|
|
524
|
+
"""Initialize erk for this repo and scaffold config.toml.
|
|
525
|
+
|
|
526
|
+
Runs in three sequential steps:
|
|
527
|
+
1. Repo verification - checks that you're in a git repository
|
|
528
|
+
2. Project setup - erk-ifies the repo (if not already)
|
|
529
|
+
3. User setup - configures shell integration and Claude Code status line
|
|
530
|
+
"""
|
|
531
|
+
# Discover available presets on demand (needed for --list-presets)
|
|
532
|
+
presets_dir = _get_presets_dir()
|
|
533
|
+
available_presets = discover_presets(presets_dir)
|
|
534
|
+
valid_choices = ["auto"] + available_presets
|
|
535
|
+
|
|
536
|
+
# Handle --list-presets flag (doesn't require repo)
|
|
537
|
+
if list_presets:
|
|
538
|
+
user_output("Available presets:")
|
|
539
|
+
for p in available_presets:
|
|
540
|
+
user_output(f" - {p}")
|
|
541
|
+
return
|
|
542
|
+
|
|
543
|
+
# Handle --shell flag: only do shell setup (doesn't require repo)
|
|
544
|
+
if shell:
|
|
545
|
+
if ctx.global_config is None:
|
|
546
|
+
config_path = ctx.erk_installation.config_path()
|
|
547
|
+
user_output(f"Global config not found at {config_path}")
|
|
548
|
+
user_output("Run 'erk init' without --shell to create global config first.")
|
|
549
|
+
raise SystemExit(1)
|
|
550
|
+
|
|
551
|
+
setup_complete = perform_shell_setup(ctx.shell)
|
|
552
|
+
if setup_complete:
|
|
553
|
+
# Show what we're about to write
|
|
554
|
+
config_path = ctx.erk_installation.config_path()
|
|
555
|
+
user_output("\nTo remember that shell setup is complete, erk needs to update:")
|
|
556
|
+
user_output(f" {config_path}")
|
|
557
|
+
|
|
558
|
+
if not user_confirm("Proceed with updating global config?", default=False):
|
|
559
|
+
user_output("\nShell integration instructions shown above.")
|
|
560
|
+
user_output("Run 'erk init --shell' to save this preference.")
|
|
561
|
+
return
|
|
562
|
+
|
|
563
|
+
# Update global config with shell_setup_complete=True
|
|
564
|
+
new_config = GlobalConfig(
|
|
565
|
+
erk_root=ctx.global_config.erk_root,
|
|
566
|
+
use_graphite=ctx.global_config.use_graphite,
|
|
567
|
+
shell_setup_complete=True,
|
|
568
|
+
show_pr_info=ctx.global_config.show_pr_info,
|
|
569
|
+
github_planning=ctx.global_config.github_planning,
|
|
570
|
+
)
|
|
571
|
+
try:
|
|
572
|
+
ctx.erk_installation.save_config(new_config)
|
|
573
|
+
user_output(click.style("✓", fg="green") + " Global config updated")
|
|
574
|
+
except PermissionError as e:
|
|
575
|
+
user_output(click.style("\n❌ Error: ", fg="red") + "Could not save global config")
|
|
576
|
+
user_output(str(e))
|
|
577
|
+
user_output("\nShell integration instructions shown above.")
|
|
578
|
+
user_output("You can use them now - erk just couldn't save.")
|
|
579
|
+
raise SystemExit(1) from e
|
|
580
|
+
return
|
|
581
|
+
|
|
582
|
+
# =========================================================================
|
|
583
|
+
# STEP 1: Repo Verification
|
|
584
|
+
# =========================================================================
|
|
585
|
+
user_output("\nStep 1: Checking repository...")
|
|
586
|
+
|
|
587
|
+
# Check if we're in a git repo (before any other setup)
|
|
588
|
+
# Use a temporary erk_root for discovery - will be replaced after global config setup
|
|
589
|
+
temp_erk_root = Path.home() / ".erk"
|
|
590
|
+
repo_or_sentinel = discover_repo_or_sentinel(ctx.cwd, temp_erk_root, RealGit())
|
|
591
|
+
|
|
592
|
+
if isinstance(repo_or_sentinel, NoRepoSentinel):
|
|
593
|
+
user_output(click.style("Error: ", fg="red") + "Not in a git repository.")
|
|
594
|
+
user_output("Run 'erk init' from within a git repository.")
|
|
595
|
+
raise SystemExit(1)
|
|
596
|
+
|
|
597
|
+
# We have a valid repo - extract the root for display
|
|
598
|
+
repo_root = repo_or_sentinel.root
|
|
599
|
+
user_output(click.style("✓", fg="green") + f" Git repository detected: {repo_root.name}")
|
|
600
|
+
|
|
601
|
+
# Handle --hooks flag: only do hook setup
|
|
602
|
+
if hooks_only:
|
|
603
|
+
offer_claude_hook_setup(repo_root)
|
|
604
|
+
return
|
|
605
|
+
|
|
606
|
+
# Handle --statusline flag: only do statusline setup
|
|
607
|
+
if statusline_only:
|
|
608
|
+
perform_statusline_setup(settings_path=None)
|
|
609
|
+
return
|
|
610
|
+
|
|
611
|
+
# Validate preset choice
|
|
612
|
+
if preset not in valid_choices:
|
|
613
|
+
user_output(f"Invalid preset '{preset}'. Available options: {', '.join(valid_choices)}")
|
|
614
|
+
raise SystemExit(1)
|
|
615
|
+
|
|
616
|
+
# =========================================================================
|
|
617
|
+
# STEP 2: Project Configuration
|
|
618
|
+
# =========================================================================
|
|
619
|
+
user_output("\nStep 2: Project configuration...")
|
|
620
|
+
|
|
621
|
+
# Check if repo is already erk-ified
|
|
622
|
+
already_erkified = is_repo_erk_ified(repo_root)
|
|
623
|
+
|
|
624
|
+
if already_erkified and not force:
|
|
625
|
+
user_output(click.style("✓", fg="green") + " Repository already configured for erk")
|
|
626
|
+
else:
|
|
627
|
+
# Check for global config first
|
|
628
|
+
if not ctx.erk_installation.config_exists():
|
|
629
|
+
config_path = ctx.erk_installation.config_path()
|
|
630
|
+
user_output(f" Global config not found at {config_path}")
|
|
631
|
+
user_output(" Please provide the path for your .erk folder.")
|
|
632
|
+
user_output(" (This directory will contain worktrees for each repository)")
|
|
633
|
+
default_erk_root = Path.home() / ".erk"
|
|
634
|
+
erk_root = click.prompt(" .erk folder", type=Path, default=str(default_erk_root))
|
|
635
|
+
erk_root = erk_root.expanduser().resolve()
|
|
636
|
+
config = create_and_save_global_config(ctx, erk_root, shell_setup_complete=False)
|
|
637
|
+
# Update context with newly created config
|
|
638
|
+
ctx = dataclasses.replace(ctx, global_config=config)
|
|
639
|
+
user_output(f" Created global config at {config_path}")
|
|
640
|
+
# Show graphite status on first init
|
|
641
|
+
has_graphite = detect_graphite(ctx.shell)
|
|
642
|
+
if has_graphite:
|
|
643
|
+
user_output(" Graphite (gt) detected - will use 'gt create' for new branches")
|
|
644
|
+
else:
|
|
645
|
+
user_output(" Graphite (gt) not detected - will use 'git' for branch creation")
|
|
646
|
+
|
|
647
|
+
# Now re-discover repo with correct erk_root
|
|
648
|
+
if ctx.global_config is not None:
|
|
649
|
+
repo_context = discover_repo_context(ctx, ctx.cwd)
|
|
650
|
+
else:
|
|
651
|
+
# Fallback (shouldn't happen, but defensive)
|
|
652
|
+
repo_context = repo_or_sentinel
|
|
653
|
+
|
|
654
|
+
# Ensure .erk directory exists
|
|
655
|
+
erk_dir = repo_context.root / ".erk"
|
|
656
|
+
erk_dir.mkdir(parents=True, exist_ok=True)
|
|
657
|
+
|
|
658
|
+
# All repo config now goes to .erk/config.toml (consolidated location)
|
|
659
|
+
cfg_path = erk_dir / "config.toml"
|
|
660
|
+
|
|
661
|
+
# Also ensure metadata directory exists (needed for worktrees dir)
|
|
662
|
+
ensure_erk_metadata_dir(repo_context)
|
|
663
|
+
|
|
664
|
+
effective_preset: str | None
|
|
665
|
+
choice = preset.lower()
|
|
666
|
+
if choice == "auto":
|
|
667
|
+
is_dagster = is_repo_named(repo_context.root, "dagster")
|
|
668
|
+
effective_preset = "dagster" if is_dagster else "generic"
|
|
669
|
+
else:
|
|
670
|
+
effective_preset = choice
|
|
671
|
+
|
|
672
|
+
content = render_config_template(presets_dir, effective_preset)
|
|
673
|
+
cfg_path.write_text(content, encoding="utf-8")
|
|
674
|
+
user_output(f" Wrote {cfg_path}")
|
|
675
|
+
|
|
676
|
+
# Create required version file
|
|
677
|
+
version_file = erk_dir / "required-erk-uv-tool-version"
|
|
678
|
+
version_file.write_text(f"{get_current_version()}\n", encoding="utf-8")
|
|
679
|
+
user_output(f" Wrote {version_file}")
|
|
680
|
+
|
|
681
|
+
# Sync artifacts (skills, commands, agents, workflows, actions)
|
|
682
|
+
sync_result = sync_artifacts(repo_context.root, force=False)
|
|
683
|
+
if sync_result.success:
|
|
684
|
+
user_output(click.style(" ✓ ", fg="green") + sync_result.message)
|
|
685
|
+
else:
|
|
686
|
+
# Non-fatal: warn but continue init
|
|
687
|
+
warn_msg = f"Artifact sync failed: {sync_result.message}"
|
|
688
|
+
user_output(click.style(" ⚠ ", fg="yellow") + warn_msg)
|
|
689
|
+
user_output(" Run 'erk artifact sync' to retry")
|
|
690
|
+
|
|
691
|
+
# Sync optional dignified-review feature if requested
|
|
692
|
+
if with_dignified_review:
|
|
693
|
+
from erk.artifacts.sync import sync_dignified_review
|
|
694
|
+
|
|
695
|
+
dr_result = sync_dignified_review(repo_context.root)
|
|
696
|
+
if dr_result.success:
|
|
697
|
+
user_output(click.style(" ✓ ", fg="green") + dr_result.message)
|
|
698
|
+
else:
|
|
699
|
+
warn_msg = f"dignified-review sync failed: {dr_result.message}"
|
|
700
|
+
user_output(click.style(" ⚠ ", fg="yellow") + warn_msg)
|
|
701
|
+
|
|
702
|
+
# Create prompt hooks directory with README
|
|
703
|
+
_create_prompt_hooks_directory(repo_root=repo_context.root)
|
|
704
|
+
|
|
705
|
+
# Skip interactive prompts if requested
|
|
706
|
+
interactive = not no_interactive
|
|
707
|
+
|
|
708
|
+
# Track backup files for cleanup at end
|
|
709
|
+
pending_backup: Path | NoBackupCreated = NoBackupCreated()
|
|
710
|
+
|
|
711
|
+
if interactive:
|
|
712
|
+
_run_gitignore_prompts(repo_context.root)
|
|
713
|
+
pending_backup = offer_claude_permission_setup(repo_context.root)
|
|
714
|
+
offer_claude_hook_setup(repo_context.root)
|
|
715
|
+
|
|
716
|
+
# Check if plans_repo is configured and offer label setup
|
|
717
|
+
from erk.cli.config import load_config as load_repo_config
|
|
718
|
+
|
|
719
|
+
repo_config = load_repo_config(repo_context.root)
|
|
720
|
+
if repo_config.plans_repo is not None:
|
|
721
|
+
offer_plans_repo_label_setup(repo_context.root, repo_config.plans_repo)
|
|
722
|
+
|
|
723
|
+
# Offer to clean up any pending backup files (at end of project setup)
|
|
724
|
+
if not isinstance(pending_backup, NoBackupCreated):
|
|
725
|
+
offer_backup_cleanup(pending_backup)
|
|
726
|
+
|
|
727
|
+
# =========================================================================
|
|
728
|
+
# STEP 3: User Configuration (always runs)
|
|
729
|
+
# =========================================================================
|
|
730
|
+
user_output("\nStep 3: User configuration...")
|
|
731
|
+
|
|
732
|
+
# Skip interactive prompts if requested
|
|
733
|
+
interactive = not no_interactive
|
|
734
|
+
|
|
735
|
+
# 3a. Global config (if not exists) - already handled in step 2 if needed
|
|
736
|
+
|
|
737
|
+
# 3b. Shell integration
|
|
738
|
+
if interactive:
|
|
739
|
+
# Only check if global config exists
|
|
740
|
+
if ctx.global_config is not None or ctx.erk_installation.config_exists():
|
|
741
|
+
config_exists = ctx.erk_installation.config_exists()
|
|
742
|
+
fresh_config = ctx.erk_installation.load_config() if config_exists else None
|
|
743
|
+
if fresh_config is not None and not fresh_config.shell_setup_complete:
|
|
744
|
+
# Check if shell integration is already in the RC file
|
|
745
|
+
shell_info = ctx.shell.detect_shell()
|
|
746
|
+
already_in_rc = False
|
|
747
|
+
if shell_info is not None:
|
|
748
|
+
shell_name, rc_path = shell_info
|
|
749
|
+
already_in_rc = has_shell_integration_in_rc(rc_path)
|
|
750
|
+
if already_in_rc:
|
|
751
|
+
# Already configured - just show message and update config flag
|
|
752
|
+
msg = f" Shell integration already configured ({shell_name})"
|
|
753
|
+
user_output(click.style("✓", fg="green") + msg)
|
|
754
|
+
# Update global config to remember this
|
|
755
|
+
new_config = GlobalConfig(
|
|
756
|
+
erk_root=fresh_config.erk_root,
|
|
757
|
+
use_graphite=fresh_config.use_graphite,
|
|
758
|
+
shell_setup_complete=True,
|
|
759
|
+
show_pr_info=fresh_config.show_pr_info,
|
|
760
|
+
github_planning=fresh_config.github_planning,
|
|
761
|
+
)
|
|
762
|
+
ctx.erk_installation.save_config(new_config)
|
|
763
|
+
|
|
764
|
+
if not already_in_rc:
|
|
765
|
+
setup_complete = perform_shell_setup(ctx.shell)
|
|
766
|
+
if setup_complete:
|
|
767
|
+
# Show what we're about to write
|
|
768
|
+
config_path = ctx.erk_installation.config_path()
|
|
769
|
+
shell_msg = "To remember that shell setup is complete, erk needs to update:"
|
|
770
|
+
user_output(f"\n {shell_msg}")
|
|
771
|
+
user_output(f" {config_path}")
|
|
772
|
+
|
|
773
|
+
prompt = " Proceed with updating global config?"
|
|
774
|
+
if not user_confirm(prompt, default=False):
|
|
775
|
+
user_output("\n Shell integration instructions shown above.")
|
|
776
|
+
user_output(" Run 'erk init --shell' to save this preference.")
|
|
777
|
+
else:
|
|
778
|
+
# Update global config with shell_setup_complete=True
|
|
779
|
+
new_config = GlobalConfig(
|
|
780
|
+
erk_root=fresh_config.erk_root,
|
|
781
|
+
use_graphite=fresh_config.use_graphite,
|
|
782
|
+
shell_setup_complete=True,
|
|
783
|
+
show_pr_info=fresh_config.show_pr_info,
|
|
784
|
+
github_planning=fresh_config.github_planning,
|
|
785
|
+
)
|
|
786
|
+
try:
|
|
787
|
+
ctx.erk_installation.save_config(new_config)
|
|
788
|
+
msg = click.style(" ✓", fg="green") + " Global config updated"
|
|
789
|
+
user_output(msg)
|
|
790
|
+
except PermissionError as e:
|
|
791
|
+
error_msg = "Could not save global config"
|
|
792
|
+
user_output(click.style("\n ❌ Error: ", fg="red") + error_msg)
|
|
793
|
+
user_output(f" {e}")
|
|
794
|
+
user_output("\n Shell integration instructions shown above.")
|
|
795
|
+
user_output(" You can use them now - erk just couldn't save.")
|
|
796
|
+
|
|
797
|
+
# 3c. Status line configuration
|
|
798
|
+
if interactive:
|
|
799
|
+
perform_statusline_setup(settings_path=None)
|
|
800
|
+
|
|
801
|
+
user_output(click.style("\n✓", fg="green") + " Initialization complete!")
|