portacode 1.3.32__py3-none-any.whl → 1.4.11.dev5__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 +2 -2
- portacode/cli.py +158 -14
- portacode/connection/client.py +127 -8
- portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +370 -4
- portacode/connection/handlers/__init__.py +16 -1
- portacode/connection/handlers/diff_handlers.py +603 -0
- portacode/connection/handlers/file_handlers.py +674 -17
- portacode/connection/handlers/project_aware_file_handlers.py +11 -0
- portacode/connection/handlers/project_state/file_system_watcher.py +31 -61
- portacode/connection/handlers/project_state/git_manager.py +139 -572
- portacode/connection/handlers/project_state/handlers.py +28 -14
- portacode/connection/handlers/project_state/manager.py +226 -101
- portacode/connection/handlers/proxmox_infra.py +790 -0
- portacode/connection/handlers/session.py +465 -84
- portacode/connection/handlers/system_handlers.py +181 -8
- portacode/connection/handlers/tab_factory.py +1 -47
- portacode/connection/handlers/update_handler.py +61 -0
- portacode/connection/terminal.py +55 -10
- 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/pairing.py +103 -0
- portacode/static/js/utils/ntp-clock.js +170 -79
- portacode/utils/diff_apply.py +456 -0
- portacode/utils/diff_renderer.py +371 -0
- portacode/utils/ntp_clock.py +45 -131
- {portacode-1.3.32.dist-info → portacode-1.4.11.dev5.dist-info}/METADATA +71 -3
- portacode-1.4.11.dev5.dist-info/RECORD +97 -0
- test_modules/test_device_online.py +1 -1
- test_modules/test_login_flow.py +8 -4
- test_modules/test_play_store_screenshots.py +294 -0
- testing_framework/.env.example +4 -1
- testing_framework/core/playwright_manager.py +63 -9
- portacode-1.3.32.dist-info/RECORD +0 -70
- {portacode-1.3.32.dist-info → portacode-1.4.11.dev5.dist-info}/WHEEL +0 -0
- {portacode-1.3.32.dist-info → portacode-1.4.11.dev5.dist-info}/entry_points.txt +0 -0
- {portacode-1.3.32.dist-info → portacode-1.4.11.dev5.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.3.32.dist-info → portacode-1.4.11.dev5.dist-info}/top_level.txt +0 -0
|
@@ -22,6 +22,17 @@ class ProjectAwareFileWriteHandler(SyncHandler):
|
|
|
22
22
|
"""Write file contents and update project state tabs."""
|
|
23
23
|
file_path = message.get("path")
|
|
24
24
|
content = message.get("content", "")
|
|
25
|
+
# Optimistic lock: ensure the client saw the correct file state
|
|
26
|
+
expected_mtime = message.get("expected_mtime")
|
|
27
|
+
if expected_mtime is not None:
|
|
28
|
+
try:
|
|
29
|
+
current_mtime = os.path.getmtime(file_path)
|
|
30
|
+
except FileNotFoundError:
|
|
31
|
+
raise ValueError(f"File not found: {file_path}")
|
|
32
|
+
if current_mtime != expected_mtime:
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f"File was modified on disk (current {current_mtime} != expected {expected_mtime})"
|
|
35
|
+
)
|
|
25
36
|
|
|
26
37
|
if not file_path:
|
|
27
38
|
raise ValueError("path parameter is required")
|
|
@@ -71,50 +71,29 @@ class FileSystemWatcher:
|
|
|
71
71
|
return
|
|
72
72
|
|
|
73
73
|
# Only process events that represent actual content changes
|
|
74
|
-
# Skip
|
|
75
|
-
|
|
76
|
-
|
|
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)
|
|
77
83
|
return
|
|
78
84
|
|
|
79
85
|
# Handle .git folder events separately for git status monitoring
|
|
80
86
|
path_parts = Path(event.src_path).parts
|
|
81
87
|
if '.git' in path_parts:
|
|
82
|
-
logger.debug("🔍 [TRACE]
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
logger.debug("🔍 [TRACE] Git file details - relative_path: %s, file: %s", git_relative_path, git_file)
|
|
90
|
-
|
|
91
|
-
# Monitor git files that indicate repository state changes
|
|
92
|
-
should_monitor_git_file = (
|
|
93
|
-
git_file == 'index' or # Staging area changes
|
|
94
|
-
git_file == 'index.lock' or # Staging operations in progress
|
|
95
|
-
git_file == 'HEAD' or # Branch switches
|
|
96
|
-
git_relative_path.startswith('refs/heads/') or # Branch updates
|
|
97
|
-
git_relative_path.startswith('refs/remotes/') or # Remote tracking branches
|
|
98
|
-
git_relative_path.startswith('logs/refs/heads/') or # Branch history
|
|
99
|
-
git_relative_path.startswith('logs/HEAD') or # HEAD history
|
|
100
|
-
git_relative_path.startswith('objects/') and event.event_type in ('created', 'modified') # New objects (commits, blobs)
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
if should_monitor_git_file:
|
|
104
|
-
logger.debug("🔍 [TRACE] ✅ Git file matches monitoring criteria: %s", event.src_path)
|
|
105
|
-
else:
|
|
106
|
-
logger.debug("🔍 [TRACE] ❌ Git file does NOT match monitoring criteria - SKIPPING: %s", event.src_path)
|
|
107
|
-
return # Skip other .git files
|
|
108
|
-
except (ValueError, IndexError):
|
|
109
|
-
logger.debug("🔍 [TRACE] ❌ Could not parse .git path - SKIPPING: %s", event.src_path)
|
|
110
|
-
return # Skip if can't parse .git path
|
|
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))
|
|
111
95
|
else:
|
|
112
|
-
logger.debug("
|
|
113
|
-
# Only log significant file changes, not every single event
|
|
114
|
-
if event.event_type in ['created', 'deleted'] or event.src_path.endswith(('.py', '.js', '.html', '.css', '.json', '.md')):
|
|
115
|
-
logger.debug("File system event: %s - %s", event.event_type, os.path.basename(event.src_path))
|
|
116
|
-
else:
|
|
117
|
-
logger.debug("File event: %s", os.path.basename(event.src_path))
|
|
96
|
+
logger.debug("File event: %s", os.path.basename(event.src_path))
|
|
118
97
|
|
|
119
98
|
# Schedule async task in the main event loop from this watchdog thread
|
|
120
99
|
logger.debug("🔍 [TRACE] About to schedule async handler - event_loop exists: %s, closed: %s",
|
|
@@ -142,6 +121,11 @@ class FileSystemWatcher:
|
|
|
142
121
|
logger.warning("Watchdog not available, cannot start watching: %s", path)
|
|
143
122
|
return
|
|
144
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
|
+
|
|
145
129
|
if path not in self.watched_paths:
|
|
146
130
|
try:
|
|
147
131
|
# Use recursive=False to watch only direct contents of each folder
|
|
@@ -158,28 +142,6 @@ class FileSystemWatcher:
|
|
|
158
142
|
else:
|
|
159
143
|
logger.debug("Path already being watched: %s", path)
|
|
160
144
|
|
|
161
|
-
def start_watching_git_directory(self, git_path: str):
|
|
162
|
-
"""Start watching a .git directory for git status changes."""
|
|
163
|
-
if not WATCHDOG_AVAILABLE or not self.observer:
|
|
164
|
-
logger.warning("Watchdog not available, cannot start watching git directory: %s", git_path)
|
|
165
|
-
return
|
|
166
|
-
|
|
167
|
-
if git_path not in self.watched_paths:
|
|
168
|
-
try:
|
|
169
|
-
# Watch .git directory recursively to catch changes in refs/, logs/, etc.
|
|
170
|
-
watch_handle = self.observer.schedule(self.event_handler, git_path, recursive=True)
|
|
171
|
-
self.watched_paths.add(git_path)
|
|
172
|
-
self.watch_handles[git_path] = watch_handle # Store handle for cleanup
|
|
173
|
-
logger.info("Started watching git directory (recursive): %s", git_path)
|
|
174
|
-
|
|
175
|
-
if not self.observer.is_alive():
|
|
176
|
-
self.observer.start()
|
|
177
|
-
logger.info("Started file system observer")
|
|
178
|
-
except Exception as e:
|
|
179
|
-
logger.error("Error starting git directory watcher for %s: %s", git_path, e)
|
|
180
|
-
else:
|
|
181
|
-
logger.debug("Git directory already being watched: %s", git_path)
|
|
182
|
-
|
|
183
145
|
def stop_watching(self, path: str):
|
|
184
146
|
"""Stop watching a specific path."""
|
|
185
147
|
if not WATCHDOG_AVAILABLE or not self.observer:
|
|
@@ -206,4 +168,12 @@ class FileSystemWatcher:
|
|
|
206
168
|
self.observer.stop()
|
|
207
169
|
self.observer.join()
|
|
208
170
|
self.watched_paths.clear()
|
|
209
|
-
self.watch_handles.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
|
+
}
|