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
@@ -0,0 +1,299 @@
1
+ from typing import List, Optional, Dict, Type
2
+ from abc import ABC, abstractmethod
3
+ import re
4
+
5
+ LINE_OVER_LINE_DEBUG = False
6
+
7
+ class SearchStrategy(ABC):
8
+ """Base class for search strategies."""
9
+ @abstractmethod
10
+ def match(self, source_lines: List[str], pattern_lines: List[str], pos: int, searcher: 'Searcher') -> bool:
11
+ pass
12
+
13
+ class ExactMatchStrategy(SearchStrategy):
14
+ """Exact match including indentation."""
15
+ def match(self, source_lines: List[str], pattern_lines: List[str], pos: int, searcher: 'Searcher') -> bool:
16
+ if pos + len(pattern_lines) > len(source_lines):
17
+ return False
18
+ return all(source_lines[pos + i] == pattern_line
19
+ for i, pattern_line in enumerate(pattern_lines))
20
+
21
+ class ExactContentStrategy(SearchStrategy):
22
+ """Exact content match ignoring all indentation."""
23
+ def match(self, source_lines: List[str], pattern_lines: List[str], pos: int, searcher: 'Searcher') -> bool:
24
+ if pos + len(pattern_lines) > len(source_lines):
25
+ return False
26
+ return all(source_lines[pos + i].strip() == pattern_line.strip()
27
+ for i, pattern_line in enumerate(pattern_lines)
28
+ if pattern_line.strip())
29
+
30
+ class IndentAwareStrategy(SearchStrategy):
31
+ """Indentation-aware matching preserving relative indentation."""
32
+ def match(self, source_lines: List[str], pattern_lines: List[str], pos: int, searcher: 'Searcher') -> bool:
33
+ if pos + len(pattern_lines) > len(source_lines):
34
+ return False
35
+ match_indent = searcher.get_indentation(source_lines[pos])
36
+ return all(source_lines[pos + i].startswith(match_indent + pattern_line)
37
+ for i, pattern_line in enumerate(pattern_lines)
38
+ if pattern_line.strip())
39
+
40
+ class ExactContentNoComments(SearchStrategy):
41
+ """Exact content match ignoring indentation, comments, and empty lines."""
42
+ def _strip_comments(self, line: str) -> str:
43
+ """Remove comments from line."""
44
+ if '#' in line:
45
+ line = line.split('#')[0]
46
+ if '//' in line:
47
+ line = line.split('//')[0]
48
+ return line.strip()
49
+
50
+ def match(self, source_lines: List[str], pattern_lines: List[str], pos: int, searcher: 'Searcher') -> bool:
51
+ if pos + len(pattern_lines) > len(source_lines):
52
+ return False
53
+
54
+ if searcher.debug_mode and LINE_OVER_LINE_DEBUG:
55
+ print("\n[DEBUG] ExactContentNoComments trying to match at line", pos + 1)
56
+
57
+ # Filter out comments and empty lines from pattern
58
+ pattern_content = [self._strip_comments(line) for line in pattern_lines]
59
+ pattern_content = [line for line in pattern_content if line]
60
+
61
+ if searcher.debug_mode and LINE_OVER_LINE_DEBUG:
62
+ print("[DEBUG] Pattern after processing:")
63
+ for i, line in enumerate(pattern_content):
64
+ print(f"[DEBUG] {i+1}: '{line}'")
65
+
66
+ # Match against source, ignoring comments and empty lines
67
+ source_idx = pos
68
+ pattern_idx = 0
69
+
70
+ while pattern_idx < len(pattern_content) and source_idx < len(source_lines):
71
+ source_line = self._strip_comments(source_lines[source_idx])
72
+ if not source_line:
73
+ source_idx += 1
74
+ continue
75
+
76
+ if searcher.debug_mode and LINE_OVER_LINE_DEBUG:
77
+ print(f"[DEBUG] Line {source_idx + 1}: '{source_line}' vs '{pattern_content[pattern_idx]}'")
78
+
79
+ if source_line != pattern_content[pattern_idx]:
80
+ if searcher.debug_mode and LINE_OVER_LINE_DEBUG:
81
+ print("[DEBUG] Line mismatch")
82
+ return False
83
+
84
+ pattern_idx += 1
85
+ source_idx += 1
86
+
87
+ match_result = pattern_idx == len(pattern_content)
88
+ if match_result and searcher.debug_mode:
89
+ print("[DEBUG] Match found")
90
+ return True
91
+
92
+ return False
93
+
94
+ class ExactContentNoCommentsFirstLinePartial(SearchStrategy):
95
+ """Match first line partially, ignoring comments."""
96
+ def _strip_comments(self, line: str) -> str:
97
+ """Remove comments from line."""
98
+ if '#' in line:
99
+ line = line.split('#')[0]
100
+ if '//' in line:
101
+ line = line.split('//')[0]
102
+ return line.strip()
103
+
104
+ def match(self, source_lines: List[str], pattern_lines: List[str], pos: int, searcher: 'Searcher') -> bool:
105
+ if pos >= len(source_lines):
106
+ return False
107
+
108
+ # Get first non-empty pattern line
109
+ pattern_content = []
110
+ for line in pattern_lines:
111
+ stripped = self._strip_comments(line)
112
+ if stripped:
113
+ pattern_content.append(stripped)
114
+ break
115
+
116
+ if not pattern_content:
117
+ return False
118
+
119
+ # Get source line content
120
+ source_line = self._strip_comments(source_lines[pos])
121
+ if not source_line:
122
+ return False
123
+
124
+ # Check if pattern content is part of source line
125
+ return pattern_content[0] in source_line
126
+
127
+ class Searcher:
128
+ """Handles pattern searching in source code with configurable strategies."""
129
+
130
+ def __init__(self, debug: bool = False):
131
+ """Initialize searcher with debug mode."""
132
+ self.debug_mode = debug
133
+
134
+ @classmethod
135
+ def set_debug(cls, enabled: bool):
136
+ """Enable or disable debug mode - deprecated, use instance property instead"""
137
+ # Remove the class-level debug setting as it's no longer needed
138
+ raise DeprecationWarning("Class-level debug setting is deprecated. Use instance debug_mode property instead.")
139
+
140
+ # Updated extension to strategy mapping to include ExactContentNoComments
141
+ EXTENSION_STRATEGIES = {
142
+ '.py': [ExactMatchStrategy(), IndentAwareStrategy(), ExactContentStrategy(), ExactContentNoComments(), ExactContentNoCommentsFirstLinePartial()],
143
+ '.java': [ExactMatchStrategy(), IndentAwareStrategy(), ExactContentStrategy(), ExactContentNoComments(), ExactContentNoCommentsFirstLinePartial()],
144
+ '.js': [ExactMatchStrategy(), IndentAwareStrategy(), ExactContentStrategy(), ExactContentNoComments(), ExactContentNoCommentsFirstLinePartial()],
145
+ '.ts': [ExactMatchStrategy(), IndentAwareStrategy(), ExactContentStrategy(), ExactContentNoComments(), ExactContentNoCommentsFirstLinePartial()],
146
+ '*': [ExactMatchStrategy(), ExactContentStrategy(), ExactContentNoComments(), ExactContentNoCommentsFirstLinePartial()] # updated default fallback
147
+ }
148
+
149
+ def get_strategies(self, file_ext: Optional[str]) -> List[SearchStrategy]:
150
+ """Get search strategies for given file extension."""
151
+ if not file_ext:
152
+ return self.EXTENSION_STRATEGIES['*']
153
+ return self.EXTENSION_STRATEGIES.get(file_ext.lower(), self.EXTENSION_STRATEGIES['*'])
154
+
155
+ @staticmethod
156
+ def get_indentation(line: str) -> str:
157
+ """Get the leading whitespace of a line."""
158
+ return re.match(r'^[ \t]*', line).group()
159
+
160
+ @staticmethod
161
+ def get_first_non_empty_line(text: str) -> tuple[str, int]:
162
+ """Get first non-empty line and its index."""
163
+ lines = text.splitlines()
164
+ for i, line in enumerate(lines):
165
+ if line.strip():
166
+ return line, i
167
+ return '', 0
168
+
169
+ @staticmethod
170
+ def get_last_non_empty_line(text: str) -> tuple[str, int]:
171
+ """Get last non-empty line and its index."""
172
+ lines = text.splitlines()
173
+ for i in range(len(lines) - 1, -1, -1):
174
+ if lines[i].strip():
175
+ return lines[i], i
176
+ return '', 0
177
+
178
+ def _build_indent_map(self, text: str) -> dict[int, int]:
179
+ """Build a map of line numbers to indentation levels."""
180
+ indent_map = {}
181
+ for i, line in enumerate(text.splitlines()):
182
+ if line.strip(): # Only track non-empty lines
183
+ indent_map[i] = len(self.get_indentation(line))
184
+ if self.debug_mode:
185
+ print(f"[DEBUG] Line {i}: Indentation level {indent_map[i]}")
186
+ return indent_map
187
+
188
+ def normalize_pattern(self, pattern: str, base_indent: str = '') -> str:
189
+ """Remove base indentation from pattern to help with matching."""
190
+ lines = pattern.splitlines()
191
+ first_line, start_idx = self.get_first_non_empty_line(pattern)
192
+ last_line, end_idx = self.get_last_non_empty_line(pattern)
193
+
194
+ # Calculate minimum indentation from first and last non-empty lines
195
+ first_indent = len(self.get_indentation(first_line))
196
+ last_indent = len(self.get_indentation(last_line))
197
+ min_indent = min(first_indent, last_indent)
198
+
199
+ if self.debug_mode:
200
+ print(f"[DEBUG] First line indent: {first_indent}")
201
+ print(f"[DEBUG] Last line indent: {last_indent}")
202
+ print(f"[DEBUG] Using minimum indent: {min_indent}")
203
+
204
+ normalized = []
205
+ for i, line in enumerate(lines):
206
+ if not line.strip():
207
+ normalized.append('')
208
+ else:
209
+ line_indent = len(self.get_indentation(line))
210
+ if line_indent < min_indent:
211
+ if self.debug_mode:
212
+ print(f"[DEBUG] Warning: Line {i} has less indentation ({line_indent}) than minimum ({min_indent})")
213
+ normalized.append(line)
214
+ else:
215
+ normalized.append(line[min_indent:])
216
+ if self.debug_mode:
217
+ print(f"[DEBUG] Normalized line {i}: '{normalized[-1]}'")
218
+
219
+ return '\n'.join(normalized)
220
+
221
+ def _find_best_match_position(self, positions: List[int], source_lines: List[str], pattern_base_indent: int) -> Optional[int]:
222
+ """Find the best matching position based on line number (earliest match)."""
223
+ if self.debug_mode:
224
+ print(f"[DEBUG] Finding best match among positions: {[p+1 for p in positions]}") # Show 1-based line numbers
225
+
226
+ if not positions:
227
+ return None
228
+
229
+ best_pos = min(positions) # Simply take the earliest match
230
+ if self.debug_mode:
231
+ print(f"[DEBUG] Selected match at line {best_pos + 1}") # Show 1-based line number
232
+ return best_pos
233
+
234
+ def try_match_with_strategies(self, source_lines: List[str], pattern_lines: List[str],
235
+ pos: int, strategies: List[SearchStrategy]) -> bool:
236
+ """Try matching using multiple strategies in order."""
237
+ if self.debug_mode and LINE_OVER_LINE_DEBUG:
238
+ print(f"\n[DEBUG] Trying to match at line {pos + 1}")
239
+
240
+ for strategy in strategies:
241
+ if strategy.match(source_lines, pattern_lines, pos, self):
242
+ if self.debug_mode:
243
+ print(f"[DEBUG] Match found with {strategy.__class__.__name__}")
244
+ print(f"[DEBUG] Stopping strategy chain at line {pos + 1}")
245
+ return True
246
+ return False
247
+
248
+ def _find_matches(self, source_lines: List[str], pattern_lines: List[str],
249
+ file_ext: Optional[str] = None) -> List[int]:
250
+ """Find all matches using configured strategies."""
251
+ strategies = self.get_strategies(file_ext)
252
+
253
+ if self.debug_mode:
254
+ print("\nTrying search strategies:")
255
+ print("-" * 50)
256
+
257
+ # Try each strategy in order of preference
258
+ for strategy in strategies:
259
+ matches = []
260
+ strategy_name = strategy.__class__.__name__.replace('Strategy', '')
261
+
262
+ if self.debug_mode:
263
+ print(f"\n→ {strategy_name}...")
264
+
265
+ for i in range(len(source_lines)):
266
+ if strategy.match(source_lines, pattern_lines, i, self):
267
+ matches.append(i)
268
+
269
+ if matches:
270
+ if self.debug_mode:
271
+ # Show 1-based line numbers consistently
272
+ print(f"✓ {strategy_name}: Found {len(matches)} match(es) at line(s) {[m+1 for m in matches]}")
273
+ return matches
274
+ elif self.debug_mode:
275
+ print(f"✗ {strategy_name}: No matches found")
276
+
277
+ return []
278
+
279
+ def exact_match(self, source: str, pattern: str) -> List[int]:
280
+ """Perform exact line-by-line matching, preserving all whitespace except newlines."""
281
+ source_lines = source.splitlines()
282
+ pattern_lines = pattern.splitlines()
283
+ pattern_len = len(pattern_lines)
284
+ matches = []
285
+
286
+ for i in range(len(source_lines) - pattern_len + 1):
287
+ is_match = True
288
+ for j in range(pattern_len):
289
+ if source_lines[i + j] != pattern_lines[j]:
290
+ is_match = False
291
+ break
292
+
293
+ if is_match:
294
+ matches.append(i)
295
+
296
+ if self.debug_mode and matches:
297
+ print(f"[DEBUG] Found {len(matches)} exact matches")
298
+
299
+ return matches
@@ -0,0 +1,39 @@
1
+ """Shell package initialization for Janito."""
2
+ from typing import Optional
3
+ from prompt_toolkit import PromptSession
4
+ from prompt_toolkit.history import FileHistory
5
+ from pathlib import Path
6
+ from rich.console import Console
7
+ from janito.config import config
8
+ from janito.workspace import workspace
9
+ from .processor import CommandProcessor
10
+
11
+ def start_shell() -> None:
12
+ """Start the Janito interactive shell."""
13
+ history_file = Path.home() / ".janito_history"
14
+ session = PromptSession(history=FileHistory(str(history_file)))
15
+ processor = CommandProcessor()
16
+
17
+ # Perform workspace analysis
18
+ console = Console()
19
+
20
+ # Use configured paths or default to workspace_dir
21
+ scan_paths = config.include if config.include else [config.workspace_dir]
22
+ workspace.collect_content(scan_paths)
23
+ workspace.analyze()
24
+
25
+ # Store workspace content in processor for session
26
+ processor.workspace_content = workspace.get_content()
27
+
28
+ while True:
29
+ try:
30
+ text = session.prompt("janito🤖 ")
31
+ if text.strip():
32
+ processor.process_command(text)
33
+ except KeyboardInterrupt:
34
+ continue
35
+ except EOFError:
36
+ break
37
+ except Exception as e:
38
+ print(f"Error: {e}")
39
+ print("Goodbye!")
janito/shell/bus.py ADDED
@@ -0,0 +1,31 @@
1
+ """Command bus implementation for Janito shell."""
2
+ from typing import Dict, Callable, Any, Optional
3
+ from dataclasses import dataclass
4
+
5
+ @dataclass
6
+ class Command:
7
+ """Command message for command bus."""
8
+ name: str
9
+ args: str
10
+
11
+ class CommandBus:
12
+ """Simple command bus implementation."""
13
+ _instance = None
14
+ _handlers: Dict[str, Callable[[Command], None]] = {}
15
+
16
+ def __new__(cls):
17
+ if cls._instance is None:
18
+ cls._instance = super().__new__(cls)
19
+ cls._instance._handlers = {}
20
+ return cls._instance
21
+
22
+ def register_handler(self, command_name: str, handler: Callable[[Command], None]) -> None:
23
+ """Register a handler for a command."""
24
+ self._handlers[command_name] = handler
25
+
26
+ def handle(self, command: Command) -> None:
27
+ """Handle a command by dispatching to appropriate handler."""
28
+ if handler := self._handlers.get(command.name):
29
+ handler(command)
30
+ else:
31
+ raise ValueError(f"No handler registered for command: {command.name}")
@@ -0,0 +1,195 @@
1
+ """Command system for Janito shell."""
2
+ from dataclasses import dataclass
3
+ from typing import Optional, Callable, Dict
4
+ from pathlib import Path
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+ from prompt_toolkit import PromptSession
8
+ from prompt_toolkit.shortcuts import clear as ptk_clear
9
+ from janito.config import config
10
+ from janito.workspace import collect_files_content
11
+ from janito.workspace.analysis import analyze_workspace_content
12
+
13
+ @dataclass
14
+ class Command:
15
+ """Command definition with handler."""
16
+ name: str
17
+ description: str
18
+ usage: Optional[str]
19
+ handler: Callable[[str], None]
20
+
21
+ class CommandSystem:
22
+ """Centralized command management system."""
23
+ _instance = None
24
+ _commands: Dict[str, Command] = {}
25
+
26
+ def __new__(cls):
27
+ if cls._instance is None:
28
+ cls._instance = super().__new__(cls)
29
+ cls._instance._commands = {}
30
+ return cls._instance
31
+
32
+ def register(self, command: Command) -> None:
33
+ """Register a command."""
34
+ self._commands[command.name] = command
35
+
36
+ def get_command(self, name: str) -> Optional[Command]:
37
+ """Get a command by name."""
38
+ return self._commands.get(name)
39
+
40
+ def get_commands(self) -> Dict[str, Command]:
41
+ """Get all registered commands."""
42
+ return self._commands.copy()
43
+
44
+ def register_alias(self, alias: str, command_name: str) -> None:
45
+ """Register an alias for a command."""
46
+ if command := self.get_command(command_name):
47
+ if alias in self._commands:
48
+ raise ValueError(f"Alias '{alias}' already registered")
49
+ self._commands[alias] = command
50
+
51
+ def handle_request(args: str) -> None:
52
+ """Handle a change request."""
53
+ if not args:
54
+ Console().print("[red]Error: Change request required[/red]")
55
+ return
56
+ from janito.cli.commands import handle_request as cli_handle_request
57
+ cli_handle_request(args)
58
+
59
+ def handle_exit(_: str) -> None:
60
+ """Handle exit command."""
61
+ raise EOFError()
62
+
63
+ def handle_clear(_: str) -> None:
64
+ """Handle clear command."""
65
+ ptk_clear()
66
+
67
+ def handle_ask(args: str) -> None:
68
+ """Handle ask command."""
69
+ if not args:
70
+ Console().print("[red]Error: Question required[/red]")
71
+ return
72
+ from janito.cli.commands import handle_ask as cli_handle_ask
73
+ cli_handle_ask(args)
74
+
75
+ def handle_help(args: str) -> None:
76
+ """Handle help command."""
77
+ console = Console()
78
+ system = CommandSystem()
79
+
80
+ command = args.strip() if args else None
81
+ if command and (cmd := system.get_command(command)):
82
+ console.print(f"\n[bold]{command}[/bold]: {cmd.description}")
83
+ if cmd.usage:
84
+ console.print(f"Usage: {cmd.usage}")
85
+ return
86
+
87
+ table = Table(title="Available Commands")
88
+ table.add_column("Command", style="cyan")
89
+ table.add_column("Description")
90
+
91
+ for name, cmd in sorted(system.get_commands().items()):
92
+ table.add_row(name, cmd.description)
93
+
94
+ console.print(table)
95
+
96
+ def handle_include(args: str) -> None:
97
+ """Handle include command."""
98
+ console = Console()
99
+ session = PromptSession()
100
+
101
+ if not args:
102
+ try:
103
+ args = session.prompt("Enter paths (space separated): ")
104
+ except (KeyboardInterrupt, EOFError):
105
+ return
106
+
107
+ paths = [p.strip() for p in args.split() if p.strip()]
108
+ if not paths:
109
+ console.print("[red]Error: At least one path required[/red]")
110
+ return
111
+
112
+ resolved_paths = []
113
+ for path_str in paths:
114
+ path = Path(path_str)
115
+ if not path.is_absolute():
116
+ path = config.workspace_dir / path
117
+ resolved_paths.append(path.resolve())
118
+
119
+ config.set_include(resolved_paths)
120
+ content = collect_files_content(resolved_paths)
121
+ analyze_workspace_content(content)
122
+
123
+ console.print("[green]Updated include paths:[/green]")
124
+ for path in resolved_paths:
125
+ console.print(f" {path}")
126
+
127
+ from prompt_toolkit.completion import PathCompleter
128
+ from prompt_toolkit.document import Document
129
+
130
+ def handle_rinclude(args: str) -> None:
131
+ """Handle recursive include command."""
132
+ console = Console()
133
+ session = PromptSession()
134
+ completer = PathCompleter(only_directories=True)
135
+
136
+ try:
137
+ if not args:
138
+ args = session.prompt("Enter directory paths (space separated): ", completer=completer)
139
+ else:
140
+ # For partial paths, show completion options
141
+ doc = Document(args)
142
+ completions = list(completer.get_completions(doc, None))
143
+ if completions:
144
+ # If single completion, use it directly
145
+ if len(completions) == 1:
146
+ args = completions[0].text
147
+ else:
148
+ # Show available completions
149
+ console.print("\nAvailable directories:")
150
+ for comp in completions:
151
+ console.print(f" {comp.text}")
152
+ return
153
+ except (KeyboardInterrupt, EOFError):
154
+ return
155
+
156
+ paths = [p.strip() for p in args.split() if p.strip()]
157
+ if not paths:
158
+ console.print("[red]Error: At least one path required[/red]")
159
+ return
160
+
161
+ resolved_paths = []
162
+ for path_str in paths:
163
+ path = Path(path_str)
164
+ if not path.is_absolute():
165
+ path = config.workspace_dir / path
166
+ resolved_paths.append(path.resolve())
167
+
168
+ config.set_recursive(resolved_paths)
169
+ config.set_include(resolved_paths)
170
+ content = collect_files_content(resolved_paths)
171
+ analyze_workspace_content(content)
172
+
173
+ console.print("[green]Updated recursive include paths:[/green]")
174
+ for path in resolved_paths:
175
+ console.print(f" {path}")
176
+
177
+ def register_commands() -> None:
178
+ """Register all available commands."""
179
+ system = CommandSystem()
180
+
181
+ # Register main commands
182
+ system.register(Command("/clear", "Clear the terminal screen", None, handle_clear))
183
+ system.register(Command("/request", "Submit a change request", "/request <change request text>", handle_request))
184
+ system.register(Command("/ask", "Ask a question about the codebase", "/ask <question>", handle_ask))
185
+ system.register(Command("/quit", "Exit the shell", None, handle_exit))
186
+ system.register(Command("/help", "Show help for commands", "/help [command]", handle_help))
187
+ system.register(Command("/include", "Set paths to include in analysis", "/include <path1> [path2 ...]", handle_include))
188
+ system.register(Command("/rinclude", "Set paths to include recursively", "/rinclude <path1> [path2 ...]", handle_rinclude))
189
+
190
+ # Register aliases
191
+ system.register_alias("clear", "/clear")
192
+ system.register_alias("quit", "/quit")
193
+ system.register_alias("help", "/help")
194
+ system.register_alias("/inc", "/include")
195
+ system.register_alias("/rinc", "/rinclude")
@@ -0,0 +1,122 @@
1
+ """Command handlers for Janito shell."""
2
+ from pathlib import Path
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+ from prompt_toolkit import PromptSession
6
+ from prompt_toolkit.completion import PathCompleter
7
+ from prompt_toolkit.shortcuts import clear as ptk_clear
8
+ from janito.config import config
9
+ from janito.scan import collect_files_content
10
+ from janito.scan.analysis import analyze_workspace_content
11
+ from .registry import CommandRegistry, get_path_completer
12
+
13
+ def handle_request(args: str) -> None:
14
+ """Handle a change request."""
15
+ if not args:
16
+ Console().print("[red]Error: Change request required[/red]")
17
+ return
18
+ from janito.cli.commands import handle_request as cli_handle_request
19
+ cli_handle_request(args)
20
+
21
+ def handle_exit(_: str) -> None:
22
+ """Handle exit command."""
23
+ raise EOFError()
24
+
25
+ def handle_clear(_: str) -> None:
26
+ """Handle clear command to clear terminal screen."""
27
+ ptk_clear()
28
+
29
+ def handle_ask(args: str) -> None:
30
+ """Handle ask command."""
31
+ if not args:
32
+ Console().print("[red]Error: Question required[/red]")
33
+ return
34
+ from janito.cli.commands import handle_ask as cli_handle_ask
35
+ cli_handle_ask(args)
36
+
37
+ def handle_help(args: str) -> None:
38
+ """Handle help command."""
39
+ console = Console()
40
+ registry = CommandRegistry()
41
+
42
+ command = args.strip() if args else None
43
+ if command and (cmd := registry.get_command(command)):
44
+ console.print(f"\n[bold]{command}[/bold]: {cmd.description}")
45
+ if cmd.usage:
46
+ console.print(f"Usage: {cmd.usage}")
47
+ return
48
+
49
+ table = Table(title="Available Commands")
50
+ table.add_column("Command", style="cyan")
51
+ table.add_column("Description")
52
+
53
+ for name, cmd in sorted(registry.get_commands().items()):
54
+ table.add_row(name, cmd.description)
55
+
56
+ console.print(table)
57
+
58
+ def handle_include(args: str) -> None:
59
+ """Handle include command with path completion."""
60
+ console = Console()
61
+ session = PromptSession()
62
+ completer = PathCompleter()
63
+
64
+ # If no args provided, prompt with completion
65
+ if not args:
66
+ try:
67
+ args = session.prompt("Enter paths (space separated): ", completer=completer)
68
+ except (KeyboardInterrupt, EOFError):
69
+ return
70
+
71
+ paths = [p.strip() for p in args.split() if p.strip()]
72
+ if not paths:
73
+ console.print("[red]Error: At least one path required[/red]")
74
+ return
75
+
76
+ resolved_paths = []
77
+ for path_str in paths:
78
+ path = Path(path_str)
79
+ if not path.is_absolute():
80
+ path = config.workdir / path
81
+ resolved_paths.append(path.resolve())
82
+
83
+ config.set_include(resolved_paths)
84
+ content = collect_files_content(resolved_paths)
85
+ analyze_workspace_content(content)
86
+
87
+ console.print(f"[green]Updated include paths:[/green]")
88
+ for path in resolved_paths:
89
+ console.print(f" {path}")
90
+
91
+ def handle_rinclude(args: str) -> None:
92
+ """Handle rinclude command with recursive path scanning."""
93
+ console = Console()
94
+ session = PromptSession()
95
+ completer = get_path_completer(only_directories=True)
96
+
97
+ if not args:
98
+ try:
99
+ args = session.prompt("Enter directory paths (space separated): ", completer=completer)
100
+ except (KeyboardInterrupt, EOFError):
101
+ return
102
+
103
+ paths = [p.strip() for p in args.split() if p.strip()]
104
+ if not paths:
105
+ console.print("[red]Error: At least one path required[/red]")
106
+ return
107
+
108
+ resolved_paths = []
109
+ for path_str in paths:
110
+ path = Path(path_str)
111
+ if not path.is_absolute():
112
+ path = config.workdir / path
113
+ resolved_paths.append(path.resolve())
114
+
115
+ config.set_recursive(resolved_paths)
116
+ config.set_include(resolved_paths)
117
+ content = collect_files_content(resolved_paths)
118
+ analyze_workspace_content(content)
119
+
120
+ console.print(f"[green]Updated recursive include paths:[/green]")
121
+ for path in resolved_paths:
122
+ console.print(f" {path}")