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
janito/fileparser.py
DELETED
@@ -1,334 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
from typing import Dict, Tuple, List, Optional, Union
|
3
|
-
from dataclasses import dataclass
|
4
|
-
import re
|
5
|
-
import ast
|
6
|
-
import sys
|
7
|
-
import os # Add this import
|
8
|
-
from rich.console import Console
|
9
|
-
from rich.panel import Panel
|
10
|
-
from janito.config import config
|
11
|
-
|
12
|
-
def validate_file_path(filepath: Path) -> Tuple[bool, str]:
|
13
|
-
"""
|
14
|
-
Validate that the file path exists and is readable
|
15
|
-
|
16
|
-
Args:
|
17
|
-
filepath: Path object to validate
|
18
|
-
|
19
|
-
Returns:
|
20
|
-
Tuple[bool, str]: (is_valid, error_message)
|
21
|
-
"""
|
22
|
-
if not filepath.exists():
|
23
|
-
return False, f"File does not exist: {filepath}"
|
24
|
-
if not os.access(filepath, os.R_OK):
|
25
|
-
return False, f"File is not readable: {filepath}"
|
26
|
-
return True, ""
|
27
|
-
|
28
|
-
def validate_file_content(content: str) -> Tuple[bool, str]:
|
29
|
-
"""
|
30
|
-
Validate that the content is not empty and contains valid text
|
31
|
-
|
32
|
-
Args:
|
33
|
-
content: String content to validate
|
34
|
-
|
35
|
-
Returns:
|
36
|
-
Tuple[bool, str]: (is_valid, error_message)
|
37
|
-
"""
|
38
|
-
if not content:
|
39
|
-
return False, "Content cannot be empty"
|
40
|
-
if not isinstance(content, str):
|
41
|
-
return False, "Content must be a string"
|
42
|
-
return True, ""
|
43
|
-
|
44
|
-
@dataclass
|
45
|
-
class FileChange:
|
46
|
-
"""Represents a file change with search/replace, search/delete, create or replace instructions"""
|
47
|
-
path: Path
|
48
|
-
description: str
|
49
|
-
is_new_file: bool
|
50
|
-
content: str = "" # For new files or replace operations
|
51
|
-
original_content: str = "" # Original content for file replacements
|
52
|
-
search_blocks: List[Tuple[str, Optional[str], Optional[str]]] = None # (search, replace, description)
|
53
|
-
replace_file: bool = False # Flag for complete file replacement
|
54
|
-
remove_file: bool = False # Flag for file deletion
|
55
|
-
|
56
|
-
def add_search_block(self, search: str, replace: Optional[str], description: Optional[str] = None) -> None:
|
57
|
-
"""Add a search/replace or search/delete block with optional description"""
|
58
|
-
if self.search_blocks is None:
|
59
|
-
self.search_blocks = []
|
60
|
-
self.search_blocks.append((search, replace, description))
|
61
|
-
|
62
|
-
@dataclass
|
63
|
-
class FileBlock:
|
64
|
-
"""Raw file block data extracted from response"""
|
65
|
-
uuid: str
|
66
|
-
filepath: str
|
67
|
-
action: str
|
68
|
-
description: str
|
69
|
-
content: str
|
70
|
-
|
71
|
-
def validate_python_syntax(content: str, filepath: Path) -> Tuple[bool, str]:
|
72
|
-
"""Validate Python syntax and return (is_valid, error_message)"""
|
73
|
-
try:
|
74
|
-
ast.parse(content)
|
75
|
-
console = Console()
|
76
|
-
try:
|
77
|
-
rel_path = filepath.relative_to(Path.cwd())
|
78
|
-
display_path = f"./{rel_path}"
|
79
|
-
except ValueError:
|
80
|
-
display_path = str(filepath)
|
81
|
-
console.print(f"[green]✓ Python syntax validation passed:[/green] {display_path}")
|
82
|
-
return True, ""
|
83
|
-
except SyntaxError as e:
|
84
|
-
error_msg = f"Line {e.lineno}: {e.msg}"
|
85
|
-
console = Console()
|
86
|
-
try:
|
87
|
-
rel_path = filepath.relative_to(Path.cwd())
|
88
|
-
display_path = f"./{rel_path}"
|
89
|
-
except ValueError:
|
90
|
-
display_path = str(filepath)
|
91
|
-
console.print(f"[red]✗ Python syntax validation failed:[/red] {display_path}")
|
92
|
-
console.print(f"[red] {error_msg}[/red]")
|
93
|
-
return False, error_msg
|
94
|
-
|
95
|
-
def count_lines(text: str) -> int:
|
96
|
-
"""Count the number of lines in a text block."""
|
97
|
-
return len(text.splitlines())
|
98
|
-
|
99
|
-
def extract_file_blocks(response_text: str) -> List[Tuple[str, str, str, str]]:
|
100
|
-
"""Extract file blocks from response text and return list of (uuid, filepath, action, description, content)"""
|
101
|
-
file_blocks = []
|
102
|
-
console = Console()
|
103
|
-
|
104
|
-
# Find file blocks with improved quoted description handling
|
105
|
-
file_start_pattern = r'## ([a-f0-9]{8}) file (.*?) (modify|create|remove|replace)(?:\s+"([^"]*?)")?\s*##'
|
106
|
-
# Find the first UUID to check for duplicates
|
107
|
-
first_match = re.search(file_start_pattern, response_text)
|
108
|
-
if not first_match:
|
109
|
-
console.print("[red]FATAL ERROR: No file blocks found in response[/red]")
|
110
|
-
sys.exit(1)
|
111
|
-
fist_uuid = first_match.group(1)
|
112
|
-
|
113
|
-
# Find all file blocks
|
114
|
-
for match in re.finditer(file_start_pattern, response_text):
|
115
|
-
block_uuid, filepath, action, description = match.groups()
|
116
|
-
|
117
|
-
# Show debug info for create actions
|
118
|
-
if config.debug and action == 'create':
|
119
|
-
console.print(f"[green]Found new file block:[/green] {filepath}")
|
120
|
-
|
121
|
-
# Now find the complete block
|
122
|
-
full_block_pattern = (
|
123
|
-
f"## {block_uuid} file.*?##\n?"
|
124
|
-
f"(.*?)"
|
125
|
-
f"## {block_uuid} file end ##"
|
126
|
-
)
|
127
|
-
|
128
|
-
block_match = re.search(full_block_pattern, response_text[match.start():], re.DOTALL)
|
129
|
-
if not block_match:
|
130
|
-
# Show context around the incomplete block
|
131
|
-
context_start = max(0, match.start() - 100)
|
132
|
-
context_end = min(len(response_text), match.start() + 100)
|
133
|
-
context = response_text[context_start:context_end]
|
134
|
-
|
135
|
-
console.print(f"\n[red]FATAL ERROR: Found file start but no matching end for {filepath}[/red]")
|
136
|
-
console.print("[red]Context around incomplete block:[/red]")
|
137
|
-
console.print(Panel(context, title="Context", border_style="red"))
|
138
|
-
sys.exit(1)
|
139
|
-
|
140
|
-
content = block_match.group(1)
|
141
|
-
# For new files, preserve the first newline if it exists
|
142
|
-
if action == 'create' and content.startswith('\n'):
|
143
|
-
content = content[1:]
|
144
|
-
|
145
|
-
file_blocks.append((block_uuid, filepath.strip(), action, description or "", content))
|
146
|
-
|
147
|
-
if config.debug:
|
148
|
-
action_type = "Creating new file" if action == "create" else "Modifying file"
|
149
|
-
console.print(f"[green]Found valid block:[/green] {action_type} {filepath}")
|
150
|
-
console.print(f"[blue]Content length:[/blue] {len(content)} chars")
|
151
|
-
|
152
|
-
return file_blocks
|
153
|
-
|
154
|
-
def extract_modification_blocks(uuid: str, content: str) -> List[Tuple[str, str, Optional[str], str]]:
|
155
|
-
"""Extract all modification blocks from content, returns list of (type, description, replace, search)"""
|
156
|
-
blocks = []
|
157
|
-
console = Console()
|
158
|
-
|
159
|
-
# Find all modification blocks in sequence
|
160
|
-
block_start = r'## ' + re.escape(uuid) + r' (search/replace|search/delete) "(.*?)" ##\n'
|
161
|
-
current_pos = 0
|
162
|
-
|
163
|
-
while True:
|
164
|
-
# Find next block start
|
165
|
-
start_match = re.search(block_start, content[current_pos:], re.DOTALL)
|
166
|
-
if not start_match:
|
167
|
-
break
|
168
|
-
|
169
|
-
block_type, description = start_match.groups()
|
170
|
-
block_start_pos = current_pos + start_match.end()
|
171
|
-
|
172
|
-
# Find block end based on type
|
173
|
-
if block_type == 'search/replace':
|
174
|
-
replace_marker = f"## {uuid} replace with ##\n"
|
175
|
-
end_marker = f"## {uuid}"
|
176
|
-
|
177
|
-
# Find replace marker
|
178
|
-
replace_pos = content.find(replace_marker, block_start_pos)
|
179
|
-
if replace_pos == -1:
|
180
|
-
# Show context around the incomplete block
|
181
|
-
context_start = max(0, current_pos + start_match.start() - 100)
|
182
|
-
context_end = min(len(content), current_pos + start_match.end() + 100)
|
183
|
-
context = content[context_start:context_end]
|
184
|
-
|
185
|
-
console.print(f"\n[red]FATAL ERROR: Missing 'replace with' marker for block:[/red] {description}")
|
186
|
-
console.print("[red]Context around incomplete block:[/red]")
|
187
|
-
console.print(Panel(context, title="Context", border_style="red"))
|
188
|
-
sys.exit(1)
|
189
|
-
|
190
|
-
# Get search content
|
191
|
-
search = content[block_start_pos:replace_pos]
|
192
|
-
|
193
|
-
# Find end of replacement
|
194
|
-
replace_start = replace_pos + len(replace_marker)
|
195
|
-
next_block = content.find(end_marker, replace_start)
|
196
|
-
if next_block == -1:
|
197
|
-
# Use rest of content if no end marker found
|
198
|
-
replace = content[replace_start:]
|
199
|
-
else:
|
200
|
-
replace = content[replace_start:next_block]
|
201
|
-
|
202
|
-
blocks.append(('replace', description, replace, search))
|
203
|
-
current_pos = next_block if next_block != -1 else len(content)
|
204
|
-
|
205
|
-
else: # search/delete
|
206
|
-
# Find either next block start or end of content
|
207
|
-
next_block_marker = content.find(f"## {uuid}", block_start_pos)
|
208
|
-
if next_block_marker == -1:
|
209
|
-
# Use rest of content if no more blocks
|
210
|
-
search = content[block_start_pos:]
|
211
|
-
else:
|
212
|
-
search = content[block_start_pos:next_block_marker]
|
213
|
-
|
214
|
-
blocks.append(('delete', description, None, search))
|
215
|
-
current_pos = next_block_marker if next_block_marker != -1 else len(content)
|
216
|
-
|
217
|
-
if config.debug:
|
218
|
-
console.print(f"[green]Found {block_type} block:[/green] {description}")
|
219
|
-
|
220
|
-
return blocks
|
221
|
-
|
222
|
-
def handle_file_block(block: FileBlock) -> FileChange:
|
223
|
-
"""Process a single file block and return a FileChange object"""
|
224
|
-
console = Console()
|
225
|
-
|
226
|
-
# Handle file removal action
|
227
|
-
if block.action == 'remove':
|
228
|
-
return FileChange(
|
229
|
-
path=Path(block.filepath),
|
230
|
-
description=block.description,
|
231
|
-
is_new_file=False,
|
232
|
-
content="",
|
233
|
-
search_blocks=[],
|
234
|
-
remove_file=True
|
235
|
-
)
|
236
|
-
|
237
|
-
# Validate file path
|
238
|
-
path = Path(block.filepath)
|
239
|
-
is_valid, error = validate_file_path(path)
|
240
|
-
if block.action == 'replace' and not is_valid:
|
241
|
-
console.print(f"[red]Invalid file path for replacement:[/red] {error}")
|
242
|
-
sys.exit(1)
|
243
|
-
|
244
|
-
# Handle file replacement action
|
245
|
-
if block.action == 'replace':
|
246
|
-
# Read original content if file exists
|
247
|
-
original_content = ""
|
248
|
-
if path.exists():
|
249
|
-
try:
|
250
|
-
original_content = path.read_text()
|
251
|
-
except Exception as e:
|
252
|
-
console.print(f"[red]Error reading original file for replacement:[/red] {e}")
|
253
|
-
sys.exit(1)
|
254
|
-
|
255
|
-
return FileChange(
|
256
|
-
path=Path(block.filepath),
|
257
|
-
description=block.description,
|
258
|
-
is_new_file=False,
|
259
|
-
content=block.content.lstrip('\n'),
|
260
|
-
original_content=original_content,
|
261
|
-
search_blocks=[],
|
262
|
-
replace_file=True
|
263
|
-
)
|
264
|
-
|
265
|
-
# Validate content for new files
|
266
|
-
if block.action == 'create':
|
267
|
-
is_valid, error = validate_file_content(block.content)
|
268
|
-
if not is_valid:
|
269
|
-
console.print(f"[red]Invalid file content for {block.filepath}:[/red] {error}")
|
270
|
-
sys.exit(1)
|
271
|
-
|
272
|
-
if block.action == 'create':
|
273
|
-
return FileChange(
|
274
|
-
path=Path(block.filepath),
|
275
|
-
description=block.description,
|
276
|
-
is_new_file=True,
|
277
|
-
content=block.content[1:] if block.content.startswith('\n') else block.content,
|
278
|
-
search_blocks=[]
|
279
|
-
)
|
280
|
-
|
281
|
-
# Extract and process modification blocks
|
282
|
-
search_blocks = []
|
283
|
-
for block_type, description, replace, search in extract_modification_blocks(block.uuid, block.content):
|
284
|
-
# Ensure consistent line endings
|
285
|
-
search = search.rstrip('\n') + '\n'
|
286
|
-
if replace is not None:
|
287
|
-
replace = replace.rstrip('\n') + '\n'
|
288
|
-
|
289
|
-
if config.debug:
|
290
|
-
console.print(f"\n[cyan]Processing {block_type} block:[/cyan] {description}")
|
291
|
-
console.print(Panel(search, title="Search Content"))
|
292
|
-
if replace:
|
293
|
-
console.print(Panel(replace, title="Replace Content"))
|
294
|
-
|
295
|
-
search_blocks.append((search, replace, description))
|
296
|
-
|
297
|
-
return FileChange(
|
298
|
-
path=Path(block.filepath),
|
299
|
-
description=block.description,
|
300
|
-
is_new_file=False,
|
301
|
-
search_blocks=search_blocks
|
302
|
-
)
|
303
|
-
|
304
|
-
def parse_block_changes(response_text: str) -> List[FileChange]:
|
305
|
-
"""Parse file changes from response blocks and return list of FileChange"""
|
306
|
-
changes = []
|
307
|
-
console = Console()
|
308
|
-
|
309
|
-
# First extract all file blocks
|
310
|
-
file_blocks = extract_file_blocks(response_text)
|
311
|
-
|
312
|
-
# Process each file block independently
|
313
|
-
for block_uuid, filepath, action, description, content in file_blocks:
|
314
|
-
path = Path(filepath)
|
315
|
-
|
316
|
-
file_block = FileBlock(
|
317
|
-
uuid=block_uuid,
|
318
|
-
filepath=filepath,
|
319
|
-
action=action,
|
320
|
-
description=description,
|
321
|
-
content=content
|
322
|
-
)
|
323
|
-
|
324
|
-
file_change = handle_file_block(file_block)
|
325
|
-
# For remove action, ensure remove_file flag is set
|
326
|
-
if action == 'remove':
|
327
|
-
file_change.remove_file = True
|
328
|
-
file_change.path = path
|
329
|
-
changes.append(file_change)
|
330
|
-
|
331
|
-
if config.debug:
|
332
|
-
console.print(f"\n[cyan]Processed {len(file_blocks)} file blocks[/cyan]")
|
333
|
-
|
334
|
-
return changes
|
janito/prompts.py
DELETED
@@ -1,81 +0,0 @@
|
|
1
|
-
import re
|
2
|
-
import uuid
|
3
|
-
from typing import List, Union
|
4
|
-
from dataclasses import dataclass
|
5
|
-
from .analysis import parse_analysis_options, AnalysisOption
|
6
|
-
|
7
|
-
# Core system prompt focused on role and purpose
|
8
|
-
SYSTEM_PROMPT = """I am Janito, your friendly software development buddy. I help you with coding tasks while being clear and concise in my responses."""
|
9
|
-
|
10
|
-
|
11
|
-
SELECTED_OPTION_PROMPT = """
|
12
|
-
Original request: {request}
|
13
|
-
|
14
|
-
Please provide detailed implementation using the following guide:
|
15
|
-
{option_text}
|
16
|
-
|
17
|
-
Current files:
|
18
|
-
<files>
|
19
|
-
{files_content}
|
20
|
-
</files>
|
21
|
-
|
22
|
-
RULES:
|
23
|
-
- When removing constants, ensure they are not used elsewhere
|
24
|
-
- When adding new features to python files, add the necessary imports
|
25
|
-
- Python imports should be inserted at the top of the file
|
26
|
-
- For complete file replacements, only use for existing files marked as modified
|
27
|
-
- File replacements must preserve the essential functionality
|
28
|
-
- When multiple changes affect the same code block, combine them into a single change
|
29
|
-
- if no changes are required answer only the reason in the format: <no_changes_required>reason for no changes<no_changes_required>
|
30
|
-
|
31
|
-
Please provide the changes in this format:
|
32
|
-
|
33
|
-
For incremental changes:
|
34
|
-
## {uuid} file <filepath> modify "short file change description" ##
|
35
|
-
## {uuid} search/replace "short change description" ##
|
36
|
-
<search_content>
|
37
|
-
## {uuid} replace with ##
|
38
|
-
<replace_content>
|
39
|
-
## {uuid} file end ##
|
40
|
-
|
41
|
-
For complete file replacement (only for existing modified files):
|
42
|
-
## {uuid} file <filepath> replace "short file description" ##
|
43
|
-
<full_file_content>
|
44
|
-
## {uuid} file end ##
|
45
|
-
|
46
|
-
For new files:
|
47
|
-
## {uuid} file <filepath> create "short file description" ##
|
48
|
-
<full_file_content>
|
49
|
-
## {uuid} file end ##
|
50
|
-
|
51
|
-
For content deletion:
|
52
|
-
## {uuid} file <filepath> modify ##
|
53
|
-
## {uuid} search/delete "short change description" ##
|
54
|
-
<content_to_delete>
|
55
|
-
## {uuid} file end ##
|
56
|
-
|
57
|
-
For file removal:
|
58
|
-
## {uuid} file <filepath> remove "short removal reason" ##
|
59
|
-
## {uuid} file end ##
|
60
|
-
|
61
|
-
RULES:
|
62
|
-
1. search_content MUST preserve the original indentation/whitespace
|
63
|
-
2. file replacement can only be used for existing files marked as
|
64
|
-
"""
|
65
|
-
|
66
|
-
def build_selected_option_prompt(option_text: str, request: str, files_content: str = "") -> str:
|
67
|
-
"""Build prompt for selected option details
|
68
|
-
|
69
|
-
Args:
|
70
|
-
option_text: Formatted text describing the selected option
|
71
|
-
request: The original user request
|
72
|
-
files_content: Content of relevant files
|
73
|
-
"""
|
74
|
-
short_uuid = str(uuid.uuid4())[:8]
|
75
|
-
|
76
|
-
return SELECTED_OPTION_PROMPT.format(
|
77
|
-
option_text=option_text,
|
78
|
-
request=request,
|
79
|
-
files_content=files_content,
|
80
|
-
uuid=short_uuid
|
81
|
-
)
|
janito/scan.py
DELETED
@@ -1,176 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
from typing import List, Tuple, Set
|
3
|
-
from rich.console import Console
|
4
|
-
from rich.columns import Columns
|
5
|
-
from rich.panel import Panel
|
6
|
-
from janito.config import config
|
7
|
-
from pathspec import PathSpec
|
8
|
-
from pathspec.patterns import GitWildMatchPattern
|
9
|
-
from collections import defaultdict
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
SPECIAL_FILES = ["README.md", "__init__.py", "__main__.py"]
|
14
|
-
|
15
|
-
def _scan_paths(paths: List[Path], workdir: Path = None) -> Tuple[List[str], List[str]]:
|
16
|
-
"""Common scanning logic used by both preview and content collection"""
|
17
|
-
content_parts = []
|
18
|
-
file_items = []
|
19
|
-
skipped_files = []
|
20
|
-
processed_files: Set[Path] = set() # Track processed files
|
21
|
-
console = Console()
|
22
|
-
|
23
|
-
# Load gitignore if it exists
|
24
|
-
gitignore_path = workdir / '.gitignore' if workdir else None
|
25
|
-
gitignore_spec = None
|
26
|
-
if (gitignore_path and gitignore_path.exists()):
|
27
|
-
with open(gitignore_path) as f:
|
28
|
-
gitignore = f.read()
|
29
|
-
gitignore_spec = PathSpec.from_lines(GitWildMatchPattern, gitignore.splitlines())
|
30
|
-
|
31
|
-
|
32
|
-
def scan_path(path: Path, level: int) -> None:
|
33
|
-
"""
|
34
|
-
Scan a path and add it to the content_parts list
|
35
|
-
level 0 means we are scanning the root directory
|
36
|
-
level 1 we provide both directory directory name and file content
|
37
|
-
level > 1 we just return
|
38
|
-
"""
|
39
|
-
if level > 1:
|
40
|
-
return
|
41
|
-
|
42
|
-
path = path.resolve()
|
43
|
-
relative_base = workdir
|
44
|
-
if path.is_dir():
|
45
|
-
relative_path = path.relative_to(relative_base)
|
46
|
-
content_parts.append(f'<directory><path>{relative_path}</path>not sent</directory>')
|
47
|
-
file_items.append(f"[blue]•[/blue] {relative_path}/")
|
48
|
-
# Check for special files
|
49
|
-
special_found = []
|
50
|
-
for special_file in SPECIAL_FILES:
|
51
|
-
special_path = path / special_file
|
52
|
-
if special_path.exists() and special_path.resolve() not in processed_files:
|
53
|
-
special_found.append(special_file)
|
54
|
-
processed_files.add(special_path.resolve())
|
55
|
-
if special_found:
|
56
|
-
file_items[-1] = f"[blue]•[/blue] {relative_path}/ [cyan]({', '.join(special_found)})[/cyan]"
|
57
|
-
for special_file in special_found:
|
58
|
-
special_path = path / special_file
|
59
|
-
try:
|
60
|
-
relative_path = special_path.relative_to(relative_base)
|
61
|
-
file_content = special_path.read_text(encoding='utf-8')
|
62
|
-
content_parts.append(f"<file>\n<path>{relative_path}</path>\n<content>\n{file_content}\n</content>\n</file>")
|
63
|
-
except UnicodeDecodeError:
|
64
|
-
skipped_files.append(str(relative_path))
|
65
|
-
console.print(f"[yellow]Warning: Skipping file due to encoding issues: {relative_path}[/yellow]")
|
66
|
-
|
67
|
-
for item in path.iterdir():
|
68
|
-
# Skip if matches gitignore patterns
|
69
|
-
if gitignore_spec:
|
70
|
-
rel_path = str(item.relative_to(workdir))
|
71
|
-
if gitignore_spec.match_file(rel_path):
|
72
|
-
continue
|
73
|
-
if item.resolve() not in processed_files: # Skip if already processed
|
74
|
-
scan_path(item, level+1)
|
75
|
-
|
76
|
-
else:
|
77
|
-
resolved_path = path.resolve()
|
78
|
-
if resolved_path in processed_files: # Skip if already processed
|
79
|
-
return
|
80
|
-
|
81
|
-
processed_files.add(resolved_path)
|
82
|
-
relative_path = path.relative_to(relative_base)
|
83
|
-
# check if file is binary
|
84
|
-
try:
|
85
|
-
if path.is_file() and path.read_bytes().find(b'\x00') != -1:
|
86
|
-
console.print(f"[red]Skipped binary file found: {relative_path}[/red]")
|
87
|
-
return
|
88
|
-
file_content = path.read_text(encoding='utf-8')
|
89
|
-
content_parts.append(f"<file>\n<path>{relative_path}</path>\n<content>\n{file_content}\n</content>\n</file>")
|
90
|
-
file_items.append(f"[cyan]•[/cyan] {relative_path}")
|
91
|
-
except UnicodeDecodeError:
|
92
|
-
skipped_files.append(str(relative_path))
|
93
|
-
console.print(f"[yellow]Warning: Skipping file due to encoding issues: {relative_path}[/yellow]")
|
94
|
-
|
95
|
-
for path in paths:
|
96
|
-
scan_path(path, 0)
|
97
|
-
|
98
|
-
if skipped_files and config.verbose:
|
99
|
-
console.print("\n[yellow]Files skipped due to encoding issues:[/yellow]")
|
100
|
-
for file in skipped_files:
|
101
|
-
console.print(f" • {file}")
|
102
|
-
|
103
|
-
return content_parts, file_items
|
104
|
-
|
105
|
-
def collect_files_content(paths: List[Path], workdir: Path = None) -> str:
|
106
|
-
"""Collect content from all files in XML format"""
|
107
|
-
console = Console()
|
108
|
-
content_parts, file_items = _scan_paths(paths, workdir)
|
109
|
-
|
110
|
-
if file_items and config.verbose:
|
111
|
-
console.print("\n[bold blue]Contents being analyzed:[/bold blue]")
|
112
|
-
console.print(Columns(file_items, padding=(0, 4), expand=True))
|
113
|
-
|
114
|
-
if config.verbose:
|
115
|
-
for part in content_parts:
|
116
|
-
if part.startswith('<file>'):
|
117
|
-
# Extract filename from XML content
|
118
|
-
path_start = part.find('<path>') + 6
|
119
|
-
path_end = part.find('</path>')
|
120
|
-
if path_start > 5 and path_end > path_start:
|
121
|
-
filepath = part[path_start:path_end]
|
122
|
-
console.print(f"[dim]Adding content from:[/dim] {filepath}")
|
123
|
-
|
124
|
-
return "\n".join(content_parts)
|
125
|
-
|
126
|
-
|
127
|
-
def preview_scan(paths: List[Path], workdir: Path = None) -> None:
|
128
|
-
"""Preview what files and directories would be scanned"""
|
129
|
-
console = Console()
|
130
|
-
_, file_items = _scan_paths(paths, workdir)
|
131
|
-
|
132
|
-
# Display working directory status
|
133
|
-
console.print("\n[bold blue]Analysis Paths:[/bold blue]")
|
134
|
-
console.print(f"[cyan]Working Directory:[/cyan] {workdir.absolute()}")
|
135
|
-
|
136
|
-
# Show if working directory is being scanned
|
137
|
-
is_workdir_scanned = any(p.resolve() == workdir.resolve() for p in paths)
|
138
|
-
if is_workdir_scanned:
|
139
|
-
console.print("[green]✓ Working directory will be scanned[/green]")
|
140
|
-
else:
|
141
|
-
console.print("[yellow]! Working directory will not be scanned[/yellow]")
|
142
|
-
|
143
|
-
# Show included paths relative to working directory
|
144
|
-
if len(paths) > (1 if is_workdir_scanned else 0):
|
145
|
-
console.print("\n[cyan]Additional Included Paths:[/cyan]")
|
146
|
-
for path in paths:
|
147
|
-
if path.resolve() != workdir.resolve():
|
148
|
-
try:
|
149
|
-
rel_path = path.relative_to(workdir)
|
150
|
-
console.print(f" • ./{rel_path}")
|
151
|
-
except ValueError:
|
152
|
-
# Path is outside working directory
|
153
|
-
console.print(f" • {path.absolute()}")
|
154
|
-
|
155
|
-
console.print("\n[bold blue]Files that will be analyzed:[/bold blue]")
|
156
|
-
console.print(Columns(file_items, padding=(0, 4), expand=True))
|
157
|
-
|
158
|
-
def is_dir_empty(path: Path) -> bool:
|
159
|
-
"""Check if directory is empty, ignoring hidden files"""
|
160
|
-
return not any(item for item in path.iterdir() if not item.name.startswith('.'))
|
161
|
-
|
162
|
-
def show_content_stats(content: str) -> None:
|
163
|
-
if not content:
|
164
|
-
return
|
165
|
-
|
166
|
-
dir_counts = defaultdict(int)
|
167
|
-
for line in content.split('\n'):
|
168
|
-
if line.startswith('<path>'):
|
169
|
-
path = Path(line.replace('<path>', '').replace('</path>', '').strip())
|
170
|
-
dir_counts[str(path.parent)] += 1
|
171
|
-
|
172
|
-
console = Console()
|
173
|
-
stats = [f"{directory} ({count} files)" for directory, count in dir_counts.items()]
|
174
|
-
columns = Columns(stats, equal=True, expand=True)
|
175
|
-
panel = Panel(columns, title="Work Context")
|
176
|
-
console.print(panel)
|
janito/tests/test_fileparser.py
DELETED
@@ -1,26 +0,0 @@
|
|
1
|
-
import pytest
|
2
|
-
from pathlib import Path
|
3
|
-
from janito.fileparser import validate_file_path, validate_file_content
|
4
|
-
|
5
|
-
def test_validate_file_path():
|
6
|
-
# Valid paths
|
7
|
-
assert validate_file_path(Path("test.py")) == (True, "")
|
8
|
-
assert validate_file_path(Path("folder/test.py")) == (True, "")
|
9
|
-
|
10
|
-
# Invalid paths
|
11
|
-
assert validate_file_path(Path("/absolute/path.py"))[0] == False
|
12
|
-
assert validate_file_path(Path("../escape.py"))[0] == False
|
13
|
-
assert validate_file_path(Path("test?.py"))[0] == False
|
14
|
-
assert validate_file_path(Path("test*.py"))[0] == False
|
15
|
-
|
16
|
-
def test_validate_file_content():
|
17
|
-
# Valid content
|
18
|
-
assert validate_file_content("print('hello')") == (True, "")
|
19
|
-
assert validate_file_content("# Empty file with comment\n") == (True, "")
|
20
|
-
|
21
|
-
# Invalid content
|
22
|
-
assert validate_file_content("")[0] == False
|
23
|
-
|
24
|
-
# Test large content
|
25
|
-
large_content = "x" * (1024 * 1024 + 1) # Slightly over 1MB
|
26
|
-
assert validate_file_content(large_content)[0] == False
|