portacode 0.3.4.dev0__py3-none-any.whl → 1.4.11.dev0__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.
Potentially problematic release.
This version of portacode might be problematic. Click here for more details.
- portacode/_version.py +16 -3
- portacode/cli.py +155 -19
- portacode/connection/client.py +152 -12
- portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +1577 -0
- portacode/connection/handlers/__init__.py +43 -1
- portacode/connection/handlers/base.py +122 -18
- portacode/connection/handlers/chunked_content.py +244 -0
- portacode/connection/handlers/diff_handlers.py +603 -0
- portacode/connection/handlers/file_handlers.py +902 -17
- portacode/connection/handlers/project_aware_file_handlers.py +226 -0
- portacode/connection/handlers/project_state/README.md +312 -0
- portacode/connection/handlers/project_state/__init__.py +92 -0
- portacode/connection/handlers/project_state/file_system_watcher.py +179 -0
- portacode/connection/handlers/project_state/git_manager.py +1502 -0
- portacode/connection/handlers/project_state/handlers.py +875 -0
- portacode/connection/handlers/project_state/manager.py +1331 -0
- portacode/connection/handlers/project_state/models.py +108 -0
- portacode/connection/handlers/project_state/utils.py +50 -0
- portacode/connection/handlers/project_state_handlers.py +45 -0
- portacode/connection/handlers/proxmox_infra.py +307 -0
- portacode/connection/handlers/registry.py +53 -10
- portacode/connection/handlers/session.py +705 -53
- portacode/connection/handlers/system_handlers.py +142 -8
- portacode/connection/handlers/tab_factory.py +389 -0
- portacode/connection/handlers/terminal_handlers.py +150 -11
- portacode/connection/handlers/update_handler.py +61 -0
- portacode/connection/multiplex.py +60 -2
- portacode/connection/terminal.py +695 -28
- portacode/keypair.py +63 -1
- portacode/link_capture/__init__.py +38 -0
- portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
- portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
- portacode/link_capture/bin/elinks +3 -0
- portacode/link_capture/bin/gio-open +3 -0
- portacode/link_capture/bin/gnome-open +3 -0
- portacode/link_capture/bin/gvfs-open +3 -0
- portacode/link_capture/bin/kde-open +3 -0
- portacode/link_capture/bin/kfmclient +3 -0
- portacode/link_capture/bin/link_capture_exec.sh +11 -0
- portacode/link_capture/bin/link_capture_wrapper.py +75 -0
- portacode/link_capture/bin/links +3 -0
- portacode/link_capture/bin/links2 +3 -0
- portacode/link_capture/bin/lynx +3 -0
- portacode/link_capture/bin/mate-open +3 -0
- portacode/link_capture/bin/netsurf +3 -0
- portacode/link_capture/bin/sensible-browser +3 -0
- portacode/link_capture/bin/w3m +3 -0
- portacode/link_capture/bin/x-www-browser +3 -0
- portacode/link_capture/bin/xdg-open +3 -0
- portacode/logging_categories.py +140 -0
- portacode/pairing.py +103 -0
- portacode/service.py +6 -0
- portacode/static/js/test-ntp-clock.html +63 -0
- portacode/static/js/utils/ntp-clock.js +232 -0
- portacode/utils/NTP_ARCHITECTURE.md +136 -0
- portacode/utils/__init__.py +1 -0
- portacode/utils/diff_apply.py +456 -0
- portacode/utils/diff_renderer.py +371 -0
- portacode/utils/ntp_clock.py +65 -0
- portacode-1.4.11.dev0.dist-info/METADATA +298 -0
- portacode-1.4.11.dev0.dist-info/RECORD +97 -0
- {portacode-0.3.4.dev0.dist-info → portacode-1.4.11.dev0.dist-info}/WHEEL +1 -1
- portacode-1.4.11.dev0.dist-info/top_level.txt +3 -0
- test_modules/README.md +296 -0
- test_modules/__init__.py +1 -0
- test_modules/test_device_online.py +44 -0
- test_modules/test_file_operations.py +743 -0
- test_modules/test_git_status_ui.py +370 -0
- test_modules/test_login_flow.py +50 -0
- test_modules/test_navigate_testing_folder.py +361 -0
- test_modules/test_play_store_screenshots.py +294 -0
- test_modules/test_terminal_buffer_performance.py +261 -0
- test_modules/test_terminal_interaction.py +80 -0
- test_modules/test_terminal_loading_race_condition.py +95 -0
- test_modules/test_terminal_start.py +56 -0
- testing_framework/.env.example +21 -0
- testing_framework/README.md +334 -0
- testing_framework/__init__.py +17 -0
- testing_framework/cli.py +326 -0
- testing_framework/core/__init__.py +1 -0
- testing_framework/core/base_test.py +336 -0
- testing_framework/core/cli_manager.py +177 -0
- testing_framework/core/hierarchical_runner.py +577 -0
- testing_framework/core/playwright_manager.py +520 -0
- testing_framework/core/runner.py +447 -0
- testing_framework/core/shared_cli_manager.py +234 -0
- testing_framework/core/test_discovery.py +112 -0
- testing_framework/requirements.txt +12 -0
- portacode-0.3.4.dev0.dist-info/METADATA +0 -236
- portacode-0.3.4.dev0.dist-info/RECORD +0 -27
- portacode-0.3.4.dev0.dist-info/top_level.txt +0 -1
- {portacode-0.3.4.dev0.dist-info → portacode-1.4.11.dev0.dist-info}/entry_points.txt +0 -0
- {portacode-0.3.4.dev0.dist-info → portacode-1.4.11.dev0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""File system monitoring for project state changes.
|
|
2
|
+
|
|
3
|
+
This module provides the FileSystemWatcher class which monitors file system
|
|
4
|
+
changes using the watchdog library and triggers project state updates when
|
|
5
|
+
files or directories are modified.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional, Set
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
# Cross-platform file system monitoring
|
|
17
|
+
try:
|
|
18
|
+
from watchdog.observers import Observer
|
|
19
|
+
from watchdog.events import FileSystemEventHandler
|
|
20
|
+
WATCHDOG_AVAILABLE = True
|
|
21
|
+
logger.info("Watchdog library available for file system monitoring")
|
|
22
|
+
except ImportError:
|
|
23
|
+
WATCHDOG_AVAILABLE = False
|
|
24
|
+
Observer = None
|
|
25
|
+
FileSystemEventHandler = None
|
|
26
|
+
logger.warning("Watchdog library not available - file system monitoring disabled")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FileSystemWatcher:
|
|
30
|
+
"""Watches file system changes for project folders."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, project_manager):
|
|
33
|
+
self.project_manager = project_manager # Reference to ProjectStateManager
|
|
34
|
+
self.observer: Optional[Observer] = None
|
|
35
|
+
self.event_handler: Optional[FileSystemEventHandler] = None
|
|
36
|
+
self.watched_paths: Set[str] = set()
|
|
37
|
+
self.watch_handles: dict = {} # Map path -> watch handle for proper cleanup
|
|
38
|
+
# Store reference to the event loop for thread-safe async task creation
|
|
39
|
+
try:
|
|
40
|
+
self.event_loop = asyncio.get_running_loop()
|
|
41
|
+
logger.debug("🔍 [TRACE] ✅ Captured event loop reference for file system watcher: %s", self.event_loop)
|
|
42
|
+
except RuntimeError:
|
|
43
|
+
self.event_loop = None
|
|
44
|
+
logger.debug("🔍 [TRACE] ❌ No running event loop found - file system events may not work correctly")
|
|
45
|
+
|
|
46
|
+
logger.debug("🔍 [TRACE] WATCHDOG_AVAILABLE: %s", WATCHDOG_AVAILABLE)
|
|
47
|
+
if WATCHDOG_AVAILABLE:
|
|
48
|
+
logger.debug("🔍 [TRACE] Initializing file system watcher...")
|
|
49
|
+
self._initialize_watcher()
|
|
50
|
+
else:
|
|
51
|
+
logger.debug("🔍 [TRACE] ❌ Watchdog not available - file monitoring disabled")
|
|
52
|
+
|
|
53
|
+
def _initialize_watcher(self):
|
|
54
|
+
"""Initialize file system watcher."""
|
|
55
|
+
if not WATCHDOG_AVAILABLE:
|
|
56
|
+
logger.warning("Watchdog not available, file monitoring disabled")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
class ProjectEventHandler(FileSystemEventHandler):
|
|
60
|
+
def __init__(self, manager, watcher):
|
|
61
|
+
self.manager = manager
|
|
62
|
+
self.watcher = watcher
|
|
63
|
+
super().__init__()
|
|
64
|
+
|
|
65
|
+
def on_any_event(self, event):
|
|
66
|
+
logger.debug("🔍 [TRACE] FileSystemWatcher detected event: %s on path: %s", event.event_type, event.src_path)
|
|
67
|
+
|
|
68
|
+
# Skip debug files to avoid feedback loops
|
|
69
|
+
if event.src_path.endswith('project_state_debug.json'):
|
|
70
|
+
logger.debug("🔍 [TRACE] Skipping debug file: %s", event.src_path)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
# Only process events that represent actual content changes
|
|
74
|
+
# Skip events that indicate read-only access or metadata churn
|
|
75
|
+
significant_event_types = {'modified', 'created', 'deleted', 'moved', 'closed_write'}
|
|
76
|
+
if event.event_type not in significant_event_types:
|
|
77
|
+
logger.debug("🔍 [TRACE] Skipping non-content event: %s", event.event_type)
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
# Reading directory contents during refresh generates metadata-only "modified" events.
|
|
81
|
+
if event.is_directory and event.event_type == 'modified':
|
|
82
|
+
logger.debug("🔍 [TRACE] Skipping directory metadata change: %s", event.src_path)
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
# Handle .git folder events separately for git status monitoring
|
|
86
|
+
path_parts = Path(event.src_path).parts
|
|
87
|
+
if '.git' in path_parts:
|
|
88
|
+
logger.debug("🔍 [TRACE] Skipping .git folder event entirely: %s", event.src_path)
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
logger.debug("🔍 [TRACE] Processing non-git file event: %s", event.src_path)
|
|
92
|
+
# Only log significant file changes, not every single event
|
|
93
|
+
if event.event_type in ['created', 'deleted'] or event.src_path.endswith(('.py', '.js', '.html', '.css', '.json', '.md')):
|
|
94
|
+
logger.debug("File system event: %s - %s", event.event_type, os.path.basename(event.src_path))
|
|
95
|
+
else:
|
|
96
|
+
logger.debug("File event: %s", os.path.basename(event.src_path))
|
|
97
|
+
|
|
98
|
+
# Schedule async task in the main event loop from this watchdog thread
|
|
99
|
+
logger.debug("🔍 [TRACE] About to schedule async handler - event_loop exists: %s, closed: %s",
|
|
100
|
+
self.watcher.event_loop is not None,
|
|
101
|
+
self.watcher.event_loop.is_closed() if self.watcher.event_loop else "N/A")
|
|
102
|
+
|
|
103
|
+
if self.watcher.event_loop and not self.watcher.event_loop.is_closed():
|
|
104
|
+
try:
|
|
105
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
106
|
+
self.manager._handle_file_change(event),
|
|
107
|
+
self.watcher.event_loop
|
|
108
|
+
)
|
|
109
|
+
logger.debug("🔍 [TRACE] ✅ Successfully scheduled file change handler for: %s", event.src_path)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.debug("🔍 [TRACE] ❌ Failed to schedule file change handler: %s", e)
|
|
112
|
+
else:
|
|
113
|
+
logger.debug("🔍 [TRACE] ❌ No event loop available to handle file change: %s", event.src_path)
|
|
114
|
+
|
|
115
|
+
self.event_handler = ProjectEventHandler(self.project_manager, self)
|
|
116
|
+
self.observer = Observer()
|
|
117
|
+
|
|
118
|
+
def start_watching(self, path: str):
|
|
119
|
+
"""Start watching a specific path."""
|
|
120
|
+
if not WATCHDOG_AVAILABLE or not self.observer:
|
|
121
|
+
logger.warning("Watchdog not available, cannot start watching: %s", path)
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
normalized_name = Path(path).name
|
|
125
|
+
if normalized_name == '.git':
|
|
126
|
+
logger.debug("Skipping watch for .git path: %s", path)
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
if path not in self.watched_paths:
|
|
130
|
+
try:
|
|
131
|
+
# Use recursive=False to watch only direct contents of each folder
|
|
132
|
+
watch_handle = self.observer.schedule(self.event_handler, path, recursive=False)
|
|
133
|
+
self.watched_paths.add(path)
|
|
134
|
+
self.watch_handles[path] = watch_handle # Store handle for cleanup
|
|
135
|
+
logger.info("Started watching path (non-recursive): %s", path)
|
|
136
|
+
|
|
137
|
+
if not self.observer.is_alive():
|
|
138
|
+
self.observer.start()
|
|
139
|
+
logger.info("Started file system observer")
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.error("Error starting file watcher for %s: %s", path, e)
|
|
142
|
+
else:
|
|
143
|
+
logger.debug("Path already being watched: %s", path)
|
|
144
|
+
|
|
145
|
+
def stop_watching(self, path: str):
|
|
146
|
+
"""Stop watching a specific path."""
|
|
147
|
+
if not WATCHDOG_AVAILABLE or not self.observer:
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
if path in self.watched_paths:
|
|
151
|
+
# Actually unschedule the watch using stored handle
|
|
152
|
+
watch_handle = self.watch_handles.get(path)
|
|
153
|
+
if watch_handle:
|
|
154
|
+
try:
|
|
155
|
+
self.observer.unschedule(watch_handle)
|
|
156
|
+
logger.info("Successfully unscheduled watch for: %s", path)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.error("Error unscheduling watch for %s: %s", path, e)
|
|
159
|
+
finally:
|
|
160
|
+
self.watch_handles.pop(path, None)
|
|
161
|
+
|
|
162
|
+
self.watched_paths.discard(path)
|
|
163
|
+
logger.debug("Stopped watching path: %s", path)
|
|
164
|
+
|
|
165
|
+
def stop_all(self):
|
|
166
|
+
"""Stop all file watching."""
|
|
167
|
+
if self.observer and self.observer.is_alive():
|
|
168
|
+
self.observer.stop()
|
|
169
|
+
self.observer.join()
|
|
170
|
+
self.watched_paths.clear()
|
|
171
|
+
self.watch_handles.clear()
|
|
172
|
+
|
|
173
|
+
def get_diagnostics(self) -> dict:
|
|
174
|
+
"""Return lightweight stats for health monitoring."""
|
|
175
|
+
return {
|
|
176
|
+
"watched_paths": len(self.watched_paths),
|
|
177
|
+
"git_watched_paths": len([path for path in self.watched_paths if path.endswith(".git")]),
|
|
178
|
+
"observer_alive": bool(self.observer and self.observer.is_alive()),
|
|
179
|
+
}
|