qssh 0.1.0__tar.gz
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.
- qssh-0.1.0/LICENSE +21 -0
- qssh-0.1.0/PKG-INFO +122 -0
- qssh-0.1.0/README.md +90 -0
- qssh-0.1.0/pyproject.toml +50 -0
- qssh-0.1.0/setup.cfg +4 -0
- qssh-0.1.0/src/qssh/__init__.py +9 -0
- qssh-0.1.0/src/qssh/cli.py +281 -0
- qssh-0.1.0/src/qssh/connector.py +143 -0
- qssh-0.1.0/src/qssh/py.typed +0 -0
- qssh-0.1.0/src/qssh/session.py +151 -0
- qssh-0.1.0/src/qssh.egg-info/PKG-INFO +122 -0
- qssh-0.1.0/src/qssh.egg-info/SOURCES.txt +14 -0
- qssh-0.1.0/src/qssh.egg-info/dependency_links.txt +1 -0
- qssh-0.1.0/src/qssh.egg-info/entry_points.txt +2 -0
- qssh-0.1.0/src/qssh.egg-info/requires.txt +3 -0
- qssh-0.1.0/src/qssh.egg-info/top_level.txt +1 -0
qssh-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
qssh-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qssh
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Quick SSH session manager - save your VM credentials and connect with a single command
|
|
5
|
+
Author-email: joan-code6 <your-email@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yourusername/qssh
|
|
8
|
+
Project-URL: Repository, https://github.com/yourusername/qssh
|
|
9
|
+
Project-URL: Issues, https://github.com/yourusername/qssh/issues
|
|
10
|
+
Keywords: ssh,session,manager,vm,cli,terminal
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: System :: Networking
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: click>=8.0.0
|
|
29
|
+
Requires-Dist: rich>=13.0.0
|
|
30
|
+
Requires-Dist: pyyaml>=6.0
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# qssh
|
|
34
|
+
|
|
35
|
+
**Quick SSH session manager** - Save your VM credentials and connect with a single command.
|
|
36
|
+
|
|
37
|
+
Tired of copy-pasting credentials every time you want to SSH into your VMs? `qssh` lets you save your session configs and connect instantly.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install qssh
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
### 1. Add a new session
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
qssh add outcraft
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
You'll be prompted for:
|
|
54
|
+
- Host (IP address or hostname)
|
|
55
|
+
- Username
|
|
56
|
+
- Port (default: 22)
|
|
57
|
+
- Authentication method (password or key file)
|
|
58
|
+
|
|
59
|
+
### 2. Connect to your VM
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
qssh outcraft
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
That's it! You're connected.
|
|
66
|
+
|
|
67
|
+
## Commands
|
|
68
|
+
|
|
69
|
+
| Command | Description |
|
|
70
|
+
|---------|-------------|
|
|
71
|
+
| `qssh <session>` | Connect to a saved session |
|
|
72
|
+
| `qssh add <name>` | Add a new session |
|
|
73
|
+
| `qssh list` | List all saved sessions |
|
|
74
|
+
| `qssh remove <name>` | Remove a session |
|
|
75
|
+
| `qssh edit <name>` | Edit an existing session |
|
|
76
|
+
| `qssh show <name>` | Show session details |
|
|
77
|
+
| `qssh config` | Show config file location |
|
|
78
|
+
|
|
79
|
+
## Examples
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Add a session for your OutCraft VM
|
|
83
|
+
qssh add outcraft
|
|
84
|
+
# Host: 192.168.1.100
|
|
85
|
+
# Username: admin
|
|
86
|
+
# Port [22]: 22
|
|
87
|
+
# Auth type (password/key) [password]: password
|
|
88
|
+
# Password: ********
|
|
89
|
+
|
|
90
|
+
# Now just connect with:
|
|
91
|
+
qssh outcraft
|
|
92
|
+
|
|
93
|
+
# List all your sessions
|
|
94
|
+
qssh list
|
|
95
|
+
|
|
96
|
+
# Remove a session
|
|
97
|
+
qssh remove old-server
|
|
98
|
+
|
|
99
|
+
# Show details of a session
|
|
100
|
+
qssh show outcraft
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Using SSH Keys
|
|
104
|
+
|
|
105
|
+
For key-based authentication:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
qssh add myserver
|
|
109
|
+
# Host: example.com
|
|
110
|
+
# Username: deploy
|
|
111
|
+
# Port [22]: 22
|
|
112
|
+
# Auth type (password/key) [password]: key
|
|
113
|
+
# Key file path: ~/.ssh/id_rsa
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Configuration
|
|
117
|
+
|
|
118
|
+
Sessions are stored in `~/.qssh/sessions.yaml`. Passwords are stored encoded (not plaintext) but for maximum security, consider using SSH keys instead.
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT License
|
qssh-0.1.0/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# qssh
|
|
2
|
+
|
|
3
|
+
**Quick SSH session manager** - Save your VM credentials and connect with a single command.
|
|
4
|
+
|
|
5
|
+
Tired of copy-pasting credentials every time you want to SSH into your VMs? `qssh` lets you save your session configs and connect instantly.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install qssh
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### 1. Add a new session
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
qssh add outcraft
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
You'll be prompted for:
|
|
22
|
+
- Host (IP address or hostname)
|
|
23
|
+
- Username
|
|
24
|
+
- Port (default: 22)
|
|
25
|
+
- Authentication method (password or key file)
|
|
26
|
+
|
|
27
|
+
### 2. Connect to your VM
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
qssh outcraft
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That's it! You're connected.
|
|
34
|
+
|
|
35
|
+
## Commands
|
|
36
|
+
|
|
37
|
+
| Command | Description |
|
|
38
|
+
|---------|-------------|
|
|
39
|
+
| `qssh <session>` | Connect to a saved session |
|
|
40
|
+
| `qssh add <name>` | Add a new session |
|
|
41
|
+
| `qssh list` | List all saved sessions |
|
|
42
|
+
| `qssh remove <name>` | Remove a session |
|
|
43
|
+
| `qssh edit <name>` | Edit an existing session |
|
|
44
|
+
| `qssh show <name>` | Show session details |
|
|
45
|
+
| `qssh config` | Show config file location |
|
|
46
|
+
|
|
47
|
+
## Examples
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Add a session for your OutCraft VM
|
|
51
|
+
qssh add outcraft
|
|
52
|
+
# Host: 192.168.1.100
|
|
53
|
+
# Username: admin
|
|
54
|
+
# Port [22]: 22
|
|
55
|
+
# Auth type (password/key) [password]: password
|
|
56
|
+
# Password: ********
|
|
57
|
+
|
|
58
|
+
# Now just connect with:
|
|
59
|
+
qssh outcraft
|
|
60
|
+
|
|
61
|
+
# List all your sessions
|
|
62
|
+
qssh list
|
|
63
|
+
|
|
64
|
+
# Remove a session
|
|
65
|
+
qssh remove old-server
|
|
66
|
+
|
|
67
|
+
# Show details of a session
|
|
68
|
+
qssh show outcraft
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Using SSH Keys
|
|
72
|
+
|
|
73
|
+
For key-based authentication:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
qssh add myserver
|
|
77
|
+
# Host: example.com
|
|
78
|
+
# Username: deploy
|
|
79
|
+
# Port [22]: 22
|
|
80
|
+
# Auth type (password/key) [password]: key
|
|
81
|
+
# Key file path: ~/.ssh/id_rsa
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Configuration
|
|
85
|
+
|
|
86
|
+
Sessions are stored in `~/.qssh/sessions.yaml`. Passwords are stored encoded (not plaintext) but for maximum security, consider using SSH keys instead.
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT License
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "qssh"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Quick SSH session manager - save your VM credentials and connect with a single command"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "joan-code6", email = "your-email@example.com"}
|
|
13
|
+
]
|
|
14
|
+
keywords = ["ssh", "session", "manager", "vm", "cli", "terminal"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Intended Audience :: System Administrators",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.8",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Topic :: System :: Networking",
|
|
29
|
+
"Topic :: Utilities",
|
|
30
|
+
]
|
|
31
|
+
requires-python = ">=3.8"
|
|
32
|
+
dependencies = [
|
|
33
|
+
"click>=8.0.0",
|
|
34
|
+
"rich>=13.0.0",
|
|
35
|
+
"pyyaml>=6.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/yourusername/qssh"
|
|
40
|
+
Repository = "https://github.com/yourusername/qssh"
|
|
41
|
+
Issues = "https://github.com/yourusername/qssh/issues"
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
qssh = "qssh.cli:main"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["src"]
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.package-data]
|
|
50
|
+
qssh = ["py.typed"]
|
qssh-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Command-line interface for qssh."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import click
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.prompt import Prompt, Confirm
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .session import Session, SessionManager
|
|
12
|
+
from .connector import SSHConnector
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
manager = SessionManager()
|
|
17
|
+
connector = SSHConnector()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@click.group(invoke_without_command=True)
|
|
21
|
+
@click.argument("session_name", required=False)
|
|
22
|
+
@click.option("--version", "-v", is_flag=True, help="Show version")
|
|
23
|
+
@click.pass_context
|
|
24
|
+
def main(ctx, session_name: str, version: bool):
|
|
25
|
+
"""qssh - Quick SSH session manager.
|
|
26
|
+
|
|
27
|
+
Connect to a saved session:
|
|
28
|
+
|
|
29
|
+
qssh <session-name>
|
|
30
|
+
|
|
31
|
+
Or use subcommands to manage sessions:
|
|
32
|
+
|
|
33
|
+
qssh add <name> Add a new session
|
|
34
|
+
|
|
35
|
+
qssh list List all sessions
|
|
36
|
+
|
|
37
|
+
qssh remove <name> Remove a session
|
|
38
|
+
"""
|
|
39
|
+
if version:
|
|
40
|
+
console.print(f"[bold blue]qssh[/] version [green]{__version__}[/]")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
if ctx.invoked_subcommand is None:
|
|
44
|
+
if session_name:
|
|
45
|
+
# Try to connect to the session
|
|
46
|
+
_connect(session_name)
|
|
47
|
+
else:
|
|
48
|
+
# Show help
|
|
49
|
+
click.echo(ctx.get_help())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _connect(name: str) -> None:
|
|
53
|
+
"""Connect to a session by name."""
|
|
54
|
+
session = manager.get(name)
|
|
55
|
+
|
|
56
|
+
if not session:
|
|
57
|
+
console.print(f"[red]Session '[bold]{name}[/bold]' not found.[/]")
|
|
58
|
+
console.print("\nAvailable sessions:")
|
|
59
|
+
_list_sessions_simple()
|
|
60
|
+
console.print(f"\n[dim]Use 'qssh add {name}' to create this session.[/]")
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
|
|
63
|
+
console.print(f"[bold blue]Connecting to[/] [green]{name}[/] ({session.username}@{session.host}:{session.port})")
|
|
64
|
+
console.print()
|
|
65
|
+
|
|
66
|
+
exit_code = connector.connect(session)
|
|
67
|
+
sys.exit(exit_code)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _list_sessions_simple() -> None:
|
|
71
|
+
"""List sessions in a simple format."""
|
|
72
|
+
sessions = manager.list_all()
|
|
73
|
+
if not sessions:
|
|
74
|
+
console.print("[dim] No sessions saved yet.[/]")
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
for s in sessions:
|
|
78
|
+
console.print(f" • [cyan]{s.name}[/] → {s.username}@{s.host}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@main.command("add")
|
|
82
|
+
@click.argument("name")
|
|
83
|
+
def add_session(name: str):
|
|
84
|
+
"""Add a new SSH session."""
|
|
85
|
+
if manager.exists(name):
|
|
86
|
+
if not Confirm.ask(f"Session '[bold]{name}[/]' already exists. Overwrite?"):
|
|
87
|
+
console.print("[yellow]Cancelled.[/]")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
console.print(Panel(f"[bold blue]Adding session:[/] [green]{name}[/]", expand=False))
|
|
91
|
+
|
|
92
|
+
# Gather session info
|
|
93
|
+
host = Prompt.ask("[bold]Host[/] (IP or hostname)")
|
|
94
|
+
username = Prompt.ask("[bold]Username[/]")
|
|
95
|
+
port = Prompt.ask("[bold]Port[/]", default="22")
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
port = int(port)
|
|
99
|
+
except ValueError:
|
|
100
|
+
console.print("[red]Invalid port number. Using 22.[/]")
|
|
101
|
+
port = 22
|
|
102
|
+
|
|
103
|
+
auth_type = Prompt.ask(
|
|
104
|
+
"[bold]Auth type[/]",
|
|
105
|
+
choices=["password", "key"],
|
|
106
|
+
default="password"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
password = None
|
|
110
|
+
key_file = None
|
|
111
|
+
|
|
112
|
+
if auth_type == "password":
|
|
113
|
+
password_raw = Prompt.ask("[bold]Password[/]", password=True)
|
|
114
|
+
if password_raw:
|
|
115
|
+
password = Session.encode_password(password_raw)
|
|
116
|
+
else:
|
|
117
|
+
key_file = Prompt.ask(
|
|
118
|
+
"[bold]Key file path[/]",
|
|
119
|
+
default="~/.ssh/id_rsa"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Create and save session
|
|
123
|
+
session = Session(
|
|
124
|
+
name=name,
|
|
125
|
+
host=host,
|
|
126
|
+
username=username,
|
|
127
|
+
port=port,
|
|
128
|
+
auth_type=auth_type,
|
|
129
|
+
password=password,
|
|
130
|
+
key_file=key_file,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
manager.add(session)
|
|
134
|
+
console.print(f"\n[green]✓[/] Session '[bold]{name}[/]' saved!")
|
|
135
|
+
console.print(f"[dim]Connect with: qssh {name}[/]")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@main.command("list")
|
|
139
|
+
def list_sessions():
|
|
140
|
+
"""List all saved sessions."""
|
|
141
|
+
sessions = manager.list_all()
|
|
142
|
+
|
|
143
|
+
if not sessions:
|
|
144
|
+
console.print("[yellow]No sessions saved yet.[/]")
|
|
145
|
+
console.print("[dim]Use 'qssh add <name>' to add one.[/]")
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
table = Table(title="SSH Sessions", show_header=True, header_style="bold blue")
|
|
149
|
+
table.add_column("Name", style="cyan", no_wrap=True)
|
|
150
|
+
table.add_column("Host", style="green")
|
|
151
|
+
table.add_column("User", style="yellow")
|
|
152
|
+
table.add_column("Port", justify="right")
|
|
153
|
+
table.add_column("Auth", style="magenta")
|
|
154
|
+
|
|
155
|
+
for session in sessions:
|
|
156
|
+
auth_display = "🔑 key" if session.auth_type == "key" else "🔒 pass"
|
|
157
|
+
table.add_row(
|
|
158
|
+
session.name,
|
|
159
|
+
session.host,
|
|
160
|
+
session.username,
|
|
161
|
+
str(session.port),
|
|
162
|
+
auth_display,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
console.print(table)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@main.command("remove")
|
|
169
|
+
@click.argument("name")
|
|
170
|
+
def remove_session(name: str):
|
|
171
|
+
"""Remove a saved session."""
|
|
172
|
+
if not manager.exists(name):
|
|
173
|
+
console.print(f"[red]Session '[bold]{name}[/bold]' not found.[/]")
|
|
174
|
+
sys.exit(1)
|
|
175
|
+
|
|
176
|
+
if Confirm.ask(f"Remove session '[bold]{name}[/]'?"):
|
|
177
|
+
manager.remove(name)
|
|
178
|
+
console.print(f"[green]✓[/] Session '[bold]{name}[/]' removed.")
|
|
179
|
+
else:
|
|
180
|
+
console.print("[yellow]Cancelled.[/]")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@main.command("edit")
|
|
184
|
+
@click.argument("name")
|
|
185
|
+
def edit_session(name: str):
|
|
186
|
+
"""Edit an existing session."""
|
|
187
|
+
session = manager.get(name)
|
|
188
|
+
|
|
189
|
+
if not session:
|
|
190
|
+
console.print(f"[red]Session '[bold]{name}[/bold]' not found.[/]")
|
|
191
|
+
sys.exit(1)
|
|
192
|
+
|
|
193
|
+
console.print(Panel(f"[bold blue]Editing session:[/] [green]{name}[/]", expand=False))
|
|
194
|
+
console.print("[dim]Press Enter to keep current value.[/]\n")
|
|
195
|
+
|
|
196
|
+
# Gather updated info
|
|
197
|
+
host = Prompt.ask("[bold]Host[/]", default=session.host)
|
|
198
|
+
username = Prompt.ask("[bold]Username[/]", default=session.username)
|
|
199
|
+
port = Prompt.ask("[bold]Port[/]", default=str(session.port))
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
port = int(port)
|
|
203
|
+
except ValueError:
|
|
204
|
+
port = session.port
|
|
205
|
+
|
|
206
|
+
auth_type = Prompt.ask(
|
|
207
|
+
"[bold]Auth type[/]",
|
|
208
|
+
choices=["password", "key"],
|
|
209
|
+
default=session.auth_type
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
password = session.password
|
|
213
|
+
key_file = session.key_file
|
|
214
|
+
|
|
215
|
+
if auth_type == "password":
|
|
216
|
+
if Confirm.ask("Update password?", default=False):
|
|
217
|
+
password_raw = Prompt.ask("[bold]Password[/]", password=True)
|
|
218
|
+
if password_raw:
|
|
219
|
+
password = Session.encode_password(password_raw)
|
|
220
|
+
key_file = None
|
|
221
|
+
else:
|
|
222
|
+
key_file = Prompt.ask(
|
|
223
|
+
"[bold]Key file path[/]",
|
|
224
|
+
default=session.key_file or "~/.ssh/id_rsa"
|
|
225
|
+
)
|
|
226
|
+
password = None
|
|
227
|
+
|
|
228
|
+
# Update session
|
|
229
|
+
updated = Session(
|
|
230
|
+
name=name,
|
|
231
|
+
host=host,
|
|
232
|
+
username=username,
|
|
233
|
+
port=port,
|
|
234
|
+
auth_type=auth_type,
|
|
235
|
+
password=password,
|
|
236
|
+
key_file=key_file,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
manager.add(updated)
|
|
240
|
+
console.print(f"\n[green]✓[/] Session '[bold]{name}[/]' updated!")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@main.command("show")
|
|
244
|
+
@click.argument("name")
|
|
245
|
+
def show_session(name: str):
|
|
246
|
+
"""Show details of a session."""
|
|
247
|
+
session = manager.get(name)
|
|
248
|
+
|
|
249
|
+
if not session:
|
|
250
|
+
console.print(f"[red]Session '[bold]{name}[/bold]' not found.[/]")
|
|
251
|
+
sys.exit(1)
|
|
252
|
+
|
|
253
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
254
|
+
table.add_column("Property", style="bold blue")
|
|
255
|
+
table.add_column("Value", style="green")
|
|
256
|
+
|
|
257
|
+
table.add_row("Name", session.name)
|
|
258
|
+
table.add_row("Host", session.host)
|
|
259
|
+
table.add_row("Username", session.username)
|
|
260
|
+
table.add_row("Port", str(session.port))
|
|
261
|
+
table.add_row("Auth Type", session.auth_type)
|
|
262
|
+
|
|
263
|
+
if session.auth_type == "key":
|
|
264
|
+
table.add_row("Key File", session.key_file or "~/.ssh/id_rsa")
|
|
265
|
+
else:
|
|
266
|
+
table.add_row("Password", "••••••••" if session.password else "[dim]not set[/]")
|
|
267
|
+
|
|
268
|
+
console.print(Panel(table, title=f"[bold]Session: {name}[/]", expand=False))
|
|
269
|
+
console.print(f"\n[dim]Connect with: qssh {name}[/]")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@main.command("config")
|
|
273
|
+
def show_config():
|
|
274
|
+
"""Show configuration file location."""
|
|
275
|
+
config_path = manager.get_config_path()
|
|
276
|
+
console.print(f"[bold blue]Config directory:[/] [green]{config_path}[/]")
|
|
277
|
+
console.print(f"[bold blue]Sessions file:[/] [green]{config_path / 'sessions.yaml'}[/]")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
if __name__ == "__main__":
|
|
281
|
+
main()
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""SSH connection handler for qssh."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import subprocess
|
|
6
|
+
import platform
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from .session import Session
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SSHConnector:
|
|
13
|
+
"""Handles SSH connections to remote hosts."""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
"""Initialize SSH connector."""
|
|
17
|
+
self.system = platform.system().lower()
|
|
18
|
+
|
|
19
|
+
def connect(self, session: Session) -> int:
|
|
20
|
+
"""Connect to a session.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
session: Session to connect to
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Exit code from SSH process
|
|
27
|
+
"""
|
|
28
|
+
if session.auth_type == "key":
|
|
29
|
+
return self._connect_with_key(session)
|
|
30
|
+
else:
|
|
31
|
+
return self._connect_with_password(session)
|
|
32
|
+
|
|
33
|
+
def _connect_with_key(self, session: Session) -> int:
|
|
34
|
+
"""Connect using SSH key authentication.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
session: Session configuration
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Exit code
|
|
41
|
+
"""
|
|
42
|
+
key_path = os.path.expanduser(session.key_file) if session.key_file else None
|
|
43
|
+
|
|
44
|
+
cmd = [
|
|
45
|
+
"ssh",
|
|
46
|
+
"-o", "StrictHostKeyChecking=accept-new",
|
|
47
|
+
"-p", str(session.port),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
if key_path:
|
|
51
|
+
cmd.extend(["-i", key_path])
|
|
52
|
+
|
|
53
|
+
cmd.append(f"{session.username}@{session.host}")
|
|
54
|
+
|
|
55
|
+
return self._run_ssh(cmd)
|
|
56
|
+
|
|
57
|
+
def _connect_with_password(self, session: Session) -> int:
|
|
58
|
+
"""Connect using password authentication.
|
|
59
|
+
|
|
60
|
+
Uses sshpass if available, otherwise prompts for password.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
session: Session configuration
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Exit code
|
|
67
|
+
"""
|
|
68
|
+
password = session.get_password()
|
|
69
|
+
|
|
70
|
+
# Check if sshpass is available for automatic password entry
|
|
71
|
+
if password and self._has_sshpass():
|
|
72
|
+
return self._connect_with_sshpass(session, password)
|
|
73
|
+
|
|
74
|
+
# Fall back to regular SSH (will prompt for password if needed)
|
|
75
|
+
cmd = [
|
|
76
|
+
"ssh",
|
|
77
|
+
"-o", "StrictHostKeyChecking=accept-new",
|
|
78
|
+
"-p", str(session.port),
|
|
79
|
+
f"{session.username}@{session.host}",
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
if password:
|
|
83
|
+
# If we have a password but no sshpass, inform the user
|
|
84
|
+
print(f"[qssh] Password stored. Copy it or install 'sshpass' for auto-login.")
|
|
85
|
+
print(f"[qssh] Password: {password}")
|
|
86
|
+
print()
|
|
87
|
+
|
|
88
|
+
return self._run_ssh(cmd)
|
|
89
|
+
|
|
90
|
+
def _connect_with_sshpass(self, session: Session, password: str) -> int:
|
|
91
|
+
"""Connect using sshpass for automatic password entry.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
session: Session configuration
|
|
95
|
+
password: Decoded password
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Exit code
|
|
99
|
+
"""
|
|
100
|
+
cmd = [
|
|
101
|
+
"sshpass", "-p", password,
|
|
102
|
+
"ssh",
|
|
103
|
+
"-o", "StrictHostKeyChecking=accept-new",
|
|
104
|
+
"-p", str(session.port),
|
|
105
|
+
f"{session.username}@{session.host}",
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
return self._run_ssh(cmd)
|
|
109
|
+
|
|
110
|
+
def _has_sshpass(self) -> bool:
|
|
111
|
+
"""Check if sshpass is available on the system."""
|
|
112
|
+
try:
|
|
113
|
+
subprocess.run(
|
|
114
|
+
["sshpass", "-V"],
|
|
115
|
+
capture_output=True,
|
|
116
|
+
check=False
|
|
117
|
+
)
|
|
118
|
+
return True
|
|
119
|
+
except FileNotFoundError:
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
def _run_ssh(self, cmd: list) -> int:
|
|
123
|
+
"""Run SSH command and return exit code.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
cmd: Command to run
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Exit code
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
# Run SSH interactively
|
|
133
|
+
result = subprocess.run(cmd)
|
|
134
|
+
return result.returncode
|
|
135
|
+
except FileNotFoundError:
|
|
136
|
+
print("[qssh] Error: SSH client not found.")
|
|
137
|
+
print("[qssh] Please ensure OpenSSH is installed and in your PATH.")
|
|
138
|
+
if self.system == "windows":
|
|
139
|
+
print("[qssh] On Windows, you can enable it in Settings > Apps > Optional Features")
|
|
140
|
+
return 1
|
|
141
|
+
except KeyboardInterrupt:
|
|
142
|
+
print("\n[qssh] Connection interrupted.")
|
|
143
|
+
return 130
|
|
File without changes
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Session management for qssh."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import base64
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from dataclasses import dataclass, asdict
|
|
7
|
+
from typing import Optional, Dict, List
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Session:
|
|
14
|
+
"""Represents an SSH session configuration."""
|
|
15
|
+
|
|
16
|
+
name: str
|
|
17
|
+
host: str
|
|
18
|
+
username: str
|
|
19
|
+
port: int = 22
|
|
20
|
+
auth_type: str = "password" # "password" or "key"
|
|
21
|
+
password: Optional[str] = None # base64 encoded
|
|
22
|
+
key_file: Optional[str] = None
|
|
23
|
+
|
|
24
|
+
def to_dict(self) -> dict:
|
|
25
|
+
"""Convert session to dictionary for storage."""
|
|
26
|
+
data = asdict(self)
|
|
27
|
+
# Don't store None values
|
|
28
|
+
return {k: v for k, v in data.items() if v is not None}
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_dict(cls, data: dict) -> "Session":
|
|
32
|
+
"""Create session from dictionary."""
|
|
33
|
+
return cls(**data)
|
|
34
|
+
|
|
35
|
+
def get_password(self) -> Optional[str]:
|
|
36
|
+
"""Decode and return the password."""
|
|
37
|
+
if self.password:
|
|
38
|
+
try:
|
|
39
|
+
return base64.b64decode(self.password.encode()).decode()
|
|
40
|
+
except Exception:
|
|
41
|
+
return self.password
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def encode_password(password: str) -> str:
|
|
46
|
+
"""Encode password for storage."""
|
|
47
|
+
return base64.b64encode(password.encode()).decode()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SessionManager:
|
|
51
|
+
"""Manages SSH sessions storage and retrieval."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, config_dir: Optional[Path] = None):
|
|
54
|
+
"""Initialize session manager.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
config_dir: Custom config directory. Defaults to ~/.qssh
|
|
58
|
+
"""
|
|
59
|
+
if config_dir is None:
|
|
60
|
+
config_dir = Path.home() / ".qssh"
|
|
61
|
+
|
|
62
|
+
self.config_dir = config_dir
|
|
63
|
+
self.sessions_file = config_dir / "sessions.yaml"
|
|
64
|
+
self._ensure_config_dir()
|
|
65
|
+
|
|
66
|
+
def _ensure_config_dir(self) -> None:
|
|
67
|
+
"""Create config directory if it doesn't exist."""
|
|
68
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
|
|
70
|
+
# Create sessions file if it doesn't exist
|
|
71
|
+
if not self.sessions_file.exists():
|
|
72
|
+
self._save_sessions({})
|
|
73
|
+
|
|
74
|
+
def _load_sessions(self) -> Dict[str, dict]:
|
|
75
|
+
"""Load sessions from file."""
|
|
76
|
+
if not self.sessions_file.exists():
|
|
77
|
+
return {}
|
|
78
|
+
|
|
79
|
+
with open(self.sessions_file, "r", encoding="utf-8") as f:
|
|
80
|
+
data = yaml.safe_load(f)
|
|
81
|
+
return data if data else {}
|
|
82
|
+
|
|
83
|
+
def _save_sessions(self, sessions: Dict[str, dict]) -> None:
|
|
84
|
+
"""Save sessions to file."""
|
|
85
|
+
with open(self.sessions_file, "w", encoding="utf-8") as f:
|
|
86
|
+
yaml.dump(sessions, f, default_flow_style=False, sort_keys=False)
|
|
87
|
+
|
|
88
|
+
def add(self, session: Session) -> None:
|
|
89
|
+
"""Add or update a session.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
session: Session to add
|
|
93
|
+
"""
|
|
94
|
+
sessions = self._load_sessions()
|
|
95
|
+
sessions[session.name] = session.to_dict()
|
|
96
|
+
self._save_sessions(sessions)
|
|
97
|
+
|
|
98
|
+
def get(self, name: str) -> Optional[Session]:
|
|
99
|
+
"""Get a session by name.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
name: Session name
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Session if found, None otherwise
|
|
106
|
+
"""
|
|
107
|
+
sessions = self._load_sessions()
|
|
108
|
+
if name in sessions:
|
|
109
|
+
return Session.from_dict(sessions[name])
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
def remove(self, name: str) -> bool:
|
|
113
|
+
"""Remove a session.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
name: Session name
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
True if removed, False if not found
|
|
120
|
+
"""
|
|
121
|
+
sessions = self._load_sessions()
|
|
122
|
+
if name in sessions:
|
|
123
|
+
del sessions[name]
|
|
124
|
+
self._save_sessions(sessions)
|
|
125
|
+
return True
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
def list_all(self) -> List[Session]:
|
|
129
|
+
"""List all sessions.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
List of all sessions
|
|
133
|
+
"""
|
|
134
|
+
sessions = self._load_sessions()
|
|
135
|
+
return [Session.from_dict(data) for data in sessions.values()]
|
|
136
|
+
|
|
137
|
+
def exists(self, name: str) -> bool:
|
|
138
|
+
"""Check if session exists.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
name: Session name
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
True if exists
|
|
145
|
+
"""
|
|
146
|
+
sessions = self._load_sessions()
|
|
147
|
+
return name in sessions
|
|
148
|
+
|
|
149
|
+
def get_config_path(self) -> Path:
|
|
150
|
+
"""Get the config directory path."""
|
|
151
|
+
return self.config_dir
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qssh
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Quick SSH session manager - save your VM credentials and connect with a single command
|
|
5
|
+
Author-email: joan-code6 <your-email@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yourusername/qssh
|
|
8
|
+
Project-URL: Repository, https://github.com/yourusername/qssh
|
|
9
|
+
Project-URL: Issues, https://github.com/yourusername/qssh/issues
|
|
10
|
+
Keywords: ssh,session,manager,vm,cli,terminal
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: System :: Networking
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: click>=8.0.0
|
|
29
|
+
Requires-Dist: rich>=13.0.0
|
|
30
|
+
Requires-Dist: pyyaml>=6.0
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# qssh
|
|
34
|
+
|
|
35
|
+
**Quick SSH session manager** - Save your VM credentials and connect with a single command.
|
|
36
|
+
|
|
37
|
+
Tired of copy-pasting credentials every time you want to SSH into your VMs? `qssh` lets you save your session configs and connect instantly.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install qssh
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
### 1. Add a new session
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
qssh add outcraft
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
You'll be prompted for:
|
|
54
|
+
- Host (IP address or hostname)
|
|
55
|
+
- Username
|
|
56
|
+
- Port (default: 22)
|
|
57
|
+
- Authentication method (password or key file)
|
|
58
|
+
|
|
59
|
+
### 2. Connect to your VM
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
qssh outcraft
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
That's it! You're connected.
|
|
66
|
+
|
|
67
|
+
## Commands
|
|
68
|
+
|
|
69
|
+
| Command | Description |
|
|
70
|
+
|---------|-------------|
|
|
71
|
+
| `qssh <session>` | Connect to a saved session |
|
|
72
|
+
| `qssh add <name>` | Add a new session |
|
|
73
|
+
| `qssh list` | List all saved sessions |
|
|
74
|
+
| `qssh remove <name>` | Remove a session |
|
|
75
|
+
| `qssh edit <name>` | Edit an existing session |
|
|
76
|
+
| `qssh show <name>` | Show session details |
|
|
77
|
+
| `qssh config` | Show config file location |
|
|
78
|
+
|
|
79
|
+
## Examples
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Add a session for your OutCraft VM
|
|
83
|
+
qssh add outcraft
|
|
84
|
+
# Host: 192.168.1.100
|
|
85
|
+
# Username: admin
|
|
86
|
+
# Port [22]: 22
|
|
87
|
+
# Auth type (password/key) [password]: password
|
|
88
|
+
# Password: ********
|
|
89
|
+
|
|
90
|
+
# Now just connect with:
|
|
91
|
+
qssh outcraft
|
|
92
|
+
|
|
93
|
+
# List all your sessions
|
|
94
|
+
qssh list
|
|
95
|
+
|
|
96
|
+
# Remove a session
|
|
97
|
+
qssh remove old-server
|
|
98
|
+
|
|
99
|
+
# Show details of a session
|
|
100
|
+
qssh show outcraft
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Using SSH Keys
|
|
104
|
+
|
|
105
|
+
For key-based authentication:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
qssh add myserver
|
|
109
|
+
# Host: example.com
|
|
110
|
+
# Username: deploy
|
|
111
|
+
# Port [22]: 22
|
|
112
|
+
# Auth type (password/key) [password]: key
|
|
113
|
+
# Key file path: ~/.ssh/id_rsa
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Configuration
|
|
117
|
+
|
|
118
|
+
Sessions are stored in `~/.qssh/sessions.yaml`. Passwords are stored encoded (not plaintext) but for maximum security, consider using SSH keys instead.
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT License
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/qssh/__init__.py
|
|
5
|
+
src/qssh/cli.py
|
|
6
|
+
src/qssh/connector.py
|
|
7
|
+
src/qssh/py.typed
|
|
8
|
+
src/qssh/session.py
|
|
9
|
+
src/qssh.egg-info/PKG-INFO
|
|
10
|
+
src/qssh.egg-info/SOURCES.txt
|
|
11
|
+
src/qssh.egg-info/dependency_links.txt
|
|
12
|
+
src/qssh.egg-info/entry_points.txt
|
|
13
|
+
src/qssh.egg-info/requires.txt
|
|
14
|
+
src/qssh.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
qssh
|