teddy-cli 0.1.0__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.
- teddy_cli-0.1.0.dist-info/LICENSE +677 -0
- teddy_cli-0.1.0.dist-info/METADATA +33 -0
- teddy_cli-0.1.0.dist-info/RECORD +143 -0
- teddy_cli-0.1.0.dist-info/WHEEL +4 -0
- teddy_cli-0.1.0.dist-info/entry_points.txt +3 -0
- teddy_executor/__init__.py +1 -0
- teddy_executor/__main__.py +335 -0
- teddy_executor/adapters/__init__.py +0 -0
- teddy_executor/adapters/inbound/__init__.py +0 -0
- teddy_executor/adapters/inbound/cli_formatter.py +107 -0
- teddy_executor/adapters/inbound/cli_helpers.py +249 -0
- teddy_executor/adapters/inbound/console_plan_reviewer.py +69 -0
- teddy_executor/adapters/inbound/session_cli_handlers.py +366 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer.py +78 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_app.py +367 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_editor.py +281 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_execution.py +213 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_helpers.py +308 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_logic.py +345 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_previews.py +227 -0
- teddy_executor/adapters/inbound/textual_plan_reviewer_widgets.py +246 -0
- teddy_executor/adapters/outbound/__init__.py +7 -0
- teddy_executor/adapters/outbound/console_interactor.py +212 -0
- teddy_executor/adapters/outbound/console_interactor_ask_loop.py +121 -0
- teddy_executor/adapters/outbound/console_interactor_helpers.py +95 -0
- teddy_executor/adapters/outbound/console_tooling.py +62 -0
- teddy_executor/adapters/outbound/filesystem_helpers.py +61 -0
- teddy_executor/adapters/outbound/litellm_adapter.py +462 -0
- teddy_executor/adapters/outbound/local_file_system_adapter.py +300 -0
- teddy_executor/adapters/outbound/local_repo_tree_generator.py +96 -0
- teddy_executor/adapters/outbound/openrouter_hydrator.py +89 -0
- teddy_executor/adapters/outbound/shell_adapter.py +344 -0
- teddy_executor/adapters/outbound/shell_command_builder.py +105 -0
- teddy_executor/adapters/outbound/system_environment_adapter.py +62 -0
- teddy_executor/adapters/outbound/system_environment_inspector.py +54 -0
- teddy_executor/adapters/outbound/system_time_adapter.py +22 -0
- teddy_executor/adapters/outbound/web_scraper_adapter.py +346 -0
- teddy_executor/adapters/outbound/web_searcher_adapter.py +122 -0
- teddy_executor/adapters/outbound/yaml_config_adapter.py +105 -0
- teddy_executor/container.py +333 -0
- teddy_executor/core/__init__.py +0 -0
- teddy_executor/core/domain/__init__.py +0 -0
- teddy_executor/core/domain/models/__init__.py +44 -0
- teddy_executor/core/domain/models/action_ports.py +28 -0
- teddy_executor/core/domain/models/change_set.py +10 -0
- teddy_executor/core/domain/models/exceptions.py +40 -0
- teddy_executor/core/domain/models/execution_report.py +65 -0
- teddy_executor/core/domain/models/orchestrator_ports.py +26 -0
- teddy_executor/core/domain/models/plan.py +85 -0
- teddy_executor/core/domain/models/planning_ports.py +43 -0
- teddy_executor/core/domain/models/project_context.py +56 -0
- teddy_executor/core/domain/models/report_assembly_data.py +18 -0
- teddy_executor/core/domain/models/session.py +17 -0
- teddy_executor/core/domain/models/shell_output.py +12 -0
- teddy_executor/core/domain/models/web_search_results.py +26 -0
- teddy_executor/core/ports/__init__.py +0 -0
- teddy_executor/core/ports/inbound/__init__.py +0 -0
- teddy_executor/core/ports/inbound/edit_simulator.py +33 -0
- teddy_executor/core/ports/inbound/get_context_use_case.py +32 -0
- teddy_executor/core/ports/inbound/init.py +15 -0
- teddy_executor/core/ports/inbound/plan_parser.py +52 -0
- teddy_executor/core/ports/inbound/plan_reviewer.py +44 -0
- teddy_executor/core/ports/inbound/plan_validator.py +26 -0
- teddy_executor/core/ports/inbound/planning_use_case.py +30 -0
- teddy_executor/core/ports/inbound/run_plan_use_case.py +60 -0
- teddy_executor/core/ports/outbound/__init__.py +34 -0
- teddy_executor/core/ports/outbound/config_service.py +29 -0
- teddy_executor/core/ports/outbound/environment_inspector.py +30 -0
- teddy_executor/core/ports/outbound/execution_report_assembler.py +19 -0
- teddy_executor/core/ports/outbound/file_system_manager.py +131 -0
- teddy_executor/core/ports/outbound/llm_client.py +90 -0
- teddy_executor/core/ports/outbound/markdown_report_formatter.py +26 -0
- teddy_executor/core/ports/outbound/prompt_manager.py +55 -0
- teddy_executor/core/ports/outbound/repo_tree_generator.py +17 -0
- teddy_executor/core/ports/outbound/session_loop_guard.py +16 -0
- teddy_executor/core/ports/outbound/session_manager.py +97 -0
- teddy_executor/core/ports/outbound/session_repository.py +65 -0
- teddy_executor/core/ports/outbound/shell_executor.py +24 -0
- teddy_executor/core/ports/outbound/system_environment.py +25 -0
- teddy_executor/core/ports/outbound/time_service.py +28 -0
- teddy_executor/core/ports/outbound/user_interactor.py +126 -0
- teddy_executor/core/ports/outbound/web_scraper.py +24 -0
- teddy_executor/core/ports/outbound/web_searcher.py +25 -0
- teddy_executor/core/services/__init__.py +0 -0
- teddy_executor/core/services/action_changeset_builder.py +90 -0
- teddy_executor/core/services/action_diff_manager.py +110 -0
- teddy_executor/core/services/action_dispatcher.py +142 -0
- teddy_executor/core/services/action_executor.py +209 -0
- teddy_executor/core/services/action_factory.py +197 -0
- teddy_executor/core/services/action_parser_complex.py +216 -0
- teddy_executor/core/services/action_parser_strategies.py +84 -0
- teddy_executor/core/services/context_service.py +437 -0
- teddy_executor/core/services/edit_simulator.py +128 -0
- teddy_executor/core/services/execution_orchestrator.py +295 -0
- teddy_executor/core/services/execution_report_assembler.py +62 -0
- teddy_executor/core/services/init_service.py +80 -0
- teddy_executor/core/services/markdown_plan_parser.py +309 -0
- teddy_executor/core/services/markdown_report_formatter.py +143 -0
- teddy_executor/core/services/parser_infrastructure.py +222 -0
- teddy_executor/core/services/parser_metadata.py +153 -0
- teddy_executor/core/services/parser_reporting.py +267 -0
- teddy_executor/core/services/plan_validator.py +82 -0
- teddy_executor/core/services/planning_service.py +242 -0
- teddy_executor/core/services/prompt_manager.py +146 -0
- teddy_executor/core/services/session_lifecycle_manager.py +228 -0
- teddy_executor/core/services/session_loop_guard.py +46 -0
- teddy_executor/core/services/session_orchestrator.py +538 -0
- teddy_executor/core/services/session_planner.py +43 -0
- teddy_executor/core/services/session_pruning_service.py +438 -0
- teddy_executor/core/services/session_replanner.py +105 -0
- teddy_executor/core/services/session_repository.py +194 -0
- teddy_executor/core/services/session_service.py +529 -0
- teddy_executor/core/services/templates/execution_report.md.j2 +290 -0
- teddy_executor/core/services/validation_rules/__init__.py +4 -0
- teddy_executor/core/services/validation_rules/edit.py +207 -0
- teddy_executor/core/services/validation_rules/edit_matcher.py +247 -0
- teddy_executor/core/services/validation_rules/edit_matcher_heuristics.py +84 -0
- teddy_executor/core/services/validation_rules/execute.py +37 -0
- teddy_executor/core/services/validation_rules/filesystem.py +73 -0
- teddy_executor/core/services/validation_rules/helpers.py +178 -0
- teddy_executor/core/services/validation_rules/message.py +29 -0
- teddy_executor/core/utils/__init__.py +1 -0
- teddy_executor/core/utils/diff.py +57 -0
- teddy_executor/core/utils/io.py +75 -0
- teddy_executor/core/utils/markdown.py +131 -0
- teddy_executor/core/utils/serialization.py +39 -0
- teddy_executor/core/utils/string.py +351 -0
- teddy_executor/prompts.py +45 -0
- teddy_executor/registries/__init__.py +1 -0
- teddy_executor/registries/infrastructure.py +147 -0
- teddy_executor/registries/reviewer.py +57 -0
- teddy_executor/registries/validators.py +47 -0
- teddy_executor/resources/__init__.py +1 -0
- teddy_executor/resources/config/.gitignore +2 -0
- teddy_executor/resources/config/__init__.py +1 -0
- teddy_executor/resources/config/config.yaml +49 -0
- teddy_executor/resources/config/init.context +5 -0
- teddy_executor/resources/config/prompts/architect.xml +462 -0
- teddy_executor/resources/config/prompts/assistant.xml +336 -0
- teddy_executor/resources/config/prompts/debugger.xml +456 -0
- teddy_executor/resources/config/prompts/developer.xml +481 -0
- teddy_executor/resources/config/prompts/pathfinder.xml +502 -0
- teddy_executor/resources/config/prompts/prototyper.xml +425 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING, Optional
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from teddy_executor.core.ports.outbound.system_environment import ISystemEnvironment
|
|
8
|
+
from teddy_executor.adapters.outbound.console_tooling import ConsoleToolingHelper
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConsoleAskLoop:
|
|
12
|
+
"""Handles the interactive loop for capturing user response via terminal or editor."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, system_env: ISystemEnvironment, tooling: ConsoleToolingHelper):
|
|
15
|
+
self._system_env = system_env
|
|
16
|
+
self._tooling = tooling
|
|
17
|
+
self._active_editor_path: Optional[str] = None
|
|
18
|
+
self._active_editor_marker: Optional[str] = None
|
|
19
|
+
|
|
20
|
+
def run(self, prompt: str) -> str:
|
|
21
|
+
"""Orchestrates the interactive loop for capturing user response."""
|
|
22
|
+
while True:
|
|
23
|
+
prompt_label = "Response (type 'e' for editor) › "
|
|
24
|
+
if self._active_editor_path:
|
|
25
|
+
prompt_label = (
|
|
26
|
+
"Editor opened. Terminal reply or [Enter] to confirm editor › "
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
typer.echo(prompt_label, nl=False, err=True)
|
|
30
|
+
try:
|
|
31
|
+
user_input = input().strip()
|
|
32
|
+
except EOFError:
|
|
33
|
+
self.cleanup()
|
|
34
|
+
return ""
|
|
35
|
+
|
|
36
|
+
if user_input.lower() == "e":
|
|
37
|
+
self._launch_editor_background(prompt)
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
if user_input:
|
|
41
|
+
self.cleanup()
|
|
42
|
+
return user_input
|
|
43
|
+
|
|
44
|
+
response = self._handle_empty_input(prompt)
|
|
45
|
+
if response is not None:
|
|
46
|
+
return response
|
|
47
|
+
|
|
48
|
+
def _handle_empty_input(self, prompt: str) -> Optional[str]:
|
|
49
|
+
"""Handles logic when Enter is pressed without terminal input."""
|
|
50
|
+
if self._active_editor_path:
|
|
51
|
+
return self._read_editor_result()
|
|
52
|
+
|
|
53
|
+
typer.echo(
|
|
54
|
+
"Press [Enter] again to confirm empty response › ",
|
|
55
|
+
nl=False,
|
|
56
|
+
err=True,
|
|
57
|
+
)
|
|
58
|
+
try:
|
|
59
|
+
confirm = input().strip()
|
|
60
|
+
except EOFError:
|
|
61
|
+
return ""
|
|
62
|
+
|
|
63
|
+
if not confirm:
|
|
64
|
+
return ""
|
|
65
|
+
|
|
66
|
+
if confirm.lower() == "e":
|
|
67
|
+
self._launch_editor_background(prompt)
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
return confirm
|
|
71
|
+
|
|
72
|
+
def cleanup(self):
|
|
73
|
+
"""Removes the temp file and resets active state."""
|
|
74
|
+
if self._active_editor_path:
|
|
75
|
+
self._system_env.delete_file(self._active_editor_path)
|
|
76
|
+
self._active_editor_path = None
|
|
77
|
+
self._active_editor_marker = None
|
|
78
|
+
|
|
79
|
+
def _launch_editor_background(self, prompt: str) -> None:
|
|
80
|
+
"""Opens a temporary file in a non-blocking external editor."""
|
|
81
|
+
marker = "<!-- Please enter your response above this line. -->"
|
|
82
|
+
initial_content = f"\n\n{marker}\n\n{prompt}\n"
|
|
83
|
+
|
|
84
|
+
temp_path = self._system_env.create_temp_file(suffix=".md")
|
|
85
|
+
with open(temp_path, "w", encoding="utf-8") as f:
|
|
86
|
+
f.write(initial_content)
|
|
87
|
+
|
|
88
|
+
editor_cmd = self._tooling.find_editor()
|
|
89
|
+
if not editor_cmd:
|
|
90
|
+
typer.echo("Error: No suitable editor found.", err=True)
|
|
91
|
+
self._system_env.delete_file(temp_path)
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
cmd = editor_cmd + [temp_path]
|
|
96
|
+
self._system_env.run_command(cmd, background=True)
|
|
97
|
+
self._active_editor_path = temp_path
|
|
98
|
+
self._active_editor_marker = marker
|
|
99
|
+
except Exception as e:
|
|
100
|
+
typer.echo(f"Error: Editor launch failed: {e}", err=True)
|
|
101
|
+
self._system_env.delete_file(temp_path)
|
|
102
|
+
|
|
103
|
+
def _read_editor_result(self) -> str:
|
|
104
|
+
"""Reads the content of the background editor's temp file."""
|
|
105
|
+
if not self._active_editor_path:
|
|
106
|
+
return ""
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
with open(self._active_editor_path, "r", encoding="utf-8") as f:
|
|
110
|
+
content = f.read()
|
|
111
|
+
|
|
112
|
+
marker = self._active_editor_marker or ""
|
|
113
|
+
if marker in content:
|
|
114
|
+
content = content.split(marker)[0]
|
|
115
|
+
|
|
116
|
+
return content.strip()
|
|
117
|
+
except Exception as e:
|
|
118
|
+
typer.echo(f"Error: Reading editor result failed: {e}", err=True)
|
|
119
|
+
return ""
|
|
120
|
+
finally:
|
|
121
|
+
self.cleanup()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from teddy_executor.adapters.inbound.cli_formatter import (
|
|
8
|
+
echo_handoff_details,
|
|
9
|
+
echo_skipped_action,
|
|
10
|
+
)
|
|
11
|
+
from teddy_executor.core.domain.models.change_set import ChangeSet
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def restore_terminal_mode():
|
|
17
|
+
"""Restores stdin to canonical/echo mode (Unix only)."""
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
if (
|
|
21
|
+
sys.platform == "win32"
|
|
22
|
+
or "PYTEST_CURRENT_TEST" in os.environ
|
|
23
|
+
or not sys.stdin.isatty()
|
|
24
|
+
):
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
import termios
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
fd = sys.stdin.fileno()
|
|
32
|
+
except Exception:
|
|
33
|
+
return
|
|
34
|
+
attrs = termios.tcgetattr(fd)
|
|
35
|
+
# iflags: Ensure ICRNL is set (Map CR to NL on input)
|
|
36
|
+
attrs[0] = attrs[0] | termios.ICRNL
|
|
37
|
+
# lflags: Re-enable canonical mode (ICANON) and echo (ECHO)
|
|
38
|
+
attrs[3] = (
|
|
39
|
+
attrs[3] | termios.ICANON | termios.ECHO | termios.ISIG | termios.IEXTEN
|
|
40
|
+
)
|
|
41
|
+
# Apply changes and FLUSH the input buffer
|
|
42
|
+
termios.tcsetattr(fd, termios.TCSAFLUSH, attrs)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger.debug("Failed to restore terminal mode: %s", e)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def prepare_external_preview_files(
|
|
48
|
+
system_env, change_set: ChangeSet, temp_files: List[str]
|
|
49
|
+
) -> List[str]:
|
|
50
|
+
"""Sets up temp files for external diff/editor."""
|
|
51
|
+
ext = "".join(change_set.path.suffixes)
|
|
52
|
+
if change_set.action_type == "CREATE":
|
|
53
|
+
preview_path = system_env.create_temp_file(suffix=f".preview{ext}")
|
|
54
|
+
temp_files.append(preview_path)
|
|
55
|
+
with open(preview_path, "w", encoding="utf-8") as f:
|
|
56
|
+
f.write(change_set.after_content)
|
|
57
|
+
return [preview_path]
|
|
58
|
+
|
|
59
|
+
before_path = system_env.create_temp_file(suffix=f".before{ext}")
|
|
60
|
+
after_path = system_env.create_temp_file(suffix=f".after{ext}")
|
|
61
|
+
temp_files.extend([before_path, after_path])
|
|
62
|
+
with open(before_path, "w", encoding="utf-8") as f:
|
|
63
|
+
f.write(change_set.before_content)
|
|
64
|
+
with open(after_path, "w", encoding="utf-8") as f:
|
|
65
|
+
f.write(change_set.after_content)
|
|
66
|
+
return [before_path, after_path]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def display_handoff_and_confirm(
|
|
70
|
+
action_type: str,
|
|
71
|
+
target_agent: Optional[str],
|
|
72
|
+
resources: List[str],
|
|
73
|
+
message: str,
|
|
74
|
+
) -> tuple[bool, str]:
|
|
75
|
+
"""Displays a handoff request and asks for confirmation."""
|
|
76
|
+
echo_handoff_details(action_type, target_agent, resources, message)
|
|
77
|
+
try:
|
|
78
|
+
response = typer.prompt(
|
|
79
|
+
"Press [Enter] to approve, or type a reason for rejection",
|
|
80
|
+
default="",
|
|
81
|
+
show_default=False,
|
|
82
|
+
err=True,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if response:
|
|
86
|
+
return False, response # Rejected
|
|
87
|
+
return True, "" # Approved
|
|
88
|
+
except (EOFError, typer.Abort):
|
|
89
|
+
typer.echo("\nAborted.", err=True)
|
|
90
|
+
return False, "Skipped due to non-interactive session."
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def print_skipped_action(action, reason: str) -> None:
|
|
94
|
+
"""Prints a colorized warning that an action was skipped."""
|
|
95
|
+
echo_skipped_action(action, reason)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
from teddy_executor.core.ports.outbound.system_environment import ISystemEnvironment
|
|
4
|
+
from teddy_executor.core.ports.outbound.config_service import IConfigService
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConsoleToolingHelper:
|
|
8
|
+
def __init__(self, system_env: ISystemEnvironment, config_service: IConfigService):
|
|
9
|
+
self._system_env = system_env
|
|
10
|
+
self._config_service = config_service
|
|
11
|
+
|
|
12
|
+
def get_diff_viewer_command(self) -> Optional[List[str]]:
|
|
13
|
+
custom_tool_str = self._system_env.get_env("TEDDY_DIFF_TOOL")
|
|
14
|
+
if custom_tool_str:
|
|
15
|
+
custom_tool_parts = shlex.split(custom_tool_str)
|
|
16
|
+
tool_name = custom_tool_parts[0]
|
|
17
|
+
if tool_path := self._system_env.which(tool_name):
|
|
18
|
+
custom_tool_parts[0] = tool_path
|
|
19
|
+
return custom_tool_parts
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
if code_path := self._system_env.which("code"):
|
|
23
|
+
return [code_path, "-r", "--diff", "--wait"]
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
def find_editor(self) -> Optional[List[str]]:
|
|
27
|
+
# 1. Check Config
|
|
28
|
+
if cmd := self._resolve_editor_cmd(self._config_service.get_setting("editor")):
|
|
29
|
+
return cmd
|
|
30
|
+
|
|
31
|
+
# 2. Check Env
|
|
32
|
+
env_editor = self._system_env.get_env("VISUAL") or self._system_env.get_env(
|
|
33
|
+
"EDITOR"
|
|
34
|
+
)
|
|
35
|
+
if cmd := self._resolve_editor_cmd(env_editor):
|
|
36
|
+
return cmd
|
|
37
|
+
|
|
38
|
+
# 3. Discovery Fallback
|
|
39
|
+
for fallback in ["code", "nano"]:
|
|
40
|
+
if path := self._system_env.which(fallback):
|
|
41
|
+
cmd = [path]
|
|
42
|
+
if fallback == "code":
|
|
43
|
+
cmd.extend(["-r", "--wait"])
|
|
44
|
+
return cmd
|
|
45
|
+
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
def _resolve_editor_cmd(self, editor_str: Optional[str]) -> Optional[List[str]]:
|
|
49
|
+
"""Parses a command string and resolves the executable path."""
|
|
50
|
+
if not editor_str:
|
|
51
|
+
return None
|
|
52
|
+
parts = shlex.split(editor_str)
|
|
53
|
+
|
|
54
|
+
if tool_path := self._system_env.which(parts[0]):
|
|
55
|
+
parts[0] = tool_path
|
|
56
|
+
# If the tool is VS Code, ensure reuse and wait flags are present
|
|
57
|
+
if parts[0].endswith("code") and "code" in parts[0].lower():
|
|
58
|
+
for flag in ["-r", "--wait"]:
|
|
59
|
+
if flag not in parts:
|
|
60
|
+
parts.append(flag)
|
|
61
|
+
return parts
|
|
62
|
+
return None
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any, Iterator, Tuple
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def load_ignore_spec(root_dir: Path) -> Any:
|
|
6
|
+
"""
|
|
7
|
+
Loads and returns a pathspec for ignores (.gitignore and .teddyignore).
|
|
8
|
+
"""
|
|
9
|
+
import pathspec
|
|
10
|
+
|
|
11
|
+
default_ignores = {
|
|
12
|
+
".git/",
|
|
13
|
+
".venv/",
|
|
14
|
+
"__pycache__/",
|
|
15
|
+
".teddy/",
|
|
16
|
+
".ruff_cache/",
|
|
17
|
+
}
|
|
18
|
+
lines = list(default_ignores)
|
|
19
|
+
|
|
20
|
+
gitignore_path = root_dir / ".gitignore"
|
|
21
|
+
if gitignore_path.is_file():
|
|
22
|
+
lines.append(gitignore_path.name)
|
|
23
|
+
lines.extend(gitignore_path.read_text(encoding="utf-8").splitlines())
|
|
24
|
+
|
|
25
|
+
teddyignore_path = root_dir / ".teddyignore"
|
|
26
|
+
if teddyignore_path.is_file():
|
|
27
|
+
lines.append(teddyignore_path.name)
|
|
28
|
+
lines.extend(teddyignore_path.read_text(encoding="utf-8").splitlines())
|
|
29
|
+
|
|
30
|
+
return pathspec.PathSpec.from_lines("gitwildmatch", lines)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def walk_recursive(
|
|
34
|
+
root_dir: Path,
|
|
35
|
+
start_dir: Path,
|
|
36
|
+
spec: Any,
|
|
37
|
+
) -> Iterator[Tuple[Path, bool]]:
|
|
38
|
+
"""
|
|
39
|
+
Recursively walks a directory, yielding (Path, is_dir) for non-ignored entries.
|
|
40
|
+
Yielded paths are absolute.
|
|
41
|
+
"""
|
|
42
|
+
for entry in start_dir.iterdir():
|
|
43
|
+
try:
|
|
44
|
+
rel_path = entry.relative_to(root_dir)
|
|
45
|
+
except ValueError:
|
|
46
|
+
# Fallback for paths outside root (e.g. symlinks)
|
|
47
|
+
rel_path = entry
|
|
48
|
+
|
|
49
|
+
rel_path_str = str(rel_path).replace("\\", "/")
|
|
50
|
+
is_real_dir = entry.is_dir() and not entry.is_symlink()
|
|
51
|
+
|
|
52
|
+
# For directories, add a trailing slash to match gitignore behavior
|
|
53
|
+
match_path = rel_path_str + "/" if is_real_dir else rel_path_str
|
|
54
|
+
|
|
55
|
+
if spec.match_file(match_path):
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
yield entry, is_real_dir
|
|
59
|
+
|
|
60
|
+
if is_real_dir:
|
|
61
|
+
yield from walk_recursive(root_dir, entry, spec)
|