erk 0.4.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- erk/__init__.py +12 -0
- erk/__main__.py +6 -0
- erk/agent_docs/__init__.py +5 -0
- erk/agent_docs/models.py +123 -0
- erk/agent_docs/operations.py +666 -0
- erk/artifacts/__init__.py +5 -0
- erk/artifacts/artifact_health.py +623 -0
- erk/artifacts/detection.py +16 -0
- erk/artifacts/discovery.py +343 -0
- erk/artifacts/models.py +63 -0
- erk/artifacts/staleness.py +56 -0
- erk/artifacts/state.py +100 -0
- erk/artifacts/sync.py +624 -0
- erk/cli/__init__.py +0 -0
- erk/cli/activation.py +132 -0
- erk/cli/alias.py +53 -0
- erk/cli/cli.py +221 -0
- erk/cli/commands/__init__.py +0 -0
- erk/cli/commands/admin.py +153 -0
- erk/cli/commands/artifact/__init__.py +1 -0
- erk/cli/commands/artifact/check.py +260 -0
- erk/cli/commands/artifact/group.py +31 -0
- erk/cli/commands/artifact/list_cmd.py +89 -0
- erk/cli/commands/artifact/show.py +62 -0
- erk/cli/commands/artifact/sync_cmd.py +39 -0
- erk/cli/commands/branch/__init__.py +26 -0
- erk/cli/commands/branch/assign_cmd.py +152 -0
- erk/cli/commands/branch/checkout_cmd.py +357 -0
- erk/cli/commands/branch/create_cmd.py +161 -0
- erk/cli/commands/branch/list_cmd.py +82 -0
- erk/cli/commands/branch/unassign_cmd.py +197 -0
- erk/cli/commands/cc/__init__.py +15 -0
- erk/cli/commands/cc/jsonl_cmd.py +20 -0
- erk/cli/commands/cc/session/AGENTS.md +30 -0
- erk/cli/commands/cc/session/CLAUDE.md +1 -0
- erk/cli/commands/cc/session/__init__.py +15 -0
- erk/cli/commands/cc/session/list_cmd.py +167 -0
- erk/cli/commands/cc/session/show_cmd.py +175 -0
- erk/cli/commands/completion.py +89 -0
- erk/cli/commands/completions.py +165 -0
- erk/cli/commands/config.py +327 -0
- erk/cli/commands/docs/__init__.py +1 -0
- erk/cli/commands/docs/group.py +16 -0
- erk/cli/commands/docs/sync.py +121 -0
- erk/cli/commands/docs/validate.py +102 -0
- erk/cli/commands/doctor.py +243 -0
- erk/cli/commands/down.py +171 -0
- erk/cli/commands/exec/__init__.py +1 -0
- erk/cli/commands/exec/group.py +164 -0
- erk/cli/commands/exec/scripts/AGENTS.md +79 -0
- erk/cli/commands/exec/scripts/CLAUDE.md +1 -0
- erk/cli/commands/exec/scripts/__init__.py +5 -0
- erk/cli/commands/exec/scripts/add_reaction_to_comment.py +69 -0
- erk/cli/commands/exec/scripts/add_remote_execution_note.py +68 -0
- erk/cli/commands/exec/scripts/check_impl.py +152 -0
- erk/cli/commands/exec/scripts/ci_update_pr_body.py +294 -0
- erk/cli/commands/exec/scripts/create_extraction_branch.py +138 -0
- erk/cli/commands/exec/scripts/create_extraction_plan.py +242 -0
- erk/cli/commands/exec/scripts/create_issue_from_session.py +103 -0
- erk/cli/commands/exec/scripts/create_plan_from_context.py +103 -0
- erk/cli/commands/exec/scripts/create_worker_impl_from_issue.py +93 -0
- erk/cli/commands/exec/scripts/detect_trunk_branch.py +121 -0
- erk/cli/commands/exec/scripts/exit_plan_mode_hook.py +777 -0
- erk/cli/commands/exec/scripts/extract_latest_plan.py +49 -0
- erk/cli/commands/exec/scripts/extract_session_from_issue.py +150 -0
- erk/cli/commands/exec/scripts/find_project_dir.py +214 -0
- erk/cli/commands/exec/scripts/generate_pr_summary.py +112 -0
- erk/cli/commands/exec/scripts/get_closing_text.py +98 -0
- erk/cli/commands/exec/scripts/get_embedded_prompt.py +62 -0
- erk/cli/commands/exec/scripts/get_plan_metadata.py +95 -0
- erk/cli/commands/exec/scripts/get_pr_body_footer.py +70 -0
- erk/cli/commands/exec/scripts/get_pr_discussion_comments.py +149 -0
- erk/cli/commands/exec/scripts/get_pr_review_comments.py +155 -0
- erk/cli/commands/exec/scripts/impl_init.py +158 -0
- erk/cli/commands/exec/scripts/impl_signal.py +375 -0
- erk/cli/commands/exec/scripts/impl_verify.py +49 -0
- erk/cli/commands/exec/scripts/issue_title_to_filename.py +34 -0
- erk/cli/commands/exec/scripts/list_sessions.py +296 -0
- erk/cli/commands/exec/scripts/mark_impl_ended.py +188 -0
- erk/cli/commands/exec/scripts/mark_impl_started.py +188 -0
- erk/cli/commands/exec/scripts/marker.py +163 -0
- erk/cli/commands/exec/scripts/objective_save_to_issue.py +109 -0
- erk/cli/commands/exec/scripts/plan_save_to_issue.py +269 -0
- erk/cli/commands/exec/scripts/plan_update_issue.py +147 -0
- erk/cli/commands/exec/scripts/post_extraction_comment.py +237 -0
- erk/cli/commands/exec/scripts/post_or_update_pr_summary.py +133 -0
- erk/cli/commands/exec/scripts/post_pr_inline_comment.py +143 -0
- erk/cli/commands/exec/scripts/post_workflow_started_comment.py +168 -0
- erk/cli/commands/exec/scripts/preprocess_session.py +777 -0
- erk/cli/commands/exec/scripts/quick_submit.py +32 -0
- erk/cli/commands/exec/scripts/rebase_with_conflict_resolution.py +260 -0
- erk/cli/commands/exec/scripts/reply_to_discussion_comment.py +173 -0
- erk/cli/commands/exec/scripts/resolve_review_thread.py +170 -0
- erk/cli/commands/exec/scripts/session_id_injector_hook.py +52 -0
- erk/cli/commands/exec/scripts/setup_impl_from_issue.py +159 -0
- erk/cli/commands/exec/scripts/slot_objective.py +102 -0
- erk/cli/commands/exec/scripts/tripwires_reminder_hook.py +20 -0
- erk/cli/commands/exec/scripts/update_dispatch_info.py +116 -0
- erk/cli/commands/exec/scripts/user_prompt_hook.py +113 -0
- erk/cli/commands/exec/scripts/validate_plan_content.py +98 -0
- erk/cli/commands/exec/scripts/wrap_plan_in_metadata_block.py +34 -0
- erk/cli/commands/implement.py +695 -0
- erk/cli/commands/implement_shared.py +649 -0
- erk/cli/commands/info/__init__.py +14 -0
- erk/cli/commands/info/release_notes_cmd.py +128 -0
- erk/cli/commands/init.py +801 -0
- erk/cli/commands/land_cmd.py +690 -0
- erk/cli/commands/log_cmd.py +137 -0
- erk/cli/commands/md/__init__.py +5 -0
- erk/cli/commands/md/check.py +118 -0
- erk/cli/commands/md/group.py +14 -0
- erk/cli/commands/navigation_helpers.py +430 -0
- erk/cli/commands/objective/__init__.py +16 -0
- erk/cli/commands/objective/list_cmd.py +47 -0
- erk/cli/commands/objective_helpers.py +132 -0
- erk/cli/commands/plan/__init__.py +32 -0
- erk/cli/commands/plan/check_cmd.py +174 -0
- erk/cli/commands/plan/close_cmd.py +69 -0
- erk/cli/commands/plan/create_cmd.py +120 -0
- erk/cli/commands/plan/docs/__init__.py +18 -0
- erk/cli/commands/plan/docs/extract_cmd.py +53 -0
- erk/cli/commands/plan/docs/unextract_cmd.py +38 -0
- erk/cli/commands/plan/docs/unextracted_cmd.py +72 -0
- erk/cli/commands/plan/extraction/__init__.py +16 -0
- erk/cli/commands/plan/extraction/complete_cmd.py +101 -0
- erk/cli/commands/plan/extraction/create_raw_cmd.py +63 -0
- erk/cli/commands/plan/get.py +71 -0
- erk/cli/commands/plan/list_cmd.py +754 -0
- erk/cli/commands/plan/log_cmd.py +440 -0
- erk/cli/commands/plan/start_cmd.py +459 -0
- erk/cli/commands/planner/__init__.py +40 -0
- erk/cli/commands/planner/configure_cmd.py +73 -0
- erk/cli/commands/planner/connect_cmd.py +96 -0
- erk/cli/commands/planner/create_cmd.py +148 -0
- erk/cli/commands/planner/list_cmd.py +51 -0
- erk/cli/commands/planner/register_cmd.py +105 -0
- erk/cli/commands/planner/set_default_cmd.py +23 -0
- erk/cli/commands/planner/unregister_cmd.py +43 -0
- erk/cli/commands/pr/__init__.py +23 -0
- erk/cli/commands/pr/check_cmd.py +112 -0
- erk/cli/commands/pr/checkout_cmd.py +165 -0
- erk/cli/commands/pr/fix_conflicts_cmd.py +82 -0
- erk/cli/commands/pr/parse_pr_reference.py +10 -0
- erk/cli/commands/pr/submit_cmd.py +360 -0
- erk/cli/commands/pr/sync_cmd.py +181 -0
- erk/cli/commands/prepare_cwd_recovery.py +60 -0
- erk/cli/commands/project/__init__.py +16 -0
- erk/cli/commands/project/init_cmd.py +91 -0
- erk/cli/commands/run/__init__.py +17 -0
- erk/cli/commands/run/list_cmd.py +189 -0
- erk/cli/commands/run/logs_cmd.py +54 -0
- erk/cli/commands/run/shared.py +19 -0
- erk/cli/commands/shell_integration.py +29 -0
- erk/cli/commands/slot/__init__.py +23 -0
- erk/cli/commands/slot/check_cmd.py +277 -0
- erk/cli/commands/slot/common.py +314 -0
- erk/cli/commands/slot/init_pool_cmd.py +157 -0
- erk/cli/commands/slot/list_cmd.py +228 -0
- erk/cli/commands/slot/repair_cmd.py +190 -0
- erk/cli/commands/stack/__init__.py +23 -0
- erk/cli/commands/stack/consolidate_cmd.py +470 -0
- erk/cli/commands/stack/list_cmd.py +79 -0
- erk/cli/commands/stack/move_cmd.py +309 -0
- erk/cli/commands/stack/split_old/README.md +64 -0
- erk/cli/commands/stack/split_old/__init__.py +5 -0
- erk/cli/commands/stack/split_old/command.py +233 -0
- erk/cli/commands/stack/split_old/display.py +116 -0
- erk/cli/commands/stack/split_old/plan.py +216 -0
- erk/cli/commands/status.py +58 -0
- erk/cli/commands/submit.py +768 -0
- erk/cli/commands/up.py +154 -0
- erk/cli/commands/upgrade.py +82 -0
- erk/cli/commands/wt/__init__.py +29 -0
- erk/cli/commands/wt/checkout_cmd.py +110 -0
- erk/cli/commands/wt/create_cmd.py +998 -0
- erk/cli/commands/wt/current_cmd.py +35 -0
- erk/cli/commands/wt/delete_cmd.py +573 -0
- erk/cli/commands/wt/list_cmd.py +332 -0
- erk/cli/commands/wt/rename_cmd.py +66 -0
- erk/cli/config.py +242 -0
- erk/cli/constants.py +29 -0
- erk/cli/core.py +65 -0
- erk/cli/debug.py +9 -0
- erk/cli/ensure-conversion-tasks.md +288 -0
- erk/cli/ensure.py +628 -0
- erk/cli/github_parsing.py +96 -0
- erk/cli/graphite.py +81 -0
- erk/cli/graphite_command.py +80 -0
- erk/cli/help_formatter.py +345 -0
- erk/cli/output.py +361 -0
- erk/cli/presets/dagster.toml +12 -0
- erk/cli/presets/generic.toml +12 -0
- erk/cli/prompt_hooks_templates/README.md +68 -0
- erk/cli/script_output.py +32 -0
- erk/cli/shell_integration/bash_wrapper.sh +32 -0
- erk/cli/shell_integration/fish_wrapper.fish +39 -0
- erk/cli/shell_integration/handler.py +338 -0
- erk/cli/shell_integration/zsh_wrapper.sh +32 -0
- erk/cli/shell_utils.py +171 -0
- erk/cli/subprocess_utils.py +92 -0
- erk/cli/uvx_detection.py +59 -0
- erk/core/__init__.py +0 -0
- erk/core/claude_executor.py +511 -0
- erk/core/claude_settings.py +317 -0
- erk/core/command_log.py +406 -0
- erk/core/commit_message_generator.py +234 -0
- erk/core/completion.py +10 -0
- erk/core/consolidation_utils.py +177 -0
- erk/core/context.py +570 -0
- erk/core/display/__init__.py +4 -0
- erk/core/display/abc.py +24 -0
- erk/core/display/real.py +30 -0
- erk/core/display_utils.py +526 -0
- erk/core/file_utils.py +87 -0
- erk/core/health_checks.py +1315 -0
- erk/core/health_checks_dogfooder/__init__.py +85 -0
- erk/core/health_checks_dogfooder/deprecated_dot_agent_config.py +64 -0
- erk/core/health_checks_dogfooder/legacy_claude_docs.py +69 -0
- erk/core/health_checks_dogfooder/legacy_config_locations.py +122 -0
- erk/core/health_checks_dogfooder/legacy_erk_docs_agent.py +61 -0
- erk/core/health_checks_dogfooder/legacy_erk_kits_folder.py +60 -0
- erk/core/health_checks_dogfooder/legacy_hook_settings.py +104 -0
- erk/core/health_checks_dogfooder/legacy_kit_yaml.py +78 -0
- erk/core/health_checks_dogfooder/legacy_kits_toml.py +43 -0
- erk/core/health_checks_dogfooder/outdated_erk_skill.py +43 -0
- erk/core/implementation_queue/__init__.py +1 -0
- erk/core/implementation_queue/github/__init__.py +8 -0
- erk/core/implementation_queue/github/abc.py +7 -0
- erk/core/implementation_queue/github/noop.py +38 -0
- erk/core/implementation_queue/github/printing.py +43 -0
- erk/core/implementation_queue/github/real.py +119 -0
- erk/core/init_utils.py +227 -0
- erk/core/output_filter.py +338 -0
- erk/core/plan_store/__init__.py +6 -0
- erk/core/planner/__init__.py +1 -0
- erk/core/planner/registry_abc.py +8 -0
- erk/core/planner/registry_fake.py +129 -0
- erk/core/planner/registry_real.py +195 -0
- erk/core/planner/types.py +7 -0
- erk/core/pr_utils.py +30 -0
- erk/core/release_notes.py +263 -0
- erk/core/repo_discovery.py +126 -0
- erk/core/script_writer.py +41 -0
- erk/core/services/__init__.py +1 -0
- erk/core/services/plan_list_service.py +94 -0
- erk/core/shell.py +51 -0
- erk/core/user_feedback.py +11 -0
- erk/core/version_check.py +55 -0
- erk/core/workflow_display.py +75 -0
- erk/core/worktree_pool.py +190 -0
- erk/core/worktree_utils.py +300 -0
- erk/data/CHANGELOG.md +438 -0
- erk/data/__init__.py +1 -0
- erk/data/claude/agents/devrun.md +180 -0
- erk/data/claude/commands/erk/__init__.py +0 -0
- erk/data/claude/commands/erk/create-extraction-plan.md +360 -0
- erk/data/claude/commands/erk/fix-conflicts.md +25 -0
- erk/data/claude/commands/erk/git-pr-push.md +345 -0
- erk/data/claude/commands/erk/implement-stacked-plan.md +96 -0
- erk/data/claude/commands/erk/land.md +193 -0
- erk/data/claude/commands/erk/objective-create.md +370 -0
- erk/data/claude/commands/erk/objective-list.md +34 -0
- erk/data/claude/commands/erk/objective-next-plan.md +220 -0
- erk/data/claude/commands/erk/objective-update-with-landed-pr.md +216 -0
- erk/data/claude/commands/erk/plan-implement.md +202 -0
- erk/data/claude/commands/erk/plan-save.md +45 -0
- erk/data/claude/commands/erk/plan-submit.md +39 -0
- erk/data/claude/commands/erk/pr-address.md +367 -0
- erk/data/claude/commands/erk/pr-submit.md +58 -0
- erk/data/claude/skills/dignified-python/SKILL.md +48 -0
- erk/data/claude/skills/dignified-python/cli-patterns.md +155 -0
- erk/data/claude/skills/dignified-python/dignified-python-core.md +1190 -0
- erk/data/claude/skills/dignified-python/subprocess.md +99 -0
- erk/data/claude/skills/dignified-python/versions/python-3.10.md +517 -0
- erk/data/claude/skills/dignified-python/versions/python-3.11.md +536 -0
- erk/data/claude/skills/dignified-python/versions/python-3.12.md +662 -0
- erk/data/claude/skills/dignified-python/versions/python-3.13.md +653 -0
- erk/data/claude/skills/erk-diff-analysis/SKILL.md +27 -0
- erk/data/claude/skills/erk-diff-analysis/references/commit-message-prompt.md +78 -0
- erk/data/claude/skills/learned-docs/SKILL.md +362 -0
- erk/data/github/actions/setup-claude-erk/action.yml +11 -0
- erk/data/github/prompts/dignified-python-review.md +125 -0
- erk/data/github/workflows/dignified-python-review.yml +61 -0
- erk/data/github/workflows/erk-impl.yml +251 -0
- erk/hooks/__init__.py +1 -0
- erk/hooks/decorators.py +319 -0
- erk/status/__init__.py +8 -0
- erk/status/collectors/__init__.py +9 -0
- erk/status/collectors/base.py +52 -0
- erk/status/collectors/git.py +76 -0
- erk/status/collectors/github.py +81 -0
- erk/status/collectors/graphite.py +80 -0
- erk/status/collectors/impl.py +145 -0
- erk/status/models/__init__.py +4 -0
- erk/status/models/status_data.py +404 -0
- erk/status/orchestrator.py +169 -0
- erk/status/renderers/__init__.py +5 -0
- erk/status/renderers/simple.py +322 -0
- erk/tui/AGENTS.md +193 -0
- erk/tui/CLAUDE.md +1 -0
- erk/tui/__init__.py +1 -0
- erk/tui/app.py +1404 -0
- erk/tui/commands/__init__.py +1 -0
- erk/tui/commands/executor.py +66 -0
- erk/tui/commands/provider.py +165 -0
- erk/tui/commands/real_executor.py +63 -0
- erk/tui/commands/registry.py +121 -0
- erk/tui/commands/types.py +36 -0
- erk/tui/data/__init__.py +1 -0
- erk/tui/data/provider.py +492 -0
- erk/tui/data/types.py +104 -0
- erk/tui/filtering/__init__.py +1 -0
- erk/tui/filtering/logic.py +43 -0
- erk/tui/filtering/types.py +55 -0
- erk/tui/jsonl_viewer/__init__.py +1 -0
- erk/tui/jsonl_viewer/app.py +61 -0
- erk/tui/jsonl_viewer/models.py +208 -0
- erk/tui/jsonl_viewer/widgets.py +204 -0
- erk/tui/sorting/__init__.py +6 -0
- erk/tui/sorting/logic.py +55 -0
- erk/tui/sorting/types.py +68 -0
- erk/tui/styles/dash.tcss +95 -0
- erk/tui/widgets/__init__.py +1 -0
- erk/tui/widgets/command_output.py +112 -0
- erk/tui/widgets/plan_table.py +276 -0
- erk/tui/widgets/status_bar.py +116 -0
- erk-0.4.5.dist-info/METADATA +376 -0
- erk-0.4.5.dist-info/RECORD +331 -0
- erk-0.4.5.dist-info/WHEEL +4 -0
- erk-0.4.5.dist-info/entry_points.txt +2 -0
- erk-0.4.5.dist-info/licenses/LICENSE.md +3 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Check for outdated erk skill that should be deleted.
|
|
2
|
+
|
|
3
|
+
This is a temporary check for early dogfooders. Delete this file once
|
|
4
|
+
all users have removed the outdated .claude/skills/erk/ directory.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from erk.core.health_checks import CheckResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def check_outdated_erk_skill(repo_root: Path) -> CheckResult:
|
|
13
|
+
"""Check for outdated erk skill directory that should be deleted.
|
|
14
|
+
|
|
15
|
+
The erk skill at .claude/skills/erk/ is outdated and no longer maintained.
|
|
16
|
+
Users should delete this directory.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
repo_root: Path to the repository root
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
CheckResult indicating whether outdated skill was found
|
|
23
|
+
"""
|
|
24
|
+
skill_dir = repo_root / ".claude" / "skills" / "erk"
|
|
25
|
+
if not skill_dir.exists():
|
|
26
|
+
return CheckResult(
|
|
27
|
+
name="outdated-erk-skill",
|
|
28
|
+
passed=True,
|
|
29
|
+
message="No outdated erk skill found",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Found outdated skill - return failure with remediation
|
|
33
|
+
return CheckResult(
|
|
34
|
+
name="outdated-erk-skill",
|
|
35
|
+
passed=False,
|
|
36
|
+
message="Outdated erk skill found at .claude/skills/erk/",
|
|
37
|
+
details=(
|
|
38
|
+
"The erk skill is outdated and should be deleted.\n"
|
|
39
|
+
"\n"
|
|
40
|
+
"Remediation:\n"
|
|
41
|
+
" rm -rf .claude/skills/erk/"
|
|
42
|
+
),
|
|
43
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Implementation queue core functionality."""
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""No-op wrapper for GitHub Actions admin operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from erk_shared.github.types import GitHubRepoLocation
|
|
6
|
+
from erk_shared.github_admin.abc import AuthStatus, GitHubAdmin
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NoopGitHubAdmin(GitHubAdmin):
|
|
10
|
+
"""No-op wrapper for GitHub Actions admin operations.
|
|
11
|
+
|
|
12
|
+
Read operations are delegated to the wrapped implementation.
|
|
13
|
+
Write operations return without executing (no-op behavior).
|
|
14
|
+
|
|
15
|
+
This wrapper prevents destructive GitHub admin operations from executing
|
|
16
|
+
in dry-run mode, while still allowing read operations for validation.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, wrapped: GitHubAdmin) -> None:
|
|
20
|
+
"""Initialize no-op wrapper with a real implementation.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
wrapped: The real GitHubAdmin implementation to wrap
|
|
24
|
+
"""
|
|
25
|
+
self._wrapped = wrapped
|
|
26
|
+
|
|
27
|
+
def get_workflow_permissions(self, location: GitHubRepoLocation) -> dict[str, Any]:
|
|
28
|
+
"""Delegate read operation to wrapped implementation."""
|
|
29
|
+
return self._wrapped.get_workflow_permissions(location)
|
|
30
|
+
|
|
31
|
+
def set_workflow_pr_permissions(self, location: GitHubRepoLocation, enabled: bool) -> None:
|
|
32
|
+
"""No-op for setting workflow permissions in dry-run mode."""
|
|
33
|
+
# Do nothing - prevents actual permission changes
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
def check_auth_status(self) -> AuthStatus:
|
|
37
|
+
"""Delegate read operation to wrapped implementation."""
|
|
38
|
+
return self._wrapped.check_auth_status()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Printing wrapper for GitHub Actions admin operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from erk_shared.github.types import GitHubRepoLocation
|
|
6
|
+
from erk_shared.github_admin.abc import AuthStatus, GitHubAdmin
|
|
7
|
+
from erk_shared.printing.base import PrintingBase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PrintingGitHubAdmin(PrintingBase, GitHubAdmin):
|
|
11
|
+
"""Wrapper that prints operations before delegating to inner implementation.
|
|
12
|
+
|
|
13
|
+
This wrapper prints styled output for operations, then delegates to the
|
|
14
|
+
wrapped implementation (which could be Real or Noop).
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
# For production
|
|
18
|
+
printing_admin = PrintingGitHubAdmin(real_admin, script_mode=False, dry_run=False)
|
|
19
|
+
|
|
20
|
+
# For dry-run
|
|
21
|
+
noop_inner = NoopGitHubAdmin(real_admin)
|
|
22
|
+
printing_admin = PrintingGitHubAdmin(noop_inner, script_mode=False, dry_run=True)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# Inherits __init__, _emit, and _format_command from PrintingBase
|
|
26
|
+
|
|
27
|
+
def get_workflow_permissions(self, location: GitHubRepoLocation) -> dict[str, Any]:
|
|
28
|
+
"""Get workflow permissions (read-only, no printing)."""
|
|
29
|
+
return self._wrapped.get_workflow_permissions(location)
|
|
30
|
+
|
|
31
|
+
def set_workflow_pr_permissions(self, location: GitHubRepoLocation, enabled: bool) -> None:
|
|
32
|
+
"""Set workflow PR permissions with printed output."""
|
|
33
|
+
self._emit(
|
|
34
|
+
self._format_command(
|
|
35
|
+
f"gh api --method PUT .../actions/permissions/workflow "
|
|
36
|
+
f"(can_approve_pull_request_reviews={str(enabled).lower()})"
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
self._wrapped.set_workflow_pr_permissions(location, enabled)
|
|
40
|
+
|
|
41
|
+
def check_auth_status(self) -> AuthStatus:
|
|
42
|
+
"""Check auth status (read-only, no printing)."""
|
|
43
|
+
return self._wrapped.check_auth_status()
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Production implementation of GitHub Actions admin operations."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from erk_shared.github.types import GitHubRepoLocation
|
|
8
|
+
from erk_shared.github_admin.abc import AuthStatus, GitHubAdmin
|
|
9
|
+
from erk_shared.subprocess_utils import run_subprocess_with_context
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RealGitHubAdmin(GitHubAdmin):
|
|
13
|
+
"""Production implementation using gh CLI.
|
|
14
|
+
|
|
15
|
+
All GitHub Actions admin operations execute actual gh commands via subprocess.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def get_workflow_permissions(self, location: GitHubRepoLocation) -> dict[str, Any]:
|
|
19
|
+
"""Get current workflow permissions using gh CLI.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
location: GitHub repository location (local root + repo identity)
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Dict with keys:
|
|
26
|
+
- default_workflow_permissions: "read" or "write"
|
|
27
|
+
- can_approve_pull_request_reviews: bool
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
RuntimeError: If gh CLI command fails
|
|
31
|
+
"""
|
|
32
|
+
repo_id = location.repo_id
|
|
33
|
+
# GH-API-AUDIT: REST - GET actions/permissions/workflow
|
|
34
|
+
cmd = [
|
|
35
|
+
"gh",
|
|
36
|
+
"api",
|
|
37
|
+
"-H",
|
|
38
|
+
"Accept: application/vnd.github+json",
|
|
39
|
+
"-H",
|
|
40
|
+
"X-GitHub-Api-Version: 2022-11-28",
|
|
41
|
+
f"/repos/{repo_id.owner}/{repo_id.repo}/actions/permissions/workflow",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
result = run_subprocess_with_context(
|
|
45
|
+
cmd,
|
|
46
|
+
operation_context=f"get workflow permissions for {repo_id.owner}/{repo_id.repo}",
|
|
47
|
+
cwd=location.root,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return json.loads(result.stdout)
|
|
51
|
+
|
|
52
|
+
def set_workflow_pr_permissions(self, location: GitHubRepoLocation, enabled: bool) -> None:
|
|
53
|
+
"""Enable/disable PR creation via workflow permissions API.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
location: GitHub repository location (local root + repo identity)
|
|
57
|
+
enabled: True to enable PR creation, False to disable
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
RuntimeError: If gh CLI command fails
|
|
61
|
+
"""
|
|
62
|
+
# CRITICAL: Must set both fields together
|
|
63
|
+
# - default_workflow_permissions: Keep as "read" (workflows declare their own)
|
|
64
|
+
# - can_approve_pull_request_reviews: This enables PR creation
|
|
65
|
+
repo_id = location.repo_id
|
|
66
|
+
# GH-API-AUDIT: REST - PUT actions/permissions/workflow
|
|
67
|
+
cmd = [
|
|
68
|
+
"gh",
|
|
69
|
+
"api",
|
|
70
|
+
"--method",
|
|
71
|
+
"PUT",
|
|
72
|
+
"-H",
|
|
73
|
+
"Accept: application/vnd.github+json",
|
|
74
|
+
"-H",
|
|
75
|
+
"X-GitHub-Api-Version: 2022-11-28",
|
|
76
|
+
f"/repos/{repo_id.owner}/{repo_id.repo}/actions/permissions/workflow",
|
|
77
|
+
"-f",
|
|
78
|
+
"default_workflow_permissions=read",
|
|
79
|
+
"-F",
|
|
80
|
+
f"can_approve_pull_request_reviews={str(enabled).lower()}",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
run_subprocess_with_context(
|
|
84
|
+
cmd,
|
|
85
|
+
operation_context=f"set workflow PR permissions for {repo_id.owner}/{repo_id.repo}",
|
|
86
|
+
cwd=location.root,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def check_auth_status(self) -> AuthStatus:
|
|
90
|
+
"""Check GitHub CLI authentication status using gh auth status."""
|
|
91
|
+
try:
|
|
92
|
+
# GH-API-AUDIT: REST - auth validation
|
|
93
|
+
result = subprocess.run(
|
|
94
|
+
["gh", "auth", "status"],
|
|
95
|
+
capture_output=True,
|
|
96
|
+
text=True,
|
|
97
|
+
check=False,
|
|
98
|
+
timeout=10,
|
|
99
|
+
)
|
|
100
|
+
if result.returncode == 0:
|
|
101
|
+
# Parse output to find username
|
|
102
|
+
# Format: "✓ Logged in to github.com account username (keyring)"
|
|
103
|
+
output = result.stdout.strip() or result.stderr.strip()
|
|
104
|
+
username = None
|
|
105
|
+
for line in output.split("\n"):
|
|
106
|
+
if "Logged in to" in line and "account" in line:
|
|
107
|
+
# Extract username from "... account username (...)"
|
|
108
|
+
parts = line.split("account")
|
|
109
|
+
if len(parts) > 1:
|
|
110
|
+
username_part = parts[1].strip()
|
|
111
|
+
username = username_part.split()[0] if username_part else None
|
|
112
|
+
break
|
|
113
|
+
return AuthStatus(authenticated=True, username=username, error=None)
|
|
114
|
+
else:
|
|
115
|
+
return AuthStatus(authenticated=False, username=None, error=None)
|
|
116
|
+
except subprocess.TimeoutExpired:
|
|
117
|
+
return AuthStatus(authenticated=False, username=None, error="Auth check timed out")
|
|
118
|
+
except OSError as e:
|
|
119
|
+
return AuthStatus(authenticated=False, username=None, error=str(e))
|
erk/core/init_utils.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Pure business logic for init command operations.
|
|
2
|
+
|
|
3
|
+
This module contains testable functions for detecting project configuration,
|
|
4
|
+
discovering presets, and rendering templates, without I/O side effects.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import tomllib
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def is_repo_erk_ified(repo_root: Path) -> bool:
|
|
13
|
+
"""Check if a repository has been initialized with erk.
|
|
14
|
+
|
|
15
|
+
A repository is considered erk-ified if it has a .erk/config.toml file.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
repo_root: Path to the repository root
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
True if .erk/config.toml exists, False otherwise
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
>>> repo_root = Path("/path/to/repo")
|
|
25
|
+
>>> is_repo_erk_ified(repo_root)
|
|
26
|
+
False
|
|
27
|
+
"""
|
|
28
|
+
config_path = repo_root / ".erk" / "config.toml"
|
|
29
|
+
return config_path.exists()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def detect_root_project_name(repo_root: Path) -> str | None:
|
|
33
|
+
"""Return the declared project name at the repo root, if any.
|
|
34
|
+
|
|
35
|
+
Checks root `pyproject.toml`'s `[project].name`. If absent, tries to heuristically
|
|
36
|
+
extract from `setup.py` by matching `name="..."` or `name='...'`.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
repo_root: Path to the repository root
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Project name if found, None otherwise
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
>>> repo_root = Path("/path/to/repo")
|
|
46
|
+
>>> # Assuming pyproject.toml exists with [project] name = "my-project"
|
|
47
|
+
>>> detect_root_project_name(repo_root)
|
|
48
|
+
'my-project'
|
|
49
|
+
"""
|
|
50
|
+
root_pyproject = repo_root / "pyproject.toml"
|
|
51
|
+
if root_pyproject.exists():
|
|
52
|
+
data = tomllib.loads(root_pyproject.read_text(encoding="utf-8"))
|
|
53
|
+
project = data.get("project") or {}
|
|
54
|
+
name = project.get("name")
|
|
55
|
+
if isinstance(name, str) and name:
|
|
56
|
+
return name
|
|
57
|
+
|
|
58
|
+
setup_py = repo_root / "setup.py"
|
|
59
|
+
if setup_py.exists():
|
|
60
|
+
text = setup_py.read_text(encoding="utf-8")
|
|
61
|
+
m = re.search(r"name\s*=\s*['\"]([^'\"]+)['\"]", text)
|
|
62
|
+
if m:
|
|
63
|
+
return m.group(1)
|
|
64
|
+
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def is_repo_named(repo_root: Path, expected_name: str) -> bool:
|
|
69
|
+
"""Return True if the root project name matches `expected_name` (case-insensitive).
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
repo_root: Path to the repository root
|
|
73
|
+
expected_name: Expected project name to match
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
True if project name matches (case-insensitive), False otherwise
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
>>> repo_root = Path("/path/to/repo")
|
|
80
|
+
>>> is_repo_named(repo_root, "dagster")
|
|
81
|
+
True
|
|
82
|
+
"""
|
|
83
|
+
name = detect_root_project_name(repo_root)
|
|
84
|
+
return (name or "").lower() == expected_name.lower()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def discover_presets(presets_dir: Path) -> list[str]:
|
|
88
|
+
"""Discover available preset names by scanning the presets directory.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
presets_dir: Path to the directory containing preset files
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Sorted list of preset names (without .toml extension)
|
|
95
|
+
|
|
96
|
+
Example:
|
|
97
|
+
>>> presets_dir = Path("/path/to/erk/presets")
|
|
98
|
+
>>> discover_presets(presets_dir)
|
|
99
|
+
['dagster', 'generic', 'python']
|
|
100
|
+
"""
|
|
101
|
+
if not presets_dir.exists():
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
return sorted(p.stem for p in presets_dir.glob("*.toml") if p.is_file())
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def render_config_template(presets_dir: Path, preset: str | None) -> str:
|
|
108
|
+
"""Return default config TOML content, optionally using a preset.
|
|
109
|
+
|
|
110
|
+
If preset is None, uses the "generic" preset by default.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
presets_dir: Path to the directory containing preset files
|
|
114
|
+
preset: Name of the preset to use, or None for "generic"
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Content of the preset file as a string
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ValueError: If the specified preset file doesn't exist
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
>>> presets_dir = Path("/path/to/erk/presets")
|
|
124
|
+
>>> content = render_config_template(presets_dir, "dagster")
|
|
125
|
+
>>> "trunk_branch" in content
|
|
126
|
+
True
|
|
127
|
+
"""
|
|
128
|
+
preset_name = preset if preset is not None else "generic"
|
|
129
|
+
preset_file = presets_dir / f"{preset_name}.toml"
|
|
130
|
+
|
|
131
|
+
if not preset_file.exists():
|
|
132
|
+
raise ValueError(f"Preset '{preset_name}' not found at {preset_file}")
|
|
133
|
+
|
|
134
|
+
return preset_file.read_text(encoding="utf-8")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_shell_wrapper_content(shell_integration_dir: Path, shell: str) -> str:
|
|
138
|
+
"""Load the shell wrapper function for the given shell type.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
shell_integration_dir: Path to the directory containing shell integration files
|
|
142
|
+
shell: Shell type (e.g., "zsh", "bash", "fish")
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Content of the shell wrapper file as a string
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
ValueError: If the shell wrapper file doesn't exist for the given shell
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
>>> shell_dir = Path("/path/to/erk/shell_integration")
|
|
152
|
+
>>> content = get_shell_wrapper_content(shell_dir, "zsh")
|
|
153
|
+
>>> "function erk" in content
|
|
154
|
+
True
|
|
155
|
+
"""
|
|
156
|
+
if shell == "fish":
|
|
157
|
+
wrapper_file = shell_integration_dir / "fish_wrapper.fish"
|
|
158
|
+
else:
|
|
159
|
+
wrapper_file = shell_integration_dir / f"{shell}_wrapper.sh"
|
|
160
|
+
|
|
161
|
+
if not wrapper_file.exists():
|
|
162
|
+
raise ValueError(f"Shell wrapper not found for {shell}")
|
|
163
|
+
|
|
164
|
+
return wrapper_file.read_text(encoding="utf-8")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# Marker string that identifies erk shell integration in RC files
|
|
168
|
+
ERK_SHELL_INTEGRATION_MARKER = "# Erk shell integration"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def has_shell_integration_in_rc(rc_path: Path) -> bool:
|
|
172
|
+
"""Check if shell RC file contains erk shell integration.
|
|
173
|
+
|
|
174
|
+
Looks for the marker comment that erk adds when shell integration is configured.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
rc_path: Path to the shell RC file (e.g., ~/.zshrc)
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
True if the marker is found in the file, False otherwise
|
|
181
|
+
(also returns False if file doesn't exist)
|
|
182
|
+
|
|
183
|
+
Example:
|
|
184
|
+
>>> rc_path = Path.home() / ".zshrc"
|
|
185
|
+
>>> has_shell_integration_in_rc(rc_path)
|
|
186
|
+
False
|
|
187
|
+
"""
|
|
188
|
+
if not rc_path.exists():
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
content = rc_path.read_text(encoding="utf-8")
|
|
192
|
+
return ERK_SHELL_INTEGRATION_MARKER in content
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def add_gitignore_entry(content: str, entry: str) -> str:
|
|
196
|
+
"""Add an entry to gitignore content if not already present.
|
|
197
|
+
|
|
198
|
+
This is a pure function that returns the potentially modified content.
|
|
199
|
+
User confirmation should be handled by the caller.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
content: Current gitignore content
|
|
203
|
+
entry: Entry to add (e.g., ".env")
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Updated gitignore content (original if entry already present)
|
|
207
|
+
|
|
208
|
+
Example:
|
|
209
|
+
>>> content = "*.pyc\\n"
|
|
210
|
+
>>> new_content = add_gitignore_entry(content, ".env")
|
|
211
|
+
>>> ".env" in new_content
|
|
212
|
+
True
|
|
213
|
+
>>> # Calling again should be idempotent
|
|
214
|
+
>>> newer_content = add_gitignore_entry(new_content, ".env")
|
|
215
|
+
>>> newer_content == new_content
|
|
216
|
+
True
|
|
217
|
+
"""
|
|
218
|
+
# Entry already present
|
|
219
|
+
if entry in content:
|
|
220
|
+
return content
|
|
221
|
+
|
|
222
|
+
# Ensure trailing newline before adding
|
|
223
|
+
if not content.endswith("\n"):
|
|
224
|
+
content += "\n"
|
|
225
|
+
|
|
226
|
+
content += f"{entry}\n"
|
|
227
|
+
return content
|