cicada-mcp 0.2.0__py3-none-any.whl → 0.3.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.
- cicada/_version_hash.py +4 -0
- cicada/cli.py +6 -748
- cicada/commands.py +1255 -0
- cicada/dead_code/__init__.py +1 -0
- cicada/{find_dead_code.py → dead_code/finder.py} +2 -1
- cicada/dependency_analyzer.py +147 -0
- cicada/entry_utils.py +92 -0
- cicada/extractors/base.py +9 -9
- cicada/extractors/call.py +17 -20
- cicada/extractors/common.py +64 -0
- cicada/extractors/dependency.py +117 -235
- cicada/extractors/doc.py +2 -49
- cicada/extractors/function.py +10 -14
- cicada/extractors/keybert.py +228 -0
- cicada/extractors/keyword.py +191 -0
- cicada/extractors/module.py +6 -10
- cicada/extractors/spec.py +8 -56
- cicada/format/__init__.py +20 -0
- cicada/{ascii_art.py → format/ascii_art.py} +1 -1
- cicada/format/formatter.py +1145 -0
- cicada/git_helper.py +134 -7
- cicada/indexer.py +322 -89
- cicada/interactive_setup.py +251 -323
- cicada/interactive_setup_helpers.py +302 -0
- cicada/keyword_expander.py +437 -0
- cicada/keyword_search.py +208 -422
- cicada/keyword_test.py +383 -16
- cicada/mcp/__init__.py +10 -0
- cicada/mcp/entry.py +17 -0
- cicada/mcp/filter_utils.py +107 -0
- cicada/mcp/pattern_utils.py +118 -0
- cicada/{mcp_server.py → mcp/server.py} +819 -73
- cicada/mcp/tools.py +473 -0
- cicada/pr_finder.py +2 -3
- cicada/pr_indexer/indexer.py +3 -2
- cicada/setup.py +167 -35
- cicada/tier.py +225 -0
- cicada/utils/__init__.py +9 -2
- cicada/utils/fuzzy_match.py +54 -0
- cicada/utils/index_utils.py +9 -0
- cicada/utils/path_utils.py +18 -0
- cicada/utils/text_utils.py +52 -1
- cicada/utils/tree_utils.py +47 -0
- cicada/version_check.py +99 -0
- cicada/watch_manager.py +320 -0
- cicada/watcher.py +431 -0
- cicada_mcp-0.3.0.dist-info/METADATA +541 -0
- cicada_mcp-0.3.0.dist-info/RECORD +70 -0
- cicada_mcp-0.3.0.dist-info/entry_points.txt +4 -0
- cicada/formatter.py +0 -864
- cicada/keybert_extractor.py +0 -286
- cicada/lightweight_keyword_extractor.py +0 -290
- cicada/mcp_entry.py +0 -683
- cicada/mcp_tools.py +0 -291
- cicada_mcp-0.2.0.dist-info/METADATA +0 -735
- cicada_mcp-0.2.0.dist-info/RECORD +0 -53
- cicada_mcp-0.2.0.dist-info/entry_points.txt +0 -4
- /cicada/{dead_code_analyzer.py → dead_code/analyzer.py} +0 -0
- /cicada/{colors.py → format/colors.py} +0 -0
- {cicada_mcp-0.2.0.dist-info → cicada_mcp-0.3.0.dist-info}/WHEEL +0 -0
- {cicada_mcp-0.2.0.dist-info → cicada_mcp-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {cicada_mcp-0.2.0.dist-info → cicada_mcp-0.3.0.dist-info}/top_level.txt +0 -0
cicada/watcher.py
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File system watcher for automatic Elixir code reindexing.
|
|
3
|
+
|
|
4
|
+
This module provides the FileWatcher class which monitors Elixir source files
|
|
5
|
+
for changes and automatically triggers incremental reindexing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import signal
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from watchdog.events import FileSystemEvent, FileSystemEventHandler
|
|
17
|
+
from watchdog.observers import Observer
|
|
18
|
+
|
|
19
|
+
from cicada.indexer import ElixirIndexer
|
|
20
|
+
from cicada.utils.storage import create_storage_dir
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ElixirFileEventHandler(FileSystemEventHandler):
|
|
26
|
+
"""
|
|
27
|
+
Event handler for file system changes.
|
|
28
|
+
|
|
29
|
+
Filters events to only process Elixir source files (.ex, .exs)
|
|
30
|
+
and ignores changes in excluded directories.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, watcher: "FileWatcher"):
|
|
34
|
+
"""
|
|
35
|
+
Initialize the event handler.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
watcher: The FileWatcher instance to notify of changes
|
|
39
|
+
"""
|
|
40
|
+
super().__init__()
|
|
41
|
+
self.watcher = watcher
|
|
42
|
+
# Reuse excluded_dirs from ElixirIndexer to avoid duplication
|
|
43
|
+
self.excluded_dirs = ElixirIndexer(verbose=False).excluded_dirs
|
|
44
|
+
|
|
45
|
+
def _is_elixir_file(self, path: str) -> bool:
|
|
46
|
+
"""Check if the path is an Elixir source file."""
|
|
47
|
+
return path.endswith((".ex", ".exs"))
|
|
48
|
+
|
|
49
|
+
def _is_excluded_path(self, path: str) -> bool:
|
|
50
|
+
"""Check if the path is in an excluded directory."""
|
|
51
|
+
path_parts = Path(path).parts
|
|
52
|
+
return any(excluded in path_parts for excluded in self.excluded_dirs)
|
|
53
|
+
|
|
54
|
+
def on_any_event(self, event: FileSystemEvent) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Handle file system events.
|
|
57
|
+
|
|
58
|
+
Filters events to only process Elixir files not in excluded directories,
|
|
59
|
+
then notifies the watcher to trigger reindexing.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
event: The file system event
|
|
63
|
+
"""
|
|
64
|
+
# Ignore directory events
|
|
65
|
+
if event.is_directory:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
# Convert src_path to string (it can be bytes or str)
|
|
69
|
+
src_path = str(event.src_path)
|
|
70
|
+
|
|
71
|
+
# Only process Elixir source files
|
|
72
|
+
if not self._is_elixir_file(src_path):
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
# Skip excluded directories
|
|
76
|
+
if self._is_excluded_path(src_path):
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
# Notify watcher of the change
|
|
80
|
+
self.watcher._on_file_change(event)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class FileWatcher:
|
|
84
|
+
"""
|
|
85
|
+
Watches Elixir source files and triggers automatic reindexing on changes.
|
|
86
|
+
|
|
87
|
+
The watcher monitors .ex and .exs files in a repository, excluding
|
|
88
|
+
standard directories like deps, _build, and node_modules. When changes
|
|
89
|
+
are detected, it debounces the events and triggers incremental reindexing.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
repo_path: str,
|
|
95
|
+
debounce_seconds: float = 2.0,
|
|
96
|
+
verbose: bool = True,
|
|
97
|
+
tier: str = "regular",
|
|
98
|
+
register_signal_handlers: bool = True,
|
|
99
|
+
):
|
|
100
|
+
"""
|
|
101
|
+
Initialize the file watcher.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
repo_path: Path to the repository to watch
|
|
105
|
+
debounce_seconds: Seconds to wait before triggering reindex after changes
|
|
106
|
+
verbose: Whether to show detailed indexing progress
|
|
107
|
+
tier: Indexing tier (fast, regular, or max)
|
|
108
|
+
register_signal_handlers: Whether to register SIGINT/SIGTERM handlers (disable for testing)
|
|
109
|
+
"""
|
|
110
|
+
self.repo_path = Path(repo_path).resolve()
|
|
111
|
+
self.debounce_seconds = debounce_seconds
|
|
112
|
+
self.verbose = verbose
|
|
113
|
+
self.tier = tier
|
|
114
|
+
|
|
115
|
+
self.observer: Observer | None = None # type: ignore[valid-type]
|
|
116
|
+
self.indexer: ElixirIndexer | None = None
|
|
117
|
+
self.debounce_timer: threading.Timer | None = None
|
|
118
|
+
self.timer_lock = threading.Lock()
|
|
119
|
+
self.running = False
|
|
120
|
+
self.shutdown_event = threading.Event()
|
|
121
|
+
self._consecutive_failures = 0 # Track consecutive reindex failures
|
|
122
|
+
|
|
123
|
+
# Set up signal handlers for graceful shutdown (unless disabled for testing)
|
|
124
|
+
if register_signal_handlers:
|
|
125
|
+
signal.signal(signal.SIGINT, self._signal_handler)
|
|
126
|
+
signal.signal(signal.SIGTERM, self._signal_handler)
|
|
127
|
+
|
|
128
|
+
def _signal_handler(self, signum: int, _frame) -> None:
|
|
129
|
+
"""Handle shutdown signals (SIGINT, SIGTERM)."""
|
|
130
|
+
signal_name = signal.Signals(signum).name
|
|
131
|
+
logger.info(f"Received {signal_name} signal")
|
|
132
|
+
print(f"\n\nReceived {signal_name} signal. Stopping watcher...")
|
|
133
|
+
|
|
134
|
+
self.shutdown_event.set()
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
self.stop_watching()
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.exception(f"Error during signal handler cleanup: {e}")
|
|
140
|
+
print(f"Warning: Error during cleanup: {e}", file=sys.stderr)
|
|
141
|
+
# Still exit even if cleanup fails
|
|
142
|
+
finally:
|
|
143
|
+
sys.exit(0)
|
|
144
|
+
|
|
145
|
+
def start_watching(self) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Start watching for file changes.
|
|
148
|
+
|
|
149
|
+
This method:
|
|
150
|
+
1. Creates storage directory if needed
|
|
151
|
+
2. Runs an initial index to ensure up-to-date state
|
|
152
|
+
3. Starts the file system observer
|
|
153
|
+
4. Blocks until interrupted (Ctrl-C)
|
|
154
|
+
"""
|
|
155
|
+
if self.running:
|
|
156
|
+
logger.warning("Watcher is already running")
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
print(f"Initializing watch mode for {self.repo_path}")
|
|
160
|
+
print(f"Debounce interval: {self.debounce_seconds}s")
|
|
161
|
+
print()
|
|
162
|
+
|
|
163
|
+
# Ensure storage directory exists
|
|
164
|
+
create_storage_dir(self.repo_path)
|
|
165
|
+
|
|
166
|
+
# Get index path
|
|
167
|
+
from cicada.utils.storage import get_index_path
|
|
168
|
+
|
|
169
|
+
index_path = get_index_path(self.repo_path)
|
|
170
|
+
|
|
171
|
+
# Create indexer instance
|
|
172
|
+
self.indexer = ElixirIndexer(verbose=self.verbose)
|
|
173
|
+
|
|
174
|
+
# Run initial index
|
|
175
|
+
print("Running initial index...")
|
|
176
|
+
try:
|
|
177
|
+
self.indexer.incremental_index_repository(
|
|
178
|
+
repo_path=str(self.repo_path),
|
|
179
|
+
output_path=str(index_path),
|
|
180
|
+
extract_keywords=True,
|
|
181
|
+
force_full=False,
|
|
182
|
+
)
|
|
183
|
+
print("\nInitial indexing complete!")
|
|
184
|
+
print()
|
|
185
|
+
except KeyboardInterrupt:
|
|
186
|
+
print("\n\nInitial indexing interrupted. Exiting...")
|
|
187
|
+
return
|
|
188
|
+
except (MemoryError, OSError) as e:
|
|
189
|
+
# System-level failures - don't continue regardless of existing index
|
|
190
|
+
print("\n" + "=" * 70)
|
|
191
|
+
print("CRITICAL: System error during initial indexing!")
|
|
192
|
+
print("=" * 70)
|
|
193
|
+
print(f"Error: {e}")
|
|
194
|
+
print("\nWatch mode cannot start due to system-level failure.")
|
|
195
|
+
if isinstance(e, MemoryError):
|
|
196
|
+
print("Your system is out of memory. Close other applications and try again.")
|
|
197
|
+
elif isinstance(e, OSError):
|
|
198
|
+
print(f"File system error: {e}")
|
|
199
|
+
print("Check disk space, permissions, and filesystem health.")
|
|
200
|
+
print("=" * 70)
|
|
201
|
+
logger.critical(f"System error during initial indexing: {e}", exc_info=True)
|
|
202
|
+
sys.exit(1)
|
|
203
|
+
except Exception as e:
|
|
204
|
+
# Other failures - check if we can use existing index
|
|
205
|
+
logger.exception("Initial indexing failed")
|
|
206
|
+
self._handle_initial_index_failure(e, index_path)
|
|
207
|
+
|
|
208
|
+
# Set up file system observer
|
|
209
|
+
event_handler = ElixirFileEventHandler(self)
|
|
210
|
+
self.observer = Observer()
|
|
211
|
+
self.observer.schedule(event_handler, str(self.repo_path), recursive=True)
|
|
212
|
+
|
|
213
|
+
# Start observing
|
|
214
|
+
self.observer.start()
|
|
215
|
+
self.running = True
|
|
216
|
+
|
|
217
|
+
print("=" * 70)
|
|
218
|
+
print("Watching for changes to Elixir files (.ex, .exs)")
|
|
219
|
+
print("=" * 70)
|
|
220
|
+
print("Press Ctrl-C to stop")
|
|
221
|
+
print()
|
|
222
|
+
|
|
223
|
+
# Block until interrupted
|
|
224
|
+
try:
|
|
225
|
+
while not self.shutdown_event.is_set():
|
|
226
|
+
time.sleep(1)
|
|
227
|
+
except KeyboardInterrupt:
|
|
228
|
+
print("\n\nStopping watcher...")
|
|
229
|
+
finally:
|
|
230
|
+
self.stop_watching()
|
|
231
|
+
|
|
232
|
+
def stop_watching(self) -> None:
|
|
233
|
+
"""Stop watching for file changes and clean up resources."""
|
|
234
|
+
if not self.running:
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
self.running = False
|
|
238
|
+
|
|
239
|
+
# Cancel pending debounce timer
|
|
240
|
+
with self.timer_lock:
|
|
241
|
+
if self.debounce_timer is not None:
|
|
242
|
+
try:
|
|
243
|
+
self.debounce_timer.cancel()
|
|
244
|
+
# Note: cancel() only prevents future execution,
|
|
245
|
+
# callback might still be running
|
|
246
|
+
logger.debug("Cancelled pending debounce timer")
|
|
247
|
+
except Exception as e:
|
|
248
|
+
logger.warning(f"Error cancelling timer: {e}")
|
|
249
|
+
finally:
|
|
250
|
+
self.debounce_timer = None
|
|
251
|
+
|
|
252
|
+
# Stop observer
|
|
253
|
+
if self.observer is not None:
|
|
254
|
+
self.observer.stop()
|
|
255
|
+
self.observer.join(timeout=5)
|
|
256
|
+
|
|
257
|
+
if self.observer.is_alive():
|
|
258
|
+
logger.warning("Observer thread did not stop within timeout")
|
|
259
|
+
print("Warning: File watcher thread did not stop cleanly", file=sys.stderr)
|
|
260
|
+
# Still clear reference to allow GC, but log the issue
|
|
261
|
+
|
|
262
|
+
self.observer = None
|
|
263
|
+
|
|
264
|
+
print("Watcher stopped.")
|
|
265
|
+
|
|
266
|
+
def _handle_initial_index_failure(self, error: Exception, index_path: Path) -> None:
|
|
267
|
+
"""Handle failure during initial indexing.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
error: The exception that occurred
|
|
271
|
+
index_path: Path to the index file
|
|
272
|
+
|
|
273
|
+
Raises:
|
|
274
|
+
SystemExit: If watch mode cannot continue
|
|
275
|
+
"""
|
|
276
|
+
if not index_path.exists():
|
|
277
|
+
# No existing index - must exit
|
|
278
|
+
print("\n" + "=" * 70)
|
|
279
|
+
print("ERROR: Initial indexing failed and no existing index found!")
|
|
280
|
+
print("=" * 70)
|
|
281
|
+
print(f"Error: {error}")
|
|
282
|
+
print("\nWatch mode cannot start without an index.")
|
|
283
|
+
print("Please fix the error and try again, or run:")
|
|
284
|
+
print(f" cicada index {self.repo_path}")
|
|
285
|
+
print("=" * 70)
|
|
286
|
+
sys.exit(1)
|
|
287
|
+
|
|
288
|
+
# Try to load existing index to verify it's usable
|
|
289
|
+
try:
|
|
290
|
+
with open(index_path) as f:
|
|
291
|
+
index = json.load(f)
|
|
292
|
+
if not index or not index.get("modules"):
|
|
293
|
+
raise ValueError("Existing index is empty or corrupted")
|
|
294
|
+
|
|
295
|
+
# Existing index appears valid
|
|
296
|
+
print("\n" + "=" * 70)
|
|
297
|
+
print("WARNING: Initial indexing failed!")
|
|
298
|
+
print("=" * 70)
|
|
299
|
+
print(f"Error: {error}")
|
|
300
|
+
print("\nAn existing index was found and verified as usable.")
|
|
301
|
+
print("Watch mode will continue, but the index may be outdated.")
|
|
302
|
+
print("\nTo fix this issue, run:")
|
|
303
|
+
print(f" cicada clean && cicada index {self.repo_path}")
|
|
304
|
+
print("=" * 70)
|
|
305
|
+
print()
|
|
306
|
+
except Exception as load_error:
|
|
307
|
+
# Existing index is corrupted or unusable
|
|
308
|
+
print("\n" + "=" * 70)
|
|
309
|
+
print("ERROR: Initial indexing failed and existing index is corrupted!")
|
|
310
|
+
print("=" * 70)
|
|
311
|
+
print(f"Indexing error: {error}")
|
|
312
|
+
print(f"Index validation error: {load_error}")
|
|
313
|
+
print("\nCannot start watch mode. Please fix the issue:")
|
|
314
|
+
print(f" cicada clean && cicada index {self.repo_path}")
|
|
315
|
+
print("=" * 70)
|
|
316
|
+
logger.error(f"Existing index corrupted: {load_error}")
|
|
317
|
+
sys.exit(1)
|
|
318
|
+
|
|
319
|
+
def _on_file_change(self, event: FileSystemEvent) -> None:
|
|
320
|
+
"""
|
|
321
|
+
Handle file change events with debouncing.
|
|
322
|
+
|
|
323
|
+
When a file changes, this method cancels any pending reindex timer
|
|
324
|
+
and starts a new one. This ensures that rapid successive changes
|
|
325
|
+
only trigger a single reindex operation.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
event: The file system event
|
|
329
|
+
"""
|
|
330
|
+
with self.timer_lock:
|
|
331
|
+
self._cancel_pending_timer()
|
|
332
|
+
self._start_new_timer()
|
|
333
|
+
|
|
334
|
+
def _cancel_pending_timer(self) -> None:
|
|
335
|
+
"""Cancel any pending debounce timer."""
|
|
336
|
+
if self.debounce_timer is None:
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
self.debounce_timer.cancel()
|
|
341
|
+
logger.debug("Cancelled previous debounce timer due to new file change")
|
|
342
|
+
except Exception as e:
|
|
343
|
+
logger.warning(f"Error cancelling previous timer: {e}")
|
|
344
|
+
|
|
345
|
+
def _start_new_timer(self) -> None:
|
|
346
|
+
"""Start a new debounce timer."""
|
|
347
|
+
self.debounce_timer = threading.Timer(
|
|
348
|
+
self.debounce_seconds,
|
|
349
|
+
self._trigger_reindex,
|
|
350
|
+
)
|
|
351
|
+
self.debounce_timer.daemon = True
|
|
352
|
+
self.debounce_timer.start()
|
|
353
|
+
|
|
354
|
+
def _trigger_reindex(self) -> None:
|
|
355
|
+
"""
|
|
356
|
+
Trigger incremental reindexing.
|
|
357
|
+
|
|
358
|
+
This method is called after the debounce period has elapsed.
|
|
359
|
+
It runs the incremental indexer and handles any errors gracefully.
|
|
360
|
+
"""
|
|
361
|
+
with self.timer_lock:
|
|
362
|
+
self.debounce_timer = None
|
|
363
|
+
|
|
364
|
+
print("\n" + "=" * 70)
|
|
365
|
+
print("File changes detected - reindexing...")
|
|
366
|
+
print("=" * 70)
|
|
367
|
+
print()
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
if self.indexer is not None:
|
|
371
|
+
from cicada.utils.storage import get_index_path
|
|
372
|
+
|
|
373
|
+
index_path = get_index_path(self.repo_path)
|
|
374
|
+
self.indexer.incremental_index_repository(
|
|
375
|
+
repo_path=str(self.repo_path),
|
|
376
|
+
output_path=str(index_path),
|
|
377
|
+
extract_keywords=True,
|
|
378
|
+
force_full=False,
|
|
379
|
+
)
|
|
380
|
+
print()
|
|
381
|
+
print("=" * 70)
|
|
382
|
+
print("Reindexing complete!")
|
|
383
|
+
print("=" * 70)
|
|
384
|
+
print()
|
|
385
|
+
|
|
386
|
+
# Reset failure counter on success
|
|
387
|
+
self._consecutive_failures = 0
|
|
388
|
+
|
|
389
|
+
except KeyboardInterrupt:
|
|
390
|
+
# Don't catch interrupts - let them propagate
|
|
391
|
+
print("\n\nReindexing interrupted.")
|
|
392
|
+
raise
|
|
393
|
+
|
|
394
|
+
except (MemoryError, OSError) as e:
|
|
395
|
+
# System-level errors - warn but continue (might be transient)
|
|
396
|
+
print()
|
|
397
|
+
print("=" * 70)
|
|
398
|
+
print(f"SYSTEM ERROR during reindexing: {e}")
|
|
399
|
+
print("=" * 70)
|
|
400
|
+
if isinstance(e, MemoryError):
|
|
401
|
+
print("Your system is out of memory.")
|
|
402
|
+
elif isinstance(e, OSError):
|
|
403
|
+
print(f"File system error: {e}")
|
|
404
|
+
print("\nWatcher will continue, but next reindex may also fail.")
|
|
405
|
+
print("If this persists, stop the watcher and check system resources.")
|
|
406
|
+
print("=" * 70)
|
|
407
|
+
print()
|
|
408
|
+
logger.error(f"System error during reindex: {e}", exc_info=True)
|
|
409
|
+
|
|
410
|
+
except Exception as e:
|
|
411
|
+
# Unexpected errors - track consecutive failures
|
|
412
|
+
print()
|
|
413
|
+
print("=" * 70)
|
|
414
|
+
print(f"ERROR during reindexing: {e}")
|
|
415
|
+
print("=" * 70)
|
|
416
|
+
print("Continuing to watch for changes...")
|
|
417
|
+
print()
|
|
418
|
+
logger.exception("Reindexing failed")
|
|
419
|
+
|
|
420
|
+
# Track consecutive failures
|
|
421
|
+
self._consecutive_failures += 1
|
|
422
|
+
|
|
423
|
+
if self._consecutive_failures >= 3:
|
|
424
|
+
print("=" * 70)
|
|
425
|
+
print("WARNING: Reindexing has failed 3 consecutive times!")
|
|
426
|
+
print("=" * 70)
|
|
427
|
+
print("The watcher may be broken. Consider stopping it and investigating.")
|
|
428
|
+
print("Check logs for details. You may need to run:")
|
|
429
|
+
print(f" cicada clean && cicada index {self.repo_path}")
|
|
430
|
+
print("=" * 70)
|
|
431
|
+
logger.error("Multiple consecutive reindex failures detected")
|