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.
Files changed (331) hide show
  1. erk/__init__.py +12 -0
  2. erk/__main__.py +6 -0
  3. erk/agent_docs/__init__.py +5 -0
  4. erk/agent_docs/models.py +123 -0
  5. erk/agent_docs/operations.py +666 -0
  6. erk/artifacts/__init__.py +5 -0
  7. erk/artifacts/artifact_health.py +623 -0
  8. erk/artifacts/detection.py +16 -0
  9. erk/artifacts/discovery.py +343 -0
  10. erk/artifacts/models.py +63 -0
  11. erk/artifacts/staleness.py +56 -0
  12. erk/artifacts/state.py +100 -0
  13. erk/artifacts/sync.py +624 -0
  14. erk/cli/__init__.py +0 -0
  15. erk/cli/activation.py +132 -0
  16. erk/cli/alias.py +53 -0
  17. erk/cli/cli.py +221 -0
  18. erk/cli/commands/__init__.py +0 -0
  19. erk/cli/commands/admin.py +153 -0
  20. erk/cli/commands/artifact/__init__.py +1 -0
  21. erk/cli/commands/artifact/check.py +260 -0
  22. erk/cli/commands/artifact/group.py +31 -0
  23. erk/cli/commands/artifact/list_cmd.py +89 -0
  24. erk/cli/commands/artifact/show.py +62 -0
  25. erk/cli/commands/artifact/sync_cmd.py +39 -0
  26. erk/cli/commands/branch/__init__.py +26 -0
  27. erk/cli/commands/branch/assign_cmd.py +152 -0
  28. erk/cli/commands/branch/checkout_cmd.py +357 -0
  29. erk/cli/commands/branch/create_cmd.py +161 -0
  30. erk/cli/commands/branch/list_cmd.py +82 -0
  31. erk/cli/commands/branch/unassign_cmd.py +197 -0
  32. erk/cli/commands/cc/__init__.py +15 -0
  33. erk/cli/commands/cc/jsonl_cmd.py +20 -0
  34. erk/cli/commands/cc/session/AGENTS.md +30 -0
  35. erk/cli/commands/cc/session/CLAUDE.md +1 -0
  36. erk/cli/commands/cc/session/__init__.py +15 -0
  37. erk/cli/commands/cc/session/list_cmd.py +167 -0
  38. erk/cli/commands/cc/session/show_cmd.py +175 -0
  39. erk/cli/commands/completion.py +89 -0
  40. erk/cli/commands/completions.py +165 -0
  41. erk/cli/commands/config.py +327 -0
  42. erk/cli/commands/docs/__init__.py +1 -0
  43. erk/cli/commands/docs/group.py +16 -0
  44. erk/cli/commands/docs/sync.py +121 -0
  45. erk/cli/commands/docs/validate.py +102 -0
  46. erk/cli/commands/doctor.py +243 -0
  47. erk/cli/commands/down.py +171 -0
  48. erk/cli/commands/exec/__init__.py +1 -0
  49. erk/cli/commands/exec/group.py +164 -0
  50. erk/cli/commands/exec/scripts/AGENTS.md +79 -0
  51. erk/cli/commands/exec/scripts/CLAUDE.md +1 -0
  52. erk/cli/commands/exec/scripts/__init__.py +5 -0
  53. erk/cli/commands/exec/scripts/add_reaction_to_comment.py +69 -0
  54. erk/cli/commands/exec/scripts/add_remote_execution_note.py +68 -0
  55. erk/cli/commands/exec/scripts/check_impl.py +152 -0
  56. erk/cli/commands/exec/scripts/ci_update_pr_body.py +294 -0
  57. erk/cli/commands/exec/scripts/create_extraction_branch.py +138 -0
  58. erk/cli/commands/exec/scripts/create_extraction_plan.py +242 -0
  59. erk/cli/commands/exec/scripts/create_issue_from_session.py +103 -0
  60. erk/cli/commands/exec/scripts/create_plan_from_context.py +103 -0
  61. erk/cli/commands/exec/scripts/create_worker_impl_from_issue.py +93 -0
  62. erk/cli/commands/exec/scripts/detect_trunk_branch.py +121 -0
  63. erk/cli/commands/exec/scripts/exit_plan_mode_hook.py +777 -0
  64. erk/cli/commands/exec/scripts/extract_latest_plan.py +49 -0
  65. erk/cli/commands/exec/scripts/extract_session_from_issue.py +150 -0
  66. erk/cli/commands/exec/scripts/find_project_dir.py +214 -0
  67. erk/cli/commands/exec/scripts/generate_pr_summary.py +112 -0
  68. erk/cli/commands/exec/scripts/get_closing_text.py +98 -0
  69. erk/cli/commands/exec/scripts/get_embedded_prompt.py +62 -0
  70. erk/cli/commands/exec/scripts/get_plan_metadata.py +95 -0
  71. erk/cli/commands/exec/scripts/get_pr_body_footer.py +70 -0
  72. erk/cli/commands/exec/scripts/get_pr_discussion_comments.py +149 -0
  73. erk/cli/commands/exec/scripts/get_pr_review_comments.py +155 -0
  74. erk/cli/commands/exec/scripts/impl_init.py +158 -0
  75. erk/cli/commands/exec/scripts/impl_signal.py +375 -0
  76. erk/cli/commands/exec/scripts/impl_verify.py +49 -0
  77. erk/cli/commands/exec/scripts/issue_title_to_filename.py +34 -0
  78. erk/cli/commands/exec/scripts/list_sessions.py +296 -0
  79. erk/cli/commands/exec/scripts/mark_impl_ended.py +188 -0
  80. erk/cli/commands/exec/scripts/mark_impl_started.py +188 -0
  81. erk/cli/commands/exec/scripts/marker.py +163 -0
  82. erk/cli/commands/exec/scripts/objective_save_to_issue.py +109 -0
  83. erk/cli/commands/exec/scripts/plan_save_to_issue.py +269 -0
  84. erk/cli/commands/exec/scripts/plan_update_issue.py +147 -0
  85. erk/cli/commands/exec/scripts/post_extraction_comment.py +237 -0
  86. erk/cli/commands/exec/scripts/post_or_update_pr_summary.py +133 -0
  87. erk/cli/commands/exec/scripts/post_pr_inline_comment.py +143 -0
  88. erk/cli/commands/exec/scripts/post_workflow_started_comment.py +168 -0
  89. erk/cli/commands/exec/scripts/preprocess_session.py +777 -0
  90. erk/cli/commands/exec/scripts/quick_submit.py +32 -0
  91. erk/cli/commands/exec/scripts/rebase_with_conflict_resolution.py +260 -0
  92. erk/cli/commands/exec/scripts/reply_to_discussion_comment.py +173 -0
  93. erk/cli/commands/exec/scripts/resolve_review_thread.py +170 -0
  94. erk/cli/commands/exec/scripts/session_id_injector_hook.py +52 -0
  95. erk/cli/commands/exec/scripts/setup_impl_from_issue.py +159 -0
  96. erk/cli/commands/exec/scripts/slot_objective.py +102 -0
  97. erk/cli/commands/exec/scripts/tripwires_reminder_hook.py +20 -0
  98. erk/cli/commands/exec/scripts/update_dispatch_info.py +116 -0
  99. erk/cli/commands/exec/scripts/user_prompt_hook.py +113 -0
  100. erk/cli/commands/exec/scripts/validate_plan_content.py +98 -0
  101. erk/cli/commands/exec/scripts/wrap_plan_in_metadata_block.py +34 -0
  102. erk/cli/commands/implement.py +695 -0
  103. erk/cli/commands/implement_shared.py +649 -0
  104. erk/cli/commands/info/__init__.py +14 -0
  105. erk/cli/commands/info/release_notes_cmd.py +128 -0
  106. erk/cli/commands/init.py +801 -0
  107. erk/cli/commands/land_cmd.py +690 -0
  108. erk/cli/commands/log_cmd.py +137 -0
  109. erk/cli/commands/md/__init__.py +5 -0
  110. erk/cli/commands/md/check.py +118 -0
  111. erk/cli/commands/md/group.py +14 -0
  112. erk/cli/commands/navigation_helpers.py +430 -0
  113. erk/cli/commands/objective/__init__.py +16 -0
  114. erk/cli/commands/objective/list_cmd.py +47 -0
  115. erk/cli/commands/objective_helpers.py +132 -0
  116. erk/cli/commands/plan/__init__.py +32 -0
  117. erk/cli/commands/plan/check_cmd.py +174 -0
  118. erk/cli/commands/plan/close_cmd.py +69 -0
  119. erk/cli/commands/plan/create_cmd.py +120 -0
  120. erk/cli/commands/plan/docs/__init__.py +18 -0
  121. erk/cli/commands/plan/docs/extract_cmd.py +53 -0
  122. erk/cli/commands/plan/docs/unextract_cmd.py +38 -0
  123. erk/cli/commands/plan/docs/unextracted_cmd.py +72 -0
  124. erk/cli/commands/plan/extraction/__init__.py +16 -0
  125. erk/cli/commands/plan/extraction/complete_cmd.py +101 -0
  126. erk/cli/commands/plan/extraction/create_raw_cmd.py +63 -0
  127. erk/cli/commands/plan/get.py +71 -0
  128. erk/cli/commands/plan/list_cmd.py +754 -0
  129. erk/cli/commands/plan/log_cmd.py +440 -0
  130. erk/cli/commands/plan/start_cmd.py +459 -0
  131. erk/cli/commands/planner/__init__.py +40 -0
  132. erk/cli/commands/planner/configure_cmd.py +73 -0
  133. erk/cli/commands/planner/connect_cmd.py +96 -0
  134. erk/cli/commands/planner/create_cmd.py +148 -0
  135. erk/cli/commands/planner/list_cmd.py +51 -0
  136. erk/cli/commands/planner/register_cmd.py +105 -0
  137. erk/cli/commands/planner/set_default_cmd.py +23 -0
  138. erk/cli/commands/planner/unregister_cmd.py +43 -0
  139. erk/cli/commands/pr/__init__.py +23 -0
  140. erk/cli/commands/pr/check_cmd.py +112 -0
  141. erk/cli/commands/pr/checkout_cmd.py +165 -0
  142. erk/cli/commands/pr/fix_conflicts_cmd.py +82 -0
  143. erk/cli/commands/pr/parse_pr_reference.py +10 -0
  144. erk/cli/commands/pr/submit_cmd.py +360 -0
  145. erk/cli/commands/pr/sync_cmd.py +181 -0
  146. erk/cli/commands/prepare_cwd_recovery.py +60 -0
  147. erk/cli/commands/project/__init__.py +16 -0
  148. erk/cli/commands/project/init_cmd.py +91 -0
  149. erk/cli/commands/run/__init__.py +17 -0
  150. erk/cli/commands/run/list_cmd.py +189 -0
  151. erk/cli/commands/run/logs_cmd.py +54 -0
  152. erk/cli/commands/run/shared.py +19 -0
  153. erk/cli/commands/shell_integration.py +29 -0
  154. erk/cli/commands/slot/__init__.py +23 -0
  155. erk/cli/commands/slot/check_cmd.py +277 -0
  156. erk/cli/commands/slot/common.py +314 -0
  157. erk/cli/commands/slot/init_pool_cmd.py +157 -0
  158. erk/cli/commands/slot/list_cmd.py +228 -0
  159. erk/cli/commands/slot/repair_cmd.py +190 -0
  160. erk/cli/commands/stack/__init__.py +23 -0
  161. erk/cli/commands/stack/consolidate_cmd.py +470 -0
  162. erk/cli/commands/stack/list_cmd.py +79 -0
  163. erk/cli/commands/stack/move_cmd.py +309 -0
  164. erk/cli/commands/stack/split_old/README.md +64 -0
  165. erk/cli/commands/stack/split_old/__init__.py +5 -0
  166. erk/cli/commands/stack/split_old/command.py +233 -0
  167. erk/cli/commands/stack/split_old/display.py +116 -0
  168. erk/cli/commands/stack/split_old/plan.py +216 -0
  169. erk/cli/commands/status.py +58 -0
  170. erk/cli/commands/submit.py +768 -0
  171. erk/cli/commands/up.py +154 -0
  172. erk/cli/commands/upgrade.py +82 -0
  173. erk/cli/commands/wt/__init__.py +29 -0
  174. erk/cli/commands/wt/checkout_cmd.py +110 -0
  175. erk/cli/commands/wt/create_cmd.py +998 -0
  176. erk/cli/commands/wt/current_cmd.py +35 -0
  177. erk/cli/commands/wt/delete_cmd.py +573 -0
  178. erk/cli/commands/wt/list_cmd.py +332 -0
  179. erk/cli/commands/wt/rename_cmd.py +66 -0
  180. erk/cli/config.py +242 -0
  181. erk/cli/constants.py +29 -0
  182. erk/cli/core.py +65 -0
  183. erk/cli/debug.py +9 -0
  184. erk/cli/ensure-conversion-tasks.md +288 -0
  185. erk/cli/ensure.py +628 -0
  186. erk/cli/github_parsing.py +96 -0
  187. erk/cli/graphite.py +81 -0
  188. erk/cli/graphite_command.py +80 -0
  189. erk/cli/help_formatter.py +345 -0
  190. erk/cli/output.py +361 -0
  191. erk/cli/presets/dagster.toml +12 -0
  192. erk/cli/presets/generic.toml +12 -0
  193. erk/cli/prompt_hooks_templates/README.md +68 -0
  194. erk/cli/script_output.py +32 -0
  195. erk/cli/shell_integration/bash_wrapper.sh +32 -0
  196. erk/cli/shell_integration/fish_wrapper.fish +39 -0
  197. erk/cli/shell_integration/handler.py +338 -0
  198. erk/cli/shell_integration/zsh_wrapper.sh +32 -0
  199. erk/cli/shell_utils.py +171 -0
  200. erk/cli/subprocess_utils.py +92 -0
  201. erk/cli/uvx_detection.py +59 -0
  202. erk/core/__init__.py +0 -0
  203. erk/core/claude_executor.py +511 -0
  204. erk/core/claude_settings.py +317 -0
  205. erk/core/command_log.py +406 -0
  206. erk/core/commit_message_generator.py +234 -0
  207. erk/core/completion.py +10 -0
  208. erk/core/consolidation_utils.py +177 -0
  209. erk/core/context.py +570 -0
  210. erk/core/display/__init__.py +4 -0
  211. erk/core/display/abc.py +24 -0
  212. erk/core/display/real.py +30 -0
  213. erk/core/display_utils.py +526 -0
  214. erk/core/file_utils.py +87 -0
  215. erk/core/health_checks.py +1315 -0
  216. erk/core/health_checks_dogfooder/__init__.py +85 -0
  217. erk/core/health_checks_dogfooder/deprecated_dot_agent_config.py +64 -0
  218. erk/core/health_checks_dogfooder/legacy_claude_docs.py +69 -0
  219. erk/core/health_checks_dogfooder/legacy_config_locations.py +122 -0
  220. erk/core/health_checks_dogfooder/legacy_erk_docs_agent.py +61 -0
  221. erk/core/health_checks_dogfooder/legacy_erk_kits_folder.py +60 -0
  222. erk/core/health_checks_dogfooder/legacy_hook_settings.py +104 -0
  223. erk/core/health_checks_dogfooder/legacy_kit_yaml.py +78 -0
  224. erk/core/health_checks_dogfooder/legacy_kits_toml.py +43 -0
  225. erk/core/health_checks_dogfooder/outdated_erk_skill.py +43 -0
  226. erk/core/implementation_queue/__init__.py +1 -0
  227. erk/core/implementation_queue/github/__init__.py +8 -0
  228. erk/core/implementation_queue/github/abc.py +7 -0
  229. erk/core/implementation_queue/github/noop.py +38 -0
  230. erk/core/implementation_queue/github/printing.py +43 -0
  231. erk/core/implementation_queue/github/real.py +119 -0
  232. erk/core/init_utils.py +227 -0
  233. erk/core/output_filter.py +338 -0
  234. erk/core/plan_store/__init__.py +6 -0
  235. erk/core/planner/__init__.py +1 -0
  236. erk/core/planner/registry_abc.py +8 -0
  237. erk/core/planner/registry_fake.py +129 -0
  238. erk/core/planner/registry_real.py +195 -0
  239. erk/core/planner/types.py +7 -0
  240. erk/core/pr_utils.py +30 -0
  241. erk/core/release_notes.py +263 -0
  242. erk/core/repo_discovery.py +126 -0
  243. erk/core/script_writer.py +41 -0
  244. erk/core/services/__init__.py +1 -0
  245. erk/core/services/plan_list_service.py +94 -0
  246. erk/core/shell.py +51 -0
  247. erk/core/user_feedback.py +11 -0
  248. erk/core/version_check.py +55 -0
  249. erk/core/workflow_display.py +75 -0
  250. erk/core/worktree_pool.py +190 -0
  251. erk/core/worktree_utils.py +300 -0
  252. erk/data/CHANGELOG.md +438 -0
  253. erk/data/__init__.py +1 -0
  254. erk/data/claude/agents/devrun.md +180 -0
  255. erk/data/claude/commands/erk/__init__.py +0 -0
  256. erk/data/claude/commands/erk/create-extraction-plan.md +360 -0
  257. erk/data/claude/commands/erk/fix-conflicts.md +25 -0
  258. erk/data/claude/commands/erk/git-pr-push.md +345 -0
  259. erk/data/claude/commands/erk/implement-stacked-plan.md +96 -0
  260. erk/data/claude/commands/erk/land.md +193 -0
  261. erk/data/claude/commands/erk/objective-create.md +370 -0
  262. erk/data/claude/commands/erk/objective-list.md +34 -0
  263. erk/data/claude/commands/erk/objective-next-plan.md +220 -0
  264. erk/data/claude/commands/erk/objective-update-with-landed-pr.md +216 -0
  265. erk/data/claude/commands/erk/plan-implement.md +202 -0
  266. erk/data/claude/commands/erk/plan-save.md +45 -0
  267. erk/data/claude/commands/erk/plan-submit.md +39 -0
  268. erk/data/claude/commands/erk/pr-address.md +367 -0
  269. erk/data/claude/commands/erk/pr-submit.md +58 -0
  270. erk/data/claude/skills/dignified-python/SKILL.md +48 -0
  271. erk/data/claude/skills/dignified-python/cli-patterns.md +155 -0
  272. erk/data/claude/skills/dignified-python/dignified-python-core.md +1190 -0
  273. erk/data/claude/skills/dignified-python/subprocess.md +99 -0
  274. erk/data/claude/skills/dignified-python/versions/python-3.10.md +517 -0
  275. erk/data/claude/skills/dignified-python/versions/python-3.11.md +536 -0
  276. erk/data/claude/skills/dignified-python/versions/python-3.12.md +662 -0
  277. erk/data/claude/skills/dignified-python/versions/python-3.13.md +653 -0
  278. erk/data/claude/skills/erk-diff-analysis/SKILL.md +27 -0
  279. erk/data/claude/skills/erk-diff-analysis/references/commit-message-prompt.md +78 -0
  280. erk/data/claude/skills/learned-docs/SKILL.md +362 -0
  281. erk/data/github/actions/setup-claude-erk/action.yml +11 -0
  282. erk/data/github/prompts/dignified-python-review.md +125 -0
  283. erk/data/github/workflows/dignified-python-review.yml +61 -0
  284. erk/data/github/workflows/erk-impl.yml +251 -0
  285. erk/hooks/__init__.py +1 -0
  286. erk/hooks/decorators.py +319 -0
  287. erk/status/__init__.py +8 -0
  288. erk/status/collectors/__init__.py +9 -0
  289. erk/status/collectors/base.py +52 -0
  290. erk/status/collectors/git.py +76 -0
  291. erk/status/collectors/github.py +81 -0
  292. erk/status/collectors/graphite.py +80 -0
  293. erk/status/collectors/impl.py +145 -0
  294. erk/status/models/__init__.py +4 -0
  295. erk/status/models/status_data.py +404 -0
  296. erk/status/orchestrator.py +169 -0
  297. erk/status/renderers/__init__.py +5 -0
  298. erk/status/renderers/simple.py +322 -0
  299. erk/tui/AGENTS.md +193 -0
  300. erk/tui/CLAUDE.md +1 -0
  301. erk/tui/__init__.py +1 -0
  302. erk/tui/app.py +1404 -0
  303. erk/tui/commands/__init__.py +1 -0
  304. erk/tui/commands/executor.py +66 -0
  305. erk/tui/commands/provider.py +165 -0
  306. erk/tui/commands/real_executor.py +63 -0
  307. erk/tui/commands/registry.py +121 -0
  308. erk/tui/commands/types.py +36 -0
  309. erk/tui/data/__init__.py +1 -0
  310. erk/tui/data/provider.py +492 -0
  311. erk/tui/data/types.py +104 -0
  312. erk/tui/filtering/__init__.py +1 -0
  313. erk/tui/filtering/logic.py +43 -0
  314. erk/tui/filtering/types.py +55 -0
  315. erk/tui/jsonl_viewer/__init__.py +1 -0
  316. erk/tui/jsonl_viewer/app.py +61 -0
  317. erk/tui/jsonl_viewer/models.py +208 -0
  318. erk/tui/jsonl_viewer/widgets.py +204 -0
  319. erk/tui/sorting/__init__.py +6 -0
  320. erk/tui/sorting/logic.py +55 -0
  321. erk/tui/sorting/types.py +68 -0
  322. erk/tui/styles/dash.tcss +95 -0
  323. erk/tui/widgets/__init__.py +1 -0
  324. erk/tui/widgets/command_output.py +112 -0
  325. erk/tui/widgets/plan_table.py +276 -0
  326. erk/tui/widgets/status_bar.py +116 -0
  327. erk-0.4.5.dist-info/METADATA +376 -0
  328. erk-0.4.5.dist-info/RECORD +331 -0
  329. erk-0.4.5.dist-info/WHEEL +4 -0
  330. erk-0.4.5.dist-info/entry_points.txt +2 -0
  331. erk-0.4.5.dist-info/licenses/LICENSE.md +3 -0
erk/cli/activation.py ADDED
@@ -0,0 +1,132 @@
1
+ """Shell activation script generation for worktree environments.
2
+
3
+ This module provides utilities for generating shell scripts that activate
4
+ worktree environments by setting up virtual environments and loading .env files.
5
+ """
6
+
7
+ import shlex
8
+ from collections.abc import Sequence
9
+ from pathlib import Path
10
+
11
+
12
+ def _render_logging_helper() -> str:
13
+ """Return shell helper functions for transparency logging.
14
+
15
+ These helpers handle ERK_QUIET and ERK_VERBOSE environment variables
16
+ to control output verbosity during worktree activation.
17
+
18
+ Normal mode (default): Shows brief progress indicators
19
+ Quiet mode (ERK_QUIET=1): Suppresses transparency output (errors still shown)
20
+ Verbose mode (ERK_VERBOSE=1): Shows full details with paths
21
+ """
22
+ return """# Transparency logging helper
23
+ __erk_log() {
24
+ [ -n "$ERK_QUIET" ] && return
25
+ local prefix="$1" msg="$2"
26
+ if [ -t 2 ]; then
27
+ printf '\\033[0;36m%s\\033[0m %s\\n' "$prefix" "$msg" >&2
28
+ else
29
+ printf '%s %s\\n' "$prefix" "$msg" >&2
30
+ fi
31
+ }
32
+ __erk_log_verbose() {
33
+ [ -z "$ERK_VERBOSE" ] && return
34
+ __erk_log "$1" "$2"
35
+ }"""
36
+
37
+
38
+ def render_activation_script(
39
+ *,
40
+ worktree_path: Path,
41
+ target_subpath: Path | None,
42
+ post_cd_commands: Sequence[str] | None,
43
+ final_message: str,
44
+ comment: str,
45
+ ) -> str:
46
+ """Return shell code that activates a worktree's venv and .env.
47
+
48
+ The script:
49
+ - cds into the worktree (optionally to a subpath within it)
50
+ - creates .venv with `uv sync` if not present
51
+ - sources `.venv/bin/activate` if present
52
+ - exports variables from `.env` if present
53
+ - runs optional post-activation commands (e.g., git pull)
54
+ Works in bash and zsh.
55
+
56
+ Args:
57
+ worktree_path: Path to the worktree directory
58
+ target_subpath: Optional relative path within the worktree to cd to.
59
+ If the subpath doesn't exist, a warning is shown and the script
60
+ falls back to the worktree root.
61
+ post_cd_commands: Optional sequence of shell commands to run after venv
62
+ activation, before final message. Useful for git pull after landing a PR.
63
+ Pass None if no post-cd commands are needed.
64
+ final_message: Shell command for final echo message
65
+ comment: Comment line for script identification
66
+
67
+ Returns:
68
+ Shell script as a string with newlines
69
+
70
+ Example:
71
+ >>> script = render_activation_script(
72
+ ... worktree_path=Path("/path/to/worktree"),
73
+ ... target_subpath=Path("src/lib"),
74
+ ... post_cd_commands=None,
75
+ ... final_message='echo "Ready: $(pwd)"',
76
+ ... comment="work activate-script",
77
+ ... )
78
+ """
79
+ wt = shlex.quote(str(worktree_path))
80
+ venv_dir = shlex.quote(str(worktree_path / ".venv"))
81
+ venv_activate = shlex.quote(str(worktree_path / ".venv" / "bin" / "activate"))
82
+
83
+ # Generate the cd command with optional subpath handling
84
+ if target_subpath is not None:
85
+ subpath_quoted = shlex.quote(str(target_subpath))
86
+ # Check if subpath exists in target worktree, fall back to root with warning
87
+ cd_command = f"""__erk_log "->" "cd {worktree_path}"
88
+ cd {wt}
89
+ # Try to preserve relative directory position
90
+ if [ -d {subpath_quoted} ]; then
91
+ cd {subpath_quoted}
92
+ else
93
+ echo "Warning: '{target_subpath}' doesn't exist in target, using worktree root" >&2
94
+ fi"""
95
+ else:
96
+ cd_command = f"""__erk_log "->" "cd {worktree_path}"
97
+ cd {wt}"""
98
+
99
+ logging_helper = _render_logging_helper()
100
+
101
+ # Build optional post-activation commands section
102
+ post_activation_section = ""
103
+ if post_cd_commands:
104
+ post_activation_section = (
105
+ "# Post-activation commands\n" + "\n".join(post_cd_commands) + "\n"
106
+ )
107
+
108
+ return f"""# {comment}
109
+ {logging_helper}
110
+ {cd_command}
111
+ # Unset VIRTUAL_ENV to avoid conflicts with previous activations
112
+ unset VIRTUAL_ENV
113
+ # Create venv if it doesn't exist
114
+ if [ ! -d {venv_dir} ]; then
115
+ echo 'Creating virtual environment with uv sync...'
116
+ uv sync
117
+ fi
118
+ if [ -f {venv_activate} ]; then
119
+ . {venv_activate}
120
+ __py_ver=$(python -c 'import sys; print(".".join(map(str, sys.version_info[:3])))')
121
+ __erk_log "->" "Activating venv: {worktree_path / ".venv"} ($__py_ver)"
122
+ fi
123
+ # Load .env into the environment (allexport)
124
+ set -a
125
+ if [ -f ./.env ]; then
126
+ __erk_log "->" "Loading .env"
127
+ . ./.env
128
+ fi
129
+ set +a
130
+ {post_activation_section}# Optional: show where we are
131
+ {final_message}
132
+ """
erk/cli/alias.py ADDED
@@ -0,0 +1,53 @@
1
+ """Command alias decorator for CLI commands."""
2
+
3
+ from collections.abc import Callable
4
+ from typing import TypeVar
5
+
6
+ import click
7
+
8
+ F = TypeVar("F", bound=click.Command)
9
+ C = TypeVar("C", bound=Callable[..., object])
10
+
11
+ # Store alias metadata on command objects
12
+ ALIAS_ATTR = "_erk_aliases"
13
+
14
+
15
+ def alias(*names: str) -> Callable[[F], F]:
16
+ """Decorator to declare aliases for a Click command.
17
+
18
+ Must be applied BEFORE @click.command (i.e., listed above it in the decorator stack).
19
+ This is because decorators are applied bottom-to-top, so @alias runs AFTER @click.command
20
+ creates the Command object.
21
+
22
+ Usage:
23
+ @alias("co")
24
+ @click.command("checkout")
25
+ def checkout_cmd(...):
26
+ ...
27
+ """
28
+
29
+ def decorator(cmd: F) -> F:
30
+ existing = getattr(cmd, ALIAS_ATTR, [])
31
+ setattr(cmd, ALIAS_ATTR, existing + list(names))
32
+ return cmd
33
+
34
+ return decorator
35
+
36
+
37
+ def get_aliases(cmd: click.Command) -> list[str]:
38
+ """Get aliases declared on a command."""
39
+ return getattr(cmd, ALIAS_ATTR, [])
40
+
41
+
42
+ def register_with_aliases(group: click.Group, cmd: click.Command, name: str | None = None) -> None:
43
+ """Register a command and its declared aliases with a group.
44
+
45
+ Args:
46
+ group: The Click group to register the command with
47
+ cmd: The command to register
48
+ name: Optional explicit name (defaults to cmd.name)
49
+ """
50
+ cmd_name = name or cmd.name
51
+ group.add_command(cmd, name=cmd_name)
52
+ for alias_name in get_aliases(cmd):
53
+ group.add_command(cmd, name=alias_name)
erk/cli/cli.py ADDED
@@ -0,0 +1,221 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import click
7
+
8
+ from erk.cli.alias import register_with_aliases
9
+ from erk.cli.commands.admin import admin_group
10
+ from erk.cli.commands.artifact.group import artifact_group
11
+ from erk.cli.commands.branch import branch_group
12
+ from erk.cli.commands.cc import cc_group
13
+ from erk.cli.commands.completion import completion_group
14
+ from erk.cli.commands.config import config_group
15
+ from erk.cli.commands.docs.group import docs_group
16
+ from erk.cli.commands.doctor import doctor_cmd
17
+ from erk.cli.commands.down import down_cmd
18
+ from erk.cli.commands.exec.group import exec_group
19
+ from erk.cli.commands.implement import implement
20
+ from erk.cli.commands.info import info_group
21
+ from erk.cli.commands.init import init_cmd
22
+ from erk.cli.commands.land_cmd import land
23
+ from erk.cli.commands.log_cmd import log_cmd
24
+ from erk.cli.commands.md.group import md_group
25
+ from erk.cli.commands.objective import objective_group
26
+ from erk.cli.commands.plan import plan_group
27
+ from erk.cli.commands.plan.list_cmd import dash
28
+ from erk.cli.commands.planner import planner_group
29
+ from erk.cli.commands.pr import pr_group
30
+ from erk.cli.commands.prepare_cwd_recovery import prepare_cwd_recovery_cmd
31
+ from erk.cli.commands.project import project_group
32
+ from erk.cli.commands.run import run_group
33
+ from erk.cli.commands.shell_integration import hidden_shell_cmd
34
+ from erk.cli.commands.slot import slot_group
35
+ from erk.cli.commands.stack import stack_group
36
+ from erk.cli.commands.up import up_cmd
37
+ from erk.cli.commands.upgrade import upgrade_cmd
38
+ from erk.cli.commands.wt import wt_group
39
+ from erk.cli.help_formatter import ErkCommandGroup
40
+ from erk.core.command_log import get_cli_args, log_command_start, register_exit_handler
41
+ from erk.core.context import create_context
42
+ from erk.core.release_notes import check_for_version_change, get_current_version
43
+ from erk.core.version_check import (
44
+ format_version_warning,
45
+ get_required_version,
46
+ is_version_mismatch,
47
+ )
48
+ from erk_shared.gateway.erk_installation.real import RealErkInstallation
49
+ from erk_shared.git.real import RealGit
50
+
51
+ CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) # terse help flags
52
+
53
+
54
+ def _show_version_change_banner() -> None:
55
+ """Show upgrade banner with full release notes if version has changed.
56
+
57
+ Displays all release notes since the last seen version and prompts user
58
+ to confirm before continuing. This function is designed to never fail -
59
+ exceptions are logged but don't break the CLI.
60
+ """
61
+ try:
62
+ erk_installation = RealErkInstallation()
63
+ changed, releases = check_for_version_change(erk_installation)
64
+ if not changed or not releases:
65
+ return
66
+
67
+ current = get_current_version()
68
+
69
+ # Build banner header
70
+ click.echo(file=sys.stderr)
71
+ click.echo(
72
+ click.style(f" ✨ erk updated to v{current}", fg="green", bold=True), file=sys.stderr
73
+ )
74
+ click.echo(click.style(" " + "─" * 50, dim=True), file=sys.stderr)
75
+ click.echo(file=sys.stderr)
76
+
77
+ # Show all releases with their items grouped by category
78
+ for release in releases:
79
+ # Skip releases with no items
80
+ if not release.items:
81
+ continue
82
+
83
+ # Version header
84
+ header = f" [{release.version}]"
85
+ if release.date:
86
+ header += f" - {release.date}"
87
+ click.echo(click.style(header, bold=True), file=sys.stderr)
88
+
89
+ # Show items grouped by category if available
90
+ if release.categories:
91
+ for category, category_items in release.categories.items():
92
+ if not category_items:
93
+ continue
94
+ click.echo(click.style(f" {category}", dim=True), file=sys.stderr)
95
+ for item_text, indent_level in category_items:
96
+ # Base indent (6 spaces) + extra indent per nesting level (2 spaces)
97
+ indent = " " + (" " * indent_level)
98
+ click.echo(f"{indent}• {item_text}", file=sys.stderr)
99
+ else:
100
+ # Fallback to flat list for releases without categories
101
+ for item_text, indent_level in release.items:
102
+ indent = " " + (" " * indent_level)
103
+ click.echo(f"{indent}• {item_text}", file=sys.stderr)
104
+ click.echo(file=sys.stderr)
105
+
106
+ click.echo(click.style(" " + "─" * 50, dim=True), file=sys.stderr)
107
+ click.echo(file=sys.stderr)
108
+
109
+ # Prompt user to continue (only if stdin is a TTY)
110
+ if sys.stdin.isatty():
111
+ click.pause(info=click.style(" Press Enter to continue...", dim=True), err=True)
112
+ except click.Abort:
113
+ # User pressed Ctrl+C or declined - exit gracefully
114
+ raise SystemExit(0) from None
115
+ except Exception as e:
116
+ # Never let release notes break the CLI, but warn so issues can be diagnosed
117
+ logging.warning("Failed to show version change banner: %s", e)
118
+
119
+
120
+ def _show_version_warning() -> None:
121
+ """Show warning if installed erk version doesn't match repo-required version.
122
+
123
+ This is designed to never fail - exceptions are logged but don't break the CLI.
124
+ """
125
+ # Skip if user has disabled version checking
126
+ if os.environ.get("ERK_SKIP_VERSION_CHECK") == "1":
127
+ return
128
+
129
+ try:
130
+ # Find git repo root (if in a git repo)
131
+ git = RealGit()
132
+ repo_root = git.get_repository_root(Path.cwd())
133
+ if repo_root is None:
134
+ return
135
+
136
+ # Read required version from repo
137
+ required = get_required_version(repo_root)
138
+ if required is None:
139
+ return
140
+
141
+ # Compare versions
142
+ installed = get_current_version()
143
+ if not is_version_mismatch(installed, required):
144
+ return
145
+
146
+ # Show warning
147
+ click.echo(format_version_warning(installed, required), err=True)
148
+ click.echo(file=sys.stderr)
149
+ except RuntimeError as e:
150
+ # Expected for global commands outside git repos
151
+ if "get repository root" in str(e):
152
+ logging.debug("Version check skipped: not in git repo")
153
+ return
154
+ logging.warning("Failed to check version: %s", e)
155
+ except Exception as e:
156
+ # Never let version checking break the CLI, but warn so issues can be diagnosed
157
+ logging.warning("Failed to check version: %s", e)
158
+
159
+
160
+ @click.group(cls=ErkCommandGroup, context_settings=CONTEXT_SETTINGS)
161
+ @click.version_option(package_name="erk")
162
+ @click.option("--debug", is_flag=True, help="Enable debug logging")
163
+ @click.pass_context
164
+ def cli(ctx: click.Context, debug: bool) -> None:
165
+ """Manage git worktrees in a global worktrees directory."""
166
+ if debug:
167
+ logging.basicConfig(level=logging.DEBUG, format="%(name)s - %(levelname)s - %(message)s")
168
+
169
+ # Show version change banner (only on actual CLI runs, not completions)
170
+ if not ctx.resilient_parsing:
171
+ _show_version_change_banner()
172
+ _show_version_warning()
173
+
174
+ # Only create context if not already provided (e.g., by tests)
175
+ if ctx.obj is None:
176
+ ctx.obj = create_context(dry_run=False)
177
+
178
+
179
+ # Register all commands
180
+ # Commands with @alias decorators use register_with_aliases() to auto-register aliases
181
+ cli.add_command(admin_group)
182
+ cli.add_command(artifact_group)
183
+ register_with_aliases(cli, branch_group) # Has @alias("br")
184
+ cli.add_command(cc_group)
185
+ cli.add_command(completion_group)
186
+ cli.add_command(config_group)
187
+ cli.add_command(doctor_cmd)
188
+ cli.add_command(down_cmd)
189
+ register_with_aliases(cli, implement) # Has @alias("impl")
190
+ cli.add_command(init_cmd)
191
+ cli.add_command(land)
192
+ admin_group.add_command(log_cmd)
193
+ cli.add_command(dash)
194
+ cli.add_command(plan_group)
195
+ cli.add_command(planner_group)
196
+ cli.add_command(pr_group)
197
+ cli.add_command(info_group)
198
+ cli.add_command(objective_group)
199
+ cli.add_command(project_group)
200
+ cli.add_command(slot_group)
201
+ cli.add_command(run_group)
202
+ cli.add_command(stack_group)
203
+ cli.add_command(up_cmd)
204
+ cli.add_command(upgrade_cmd)
205
+ cli.add_command(wt_group)
206
+ cli.add_command(hidden_shell_cmd)
207
+ cli.add_command(prepare_cwd_recovery_cmd)
208
+
209
+ # Additional command groups
210
+ cli.add_command(docs_group)
211
+ cli.add_command(exec_group)
212
+ cli.add_command(md_group)
213
+
214
+
215
+ def main() -> None:
216
+ """CLI entry point used by the `erk` console script."""
217
+ # Log command start and register exit handler for completion logging
218
+ entry_id = log_command_start(get_cli_args(), Path.cwd())
219
+ register_exit_handler(entry_id)
220
+
221
+ cli()
File without changes
@@ -0,0 +1,153 @@
1
+ """Admin commands for repository configuration."""
2
+
3
+ from typing import Literal
4
+
5
+ import click
6
+
7
+ from erk.cli.core import discover_repo_context
8
+ from erk.core.context import ErkContext
9
+ from erk.core.implementation_queue.github.real import RealGitHubAdmin
10
+ from erk_shared.github.types import GitHubRepoLocation
11
+ from erk_shared.output.output import user_output
12
+
13
+
14
+ @click.group("admin")
15
+ def admin_group() -> None:
16
+ """Administrative commands for repository configuration."""
17
+ pass
18
+
19
+
20
+ @admin_group.command("github-pr-setting")
21
+ @click.option(
22
+ "--enable",
23
+ "action",
24
+ flag_value="enable",
25
+ help="Enable PR creation for GitHub Actions workflows",
26
+ )
27
+ @click.option(
28
+ "--disable",
29
+ "action",
30
+ flag_value="disable",
31
+ help="Disable PR creation for GitHub Actions workflows",
32
+ )
33
+ @click.pass_obj
34
+ def github_pr_setting(ctx: ErkContext, action: Literal["enable", "disable"] | None) -> None:
35
+ """Manage GitHub Actions workflow permission for PR creation.
36
+
37
+ Without flags: Display current setting
38
+ With --enable: Enable PR creation for workflows
39
+ With --disable: Disable PR creation for workflows
40
+
41
+ This setting controls whether GitHub Actions workflows can create
42
+ and approve pull requests in your repository.
43
+
44
+ GitHub UI location: Settings > Actions > General > Workflow permissions
45
+ """
46
+ # Discover repository context
47
+ repo = discover_repo_context(ctx, ctx.cwd)
48
+
49
+ # Check for GitHub identity
50
+ if repo.github is None:
51
+ user_output(click.style("Error: ", fg="red") + "Not a GitHub repository")
52
+ user_output("This command requires the repository to have a GitHub remote configured.")
53
+ raise SystemExit(1)
54
+
55
+ # Create admin interface
56
+ # TODO: Use injected admin from context when dry-run support is added
57
+ admin = RealGitHubAdmin()
58
+ location = GitHubRepoLocation(root=repo.root, repo_id=repo.github)
59
+
60
+ if action is None:
61
+ # Display current setting
62
+ try:
63
+ perms = admin.get_workflow_permissions(location)
64
+ enabled = perms.get("can_approve_pull_request_reviews", False)
65
+
66
+ user_output(click.style("GitHub Actions PR Creation Setting", bold=True))
67
+ user_output("")
68
+
69
+ status_text = "Enabled" if enabled else "Disabled"
70
+ status_color = "green" if enabled else "red"
71
+ user_output(f"Current status: {click.style(status_text, fg=status_color)}")
72
+ user_output("")
73
+
74
+ if enabled:
75
+ user_output("Workflows can create and approve pull requests in this repository.")
76
+ else:
77
+ user_output("Workflows cannot create pull requests in this repository.")
78
+
79
+ user_output("")
80
+ user_output(click.style("GitHub UI location:", fg="white", dim=True))
81
+ user_output(
82
+ click.style(
83
+ " Settings > Actions > General > Workflow permissions",
84
+ fg="white",
85
+ dim=True,
86
+ )
87
+ )
88
+
89
+ except RuntimeError as e:
90
+ user_output(click.style("Error: ", fg="red") + str(e))
91
+ raise SystemExit(1) from e
92
+
93
+ elif action == "enable":
94
+ # Enable PR creation
95
+ try:
96
+ admin.set_workflow_pr_permissions(location, enabled=True)
97
+
98
+ user_output(
99
+ click.style("✓", fg="green") + " Enabled PR creation for GitHub Actions workflows"
100
+ )
101
+ user_output("")
102
+ user_output("Workflows can now create and approve pull requests.")
103
+
104
+ except RuntimeError as e:
105
+ user_output(click.style("Error: ", fg="red") + str(e))
106
+ raise SystemExit(1) from e
107
+
108
+ elif action == "disable":
109
+ # Disable PR creation
110
+ try:
111
+ admin.set_workflow_pr_permissions(location, enabled=False)
112
+
113
+ user_output(
114
+ click.style("✓", fg="green") + " Disabled PR creation for GitHub Actions workflows"
115
+ )
116
+ user_output("")
117
+ user_output("Workflows can no longer create pull requests.")
118
+
119
+ except RuntimeError as e:
120
+ user_output(click.style("Error: ", fg="red") + str(e))
121
+ raise SystemExit(1) from e
122
+
123
+
124
+ @admin_group.command("upgrade-repo")
125
+ @click.pass_obj
126
+ def upgrade_repo(ctx: ErkContext) -> None:
127
+ """Upgrade repo to match installed erk version.
128
+
129
+ Updates .erk/required-erk-uv-tool-version and prints next steps.
130
+ """
131
+ from erk.core.release_notes import get_current_version
132
+
133
+ repo = discover_repo_context(ctx, ctx.cwd)
134
+ current_version = get_current_version()
135
+
136
+ # Check if this is an erk-managed repository
137
+ erk_dir = repo.root / ".erk"
138
+ if not erk_dir.exists():
139
+ user_output(click.style("Error: ", fg="red") + "Not an erk-managed repository")
140
+ user_output(f"The directory {repo.root} does not contain a .erk directory.")
141
+ user_output("This command only works in repositories initialized with erk.")
142
+ raise SystemExit(1)
143
+
144
+ # Update version file
145
+ version_file = erk_dir / "required-erk-uv-tool-version"
146
+ version_file.write_text(f"{current_version}\n", encoding="utf-8")
147
+ user_output(f"Updated required version to {current_version}")
148
+
149
+ # Print next steps
150
+ user_output("")
151
+ user_output("Next steps:")
152
+ user_output(" erk artifact sync # Sync skills, commands, hooks")
153
+ user_output(" erk doctor # Verify the upgrade")
@@ -0,0 +1 @@
1
+ # Empty file