janito 0.4.0__py3-none-any.whl → 0.6.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 +1 -1
- janito/__main__.py +102 -326
- janito/agents/__init__.py +16 -0
- janito/agents/agent.py +21 -0
- janito/{claude.py → agents/claudeai.py} +13 -17
- janito/agents/openai.py +53 -0
- janito/agents/test.py +34 -0
- janito/change/__init__.py +32 -0
- janito/change/__main__.py +0 -0
- janito/change/analysis/__init__.py +23 -0
- janito/change/analysis/__main__.py +7 -0
- janito/change/analysis/analyze.py +61 -0
- janito/change/analysis/formatting.py +78 -0
- janito/change/analysis/options.py +81 -0
- janito/change/analysis/prompts.py +98 -0
- janito/change/analysis/view/__init__.py +9 -0
- janito/change/analysis/view/terminal.py +171 -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 +245 -0
- janito/change/applier/workspace_dir.py +58 -0
- janito/change/core.py +131 -0
- janito/change/history.py +44 -0
- janito/change/operations.py +7 -0
- janito/change/parser.py +289 -0
- janito/change/play.py +54 -0
- janito/change/preview.py +82 -0
- janito/change/prompts.py +126 -0
- janito/change/test.py +0 -0
- janito/change/validator.py +251 -0
- janito/change/viewer/__init__.py +11 -0
- janito/change/viewer/content.py +66 -0
- janito/change/viewer/diff.py +43 -0
- janito/change/viewer/pager.py +56 -0
- janito/change/viewer/panels.py +555 -0
- janito/change/viewer/styling.py +103 -0
- janito/change/viewer/themes.py +55 -0
- 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/__init__.py +2 -0
- janito/cli/base.py +30 -0
- janito/cli/commands.py +45 -0
- janito/cli/functions.py +111 -0
- janito/cli/handlers/ask.py +22 -0
- janito/cli/handlers/demo.py +22 -0
- janito/cli/handlers/request.py +24 -0
- janito/cli/handlers/scan.py +9 -0
- janito/cli/history.py +61 -0
- janito/cli/registry.py +26 -0
- janito/common.py +41 -10
- janito/config.py +71 -6
- 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/prompts.py +1 -65
- janito/qa.py +8 -5
- janito/review.py +13 -0
- janito/search_replace/README.md +146 -0
- janito/search_replace/__init__.py +6 -0
- janito/search_replace/__main__.py +21 -0
- janito/search_replace/core.py +119 -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 +299 -0
- janito/shell/__init__.py +39 -0
- janito/shell/bus.py +31 -0
- janito/shell/commands.py +195 -0
- janito/shell/handlers.py +122 -0
- janito/shell/history.py +20 -0
- janito/shell/processor.py +52 -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 +7 -0
- janito/workspace/analysis.py +121 -0
- janito/workspace/manager.py +48 -0
- janito/workspace/scan.py +232 -0
- janito-0.6.0.dist-info/METADATA +185 -0
- janito-0.6.0.dist-info/RECORD +95 -0
- {janito-0.4.0.dist-info → janito-0.6.0.dist-info}/WHEEL +1 -1
- janito/analysis.py +0 -281
- janito/changeapplier.py +0 -436
- janito/changeviewer.py +0 -350
- janito/console.py +0 -330
- janito/contentchange.py +0 -84
- janito/contextparser.py +0 -113
- janito/fileparser.py +0 -125
- janito/scan.py +0 -137
- janito-0.4.0.dist-info/METADATA +0 -164
- janito-0.4.0.dist-info/RECORD +0 -21
- {janito-0.4.0.dist-info → janito-0.6.0.dist-info}/entry_points.txt +0 -0
- {janito-0.4.0.dist-info → janito-0.6.0.dist-info}/licenses/LICENSE +0 -0
janito/changeapplier.py
DELETED
@@ -1,436 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
from typing import Dict, Tuple, Optional, List
|
3
|
-
import tempfile
|
4
|
-
import shutil
|
5
|
-
import subprocess
|
6
|
-
from rich.console import Console
|
7
|
-
from rich.prompt import Confirm
|
8
|
-
from rich.panel import Panel
|
9
|
-
from rich import box
|
10
|
-
from datetime import datetime
|
11
|
-
from janito.fileparser import FileChange, validate_python_syntax
|
12
|
-
from janito.changeviewer import preview_all_changes
|
13
|
-
from janito.contextparser import apply_changes, parse_change_block
|
14
|
-
from janito.config import config
|
15
|
-
|
16
|
-
def run_test_command(preview_dir: Path, test_cmd: str) -> Tuple[bool, str, Optional[str]]:
|
17
|
-
"""Run test command in preview directory.
|
18
|
-
Returns (success, output, error)"""
|
19
|
-
try:
|
20
|
-
result = subprocess.run(
|
21
|
-
test_cmd,
|
22
|
-
shell=True,
|
23
|
-
cwd=preview_dir,
|
24
|
-
capture_output=True,
|
25
|
-
text=True,
|
26
|
-
timeout=300 # 5 minute timeout
|
27
|
-
)
|
28
|
-
return (
|
29
|
-
result.returncode == 0,
|
30
|
-
result.stdout,
|
31
|
-
result.stderr if result.returncode != 0 else None
|
32
|
-
)
|
33
|
-
except subprocess.TimeoutExpired:
|
34
|
-
return False, "", "Test command timed out after 5 minutes"
|
35
|
-
except Exception as e:
|
36
|
-
return False, "", f"Error running test: {str(e)}"
|
37
|
-
|
38
|
-
def format_context_preview(lines: List[str], max_lines: int = 5) -> str:
|
39
|
-
"""Format context lines for display, limiting the number of lines shown"""
|
40
|
-
if not lines:
|
41
|
-
return "No context lines"
|
42
|
-
preview = lines[:max_lines]
|
43
|
-
suffix = f"\n... and {len(lines) - max_lines} more lines" if len(lines) > max_lines else ""
|
44
|
-
return "\n".join(preview) + suffix
|
45
|
-
|
46
|
-
def format_whitespace_debug(text: str) -> str:
|
47
|
-
"""Format text with visible whitespace markers"""
|
48
|
-
return text.replace(' ', '·').replace('\t', '→').replace('\n', '↵\n')
|
49
|
-
|
50
|
-
def parse_and_apply_changes_sequence(input_text: str, changes_text: str) -> str:
|
51
|
-
"""
|
52
|
-
Parse and apply changes to text:
|
53
|
-
= Find and keep line (preserving whitespace)
|
54
|
-
< Remove line at current position
|
55
|
-
> Add line at current position
|
56
|
-
"""
|
57
|
-
def find_initial_start(text_lines, sequence):
|
58
|
-
for i in range(len(text_lines) - len(sequence) + 1):
|
59
|
-
matches = True
|
60
|
-
for j, seq_line in enumerate(sequence):
|
61
|
-
if text_lines[i + j] != seq_line:
|
62
|
-
matches = False
|
63
|
-
break
|
64
|
-
if matches:
|
65
|
-
return i
|
66
|
-
|
67
|
-
if config.debug and i < 20: # Show first 20 attempted matches
|
68
|
-
console = Console()
|
69
|
-
console.print(f"\n[cyan]Checking position {i}:[/cyan]")
|
70
|
-
for j, seq_line in enumerate(sequence):
|
71
|
-
if i + j < len(text_lines):
|
72
|
-
match_status = "=" if text_lines[i + j] == seq_line else "≠"
|
73
|
-
console.print(f" {match_status} Expected: '{seq_line}'")
|
74
|
-
console.print(f" Found: '{text_lines[i + j]}'")
|
75
|
-
return -1
|
76
|
-
|
77
|
-
input_lines = input_text.splitlines()
|
78
|
-
changes = changes_text.splitlines()
|
79
|
-
|
80
|
-
sequence = []
|
81
|
-
# Find the context sequence in the input text
|
82
|
-
for line in changes:
|
83
|
-
if line[0] == '=':
|
84
|
-
sequence.append(line[1:])
|
85
|
-
else:
|
86
|
-
break
|
87
|
-
|
88
|
-
start_pos = find_initial_start(input_lines, sequence)
|
89
|
-
|
90
|
-
if start_pos == -1:
|
91
|
-
if config.debug:
|
92
|
-
console = Console()
|
93
|
-
console.print("\n[red]Failed to find context sequence match in file:[/red]")
|
94
|
-
console.print("[yellow]File content:[/yellow]")
|
95
|
-
for i, line in enumerate(input_lines):
|
96
|
-
console.print(f" {i+1:2d} | '{line}'")
|
97
|
-
return input_text
|
98
|
-
|
99
|
-
if config.debug:
|
100
|
-
console = Console()
|
101
|
-
console.print(f"\n[green]Found context match at line {start_pos + 1}[/green]")
|
102
|
-
|
103
|
-
result_lines = input_lines[:start_pos]
|
104
|
-
i = start_pos
|
105
|
-
|
106
|
-
for change in changes:
|
107
|
-
if not change:
|
108
|
-
if config.debug:
|
109
|
-
console.print(f" Preserving empty line")
|
110
|
-
continue
|
111
|
-
|
112
|
-
prefix = change[0]
|
113
|
-
content = change[1:]
|
114
|
-
|
115
|
-
if prefix == '=':
|
116
|
-
if config.debug:
|
117
|
-
console.print(f" Keep: '{content}'")
|
118
|
-
result_lines.append(content)
|
119
|
-
i += 1
|
120
|
-
elif prefix == '<':
|
121
|
-
if config.debug:
|
122
|
-
console.print(f" Delete: '{content}'")
|
123
|
-
i += 1
|
124
|
-
elif prefix == '>':
|
125
|
-
if config.debug:
|
126
|
-
console.print(f" Add: '{content}'")
|
127
|
-
result_lines.append(content)
|
128
|
-
|
129
|
-
result_lines.extend(input_lines[i:])
|
130
|
-
|
131
|
-
if config.debug:
|
132
|
-
console.print("\n[yellow]Final result:[/yellow]")
|
133
|
-
for i, line in enumerate(result_lines):
|
134
|
-
console.print(f" {i+1:2d} | '{line}'")
|
135
|
-
|
136
|
-
return '\n'.join(result_lines)
|
137
|
-
|
138
|
-
def get_line_boundaries(text: str) -> List[Tuple[int, int, int, int]]:
|
139
|
-
"""Return list of (content_start, content_end, full_start, full_end) for each line.
|
140
|
-
content_start/end exclude leading/trailing whitespace
|
141
|
-
full_start/end include the whitespace and line endings"""
|
142
|
-
boundaries = []
|
143
|
-
start = 0
|
144
|
-
for line in text.splitlines(keepends=True):
|
145
|
-
content = line.strip()
|
146
|
-
if content:
|
147
|
-
content_start = start + len(line) - len(line.lstrip())
|
148
|
-
content_end = start + len(line.rstrip())
|
149
|
-
boundaries.append((content_start, content_end, start, start + len(line)))
|
150
|
-
else:
|
151
|
-
# Empty or whitespace-only lines
|
152
|
-
boundaries.append((start, start, start, start + len(line)))
|
153
|
-
start += len(line)
|
154
|
-
return boundaries
|
155
|
-
|
156
|
-
def normalize_content(text: str) -> Tuple[str, List[Tuple[int, int, int, int]]]:
|
157
|
-
"""Normalize text for searching while preserving position mapping.
|
158
|
-
Returns (normalized_text, line_boundaries)"""
|
159
|
-
# Replace Windows line endings
|
160
|
-
text = text.replace('\r\n', '\n')
|
161
|
-
text = text.replace('\r', '\n')
|
162
|
-
|
163
|
-
# Get line boundaries before normalization
|
164
|
-
boundaries = get_line_boundaries(text)
|
165
|
-
|
166
|
-
# Create normalized version with stripped lines
|
167
|
-
normalized = '\n'.join(line.strip() for line in text.splitlines())
|
168
|
-
|
169
|
-
return normalized, boundaries
|
170
|
-
|
171
|
-
def find_text_positions(text: str, search: str) -> List[Tuple[int, int]]:
|
172
|
-
"""Find all non-overlapping positions of search text in content,
|
173
|
-
comparing without leading/trailing whitespace but returning original positions."""
|
174
|
-
normalized_text, text_boundaries = normalize_content(text)
|
175
|
-
normalized_search, search_boundaries = normalize_content(search)
|
176
|
-
|
177
|
-
positions = []
|
178
|
-
start = 0
|
179
|
-
while True:
|
180
|
-
# Find next occurrence in normalized text
|
181
|
-
pos = normalized_text.find(normalized_search, start)
|
182
|
-
if pos == -1:
|
183
|
-
break
|
184
|
-
|
185
|
-
# Find the corresponding original text boundaries
|
186
|
-
search_lines = normalized_search.count('\n') + 1
|
187
|
-
|
188
|
-
# Get text line number at position
|
189
|
-
line_num = normalized_text.count('\n', 0, pos)
|
190
|
-
|
191
|
-
if line_num + search_lines <= len(text_boundaries):
|
192
|
-
# Get original start position from first line
|
193
|
-
orig_start = text_boundaries[line_num][2] # full_start
|
194
|
-
# Get original end position from last line
|
195
|
-
orig_end = text_boundaries[line_num + search_lines - 1][3] # full_end
|
196
|
-
|
197
|
-
positions.append((orig_start, orig_end))
|
198
|
-
|
199
|
-
start = pos + len(normalized_search)
|
200
|
-
|
201
|
-
return positions
|
202
|
-
|
203
|
-
def adjust_indentation(original: str, replacement: str) -> str:
|
204
|
-
"""Adjust replacement text indentation based on original text"""
|
205
|
-
if not original or not replacement:
|
206
|
-
return replacement
|
207
|
-
|
208
|
-
# Get first non-empty lines to compare indentation
|
209
|
-
orig_lines = original.splitlines()
|
210
|
-
repl_lines = replacement.splitlines()
|
211
|
-
|
212
|
-
orig_first = next((l for l in orig_lines if l.strip()), '')
|
213
|
-
repl_first = next((l for l in repl_lines if l.strip()), '')
|
214
|
-
|
215
|
-
# Calculate indentation difference
|
216
|
-
orig_indent = len(orig_first) - len(orig_first.lstrip())
|
217
|
-
repl_indent = len(repl_first) - len(repl_first.lstrip())
|
218
|
-
indent_delta = orig_indent - repl_indent
|
219
|
-
|
220
|
-
if indent_delta == 0:
|
221
|
-
return replacement
|
222
|
-
|
223
|
-
# Adjust indentation for all lines
|
224
|
-
adjusted_lines = []
|
225
|
-
for line in repl_lines:
|
226
|
-
if not line.strip(): # Preserve empty lines
|
227
|
-
adjusted_lines.append(line)
|
228
|
-
continue
|
229
|
-
|
230
|
-
current_indent = len(line) - len(line.lstrip())
|
231
|
-
new_indent = max(0, current_indent + indent_delta)
|
232
|
-
adjusted_lines.append(' ' * new_indent + line.lstrip())
|
233
|
-
|
234
|
-
return '\n'.join(adjusted_lines)
|
235
|
-
|
236
|
-
def apply_single_change(filepath: Path, change: FileChange, workdir: Path, preview_dir: Path) -> Tuple[bool, Optional[str]]:
|
237
|
-
"""Apply a single file change"""
|
238
|
-
preview_path = preview_dir / filepath
|
239
|
-
preview_path.parent.mkdir(parents=True, exist_ok=True)
|
240
|
-
|
241
|
-
if config.debug:
|
242
|
-
console = Console()
|
243
|
-
console.print(f"\n[cyan]Processing change for {filepath}[/cyan]")
|
244
|
-
console.print(f"[dim]Change type: {'new file' if change.is_new_file else 'modification'}[/dim]")
|
245
|
-
|
246
|
-
if change.is_new_file:
|
247
|
-
if config.debug:
|
248
|
-
console.print("[cyan]Creating new file with content:[/cyan]")
|
249
|
-
console.print(Panel(change.content, title="New File Content"))
|
250
|
-
preview_path.write_text(change.content)
|
251
|
-
return True, None
|
252
|
-
|
253
|
-
orig_path = workdir / filepath
|
254
|
-
if not orig_path.exists():
|
255
|
-
return False, f"Cannot modify non-existent file {filepath}"
|
256
|
-
|
257
|
-
content = orig_path.read_text()
|
258
|
-
modified = content
|
259
|
-
|
260
|
-
for search, replace, description in change.search_blocks:
|
261
|
-
if config.debug:
|
262
|
-
console.print(f"\n[cyan]Processing search block:[/cyan] {description or 'no description'}")
|
263
|
-
console.print("[yellow]Search text:[/yellow]")
|
264
|
-
console.print(Panel(format_whitespace_debug(search)))
|
265
|
-
if replace is not None:
|
266
|
-
console.print("[yellow]Replace with:[/yellow]")
|
267
|
-
console.print(Panel(format_whitespace_debug(replace)))
|
268
|
-
else:
|
269
|
-
console.print("[yellow]Action:[/yellow] Delete text")
|
270
|
-
|
271
|
-
positions = find_text_positions(modified, search)
|
272
|
-
|
273
|
-
if config.debug:
|
274
|
-
console.print(f"[cyan]Found {len(positions)} matches[/cyan]")
|
275
|
-
|
276
|
-
if not positions:
|
277
|
-
error_context = f" ({description})" if description else ""
|
278
|
-
debug_search = format_whitespace_debug(search)
|
279
|
-
debug_content = format_whitespace_debug(modified)
|
280
|
-
error_msg = (
|
281
|
-
f"Could not find search text in {filepath}{error_context}:\n\n"
|
282
|
-
f"[yellow]Search text (with whitespace markers):[/yellow]\n"
|
283
|
-
f"{debug_search}\n\n"
|
284
|
-
f"[yellow]File content (with whitespace markers):[/yellow]\n"
|
285
|
-
f"{debug_content}"
|
286
|
-
)
|
287
|
-
return False, error_msg
|
288
|
-
|
289
|
-
# Apply replacements from end to start to maintain position validity
|
290
|
-
for start, end in reversed(positions):
|
291
|
-
if config.debug:
|
292
|
-
console.print(f"\n[cyan]Replacing text at positions {start}-{end}:[/cyan]")
|
293
|
-
console.print("[yellow]Original segment:[/yellow]")
|
294
|
-
console.print(Panel(format_whitespace_debug(modified[start:end])))
|
295
|
-
if replace is not None:
|
296
|
-
console.print("[yellow]Replacing with:[/yellow]")
|
297
|
-
console.print(Panel(format_whitespace_debug(replace)))
|
298
|
-
|
299
|
-
# Adjust replacement text indentation
|
300
|
-
original_segment = modified[start:end]
|
301
|
-
adjusted_replace = adjust_indentation(original_segment, replace) if replace else ""
|
302
|
-
|
303
|
-
if config.debug and replace:
|
304
|
-
console.print("[yellow]Adjusted replacement:[/yellow]")
|
305
|
-
console.print(Panel(format_whitespace_debug(adjusted_replace)))
|
306
|
-
|
307
|
-
modified = modified[:start] + adjusted_replace + modified[end:]
|
308
|
-
|
309
|
-
if modified == content:
|
310
|
-
if config.debug:
|
311
|
-
console.print("\n[yellow]No changes were applied to the file[/yellow]")
|
312
|
-
return False, "No changes were applied"
|
313
|
-
|
314
|
-
if config.debug:
|
315
|
-
console.print("\n[green]Changes applied successfully[/green]")
|
316
|
-
|
317
|
-
preview_path.write_text(modified)
|
318
|
-
return True, None
|
319
|
-
|
320
|
-
def preview_and_apply_changes(changes: Dict[Path, FileChange], workdir: Path, test_cmd: str = None) -> bool:
|
321
|
-
"""Preview changes and apply if confirmed"""
|
322
|
-
console = Console()
|
323
|
-
|
324
|
-
if not changes:
|
325
|
-
console.print("\n[yellow]No changes were found to apply[/yellow]")
|
326
|
-
return False
|
327
|
-
|
328
|
-
# Show change preview before applying
|
329
|
-
preview_all_changes(console, changes)
|
330
|
-
|
331
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
332
|
-
preview_dir = Path(temp_dir)
|
333
|
-
console.print("\n[blue]Creating preview in temporary directory...[/blue]")
|
334
|
-
|
335
|
-
# Create backup directory
|
336
|
-
backup_dir = workdir / '.janito' / 'backups' / datetime.now().strftime('%Y%m%d_%H%M%S')
|
337
|
-
backup_dir.parent.mkdir(parents=True, exist_ok=True)
|
338
|
-
|
339
|
-
# Copy existing files to preview directory
|
340
|
-
if workdir.exists():
|
341
|
-
# Create backup before applying changes
|
342
|
-
if config.verbose:
|
343
|
-
console.print(f"[blue]Creating backup at:[/blue] {backup_dir}")
|
344
|
-
shutil.copytree(workdir, backup_dir, ignore=shutil.ignore_patterns('.janito'))
|
345
|
-
# Copy to preview directory
|
346
|
-
shutil.copytree(workdir, preview_dir, dirs_exist_ok=True)
|
347
|
-
|
348
|
-
# Create restore script
|
349
|
-
restore_script = workdir / '.janito' / 'restore.sh'
|
350
|
-
restore_script.parent.mkdir(parents=True, exist_ok=True)
|
351
|
-
script_content = f"""#!/bin/bash
|
352
|
-
# Restore script generated by Janito
|
353
|
-
# Restores files from backup created at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
354
|
-
|
355
|
-
# Exit on error
|
356
|
-
set -e
|
357
|
-
|
358
|
-
# Check if backup directory exists
|
359
|
-
if [ ! -d "{backup_dir}" ]; then
|
360
|
-
echo "Error: Backup directory not found at {backup_dir}"
|
361
|
-
exit 1
|
362
|
-
fi
|
363
|
-
|
364
|
-
# Restore files from backup
|
365
|
-
echo "Restoring files from backup..."
|
366
|
-
cp -r "{backup_dir}"/* "{workdir}/"
|
367
|
-
|
368
|
-
echo "Files restored successfully from {backup_dir}"
|
369
|
-
"""
|
370
|
-
restore_script.write_text(script_content)
|
371
|
-
restore_script.chmod(0o755) # Make script executable
|
372
|
-
|
373
|
-
if config.verbose:
|
374
|
-
console.print(f"[blue]Created restore script at:[/blue] {restore_script}")
|
375
|
-
|
376
|
-
|
377
|
-
# Apply changes to preview directory
|
378
|
-
any_errors = False
|
379
|
-
for filepath, change in changes.items():
|
380
|
-
console.print(f"[dim]Previewing changes for {filepath}...[/dim]")
|
381
|
-
success, error = apply_single_change(filepath, change, workdir, preview_dir)
|
382
|
-
if not success:
|
383
|
-
if "file already exists" in str(error):
|
384
|
-
console.print(f"\n[red]Error: Cannot create {filepath}[/red]")
|
385
|
-
console.print("[red]File already exists and overwriting is not allowed.[/red]")
|
386
|
-
else:
|
387
|
-
console.print(f"\n[red]Error previewing changes for {filepath}:[/red]")
|
388
|
-
console.print(f"[red]{error}[/red]")
|
389
|
-
any_errors = True
|
390
|
-
continue
|
391
|
-
|
392
|
-
if any_errors:
|
393
|
-
console.print("\n[red]Some changes could not be previewed. Aborting.[/red]")
|
394
|
-
return False
|
395
|
-
|
396
|
-
# Validate Python syntax for all modified Python files
|
397
|
-
python_files = [f for f in changes.keys() if f.suffix == '.py']
|
398
|
-
for filepath in python_files:
|
399
|
-
preview_path = preview_dir / filepath
|
400
|
-
is_valid, error_msg = validate_python_syntax(preview_path.read_text(), preview_path)
|
401
|
-
if not is_valid:
|
402
|
-
console.print(f"\n[red]Python syntax validation failed for {filepath}:[/red]")
|
403
|
-
console.print(f"[red]{error_msg}[/red]")
|
404
|
-
return False
|
405
|
-
|
406
|
-
# Run tests if specified
|
407
|
-
if test_cmd:
|
408
|
-
console.print(f"\n[cyan]Testing changes in preview directory:[/cyan] {test_cmd}")
|
409
|
-
success, output, error = run_test_command(preview_dir, test_cmd)
|
410
|
-
|
411
|
-
if output:
|
412
|
-
console.print("\n[bold]Test Output:[/bold]")
|
413
|
-
console.print(Panel(output, box=box.ROUNDED))
|
414
|
-
|
415
|
-
if not success:
|
416
|
-
console.print("\n[red bold]Tests failed in preview. Changes will not be applied.[/red bold]")
|
417
|
-
if error:
|
418
|
-
console.print(Panel(error, title="Error", border_style="red"))
|
419
|
-
return False
|
420
|
-
|
421
|
-
# Final confirmation to apply to working directory
|
422
|
-
if not Confirm.ask("\n[cyan bold]Apply previewed changes to working directory?[/cyan bold]"):
|
423
|
-
console.print("\n[yellow]Changes were only previewed, not applied to working directory[/yellow]")
|
424
|
-
return False
|
425
|
-
|
426
|
-
# Copy changes to actual files
|
427
|
-
console.print("\n[blue]Applying changes to working directory...[/blue]")
|
428
|
-
for filepath, _ in changes.items():
|
429
|
-
console.print(f"[dim]Applying changes to {filepath}...[/dim]")
|
430
|
-
preview_path = preview_dir / filepath
|
431
|
-
target_path = workdir / filepath
|
432
|
-
target_path.parent.mkdir(parents=True, exist_ok=True)
|
433
|
-
shutil.copy2(preview_path, target_path)
|
434
|
-
|
435
|
-
console.print("\n[green]Changes successfully applied to working directory![/green]")
|
436
|
-
return True
|