janito 0.1.0__tar.gz
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-0.1.0/LICENSE +21 -0
- janito-0.1.0/PKG-INFO +106 -0
- janito-0.1.0/README.md +84 -0
- janito-0.1.0/janito/__init__.py +6 -0
- janito-0.1.0/janito/__main__.py +9 -0
- janito-0.1.0/janito/change.py +382 -0
- janito-0.1.0/janito/claude.py +112 -0
- janito-0.1.0/janito/commands.py +377 -0
- janito-0.1.0/janito/console.py +354 -0
- janito-0.1.0/janito/janito.py +354 -0
- janito-0.1.0/janito/prompts.py +181 -0
- janito-0.1.0/janito/watcher.py +82 -0
- janito-0.1.0/janito/workspace.py +169 -0
- janito-0.1.0/janito/xmlchangeparser.py +202 -0
- janito-0.1.0/janito.egg-info/PKG-INFO +106 -0
- janito-0.1.0/janito.egg-info/SOURCES.txt +23 -0
- janito-0.1.0/janito.egg-info/dependency_links.txt +1 -0
- janito-0.1.0/janito.egg-info/requires.txt +6 -0
- janito-0.1.0/janito.egg-info/top_level.txt +2 -0
- janito-0.1.0/pyproject.toml +28 -0
- janito-0.1.0/setup.cfg +4 -0
- janito-0.1.0/setup.py +38 -0
- janito-0.1.0/tests/__init__.py +4 -0
- janito-0.1.0/tests/conftest.py +9 -0
- janito-0.1.0/tests/test_change.py +393 -0
janito-0.1.0/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) [year] [fullname]
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
janito-0.1.0/PKG-INFO
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: janito
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: Language-Driven Software Development Assistant powered by Claude AI
|
5
|
+
Home-page: https://github.com/joaompinto/janito
|
6
|
+
Author: João M. Pinto
|
7
|
+
Author-email: "João M. Pinto" <lamego.pinto@gmail.com>
|
8
|
+
License: MIT
|
9
|
+
Project-URL: Homepage, https://github.com/joaompinto/janito
|
10
|
+
Project-URL: Documentation, https://github.com/joaompinto/janito#readme
|
11
|
+
Project-URL: Repository, https://github.com/joaompinto/janito.git
|
12
|
+
Keywords: ai,development,claude,assistant,code
|
13
|
+
Requires-Python: >=3.8
|
14
|
+
Description-Content-Type: text/markdown
|
15
|
+
License-File: LICENSE
|
16
|
+
Requires-Dist: anthropic
|
17
|
+
Requires-Dist: prompt_toolkit
|
18
|
+
Requires-Dist: rich
|
19
|
+
Requires-Dist: typer
|
20
|
+
Requires-Dist: watchdog
|
21
|
+
Requires-Dist: pytest
|
22
|
+
|
23
|
+
|
24
|
+
Janito is an open source Language-Driven Software Development Assistant powered by Claude AI. It helps developers understand, modify, and improve their Python code through natural language interaction.
|
25
|
+
|
26
|
+
## Features
|
27
|
+
|
28
|
+
- Natural language code interactions
|
29
|
+
- File system monitoring and auto-restart
|
30
|
+
- Interactive command-line interface
|
31
|
+
- Workspace management and visualization
|
32
|
+
- History and session management
|
33
|
+
- Debug mode for troubleshooting
|
34
|
+
- Syntax error detection and fixing
|
35
|
+
- Python file execution
|
36
|
+
- File editing with system editor
|
37
|
+
|
38
|
+
## Installation
|
39
|
+
|
40
|
+
```bash
|
41
|
+
# Install package
|
42
|
+
pip install janito
|
43
|
+
```
|
44
|
+
|
45
|
+
## Usage
|
46
|
+
|
47
|
+
Start Janito in your project directory:
|
48
|
+
|
49
|
+
```bash
|
50
|
+
python -m janito
|
51
|
+
```
|
52
|
+
|
53
|
+
Or launch with options:
|
54
|
+
|
55
|
+
```bash
|
56
|
+
python -m janito --debug # Enable debug mode
|
57
|
+
python -m janito --no-watch # Disable file watching
|
58
|
+
```
|
59
|
+
|
60
|
+
### Commands
|
61
|
+
|
62
|
+
- `.help` - Show help information
|
63
|
+
- `.exit` - Exit the console
|
64
|
+
- `.clear` - Clear console output
|
65
|
+
- `.debug` - Toggle debug mode
|
66
|
+
- `.workspace` - Show workspace structure
|
67
|
+
- `.last` - Show last Claude response
|
68
|
+
- `.show <file>` - Show file content with syntax highlighting
|
69
|
+
- `.check` - Check workspace Python files for syntax errors
|
70
|
+
- `.p <file>` - Run a Python file
|
71
|
+
- `.python <file>` - Run a Python file (alias for .p)
|
72
|
+
- `.edit <file>` - Open file in system editor
|
73
|
+
|
74
|
+
### Input Formats
|
75
|
+
|
76
|
+
- `!request` - Request file changes (e.g. '!add logging to utils.py')
|
77
|
+
- `request?` - Get information and analysis without changes
|
78
|
+
- `request` - General discussion and queries
|
79
|
+
- `$command` - Execute shell commands
|
80
|
+
|
81
|
+
## Configuration
|
82
|
+
|
83
|
+
Requires an Anthropic API key set via environment variable:
|
84
|
+
|
85
|
+
```bash
|
86
|
+
export ANTHROPIC_API_KEY='your_api_key_here'
|
87
|
+
```
|
88
|
+
|
89
|
+
## Development
|
90
|
+
|
91
|
+
The package consists of several modules:
|
92
|
+
|
93
|
+
- `janito.py` - Core functionality and CLI interface
|
94
|
+
- `change.py` - File modification and change tracking
|
95
|
+
- `claude.py` - Claude API interaction
|
96
|
+
- `console.py` - Interactive console and command handling
|
97
|
+
- `commands.py` - Command implementations
|
98
|
+
- `prompts.py` - Prompt templates and builders
|
99
|
+
- `watcher.py` - File system monitoring
|
100
|
+
- `workspace.py` - Workspace analysis and management
|
101
|
+
- `xmlchangeparser.py` - XML parser for file changes
|
102
|
+
- `watcher.py` - File system monitoring
|
103
|
+
|
104
|
+
## License
|
105
|
+
|
106
|
+
MIT License
|
janito-0.1.0/README.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
|
2
|
+
Janito is an open source Language-Driven Software Development Assistant powered by Claude AI. It helps developers understand, modify, and improve their Python code through natural language interaction.
|
3
|
+
|
4
|
+
## Features
|
5
|
+
|
6
|
+
- Natural language code interactions
|
7
|
+
- File system monitoring and auto-restart
|
8
|
+
- Interactive command-line interface
|
9
|
+
- Workspace management and visualization
|
10
|
+
- History and session management
|
11
|
+
- Debug mode for troubleshooting
|
12
|
+
- Syntax error detection and fixing
|
13
|
+
- Python file execution
|
14
|
+
- File editing with system editor
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
```bash
|
19
|
+
# Install package
|
20
|
+
pip install janito
|
21
|
+
```
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
Start Janito in your project directory:
|
26
|
+
|
27
|
+
```bash
|
28
|
+
python -m janito
|
29
|
+
```
|
30
|
+
|
31
|
+
Or launch with options:
|
32
|
+
|
33
|
+
```bash
|
34
|
+
python -m janito --debug # Enable debug mode
|
35
|
+
python -m janito --no-watch # Disable file watching
|
36
|
+
```
|
37
|
+
|
38
|
+
### Commands
|
39
|
+
|
40
|
+
- `.help` - Show help information
|
41
|
+
- `.exit` - Exit the console
|
42
|
+
- `.clear` - Clear console output
|
43
|
+
- `.debug` - Toggle debug mode
|
44
|
+
- `.workspace` - Show workspace structure
|
45
|
+
- `.last` - Show last Claude response
|
46
|
+
- `.show <file>` - Show file content with syntax highlighting
|
47
|
+
- `.check` - Check workspace Python files for syntax errors
|
48
|
+
- `.p <file>` - Run a Python file
|
49
|
+
- `.python <file>` - Run a Python file (alias for .p)
|
50
|
+
- `.edit <file>` - Open file in system editor
|
51
|
+
|
52
|
+
### Input Formats
|
53
|
+
|
54
|
+
- `!request` - Request file changes (e.g. '!add logging to utils.py')
|
55
|
+
- `request?` - Get information and analysis without changes
|
56
|
+
- `request` - General discussion and queries
|
57
|
+
- `$command` - Execute shell commands
|
58
|
+
|
59
|
+
## Configuration
|
60
|
+
|
61
|
+
Requires an Anthropic API key set via environment variable:
|
62
|
+
|
63
|
+
```bash
|
64
|
+
export ANTHROPIC_API_KEY='your_api_key_here'
|
65
|
+
```
|
66
|
+
|
67
|
+
## Development
|
68
|
+
|
69
|
+
The package consists of several modules:
|
70
|
+
|
71
|
+
- `janito.py` - Core functionality and CLI interface
|
72
|
+
- `change.py` - File modification and change tracking
|
73
|
+
- `claude.py` - Claude API interaction
|
74
|
+
- `console.py` - Interactive console and command handling
|
75
|
+
- `commands.py` - Command implementations
|
76
|
+
- `prompts.py` - Prompt templates and builders
|
77
|
+
- `watcher.py` - File system monitoring
|
78
|
+
- `workspace.py` - Workspace analysis and management
|
79
|
+
- `xmlchangeparser.py` - XML parser for file changes
|
80
|
+
- `watcher.py` - File system monitoring
|
81
|
+
|
82
|
+
## License
|
83
|
+
|
84
|
+
MIT License
|
@@ -0,0 +1,382 @@
|
|
1
|
+
from typing import Optional, List, Set
|
2
|
+
import re
|
3
|
+
from pathlib import Path
|
4
|
+
import ast
|
5
|
+
import shutil
|
6
|
+
from rich.syntax import Syntax
|
7
|
+
from rich.console import Console
|
8
|
+
from rich.markdown import Markdown
|
9
|
+
import tempfile
|
10
|
+
import os # Add this import
|
11
|
+
from janito.workspace import Workspace
|
12
|
+
from janito.xmlchangeparser import XMLChangeParser, XMLChange
|
13
|
+
|
14
|
+
class FileChangeHandler:
|
15
|
+
def __init__(self, interactive=True):
|
16
|
+
self.preview_dir = Path(tempfile.mkdtemp(prefix='janito_preview_'))
|
17
|
+
self.console = Console()
|
18
|
+
self.workspace = Workspace()
|
19
|
+
self.xml_parser = XMLChangeParser()
|
20
|
+
self.interactive = interactive
|
21
|
+
|
22
|
+
# Remove generate_changes_prompt method as it's not being used
|
23
|
+
|
24
|
+
# Remove _parse_xml_response method as it's replaced by xml_parser
|
25
|
+
|
26
|
+
def test_parse_empty_block(self) -> bool:
|
27
|
+
"""Test parsing of XML with empty content blocks"""
|
28
|
+
test_xml = '''<fileChanges>
|
29
|
+
<change path="hello.py" operation="create">
|
30
|
+
<block description="Create new file hello.py">
|
31
|
+
<oldContent></oldContent>
|
32
|
+
<newContent></newContent>
|
33
|
+
</block>
|
34
|
+
</change>
|
35
|
+
</fileChanges>'''
|
36
|
+
|
37
|
+
changes = self.xml_parser.parse_response(test_xml)
|
38
|
+
if not changes:
|
39
|
+
self.console.print("[red]Error: No changes parsed[/]")
|
40
|
+
return False
|
41
|
+
|
42
|
+
change = changes[0]
|
43
|
+
if (change.path.name != "hello.py" or
|
44
|
+
change.operation != "create" or
|
45
|
+
not change.blocks or
|
46
|
+
change.blocks[0].description != "Create new file hello.py"):
|
47
|
+
self.console.print("[red]Error: Parsed change does not match expected structure[/]")
|
48
|
+
return False
|
49
|
+
|
50
|
+
block = change.blocks[0]
|
51
|
+
if block.old_content != [] or block.new_content != []:
|
52
|
+
self.console.print("[red]Error: Content lists should be empty[/]")
|
53
|
+
return False
|
54
|
+
|
55
|
+
self.console.print("[green]Empty block parsing test passed[/]")
|
56
|
+
return True
|
57
|
+
|
58
|
+
def _validate_syntax(self, filepath: Path) -> tuple[Optional[SyntaxError], bool]:
|
59
|
+
"""Validate file syntax
|
60
|
+
Returns (error, supported):
|
61
|
+
- (None, True) -> valid syntax
|
62
|
+
- (SyntaxError, True) -> invalid syntax
|
63
|
+
- (None, False) -> unsupported file type
|
64
|
+
"""
|
65
|
+
# Add more file types as needed
|
66
|
+
SUPPORTED_TYPES = {
|
67
|
+
'.py': self._validate_python_syntax,
|
68
|
+
}
|
69
|
+
|
70
|
+
validator = SUPPORTED_TYPES.get(filepath.suffix)
|
71
|
+
if not validator:
|
72
|
+
return None, False
|
73
|
+
|
74
|
+
try:
|
75
|
+
error = validator(filepath)
|
76
|
+
return error, True
|
77
|
+
except Exception as e:
|
78
|
+
return SyntaxError(str(e)), True
|
79
|
+
|
80
|
+
def _validate_python_syntax(self, filepath: Path) -> Optional[SyntaxError]:
|
81
|
+
"""Validate Python syntax"""
|
82
|
+
try:
|
83
|
+
with open(filepath) as f:
|
84
|
+
ast.parse(f.read())
|
85
|
+
return None
|
86
|
+
except SyntaxError as e:
|
87
|
+
return e
|
88
|
+
|
89
|
+
def _apply_indentation(self, new_content: List[str], base_indent: int, first_line_indent: Optional[int] = None) -> List[str]:
|
90
|
+
"""Apply consistent indentation to new content
|
91
|
+
Args:
|
92
|
+
new_content: List of lines to indent
|
93
|
+
base_indent: Base indentation level to apply
|
94
|
+
first_line_indent: Optional indentation of first line in original block for relative indenting
|
95
|
+
Returns:
|
96
|
+
List of indented lines
|
97
|
+
"""
|
98
|
+
if not new_content:
|
99
|
+
return []
|
100
|
+
|
101
|
+
indented_content = []
|
102
|
+
for i, line in enumerate(new_content):
|
103
|
+
if not line.strip():
|
104
|
+
indented_content.append('')
|
105
|
+
continue
|
106
|
+
|
107
|
+
# For first non-empty line, use base indentation
|
108
|
+
if not indented_content or all(not l.strip() for l in indented_content):
|
109
|
+
curr_indent = base_indent
|
110
|
+
else:
|
111
|
+
# Calculate relative indentation from first line
|
112
|
+
if first_line_indent is None:
|
113
|
+
first_line_indent = len(new_content[0]) - len(new_content[0].lstrip())
|
114
|
+
# Maintain relative indentation from first line
|
115
|
+
curr_indent = base_indent + (len(line) - len(line.lstrip()) - first_line_indent)
|
116
|
+
indented_content.append(' ' * max(0, curr_indent) + line.lstrip())
|
117
|
+
|
118
|
+
return indented_content
|
119
|
+
|
120
|
+
def _create_preview_files(self, changes: List[XMLChange]) -> dict[Path, Path]:
|
121
|
+
"""Create preview files for all changes"""
|
122
|
+
preview_files = {}
|
123
|
+
|
124
|
+
for change in changes:
|
125
|
+
preview_path = self.preview_dir / change.path.name
|
126
|
+
|
127
|
+
if change.operation == 'create':
|
128
|
+
# For new files, use direct content or block content
|
129
|
+
content = change.content
|
130
|
+
if not content.strip() and change.blocks:
|
131
|
+
content = "\n".join(change.blocks[0].new_content)
|
132
|
+
preview_path.write_text(content)
|
133
|
+
|
134
|
+
elif change.operation == 'modify' and change.path.exists():
|
135
|
+
original_text = change.path.read_text()
|
136
|
+
original_content = original_text.splitlines()
|
137
|
+
modified_content = original_content.copy()
|
138
|
+
|
139
|
+
for block in change.blocks:
|
140
|
+
if not block.old_content or (len(block.old_content) == 1 and not block.old_content[0].strip()):
|
141
|
+
if block.new_content:
|
142
|
+
if modified_content and not original_text.endswith('\n'):
|
143
|
+
modified_content[-1] = modified_content[-1] + '\n'
|
144
|
+
# Get the last line's indentation for appends
|
145
|
+
base_indent = len(modified_content[-1]) - len(modified_content[-1].lstrip()) if modified_content else 0
|
146
|
+
indented_content = self._apply_indentation(block.new_content, base_indent)
|
147
|
+
modified_content.extend(indented_content)
|
148
|
+
else:
|
149
|
+
result = self._find_block_start(modified_content, block.old_content, preview_path)
|
150
|
+
if result is None:
|
151
|
+
continue
|
152
|
+
start_idx, base_indent = result
|
153
|
+
|
154
|
+
# For single-line deletions/replacements
|
155
|
+
if len(block.old_content) == 1:
|
156
|
+
if block.new_content:
|
157
|
+
# Replace single line
|
158
|
+
indented_content = self._apply_indentation(block.new_content, base_indent)
|
159
|
+
modified_content[start_idx:start_idx + 1] = indented_content
|
160
|
+
else:
|
161
|
+
# Delete single line
|
162
|
+
del modified_content[start_idx]
|
163
|
+
else:
|
164
|
+
# Multi-line block handling
|
165
|
+
end_idx = start_idx + len([l for l in block.old_content if l.strip()])
|
166
|
+
if block.new_content:
|
167
|
+
indented_content = self._apply_indentation(block.new_content, base_indent)
|
168
|
+
modified_content[start_idx:end_idx] = indented_content
|
169
|
+
else:
|
170
|
+
del modified_content[start_idx:end_idx]
|
171
|
+
|
172
|
+
preview_path.write_text('\n'.join(modified_content))
|
173
|
+
|
174
|
+
preview_files[change.path] = preview_path
|
175
|
+
|
176
|
+
return preview_files
|
177
|
+
|
178
|
+
def _preview_changes(self, changes: List[XMLChange], raw_response: str = None) -> tuple[bool, bool]:
|
179
|
+
"""Show preview of all changes and ask for confirmation
|
180
|
+
Returns: (success, has_syntax_errors)"""
|
181
|
+
# Create preview files
|
182
|
+
preview_files = self._create_preview_files(changes)
|
183
|
+
|
184
|
+
# Validate syntax for all preview files
|
185
|
+
validation_status = {}
|
186
|
+
has_syntax_errors = False
|
187
|
+
for orig_path, preview_path in preview_files.items():
|
188
|
+
error, supported = self._validate_syntax(preview_path)
|
189
|
+
if not supported:
|
190
|
+
validation_status[orig_path] = "[yellow]? Syntax check not supported[/]"
|
191
|
+
elif error:
|
192
|
+
validation_status[orig_path] = f"[red]✗ {str(error)}[/]"
|
193
|
+
has_syntax_errors = True
|
194
|
+
else:
|
195
|
+
validation_status[orig_path] = "[green]✓ Valid syntax[/]"
|
196
|
+
|
197
|
+
self.console.print("\n[cyan]Preview of changes to be applied:[/]")
|
198
|
+
self.console.print("=" * 80)
|
199
|
+
|
200
|
+
for change in changes:
|
201
|
+
if change.operation == 'create':
|
202
|
+
preview_content = preview_files[change.path].read_text()
|
203
|
+
status = validation_status.get(change.path, '')
|
204
|
+
self.console.print(f"\n[green]CREATE NEW FILE: {change.path}[/] {status}")
|
205
|
+
syntax = Syntax(preview_content, "python", theme="monokai")
|
206
|
+
self.console.print(syntax)
|
207
|
+
continue
|
208
|
+
|
209
|
+
if not change.path.exists():
|
210
|
+
self.console.print(f"\n[red]SKIP: File not found - {change.path}[/]")
|
211
|
+
continue
|
212
|
+
|
213
|
+
status = validation_status.get(change.path, '')
|
214
|
+
self.console.print(f"\n[yellow]MODIFY FILE: {change.path}[/] {status}")
|
215
|
+
for block in change.blocks:
|
216
|
+
self.console.print(f"\n[cyan]{block.description}[/]")
|
217
|
+
|
218
|
+
if not block.old_content or (len(block.old_content) == 1 and not block.old_content[0].strip()):
|
219
|
+
if block.new_content:
|
220
|
+
# Get the last line's indentation
|
221
|
+
file_lines = change.path.read_text().splitlines() if change.path.exists() else []
|
222
|
+
base_indent = len(file_lines[-1]) - len(file_lines[-1].lstrip()) if file_lines else 0
|
223
|
+
indented_content = self._apply_indentation(block.new_content, base_indent)
|
224
|
+
self.console.print("[green]Append to end of file:[/]")
|
225
|
+
syntax = Syntax("\n".join(indented_content), "python", theme="monokai")
|
226
|
+
self.console.print(syntax)
|
227
|
+
else:
|
228
|
+
self.console.print("[black on red]Remove:[/]")
|
229
|
+
syntax = Syntax("\n".join(block.old_content), "python", theme="monokai")
|
230
|
+
self.console.print(syntax)
|
231
|
+
if block.new_content: # Only show replacement if there is new content
|
232
|
+
self.console.print("\n[black on green]Replace with:[/]")
|
233
|
+
syntax = Syntax("\n".join(block.new_content), "python", theme="monokai")
|
234
|
+
self.console.print(syntax)
|
235
|
+
else:
|
236
|
+
self.console.print("[black on yellow](Content will be deleted)[/]")
|
237
|
+
|
238
|
+
self.console.print("\n" + "=" * 80)
|
239
|
+
|
240
|
+
if has_syntax_errors:
|
241
|
+
self.console.print("\n[red]⚠️ Error: Cannot apply changes - syntax errors detected![/]")
|
242
|
+
return False, has_syntax_errors
|
243
|
+
|
244
|
+
# Only ask for confirmation if interactive and no syntax errors
|
245
|
+
if self.interactive:
|
246
|
+
try:
|
247
|
+
response = input("\nApply these changes? [y/N] ").lower().strip()
|
248
|
+
return response == 'y', has_syntax_errors
|
249
|
+
except EOFError:
|
250
|
+
self.console.print("\n[yellow]Changes cancelled (Ctrl-D)[/]")
|
251
|
+
return False, has_syntax_errors
|
252
|
+
return True, has_syntax_errors
|
253
|
+
|
254
|
+
def process_changes(self, response: str) -> bool:
|
255
|
+
try:
|
256
|
+
if not (match := re.search(r'<fileChanges>(.*?)</fileChanges>', response, re.DOTALL)):
|
257
|
+
self.console.print("[red]No file changes found in response[/]")
|
258
|
+
self.console.print("\nResponse content:")
|
259
|
+
self.console.print(response)
|
260
|
+
return False
|
261
|
+
|
262
|
+
xml_content = f"<fileChanges>{match.group(1)}</fileChanges>"
|
263
|
+
self.console.print("[cyan]Found change block, parsing...[/]")
|
264
|
+
|
265
|
+
changes = self.xml_parser.parse_response(xml_content)
|
266
|
+
if not changes:
|
267
|
+
self.console.print("[red]No valid changes found after parsing[/]")
|
268
|
+
return False
|
269
|
+
|
270
|
+
try:
|
271
|
+
# First phase: Create and validate all preview files
|
272
|
+
preview_result, has_syntax_errors = self._preview_changes(changes, raw_response=response)
|
273
|
+
if not preview_result:
|
274
|
+
if self.interactive and not has_syntax_errors:
|
275
|
+
self.console.print("[yellow]Changes cancelled by user[/]")
|
276
|
+
return False
|
277
|
+
|
278
|
+
# Second phase: Pre-validate all files can be written to
|
279
|
+
preview_files = self._create_preview_files(changes)
|
280
|
+
for change in changes:
|
281
|
+
preview_path = preview_files.get(change.path)
|
282
|
+
if not preview_path or not preview_path.exists():
|
283
|
+
self.console.print(f"[red]Preview file missing for {change.path}[/]")
|
284
|
+
return False
|
285
|
+
|
286
|
+
try:
|
287
|
+
# Validate write permissions and parent directory creation
|
288
|
+
change.path.parent.mkdir(parents=True, exist_ok=True)
|
289
|
+
# Test write permission without actually writing
|
290
|
+
if change.path.exists():
|
291
|
+
os.access(change.path, os.W_OK)
|
292
|
+
else:
|
293
|
+
change.path.parent.joinpath('test').touch()
|
294
|
+
change.path.parent.joinpath('test').unlink()
|
295
|
+
except (OSError, IOError) as e:
|
296
|
+
self.console.print(f"[red]Cannot write to {change.path}: {e}[/]")
|
297
|
+
return False
|
298
|
+
|
299
|
+
# Final phase: Apply all changes in a batch
|
300
|
+
self.console.print("\n[cyan]Applying changes...[/]")
|
301
|
+
try:
|
302
|
+
# Copy all files in a single transaction-like batch
|
303
|
+
for change in changes:
|
304
|
+
preview_path = preview_files[change.path]
|
305
|
+
shutil.copy2(preview_path, change.path)
|
306
|
+
self.console.print(f"[green]{'Created' if change.operation == 'create' else 'Updated'} file: {change.path}[/]")
|
307
|
+
|
308
|
+
self.console.print("\n[green]✓ All changes applied successfully[/]")
|
309
|
+
return True
|
310
|
+
|
311
|
+
except (OSError, IOError) as e:
|
312
|
+
self.console.print(f"[red]Error applying changes: {e}[/]")
|
313
|
+
return False
|
314
|
+
|
315
|
+
except KeyboardInterrupt:
|
316
|
+
self.console.print("[yellow]Changes cancelled by user (Ctrl-C)[/]")
|
317
|
+
return False
|
318
|
+
|
319
|
+
except EOFError:
|
320
|
+
self.console.print("\n[yellow]Changes cancelled (Ctrl-D)[/]")
|
321
|
+
return False
|
322
|
+
except KeyboardInterrupt:
|
323
|
+
self.console.print("\n[yellow]Changes cancelled (Ctrl-C)[/]")
|
324
|
+
return False
|
325
|
+
except Exception as e:
|
326
|
+
self.console.print(f"\n[red]Error applying changes: {e}[/]")
|
327
|
+
return False
|
328
|
+
|
329
|
+
def _find_block_start(self, content: List[str], old_content: List[str], filepath: Path = None) -> Optional[tuple[int, int]]:
|
330
|
+
"""Find the start of the indentation block containing old_content"""
|
331
|
+
try:
|
332
|
+
if not old_content:
|
333
|
+
return None
|
334
|
+
|
335
|
+
# Convert string content to lines if needed
|
336
|
+
lines = content if isinstance(content, list) else content.split('\n')
|
337
|
+
|
338
|
+
# For single-line content, do exact string matching
|
339
|
+
if len(old_content) == 1:
|
340
|
+
for i, line in enumerate(lines):
|
341
|
+
if line.strip() == old_content[0].strip():
|
342
|
+
return (i, len(line) - len(line.lstrip()))
|
343
|
+
self.console.print(f"[yellow]Warning: Line not found in {filepath.name if filepath else 'unknown file'}: {old_content[0]}[/]")
|
344
|
+
return None
|
345
|
+
|
346
|
+
# For multi-line blocks, use existing block matching logic
|
347
|
+
first_line = next((line for line in old_content if line.strip()), '')
|
348
|
+
target_indent = len(first_line) - len(first_line.lstrip())
|
349
|
+
|
350
|
+
# Search for the block
|
351
|
+
for i in range(len(lines) - len(old_content) + 1):
|
352
|
+
# Check if block matches at this position
|
353
|
+
matches = True
|
354
|
+
for j, old_line in enumerate(old_content):
|
355
|
+
if not old_line.strip(): # Skip empty lines
|
356
|
+
continue
|
357
|
+
if i + j >= len(lines):
|
358
|
+
matches = False
|
359
|
+
break
|
360
|
+
if lines[i + j].lstrip() != old_line.lstrip():
|
361
|
+
matches = False
|
362
|
+
break
|
363
|
+
if matches:
|
364
|
+
return (i, target_indent)
|
365
|
+
|
366
|
+
self.console.print(f"[yellow]Warning: Block not found in {filepath.name if filepath else 'unknown file'}[/]")
|
367
|
+
return None
|
368
|
+
|
369
|
+
except Exception as e:
|
370
|
+
self.console.print(f"[yellow]Error finding block in {filepath.name if filepath else 'unknown file'}: {e}[/]")
|
371
|
+
return None
|
372
|
+
|
373
|
+
def cleanup(self):
|
374
|
+
"""Clean up preview directory"""
|
375
|
+
try:
|
376
|
+
shutil.rmtree(self.preview_dir)
|
377
|
+
except (OSError, IOError) as e:
|
378
|
+
self.console.print(f"[yellow]Warning: Failed to clean up preview directory: {e}[/]")
|
379
|
+
|
380
|
+
def __del__(self):
|
381
|
+
"""Ensure cleanup on destruction"""
|
382
|
+
self.cleanup()
|