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.
Files changed (104) hide show
  1. janito/__init__.py +1 -1
  2. janito/__main__.py +102 -326
  3. janito/agents/__init__.py +16 -0
  4. janito/agents/agent.py +21 -0
  5. janito/{claude.py → agents/claudeai.py} +13 -17
  6. janito/agents/openai.py +53 -0
  7. janito/agents/test.py +34 -0
  8. janito/change/__init__.py +32 -0
  9. janito/change/__main__.py +0 -0
  10. janito/change/analysis/__init__.py +23 -0
  11. janito/change/analysis/__main__.py +7 -0
  12. janito/change/analysis/analyze.py +61 -0
  13. janito/change/analysis/formatting.py +78 -0
  14. janito/change/analysis/options.py +81 -0
  15. janito/change/analysis/prompts.py +98 -0
  16. janito/change/analysis/view/__init__.py +9 -0
  17. janito/change/analysis/view/terminal.py +171 -0
  18. janito/change/applier/__init__.py +5 -0
  19. janito/change/applier/file.py +58 -0
  20. janito/change/applier/main.py +156 -0
  21. janito/change/applier/text.py +245 -0
  22. janito/change/applier/workspace_dir.py +58 -0
  23. janito/change/core.py +131 -0
  24. janito/change/history.py +44 -0
  25. janito/change/operations.py +7 -0
  26. janito/change/parser.py +289 -0
  27. janito/change/play.py +54 -0
  28. janito/change/preview.py +82 -0
  29. janito/change/prompts.py +126 -0
  30. janito/change/test.py +0 -0
  31. janito/change/validator.py +251 -0
  32. janito/change/viewer/__init__.py +11 -0
  33. janito/change/viewer/content.py +66 -0
  34. janito/change/viewer/diff.py +43 -0
  35. janito/change/viewer/pager.py +56 -0
  36. janito/change/viewer/panels.py +555 -0
  37. janito/change/viewer/styling.py +103 -0
  38. janito/change/viewer/themes.py +55 -0
  39. janito/clear_statement_parser/clear_statement_format.txt +328 -0
  40. janito/clear_statement_parser/examples.txt +326 -0
  41. janito/clear_statement_parser/models.py +104 -0
  42. janito/clear_statement_parser/parser.py +496 -0
  43. janito/cli/__init__.py +2 -0
  44. janito/cli/base.py +30 -0
  45. janito/cli/commands.py +45 -0
  46. janito/cli/functions.py +111 -0
  47. janito/cli/handlers/ask.py +22 -0
  48. janito/cli/handlers/demo.py +22 -0
  49. janito/cli/handlers/request.py +24 -0
  50. janito/cli/handlers/scan.py +9 -0
  51. janito/cli/history.py +61 -0
  52. janito/cli/registry.py +26 -0
  53. janito/common.py +41 -10
  54. janito/config.py +71 -6
  55. janito/demo/__init__.py +4 -0
  56. janito/demo/data.py +13 -0
  57. janito/demo/mock_data.py +20 -0
  58. janito/demo/operations.py +45 -0
  59. janito/demo/runner.py +59 -0
  60. janito/demo/scenarios.py +32 -0
  61. janito/prompts.py +1 -65
  62. janito/qa.py +8 -5
  63. janito/review.py +13 -0
  64. janito/search_replace/README.md +146 -0
  65. janito/search_replace/__init__.py +6 -0
  66. janito/search_replace/__main__.py +21 -0
  67. janito/search_replace/core.py +119 -0
  68. janito/search_replace/parser.py +52 -0
  69. janito/search_replace/play.py +61 -0
  70. janito/search_replace/replacer.py +36 -0
  71. janito/search_replace/searcher.py +299 -0
  72. janito/shell/__init__.py +39 -0
  73. janito/shell/bus.py +31 -0
  74. janito/shell/commands.py +195 -0
  75. janito/shell/handlers.py +122 -0
  76. janito/shell/history.py +20 -0
  77. janito/shell/processor.py +52 -0
  78. janito/tui/__init__.py +21 -0
  79. janito/tui/base.py +22 -0
  80. janito/tui/flows/__init__.py +5 -0
  81. janito/tui/flows/changes.py +65 -0
  82. janito/tui/flows/content.py +128 -0
  83. janito/tui/flows/selection.py +117 -0
  84. janito/tui/screens/__init__.py +3 -0
  85. janito/tui/screens/app.py +1 -0
  86. janito/workspace/__init__.py +7 -0
  87. janito/workspace/analysis.py +121 -0
  88. janito/workspace/manager.py +48 -0
  89. janito/workspace/scan.py +232 -0
  90. janito-0.6.0.dist-info/METADATA +185 -0
  91. janito-0.6.0.dist-info/RECORD +95 -0
  92. {janito-0.4.0.dist-info → janito-0.6.0.dist-info}/WHEEL +1 -1
  93. janito/analysis.py +0 -281
  94. janito/changeapplier.py +0 -436
  95. janito/changeviewer.py +0 -350
  96. janito/console.py +0 -330
  97. janito/contentchange.py +0 -84
  98. janito/contextparser.py +0 -113
  99. janito/fileparser.py +0 -125
  100. janito/scan.py +0 -137
  101. janito-0.4.0.dist-info/METADATA +0 -164
  102. janito-0.4.0.dist-info/RECORD +0 -21
  103. {janito-0.4.0.dist-info → janito-0.6.0.dist-info}/entry_points.txt +0 -0
  104. {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