ssh-to-code 1.0.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.
- config_manager.py +141 -0
- host_selector.py +237 -0
- save_config.py +340 -0
- ssh_dir_browser.py +362 -0
- ssh_handler.py +233 -0
- ssh_to_code-1.0.0.dist-info/METADATA +384 -0
- ssh_to_code-1.0.0.dist-info/RECORD +12 -0
- ssh_to_code-1.0.0.dist-info/WHEEL +5 -0
- ssh_to_code-1.0.0.dist-info/entry_points.txt +2 -0
- ssh_to_code-1.0.0.dist-info/licenses/LICENSE +21 -0
- ssh_to_code-1.0.0.dist-info/top_level.txt +6 -0
- vscode_integration.py +185 -0
config_manager.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Configuration Manager
|
|
4
|
+
Handle loading and saving SSH host configurations
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from typing import Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConfigManager:
|
|
13
|
+
"""Manage SSH host configurations"""
|
|
14
|
+
|
|
15
|
+
DEFAULT_CONFIG_PATH = os.path.expanduser('~/.ssh-dir-browser.json')
|
|
16
|
+
|
|
17
|
+
def __init__(self, config_path: Optional[str] = None):
|
|
18
|
+
self.config_path = config_path or self.DEFAULT_CONFIG_PATH
|
|
19
|
+
self.config = self._load_config()
|
|
20
|
+
|
|
21
|
+
def _load_config(self) -> Dict:
|
|
22
|
+
"""Load configuration from file"""
|
|
23
|
+
if not os.path.exists(self.config_path):
|
|
24
|
+
return {
|
|
25
|
+
'version': '1.0',
|
|
26
|
+
'hosts': [],
|
|
27
|
+
'preferences': {
|
|
28
|
+
'default_start_path': '~',
|
|
29
|
+
'save_last_path': True,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
with open(self.config_path, 'r') as f:
|
|
35
|
+
return json.load(f)
|
|
36
|
+
except Exception as e:
|
|
37
|
+
print(f"Warning: Failed to load config: {e}")
|
|
38
|
+
return {'version': '1.0', 'hosts': [], 'preferences': {}}
|
|
39
|
+
|
|
40
|
+
def save_config(self) -> bool:
|
|
41
|
+
"""Save configuration to file"""
|
|
42
|
+
try:
|
|
43
|
+
# Create directory if needed
|
|
44
|
+
config_dir = os.path.dirname(self.config_path)
|
|
45
|
+
if config_dir and not os.path.exists(config_dir):
|
|
46
|
+
os.makedirs(config_dir)
|
|
47
|
+
|
|
48
|
+
with open(self.config_path, 'w') as f:
|
|
49
|
+
json.dump(self.config, f, indent=2)
|
|
50
|
+
return True
|
|
51
|
+
except Exception as e:
|
|
52
|
+
print(f"Error saving config: {e}")
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
def get_hosts(self) -> List[Dict]:
|
|
56
|
+
"""Get list of configured hosts"""
|
|
57
|
+
return self.config.get('hosts', [])
|
|
58
|
+
|
|
59
|
+
def add_host(self, name: str, hostname: str, username: str,
|
|
60
|
+
port: int = 22, key_file: Optional[str] = None,
|
|
61
|
+
default_path: Optional[str] = None) -> bool:
|
|
62
|
+
"""Add a new host configuration"""
|
|
63
|
+
# Check if host already exists
|
|
64
|
+
for host in self.config['hosts']:
|
|
65
|
+
if host['name'] == name:
|
|
66
|
+
print(f"Host '{name}' already exists")
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
host_config = {
|
|
70
|
+
'name': name,
|
|
71
|
+
'hostname': hostname,
|
|
72
|
+
'username': username,
|
|
73
|
+
'port': port,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if key_file:
|
|
77
|
+
host_config['key_file'] = key_file
|
|
78
|
+
if default_path:
|
|
79
|
+
host_config['default_path'] = default_path
|
|
80
|
+
|
|
81
|
+
self.config['hosts'].append(host_config)
|
|
82
|
+
return self.save_config()
|
|
83
|
+
|
|
84
|
+
def remove_host(self, name: str) -> bool:
|
|
85
|
+
"""Remove a host configuration"""
|
|
86
|
+
original_len = len(self.config['hosts'])
|
|
87
|
+
self.config['hosts'] = [h for h in self.config['hosts'] if h['name'] != name]
|
|
88
|
+
|
|
89
|
+
if len(self.config['hosts']) < original_len:
|
|
90
|
+
return self.save_config()
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
def get_host(self, name: str) -> Optional[Dict]:
|
|
94
|
+
"""Get a specific host configuration"""
|
|
95
|
+
for host in self.config['hosts']:
|
|
96
|
+
if host['name'] == name:
|
|
97
|
+
return host
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def update_host_last_path(self, name: str, path: str) -> bool:
|
|
101
|
+
"""Update the last visited path for a host"""
|
|
102
|
+
for host in self.config['hosts']:
|
|
103
|
+
if host['name'] == name:
|
|
104
|
+
host['last_path'] = path
|
|
105
|
+
return self.save_config()
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
def get_preference(self, key: str, default=None):
|
|
109
|
+
"""Get a preference value"""
|
|
110
|
+
return self.config.get('preferences', {}).get(key, default)
|
|
111
|
+
|
|
112
|
+
def set_preference(self, key: str, value) -> bool:
|
|
113
|
+
"""Set a preference value"""
|
|
114
|
+
if 'preferences' not in self.config:
|
|
115
|
+
self.config['preferences'] = {}
|
|
116
|
+
self.config['preferences'][key] = value
|
|
117
|
+
return self.save_config()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def main():
|
|
121
|
+
"""Test configuration manager"""
|
|
122
|
+
config = ConfigManager()
|
|
123
|
+
|
|
124
|
+
print("SSH Directory Browser - Configuration Manager")
|
|
125
|
+
print("=" * 50)
|
|
126
|
+
|
|
127
|
+
# List hosts
|
|
128
|
+
hosts = config.get_hosts()
|
|
129
|
+
print(f"\nConfigured hosts: {len(hosts)}")
|
|
130
|
+
for host in hosts:
|
|
131
|
+
print(f" - {host['name']}: {host['username']}@{host['hostname']}:{host.get('port', 22)}")
|
|
132
|
+
|
|
133
|
+
# Show preferences
|
|
134
|
+
print(f"\nPreferences:")
|
|
135
|
+
prefs = config.config.get('preferences', {})
|
|
136
|
+
for key, value in prefs.items():
|
|
137
|
+
print(f" {key}: {value}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
main()
|
host_selector.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SSH Host Selector
|
|
4
|
+
Select from saved SSH hosts and launch the directory browser
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import curses
|
|
9
|
+
from typing import List, Dict, Optional
|
|
10
|
+
from config_manager import ConfigManager
|
|
11
|
+
from ssh_handler import SSHHandler
|
|
12
|
+
from ssh_dir_browser import DirectoryBrowser
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HostSelector:
|
|
16
|
+
"""Interactive host selection UI"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, config_manager: ConfigManager):
|
|
19
|
+
self.config_manager = config_manager
|
|
20
|
+
self.hosts = config_manager.get_hosts()
|
|
21
|
+
self.selected_index = 0
|
|
22
|
+
self.scroll_offset = 0
|
|
23
|
+
|
|
24
|
+
def draw_ui(self, stdscr):
|
|
25
|
+
"""Draw the host selection UI"""
|
|
26
|
+
stdscr.clear()
|
|
27
|
+
height, width = stdscr.getmaxyx()
|
|
28
|
+
|
|
29
|
+
# Header
|
|
30
|
+
header = "SSH Directory Browser - Select Host"
|
|
31
|
+
stdscr.addstr(0, 0, header[:width-1], curses.A_REVERSE | curses.A_BOLD)
|
|
32
|
+
|
|
33
|
+
# Info line
|
|
34
|
+
if not self.hosts:
|
|
35
|
+
info = "No saved hosts. Press 'n' to add a new host or 'q' to quit."
|
|
36
|
+
stdscr.addstr(2, 2, info[:width-3])
|
|
37
|
+
else:
|
|
38
|
+
info = f"Select a host to connect ({len(self.hosts)} available)"
|
|
39
|
+
stdscr.addstr(2, 2, info[:width-3], curses.A_DIM)
|
|
40
|
+
|
|
41
|
+
# Help line
|
|
42
|
+
help_text = "↑/↓: Navigate | Enter: Connect | 'n': New host | 'd': Delete | 'q': Quit"
|
|
43
|
+
stdscr.addstr(3, 0, help_text[:width-1], curses.A_DIM)
|
|
44
|
+
|
|
45
|
+
# Separator
|
|
46
|
+
stdscr.addstr(4, 0, "─" * (width-1))
|
|
47
|
+
|
|
48
|
+
# Host list
|
|
49
|
+
if self.hosts:
|
|
50
|
+
display_height = height - 7
|
|
51
|
+
|
|
52
|
+
# Calculate visible range
|
|
53
|
+
if self.selected_index < self.scroll_offset:
|
|
54
|
+
self.scroll_offset = self.selected_index
|
|
55
|
+
elif self.selected_index >= self.scroll_offset + display_height:
|
|
56
|
+
self.scroll_offset = self.selected_index - display_height + 1
|
|
57
|
+
|
|
58
|
+
# Display hosts
|
|
59
|
+
for i in range(self.scroll_offset, min(len(self.hosts), self.scroll_offset + display_height)):
|
|
60
|
+
y = 5 + i - self.scroll_offset
|
|
61
|
+
host = self.hosts[i]
|
|
62
|
+
|
|
63
|
+
# Format display
|
|
64
|
+
name = host['name']
|
|
65
|
+
connection = f"{host['username']}@{host['hostname']}:{host.get('port', 22)}"
|
|
66
|
+
display = f" {name:20} → {connection}"
|
|
67
|
+
|
|
68
|
+
# Add default path if exists
|
|
69
|
+
if 'default_path' in host:
|
|
70
|
+
display += f" ({host['default_path']})"
|
|
71
|
+
|
|
72
|
+
# Highlight selected
|
|
73
|
+
attr = curses.A_REVERSE if i == self.selected_index else curses.A_NORMAL
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
stdscr.addstr(y, 0, display[:width-1], attr)
|
|
77
|
+
except curses.error:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
# Status bar
|
|
81
|
+
status_y = height - 2
|
|
82
|
+
stdscr.addstr(status_y, 0, "─" * (width-1))
|
|
83
|
+
status = f"Hosts: {len(self.hosts)} | Selected: {self.selected_index + 1}/{len(self.hosts)}" if self.hosts else "No hosts configured"
|
|
84
|
+
stdscr.addstr(status_y + 1, 0, status[:width-1])
|
|
85
|
+
|
|
86
|
+
stdscr.refresh()
|
|
87
|
+
|
|
88
|
+
def run(self, stdscr):
|
|
89
|
+
"""Main event loop"""
|
|
90
|
+
curses.curs_set(0)
|
|
91
|
+
stdscr.keypad(True)
|
|
92
|
+
|
|
93
|
+
while True:
|
|
94
|
+
self.draw_ui(stdscr)
|
|
95
|
+
key = stdscr.getch()
|
|
96
|
+
|
|
97
|
+
if key == ord('q') or key == ord('Q'):
|
|
98
|
+
return None
|
|
99
|
+
elif key == curses.KEY_UP:
|
|
100
|
+
if self.selected_index > 0:
|
|
101
|
+
self.selected_index -= 1
|
|
102
|
+
elif key == curses.KEY_DOWN:
|
|
103
|
+
if self.selected_index < len(self.hosts) - 1:
|
|
104
|
+
self.selected_index += 1
|
|
105
|
+
elif key == ord('\n') or key == curses.KEY_ENTER:
|
|
106
|
+
if self.hosts and self.selected_index < len(self.hosts):
|
|
107
|
+
return self.hosts[self.selected_index]
|
|
108
|
+
elif key == ord('n') or key == ord('N'):
|
|
109
|
+
# Add new host (exit to command line for this)
|
|
110
|
+
return 'NEW_HOST'
|
|
111
|
+
elif key == ord('d') or key == ord('D'):
|
|
112
|
+
# Delete selected host
|
|
113
|
+
if self.hosts and self.selected_index < len(self.hosts):
|
|
114
|
+
host = self.hosts[self.selected_index]
|
|
115
|
+
self.config_manager.remove_host(host['name'])
|
|
116
|
+
self.hosts = self.config_manager.get_hosts()
|
|
117
|
+
if self.selected_index >= len(self.hosts) and self.selected_index > 0:
|
|
118
|
+
self.selected_index -= 1
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def connect_to_host(host_config: Dict) -> bool:
|
|
122
|
+
"""Connect to a host and launch directory browser"""
|
|
123
|
+
print(f"\nConnecting to {host_config['username']}@{host_config['hostname']}:{host_config.get('port', 22)}...")
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
ssh = SSHHandler(
|
|
127
|
+
hostname=host_config['hostname'],
|
|
128
|
+
username=host_config['username'],
|
|
129
|
+
port=host_config.get('port', 22),
|
|
130
|
+
key_filename=host_config.get('key_file'),
|
|
131
|
+
use_password=False
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if not ssh.connect():
|
|
135
|
+
print("Failed to connect to SSH server")
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
print("Connected successfully!")
|
|
139
|
+
|
|
140
|
+
# Determine start path
|
|
141
|
+
start_path = host_config.get('default_path') or host_config.get('last_path') or '~'
|
|
142
|
+
if start_path == '~':
|
|
143
|
+
home_cmd = "echo $HOME"
|
|
144
|
+
start_path = ssh.execute_command(home_cmd).strip() or "/"
|
|
145
|
+
|
|
146
|
+
# Launch browser
|
|
147
|
+
browser = DirectoryBrowser(ssh, start_path)
|
|
148
|
+
curses.wrapper(browser.run)
|
|
149
|
+
|
|
150
|
+
# Save last path
|
|
151
|
+
config = ConfigManager()
|
|
152
|
+
config.update_host_last_path(host_config['name'], browser.current_path)
|
|
153
|
+
|
|
154
|
+
# Cleanup
|
|
155
|
+
ssh.disconnect()
|
|
156
|
+
print("\nDisconnected. Goodbye!")
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
except KeyboardInterrupt:
|
|
160
|
+
print("\n\nInterrupted by user")
|
|
161
|
+
return False
|
|
162
|
+
except Exception as e:
|
|
163
|
+
print(f"\nError: {str(e)}")
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def main():
|
|
168
|
+
"""Main entry point for host selector"""
|
|
169
|
+
import argparse
|
|
170
|
+
|
|
171
|
+
parser = argparse.ArgumentParser(description="SSH Host Selector")
|
|
172
|
+
parser.add_argument('host_name', nargs='?', help='Saved host name to connect to directly')
|
|
173
|
+
args, remaining = parser.parse_known_args()
|
|
174
|
+
|
|
175
|
+
config = ConfigManager()
|
|
176
|
+
|
|
177
|
+
# If host name provided, connect directly
|
|
178
|
+
if args.host_name:
|
|
179
|
+
host_config = config.get_host(args.host_name)
|
|
180
|
+
if host_config:
|
|
181
|
+
print(f"Connecting to saved host: {args.host_name}")
|
|
182
|
+
connect_to_host(host_config)
|
|
183
|
+
return
|
|
184
|
+
else:
|
|
185
|
+
print(f"❌ Host '{args.host_name}' not found in saved configurations")
|
|
186
|
+
print("\nAvailable hosts:")
|
|
187
|
+
hosts = config.get_hosts()
|
|
188
|
+
if hosts:
|
|
189
|
+
for host in hosts:
|
|
190
|
+
print(f" - {host['name']}")
|
|
191
|
+
else:
|
|
192
|
+
print(" (none)")
|
|
193
|
+
print("\nAdd one with: python save_config.py add")
|
|
194
|
+
sys.exit(1)
|
|
195
|
+
|
|
196
|
+
# If no hosts configured, show help
|
|
197
|
+
hosts = config.get_hosts()
|
|
198
|
+
if not hosts:
|
|
199
|
+
print("SSH Directory Browser - Host Selector")
|
|
200
|
+
print("=" * 50)
|
|
201
|
+
print("\nNo saved hosts found.")
|
|
202
|
+
print("\nYou can:")
|
|
203
|
+
print(" 1. Use the direct connection mode:")
|
|
204
|
+
print(" ./ssh-browse user@hostname")
|
|
205
|
+
print("\n 2. Add a host to configuration:")
|
|
206
|
+
print(" python save_config.py add")
|
|
207
|
+
print("\n 3. Or use the quick add:")
|
|
208
|
+
print(" python save_config.py quick myhost example.com user")
|
|
209
|
+
print("\n 4. Edit the config file manually:")
|
|
210
|
+
print(f" {config.config_path}")
|
|
211
|
+
sys.exit(0)
|
|
212
|
+
|
|
213
|
+
# Show host selector
|
|
214
|
+
try:
|
|
215
|
+
selector = HostSelector(config)
|
|
216
|
+
selected = curses.wrapper(selector.run)
|
|
217
|
+
|
|
218
|
+
if selected is None:
|
|
219
|
+
print("Cancelled.")
|
|
220
|
+
sys.exit(0)
|
|
221
|
+
elif selected == 'NEW_HOST':
|
|
222
|
+
print("\nTo add a new host, use:")
|
|
223
|
+
print(" python save_config.py add")
|
|
224
|
+
print("Or edit the config file:")
|
|
225
|
+
print(f" {config.config_path}")
|
|
226
|
+
sys.exit(0)
|
|
227
|
+
else:
|
|
228
|
+
# Connect to selected host
|
|
229
|
+
connect_to_host(selected)
|
|
230
|
+
|
|
231
|
+
except KeyboardInterrupt:
|
|
232
|
+
print("\n\nInterrupted by user")
|
|
233
|
+
sys.exit(0)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
if __name__ == "__main__":
|
|
237
|
+
main()
|