janito 0.5.0__py3-none-any.whl → 0.7.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.
- janito/__init__.py +0 -47
- janito/__main__.py +105 -17
- janito/agents/__init__.py +9 -9
- janito/agents/agent.py +10 -3
- janito/agents/claudeai.py +15 -34
- janito/agents/openai.py +5 -1
- janito/change/__init__.py +29 -16
- janito/change/__main__.py +0 -0
- janito/{analysis → change/analysis}/__init__.py +5 -15
- janito/change/analysis/__main__.py +7 -0
- janito/change/analysis/analyze.py +62 -0
- janito/change/analysis/formatting.py +78 -0
- janito/change/analysis/options.py +81 -0
- janito/{analysis → change/analysis}/prompts.py +33 -18
- janito/change/analysis/view/__init__.py +9 -0
- janito/change/analysis/view/terminal.py +181 -0
- janito/change/applier/__init__.py +5 -0
- janito/change/applier/file.py +58 -0
- janito/change/applier/main.py +156 -0
- janito/change/applier/text.py +247 -0
- janito/change/applier/workspace_dir.py +58 -0
- janito/change/core.py +124 -0
- janito/{changehistory.py → change/history.py} +12 -14
- janito/change/operations.py +7 -0
- janito/change/parser.py +287 -0
- janito/change/play.py +54 -0
- janito/change/preview.py +82 -0
- janito/change/prompts.py +121 -0
- janito/change/test.py +0 -0
- janito/change/validator.py +269 -0
- janito/{changeviewer → change/viewer}/__init__.py +3 -4
- janito/change/viewer/content.py +66 -0
- janito/{changeviewer → change/viewer}/diff.py +19 -4
- janito/change/viewer/panels.py +533 -0
- janito/change/viewer/styling.py +114 -0
- janito/{changeviewer → change/viewer}/themes.py +3 -5
- janito/clear_statement_parser/clear_statement_format.txt +328 -0
- janito/clear_statement_parser/examples.txt +326 -0
- janito/clear_statement_parser/models.py +104 -0
- janito/clear_statement_parser/parser.py +496 -0
- janito/cli/base.py +30 -0
- janito/cli/commands.py +75 -40
- janito/cli/functions.py +19 -194
- janito/cli/history.py +61 -0
- janito/common.py +65 -8
- janito/config.py +70 -5
- janito/demo/__init__.py +4 -0
- janito/demo/data.py +13 -0
- janito/demo/mock_data.py +20 -0
- janito/demo/operations.py +45 -0
- janito/demo/runner.py +59 -0
- janito/demo/scenarios.py +32 -0
- janito/prompt.py +36 -0
- janito/qa.py +6 -14
- janito/search_replace/README.md +192 -0
- janito/search_replace/__init__.py +7 -0
- janito/search_replace/__main__.py +21 -0
- janito/search_replace/core.py +120 -0
- janito/search_replace/logger.py +35 -0
- janito/search_replace/parser.py +52 -0
- janito/search_replace/play.py +61 -0
- janito/search_replace/replacer.py +36 -0
- janito/search_replace/searcher.py +411 -0
- janito/search_replace/strategy_result.py +10 -0
- janito/shell/__init__.py +38 -0
- janito/shell/bus.py +31 -0
- janito/shell/commands.py +136 -0
- janito/shell/history.py +20 -0
- janito/shell/processor.py +32 -0
- janito/shell/prompt.py +48 -0
- janito/shell/registry.py +60 -0
- janito/tui/__init__.py +21 -0
- janito/tui/base.py +22 -0
- janito/tui/flows/__init__.py +5 -0
- janito/tui/flows/changes.py +65 -0
- janito/tui/flows/content.py +128 -0
- janito/tui/flows/selection.py +117 -0
- janito/tui/screens/__init__.py +3 -0
- janito/tui/screens/app.py +1 -0
- janito/workspace/__init__.py +6 -0
- janito/workspace/analysis.py +121 -0
- janito/workspace/show.py +141 -0
- janito/workspace/stats.py +43 -0
- janito/workspace/types.py +98 -0
- janito/workspace/workset.py +108 -0
- janito/workspace/workspace.py +114 -0
- janito-0.7.0.dist-info/METADATA +167 -0
- janito-0.7.0.dist-info/RECORD +96 -0
- {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/WHEEL +1 -1
- janito/_contextparser.py +0 -113
- janito/analysis/display.py +0 -149
- janito/analysis/options.py +0 -112
- janito/change/applier.py +0 -269
- janito/change/content.py +0 -62
- janito/change/indentation.py +0 -33
- janito/change/position.py +0 -169
- janito/changeviewer/panels.py +0 -268
- janito/changeviewer/styling.py +0 -59
- janito/console/__init__.py +0 -3
- janito/console/commands.py +0 -112
- janito/console/core.py +0 -62
- janito/console/display.py +0 -157
- janito/fileparser.py +0 -334
- janito/prompts.py +0 -81
- janito/scan.py +0 -176
- janito/tests/test_fileparser.py +0 -26
- janito-0.5.0.dist-info/METADATA +0 -146
- janito-0.5.0.dist-info/RECORD +0 -45
- {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/entry_points.txt +0 -0
- {janito-0.5.0.dist-info → janito-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,23 +1,24 @@
|
|
1
1
|
"""User prompts and input handling for analysis."""
|
2
2
|
|
3
|
-
from typing import List
|
3
|
+
from typing import List, Dict
|
4
4
|
from rich.console import Console
|
5
5
|
from rich.panel import Panel
|
6
6
|
from rich.rule import Rule
|
7
7
|
from rich.prompt import Prompt
|
8
8
|
from rich import box
|
9
9
|
|
10
|
+
|
11
|
+
from .options import AnalysisOption
|
12
|
+
|
10
13
|
# Keep only prompt-related functionality
|
11
14
|
CHANGE_ANALYSIS_PROMPT = """
|
12
|
-
Current files:
|
13
|
-
<files>
|
14
|
-
{files_content}
|
15
|
-
</files>
|
16
15
|
|
17
|
-
|
16
|
+
|
17
|
+
Considering the above workset content, provide 3 sections, each identified by a keyword and representing an option.
|
18
18
|
Each option should include a concise description and a list of affected files.
|
19
|
-
1st option should be
|
20
|
-
Do not use style as keyword, instead focus on the changes
|
19
|
+
1st option should be basic style change, 2nd organized style, 3rd exntensible style.
|
20
|
+
Do not use style as keyword, instead focus on the changes summary.
|
21
|
+
|
21
22
|
Use the following format:
|
22
23
|
|
23
24
|
A. Keyword summary of the change
|
@@ -44,32 +45,46 @@ Request:
|
|
44
45
|
def prompt_user(message: str, choices: List[str] = None) -> str:
|
45
46
|
"""Display a prominent user prompt with optional choices"""
|
46
47
|
console = Console()
|
48
|
+
term_width = console.width or 80
|
47
49
|
console.print()
|
48
|
-
console.print(Rule(" User Input Required ", style="bold cyan"))
|
50
|
+
console.print(Rule(" User Input Required ", style="bold cyan", align="center"))
|
49
51
|
|
50
52
|
if choices:
|
51
53
|
choice_text = f"[cyan]Options: {', '.join(choices)}[/cyan]"
|
52
|
-
console.print(Panel(choice_text, box=box.ROUNDED))
|
54
|
+
console.print(Panel(choice_text, box=box.ROUNDED, justify="center"))
|
53
55
|
|
54
|
-
|
56
|
+
# Center the prompt with padding
|
57
|
+
padding = (term_width - len(message)) // 2
|
58
|
+
padded_message = " " * padding + message
|
59
|
+
return Prompt.ask(f"[bold cyan]{padded_message}[/bold cyan]")
|
55
60
|
|
56
|
-
def validate_option_letter(letter: str, options:
|
61
|
+
def validate_option_letter(letter: str, options: Dict[str, AnalysisOption]) -> bool:
|
57
62
|
"""Validate if the given letter is a valid option or 'M' for modify"""
|
58
|
-
|
63
|
+
if letter.upper() == 'M':
|
64
|
+
return True
|
65
|
+
return letter.upper() in options
|
59
66
|
|
60
67
|
def get_option_selection() -> str:
|
61
68
|
"""Get user input for option selection with modify option"""
|
62
69
|
console = Console()
|
63
|
-
console.
|
70
|
+
term_width = console.width or 80
|
71
|
+
message = "Enter option letter or 'M' to modify request"
|
72
|
+
padding = (term_width - len(message)) // 2
|
73
|
+
padded_message = " " * padding + message
|
74
|
+
|
75
|
+
console.print(f"\n[cyan]{padded_message}[/cyan]")
|
64
76
|
while True:
|
65
77
|
letter = prompt_user("Select option").strip().upper()
|
66
78
|
if letter == 'M' or (letter.isalpha() and len(letter) == 1):
|
67
79
|
return letter
|
68
|
-
|
80
|
+
|
81
|
+
error_msg = "Please enter a valid letter or 'M'"
|
82
|
+
error_padding = (term_width - len(error_msg)) // 2
|
83
|
+
padded_error = " " * error_padding + error_msg
|
84
|
+
console.print(f"[red]{padded_error}[/red]")
|
69
85
|
|
70
|
-
def build_request_analysis_prompt(
|
86
|
+
def build_request_analysis_prompt(request: str) -> str:
|
71
87
|
"""Build prompt for information requests"""
|
72
88
|
return CHANGE_ANALYSIS_PROMPT.format(
|
73
|
-
files_content=files_content,
|
74
89
|
request=request
|
75
|
-
)
|
90
|
+
)
|
@@ -0,0 +1,181 @@
|
|
1
|
+
"""Terminal UI components for analysis display."""
|
2
|
+
|
3
|
+
from typing import Dict, List, Optional
|
4
|
+
from rich.console import Console
|
5
|
+
from rich.columns import Columns
|
6
|
+
from rich.text import Text
|
7
|
+
from rich.panel import Panel
|
8
|
+
from rich.rule import Rule
|
9
|
+
from rich.padding import Padding
|
10
|
+
from rich.prompt import Prompt
|
11
|
+
from rich import box
|
12
|
+
from rich.style import Style
|
13
|
+
from rich.segment import Segment
|
14
|
+
from rich.containers import Renderables
|
15
|
+
from pathlib import Path
|
16
|
+
|
17
|
+
from ..options import AnalysisOption
|
18
|
+
from ..formatting import (
|
19
|
+
COLUMN_SPACING,
|
20
|
+
MIN_PANEL_WIDTH,
|
21
|
+
SECTION_PADDING,
|
22
|
+
STATUS_COLORS,
|
23
|
+
STRUCTURAL_COLORS,
|
24
|
+
create_header,
|
25
|
+
create_section_header,
|
26
|
+
format_file_path
|
27
|
+
)
|
28
|
+
|
29
|
+
def prompt_user(message: str, choices: List[str] = None) -> str:
|
30
|
+
"""Display a prominent user prompt with optional choices"""
|
31
|
+
console = Console()
|
32
|
+
term_width = console.width or 80
|
33
|
+
console.print()
|
34
|
+
console.print(Rule(" User Input Required ", style="bold cyan", align="center"))
|
35
|
+
|
36
|
+
if choices:
|
37
|
+
choice_text = f"[cyan]Options: {', '.join(choices)}[/cyan]"
|
38
|
+
console.print(Panel(choice_text, box=box.ROUNDED, justify="center"))
|
39
|
+
|
40
|
+
message_text = Text(message, style="bold cyan")
|
41
|
+
padded_message = Padding(message_text, pad=(0, "center"))
|
42
|
+
console.print(padded_message)
|
43
|
+
return Prompt.ask("")
|
44
|
+
|
45
|
+
def get_option_selection() -> str:
|
46
|
+
"""Get user input for option selection with modify option"""
|
47
|
+
console = Console()
|
48
|
+
term_width = console.width or 80
|
49
|
+
message = "Enter option letter or 'M' to modify request"
|
50
|
+
padding = (term_width - len(message)) // 2
|
51
|
+
padded_message = " " * padding + message
|
52
|
+
|
53
|
+
console.print(f"\n[cyan]{padded_message}[/cyan]")
|
54
|
+
while True:
|
55
|
+
letter = prompt_user("Select option").strip().upper()
|
56
|
+
if letter == 'M' or (letter.isalpha() and len(letter) == 1):
|
57
|
+
return letter
|
58
|
+
|
59
|
+
error_msg = "Please enter a valid letter or 'M'"
|
60
|
+
error_text = Text(error_msg, style="red")
|
61
|
+
padded_error = Padding(error_text, pad=(0, "center"))
|
62
|
+
console.print(padded_error)
|
63
|
+
|
64
|
+
def _create_option_content(option: AnalysisOption) -> Text:
|
65
|
+
"""Create rich formatted content for a single option."""
|
66
|
+
content = Text()
|
67
|
+
content.append("\n")
|
68
|
+
|
69
|
+
header = create_header(f"Option {option.letter} - {option.summary}")
|
70
|
+
content.append(header)
|
71
|
+
content.append("\n")
|
72
|
+
|
73
|
+
if option.description_items:
|
74
|
+
for item in option.description_items:
|
75
|
+
content.append("• ", style="cyan")
|
76
|
+
content.append(f"{item}\n")
|
77
|
+
content.append("\n")
|
78
|
+
|
79
|
+
# Add consistent padding before file list
|
80
|
+
content.append("\n" * 2)
|
81
|
+
|
82
|
+
if option.affected_files:
|
83
|
+
files = {status: [] for status in ['New', 'Modified', 'Removed']}
|
84
|
+
for file in option.affected_files:
|
85
|
+
if '(new)' in file.lower():
|
86
|
+
files['New'].append(file)
|
87
|
+
elif '(removed)' in file.lower():
|
88
|
+
files['Removed'].append(file)
|
89
|
+
else:
|
90
|
+
files['Modified'].append(file)
|
91
|
+
|
92
|
+
for status, status_files in files.items():
|
93
|
+
if status_files:
|
94
|
+
content.append(create_section_header(f"{status} Files"))
|
95
|
+
content.append("\n")
|
96
|
+
sorted_files = sorted(status_files)
|
97
|
+
prev_path = None
|
98
|
+
seen_dirs = {}
|
99
|
+
for file in sorted_files:
|
100
|
+
path = option.get_clean_path(file)
|
101
|
+
current_parts = Path(path).parts
|
102
|
+
parent_dir = str(Path(path).parent)
|
103
|
+
|
104
|
+
if parent_dir != '.':
|
105
|
+
is_repeated = parent_dir in seen_dirs
|
106
|
+
if not is_repeated:
|
107
|
+
content.append(parent_dir, style=STRUCTURAL_COLORS['directory'])
|
108
|
+
content.append("/", style=STRUCTURAL_COLORS['separator'])
|
109
|
+
seen_dirs[parent_dir] = True
|
110
|
+
else:
|
111
|
+
dir_width = len(parent_dir)
|
112
|
+
# Calculate padding to match full directory width
|
113
|
+
arrow = "↑"
|
114
|
+
total_padding = dir_width - len(arrow)
|
115
|
+
left_padding = total_padding // 2
|
116
|
+
right_padding = total_padding - left_padding
|
117
|
+
content.append(" " * left_padding + arrow + " " * right_padding,
|
118
|
+
style=STRUCTURAL_COLORS['repeat'])
|
119
|
+
content.append("/", style=STRUCTURAL_COLORS['separator'])
|
120
|
+
content.append(current_parts[-1], style=STATUS_COLORS[status.lower()])
|
121
|
+
else:
|
122
|
+
content.append(current_parts[-1], style=STATUS_COLORS[status.lower()])
|
123
|
+
content.append("\n")
|
124
|
+
content.append("\n")
|
125
|
+
|
126
|
+
content.append("\n")
|
127
|
+
|
128
|
+
return content
|
129
|
+
|
130
|
+
def create_columns_layout(options_content: List[Text], term_width: int) -> Columns:
|
131
|
+
"""Create a columns layout with consistent spacing."""
|
132
|
+
num_columns = len(options_content)
|
133
|
+
spacing = COLUMN_SPACING * (num_columns - 1)
|
134
|
+
safety_margin = 4 + 2
|
135
|
+
|
136
|
+
usable_width = term_width - spacing - safety_margin
|
137
|
+
column_width = max((usable_width // num_columns), MIN_PANEL_WIDTH)
|
138
|
+
|
139
|
+
# Create padded content items
|
140
|
+
rendered_items: List[Renderables] = [
|
141
|
+
Padding(content, (0, COLUMN_SPACING // 2))
|
142
|
+
for content in options_content
|
143
|
+
]
|
144
|
+
|
145
|
+
return Columns(
|
146
|
+
rendered_items,
|
147
|
+
equal=True,
|
148
|
+
expand=True,
|
149
|
+
width=column_width,
|
150
|
+
align="left",
|
151
|
+
padding=(0, 0),
|
152
|
+
)
|
153
|
+
|
154
|
+
def format_analysis(analysis: str, raw: bool = False) -> None:
|
155
|
+
"""Format and display the analysis output."""
|
156
|
+
from ..options import parse_analysis_options
|
157
|
+
|
158
|
+
console = Console()
|
159
|
+
term_width = console.width or 100
|
160
|
+
|
161
|
+
if raw:
|
162
|
+
console.print(analysis)
|
163
|
+
return
|
164
|
+
|
165
|
+
options = parse_analysis_options(analysis)
|
166
|
+
if not options:
|
167
|
+
console.print("\n[yellow]Warning: No valid options found in response.[/yellow]\n")
|
168
|
+
console.print(analysis)
|
169
|
+
return
|
170
|
+
|
171
|
+
columns_content = [_create_option_content(options[letter])
|
172
|
+
for letter in sorted(options.keys())]
|
173
|
+
|
174
|
+
columns = create_columns_layout(columns_content, term_width)
|
175
|
+
|
176
|
+
console.print("\n")
|
177
|
+
console.print(Text("Analysis Options", style="bold cyan"))
|
178
|
+
console.print(Text("─" * term_width, style="cyan dim"))
|
179
|
+
console.print(columns)
|
180
|
+
console.print(Text("─" * term_width, style="cyan dim"))
|
181
|
+
console.print("\n")
|
@@ -0,0 +1,58 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import Tuple, Optional
|
3
|
+
from rich.console import Console
|
4
|
+
from ..parser import FileChange, ChangeOperation
|
5
|
+
|
6
|
+
class FileChangeApplier:
|
7
|
+
def __init__(self, preview_dir: Path, console: Console = None):
|
8
|
+
self.preview_dir = preview_dir
|
9
|
+
self.console = console or Console()
|
10
|
+
|
11
|
+
def apply_file_operation(self, change: FileChange) -> Tuple[bool, Optional[str]]:
|
12
|
+
"""Apply a file operation (create/replace/remove/rename/move)
|
13
|
+
Returns: (success, error_message)"""
|
14
|
+
path = self.preview_dir / change.name
|
15
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
16
|
+
|
17
|
+
# Store original content before any changes
|
18
|
+
if path.exists():
|
19
|
+
change.original_content = path.read_text()
|
20
|
+
|
21
|
+
if change.operation == ChangeOperation.REMOVE_FILE:
|
22
|
+
return self._handle_remove(path)
|
23
|
+
elif change.operation in (ChangeOperation.CREATE_FILE, ChangeOperation.REPLACE_FILE):
|
24
|
+
return self._handle_create_replace(path, change)
|
25
|
+
elif change.operation in (ChangeOperation.RENAME_FILE, ChangeOperation.MOVE_FILE):
|
26
|
+
return self._handle_move(path, change)
|
27
|
+
|
28
|
+
return False, f"Unsupported operation: {change.operation}"
|
29
|
+
|
30
|
+
def _handle_remove(self, path: Path) -> Tuple[bool, Optional[str]]:
|
31
|
+
"""Handle file removal"""
|
32
|
+
if path.exists():
|
33
|
+
path.unlink()
|
34
|
+
return True, None
|
35
|
+
|
36
|
+
def _handle_create_replace(self, path: Path, change: FileChange) -> Tuple[bool, Optional[str]]:
|
37
|
+
"""Handle file creation or replacement"""
|
38
|
+
if change.operation == ChangeOperation.CREATE_FILE and path.exists():
|
39
|
+
return False, f"Cannot create file {path} - already exists"
|
40
|
+
|
41
|
+
if change.content is not None:
|
42
|
+
path.write_text(change.content)
|
43
|
+
return True, None
|
44
|
+
|
45
|
+
return False, "No content provided for create/replace operation"
|
46
|
+
|
47
|
+
def _handle_move(self, path: Path, change: FileChange) -> Tuple[bool, Optional[str]]:
|
48
|
+
"""Handle file move/rename operations"""
|
49
|
+
if not path.exists():
|
50
|
+
return False, f"Cannot move/rename non-existent file {path}"
|
51
|
+
|
52
|
+
if not change.target:
|
53
|
+
return False, "No target path provided for move/rename operation"
|
54
|
+
|
55
|
+
new_path = self.preview_dir / change.target
|
56
|
+
new_path.parent.mkdir(parents=True, exist_ok=True)
|
57
|
+
path.rename(new_path)
|
58
|
+
return True, None
|
@@ -0,0 +1,156 @@
|
|
1
|
+
"""
|
2
|
+
Applies file changes to preview directory and runs tests
|
3
|
+
|
4
|
+
The following situations should result in error:
|
5
|
+
- Creating a file that already exists
|
6
|
+
- Replace operation on a non-existent file
|
7
|
+
- Rename operation on a non-existent file
|
8
|
+
- Modify operation with search text not found
|
9
|
+
- No changes applied to a file
|
10
|
+
"""
|
11
|
+
|
12
|
+
from pathlib import Path
|
13
|
+
from typing import Tuple, Optional, List, Set
|
14
|
+
from rich.console import Console
|
15
|
+
from rich.panel import Panel
|
16
|
+
from rich import box
|
17
|
+
import subprocess
|
18
|
+
from ..validator import validate_python_syntax
|
19
|
+
from .workspace_dir import apply_changes as apply_to_workspace_dir_impl
|
20
|
+
from janito.config import config
|
21
|
+
from .file import FileChangeApplier
|
22
|
+
from .text import TextChangeApplier
|
23
|
+
from ..parser import FileChange, ChangeOperation
|
24
|
+
from ..validator import validate_all_changes
|
25
|
+
|
26
|
+
|
27
|
+
class ChangeApplier:
|
28
|
+
"""Handles applying changes to files."""
|
29
|
+
|
30
|
+
def __init__(self, preview_dir: Path, debug: bool = False):
|
31
|
+
self.preview_dir = preview_dir
|
32
|
+
self.debug = debug
|
33
|
+
self.console = Console()
|
34
|
+
self.file_applier = FileChangeApplier(preview_dir, self.console)
|
35
|
+
self.text_applier = TextChangeApplier(self.console)
|
36
|
+
|
37
|
+
def run_test_command(self, test_cmd: str) -> Tuple[bool, str, Optional[str]]:
|
38
|
+
"""Run test command in preview directory.
|
39
|
+
Returns (success, output, error)"""
|
40
|
+
try:
|
41
|
+
result = subprocess.run(
|
42
|
+
test_cmd,
|
43
|
+
shell=True,
|
44
|
+
cwd=self.preview_dir,
|
45
|
+
capture_output=True,
|
46
|
+
text=True,
|
47
|
+
timeout=300 # 5 minute timeout
|
48
|
+
)
|
49
|
+
return (
|
50
|
+
result.returncode == 0,
|
51
|
+
result.stdout,
|
52
|
+
result.stderr if result.returncode != 0 else None
|
53
|
+
)
|
54
|
+
except subprocess.TimeoutExpired:
|
55
|
+
return False, "", "Test command timed out after 5 minutes"
|
56
|
+
except Exception as e:
|
57
|
+
return False, "", f"Error running test: {str(e)}"
|
58
|
+
|
59
|
+
def apply_changes(self, changes: List[FileChange], debug: bool = None) -> tuple[bool, Set[Path]]:
|
60
|
+
"""Apply changes in preview directory, runs tests if specified.
|
61
|
+
Returns (success, modified_files)"""
|
62
|
+
debug = debug if debug is not None else self.debug
|
63
|
+
console = Console()
|
64
|
+
|
65
|
+
# Validate all changes using consolidated validator
|
66
|
+
is_valid, error = validate_all_changes(changes, set(Path(c.name) for c in changes))
|
67
|
+
if not is_valid:
|
68
|
+
console.print(f"\n[red]{error}[/red]")
|
69
|
+
return False, set()
|
70
|
+
|
71
|
+
# Track modified files and apply changes
|
72
|
+
modified_files: Set[Path] = set()
|
73
|
+
for change in changes:
|
74
|
+
if config.verbose:
|
75
|
+
console.print(f"[dim]Previewing changes for {change.name}...[/dim]")
|
76
|
+
success, error = self.apply_single_change(change, debug)
|
77
|
+
if not success:
|
78
|
+
console.print(f"\n[red]Error previewing {change.name}: {error}[/red]")
|
79
|
+
return False, modified_files
|
80
|
+
if not change.operation == 'remove_file':
|
81
|
+
modified_files.add(change.name)
|
82
|
+
elif change.operation == 'rename_file':
|
83
|
+
modified_files.add(change.target)
|
84
|
+
|
85
|
+
# Validate Python syntax (skip deleted and moved files)
|
86
|
+
python_files = {f for f in modified_files if f.suffix == '.py'}
|
87
|
+
|
88
|
+
for change in changes:
|
89
|
+
if change.operation == ChangeOperation.REMOVE_FILE:
|
90
|
+
python_files.discard(change.name) # Skip validation for deleted files
|
91
|
+
elif change.operation in (ChangeOperation.RENAME_FILE, ChangeOperation.MOVE_FILE):
|
92
|
+
python_files.discard(change.source) # Skip validation for moved/renamed sources
|
93
|
+
|
94
|
+
for path in python_files:
|
95
|
+
preview_path = self.preview_dir / path
|
96
|
+
is_valid, error_msg = validate_python_syntax(preview_path.read_text(), preview_path)
|
97
|
+
if not is_valid:
|
98
|
+
console.print(f"\n[red]Python syntax validation failed for {path}:[/red]")
|
99
|
+
console.print(f"[red]{error_msg}[/red]")
|
100
|
+
return False, modified_files
|
101
|
+
|
102
|
+
# Show success message with syntax validation status
|
103
|
+
console.print("\n[cyan]Changes applied successfully.[/cyan]")
|
104
|
+
if python_files:
|
105
|
+
console.print(f"[green]✓ Python syntax validated for {len(python_files)} file(s)[/green]")
|
106
|
+
|
107
|
+
# Run tests if specified
|
108
|
+
if config.test_cmd:
|
109
|
+
console.print(f"\n[cyan]Testing changes in preview directory:[/cyan] {config.test_cmd}")
|
110
|
+
success, output, error = self.run_test_command(config.test_cmd)
|
111
|
+
if output:
|
112
|
+
console.print("\n[bold]Test Output:[/bold]")
|
113
|
+
console.print(Panel(output, box=box.ROUNDED))
|
114
|
+
if not success:
|
115
|
+
console.print("\n[red bold]Tests failed in preview.[/red bold]")
|
116
|
+
if error:
|
117
|
+
console.print(Panel(error, title="Error", border_style="red"))
|
118
|
+
return False, modified_files
|
119
|
+
|
120
|
+
return True, modified_files
|
121
|
+
|
122
|
+
def apply_single_change(self, change: FileChange, debug: bool) -> Tuple[bool, Optional[str]]:
|
123
|
+
"""Apply a single file change to preview directory"""
|
124
|
+
path = self.preview_dir / change.name # Changed back from path to name
|
125
|
+
|
126
|
+
# Handle file operations first
|
127
|
+
if change.operation != ChangeOperation.MODIFY_FILE:
|
128
|
+
return self.file_applier.apply_file_operation(change)
|
129
|
+
|
130
|
+
# Handle text modifications
|
131
|
+
if not path.exists():
|
132
|
+
original_path = Path(change.name) # Changed back from path to name
|
133
|
+
if not original_path.exists():
|
134
|
+
return False, f"Original file not found: {original_path}"
|
135
|
+
if self.console:
|
136
|
+
self.console.print(f"[dim]Copying {original_path} to preview directory {path}[/dim]")
|
137
|
+
path.write_text(original_path.read_text())
|
138
|
+
|
139
|
+
current_content = path.read_text()
|
140
|
+
success, modified_content, error = self.text_applier.apply_modifications(
|
141
|
+
current_content,
|
142
|
+
change.text_changes,
|
143
|
+
path,
|
144
|
+
debug
|
145
|
+
)
|
146
|
+
|
147
|
+
if not success:
|
148
|
+
return False, error
|
149
|
+
|
150
|
+
path.write_text(modified_content)
|
151
|
+
return True, None
|
152
|
+
|
153
|
+
def apply_to_workspace_dir(self, changes: List[FileChange], debug: bool = None) -> bool:
|
154
|
+
"""Apply changes from preview to working directory"""
|
155
|
+
debug = debug if debug is not None else self.debug
|
156
|
+
return apply_to_workspace_dir_impl(changes, self.preview_dir, Console())
|