From 37db74f47daeeb41171e7d4e516b6b82d5885c4a Mon Sep 17 00:00:00 2001 From: Fede Luzzi Date: Tue, 5 May 2026 18:24:31 -0300 Subject: [PATCH] refactor(core): stabilize gRPC streaming, plugin invocation, and CLI UX - Implement threaded plugin execution with Queue-based streaming in PluginService - Refactor remote logger to preserve ANSI colors and fix TTY line endings (\r\n) - Intelligent terminal filtering: disable SSM screen-clearing filter after success - Sanitize SSH-only flags in core.py when using SFTP protocol - Rewrite completion tree with pre/post-node states and flag deduplication - Update gRPC unit tests to match new streaming response structure --- connpy/completion.py | 28 ++++++++++++ connpy/core.py | 11 ++++- connpy/grpc_layer/server.py | 34 ++++++++++++++- connpy/grpc_layer/stubs.py | 72 ++++++++++++++++++++----------- connpy/services/plugin_service.py | 55 +++++++++++++++-------- connpy/tests/test_grpc_layer.py | 8 ++-- 6 files changed, 159 insertions(+), 49 deletions(-) diff --git a/connpy/completion.py b/connpy/completion.py index 016f37c..6de9e27 100755 --- a/connpy/completion.py +++ b/connpy/completion.py @@ -184,9 +184,37 @@ def _build_tree(nodes, folders, profiles, plugins, configdir): "folders": None, } + # --- Connect (default command) --- + # Long flags are offered; short forms (-d/-t) only used for navigation. + # Two states: before node (offer nodes + remaining long flags) + # after node (offer only remaining long flags, no more nodes) + connect_flags_long = ["--debug", "--sftp"] + connect_flags_all = ["--debug", "-d", "--sftp", "-t"] + + # Post-node: only offer remaining long flags + connect_after_node = {"__exclude_used__": True} + for f in connect_flags_all: + connect_after_node[f] = connect_after_node + + # Pre-node: offer nodes + remaining long flags, consume node → post-node state + connect_dict = {"__exclude_used__": True} + connect_dict["__extra__"] = lambda w: ( + list(nodes) + list(folders) + (list(plugins.keys()) if plugins else []) + ) + connect_dict["*"] = connect_after_node + for f in connect_flags_all: + connect_dict[f] = connect_dict + # --- Main Tree --- return { + # Root: offer nodes + long flags; after a node go to post-node state "__extra__": lambda w: list(nodes) + list(folders) + (list(plugins.keys()) if plugins else []), + "*": connect_after_node, + + "--debug": connect_dict, + "-d": connect_dict, + "--sftp": connect_dict, + "-t": connect_dict, "--add": {"profile": _profile_values}, "--del": {"profile": _profile_values, "__extra__": _nodes_folders}, diff --git a/connpy/core.py b/connpy/core.py index 6ccbc42..ba00ee9 100755 --- a/connpy/core.py +++ b/connpy/core.py @@ -348,7 +348,8 @@ class node: x.start() if debug: if 'mylog' in dir(self): - print(self.mylog.getvalue().decode()) + if not async_mode: + print(self.mylog.getvalue().decode()) def _teardown_interact_environment(self): if 'logfile' in dir(self) and hasattr(self, 'mylog'): @@ -760,7 +761,12 @@ class node: elif self.protocol == "sftp": cmd += " -P " + self.port if self.options: - cmd += " " + self.options + opts = self.options + if self.protocol == "sftp": + # Strip SSH-only flags that sftp doesn't support + opts = re.sub(r'(?