ntermqt 0.1.1__tar.gz → 0.1.3__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.
- {ntermqt-0.1.1/ntermqt.egg-info → ntermqt-0.1.3}/PKG-INFO +118 -11
- {ntermqt-0.1.1 → ntermqt-0.1.3}/README.md +108 -10
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/__main__.py +132 -9
- ntermqt-0.1.3/nterm/scripting/__init__.py +43 -0
- ntermqt-0.1.3/nterm/scripting/api.py +447 -0
- ntermqt-0.1.3/nterm/scripting/cli.py +305 -0
- ntermqt-0.1.3/nterm/session/local_terminal.py +225 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/session/pty_transport.py +105 -91
- {ntermqt-0.1.1 → ntermqt-0.1.3/ntermqt.egg-info}/PKG-INFO +118 -11
- {ntermqt-0.1.1 → ntermqt-0.1.3}/ntermqt.egg-info/SOURCES.txt +4 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/ntermqt.egg-info/entry_points.txt +1 -0
- ntermqt-0.1.3/ntermqt.egg-info/requires.txt +30 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/pyproject.toml +10 -3
- ntermqt-0.1.1/ntermqt.egg-info/requires.txt +0 -15
- {ntermqt-0.1.1 → ntermqt-0.1.3}/MANIFEST.in +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/__init__.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/askpass/__init__.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/askpass/server.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/config.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/connection/__init__.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/connection/profile.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/manager/__init__.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/manager/connect_dialog.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/manager/editor.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/manager/io.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/manager/models.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/manager/settings.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/manager/tree.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/resources.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/session/__init__.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/session/askpass_ssh.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/session/base.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/session/interactive_ssh.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/session/ssh.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/__init__.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/bridge.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/resources/terminal.html +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/resources/terminal.js +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/resources/xterm-addon-fit.min.js +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/resources/xterm-addon-unicode11.min.js +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/resources/xterm-addon-web-links.min.js +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/resources/xterm.css +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/resources/xterm.min.js +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/terminal/widget.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/__init__.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/engine.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/stylesheet.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/themes/clean.yaml +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/themes/default.yaml +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/themes/dracula.yaml +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/themes/enterprise_dark.yaml +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/themes/enterprise_hybrid.yaml +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/themes/enterprise_light.yaml +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/themes/gruvbox_dark.yaml +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/themes/gruvbox_hybrid.yaml +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/theme/themes/gruvbox_light.yaml +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/vault/__init__.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/vault/credential_manager.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/vault/keychain.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/vault/manager_ui.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/vault/profile.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/vault/resolver.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/nterm/vault/store.py +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/ntermqt.egg-info/dependency_links.txt +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/ntermqt.egg-info/top_level.txt +0 -0
- {ntermqt-0.1.1 → ntermqt-0.1.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ntermqt
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Modern SSH terminal widget for PyQt6 with credential vault and jump host support
|
|
5
5
|
Author: Scott Peterman
|
|
6
6
|
License: GPL-3.0
|
|
@@ -25,14 +25,23 @@ Requires-Dist: PyQt6-WebEngine>=6.4.0
|
|
|
25
25
|
Requires-Dist: paramiko>=3.0.0
|
|
26
26
|
Requires-Dist: cryptography>=41.0.0
|
|
27
27
|
Requires-Dist: pyyaml>=6.0
|
|
28
|
+
Requires-Dist: click>=8.0.0
|
|
29
|
+
Requires-Dist: pexpect>=4.8.0; sys_platform != "win32"
|
|
30
|
+
Requires-Dist: pywinpty>=2.0.0; sys_platform == "win32"
|
|
28
31
|
Provides-Extra: keyring
|
|
29
32
|
Requires-Dist: keyring>=24.0.0; extra == "keyring"
|
|
33
|
+
Provides-Extra: scripting
|
|
34
|
+
Requires-Dist: ipython>=8.0.0; extra == "scripting"
|
|
30
35
|
Provides-Extra: dev
|
|
31
36
|
Requires-Dist: pytest; extra == "dev"
|
|
32
37
|
Requires-Dist: black; extra == "dev"
|
|
33
38
|
Requires-Dist: pyinstaller; extra == "dev"
|
|
34
39
|
Requires-Dist: build; extra == "dev"
|
|
35
40
|
Requires-Dist: twine; extra == "dev"
|
|
41
|
+
Requires-Dist: ipython>=8.0.0; extra == "dev"
|
|
42
|
+
Provides-Extra: all
|
|
43
|
+
Requires-Dist: keyring>=24.0.0; extra == "all"
|
|
44
|
+
Requires-Dist: ipython>=8.0.0; extra == "all"
|
|
36
45
|
|
|
37
46
|
# nterm
|
|
38
47
|
|
|
@@ -74,6 +83,12 @@ Built for managing hundreds of devices through bastion hosts with hardware secur
|
|
|
74
83
|
- Cross-platform keychain: macOS Keychain, Windows Credential Locker, Linux Secret Service
|
|
75
84
|
- Full PyQt6 management UI
|
|
76
85
|
|
|
86
|
+
**Scripting API** *(Experimental)*
|
|
87
|
+
- Query device inventory and credentials programmatically
|
|
88
|
+
- Built-in IPython console with API pre-loaded
|
|
89
|
+
- CLI for shell scripts and automation
|
|
90
|
+
- Foundation for MCP tools and agentic workflows
|
|
91
|
+
|
|
77
92
|
---
|
|
78
93
|
|
|
79
94
|
## Screenshots
|
|
@@ -88,15 +103,48 @@ Built for managing hundreds of devices through bastion hosts with hardware secur
|
|
|
88
103
|
|
|
89
104
|
---
|
|
90
105
|
|
|
106
|
+
## Dev Console
|
|
107
|
+
|
|
108
|
+
nterm includes a built-in development console accessible via **Dev → IPython** or **Dev → Shell**. Open in a tab alongside your SSH sessions, or pop out to a separate window.
|
|
109
|
+
|
|
110
|
+

|
|
111
|
+
|
|
112
|
+
The IPython console runs in the same Python environment as nterm, with the scripting API pre-loaded. Query your device inventory, inspect credentials, and prototype automation workflows without leaving the app.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
# Available immediately when IPython opens
|
|
116
|
+
api.devices() # List all saved devices
|
|
117
|
+
api.search("leaf") # Search by name/hostname
|
|
118
|
+
api.credentials() # List credentials (after api.unlock())
|
|
119
|
+
api.help() # Show all commands
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Use cases:**
|
|
123
|
+
- Debug connection issues with live access to session objects
|
|
124
|
+
- Prototype automation scripts against your real device inventory
|
|
125
|
+
- Test credential resolution patterns
|
|
126
|
+
- Build and test MCP tools interactively
|
|
127
|
+
|
|
128
|
+
Requires the `scripting` extra: `pip install ntermqt[scripting]`
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
91
132
|
## Installation
|
|
92
133
|
|
|
134
|
+
### Be aware due to a naming conflict, the pypi package is actually "ntermqt"
|
|
135
|
+
|
|
93
136
|
### From PyPI
|
|
94
137
|
|
|
138
|
+
https://pypi.org/project/ntermqt/
|
|
139
|
+
|
|
95
140
|
```bash
|
|
96
|
-
pip install
|
|
141
|
+
pip install ntermqt
|
|
97
142
|
|
|
98
|
-
#
|
|
99
|
-
pip install
|
|
143
|
+
# With optional scripting support (IPython)
|
|
144
|
+
pip install ntermqt[scripting]
|
|
145
|
+
|
|
146
|
+
# With all optional features
|
|
147
|
+
pip install ntermqt[all]
|
|
100
148
|
|
|
101
149
|
# Run
|
|
102
150
|
nterm
|
|
@@ -114,10 +162,7 @@ source .venv/bin/activate # Linux/macOS
|
|
|
114
162
|
# .venv\Scripts\activate # Windows
|
|
115
163
|
|
|
116
164
|
# Install in development mode
|
|
117
|
-
pip install -e .
|
|
118
|
-
|
|
119
|
-
# Optional: system keychain support
|
|
120
|
-
pip install keyring
|
|
165
|
+
pip install -e ".[all]"
|
|
121
166
|
|
|
122
167
|
# Run
|
|
123
168
|
nterm
|
|
@@ -137,12 +182,70 @@ python -m nterm
|
|
|
137
182
|
|
|
138
183
|
| Platform | PTY | Keychain |
|
|
139
184
|
|----------|-----|----------|
|
|
140
|
-
| Linux | ✅
|
|
141
|
-
| macOS | ✅
|
|
185
|
+
| Linux | ✅ pexpect | Secret Service |
|
|
186
|
+
| macOS | ✅ pexpect | macOS Keychain |
|
|
142
187
|
| Windows 10+ | ✅ pywinpty | Credential Locker |
|
|
143
188
|
|
|
144
189
|
---
|
|
145
190
|
|
|
191
|
+
## Scripting API *(Experimental)*
|
|
192
|
+
|
|
193
|
+
nterm includes a scripting API for programmatic access to your device inventory and credential vault. Use it from IPython, CLI, or Python scripts.
|
|
194
|
+
|
|
195
|
+
### IPython Console
|
|
196
|
+
|
|
197
|
+
Open **Dev → IPython → Open in Tab** to get an interactive console with the API pre-loaded:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
api.devices() # List all saved devices
|
|
201
|
+
api.search("leaf") # Search by name/hostname
|
|
202
|
+
api.devices("eng-*") # Glob pattern filter
|
|
203
|
+
|
|
204
|
+
api.unlock("vault-password") # Unlock credential vault
|
|
205
|
+
api.credentials() # List credentials (metadata only)
|
|
206
|
+
|
|
207
|
+
api.help() # Show all commands
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### CLI
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
nterm-cli devices # List all devices
|
|
214
|
+
nterm-cli search leaf # Search devices
|
|
215
|
+
nterm-cli device eng-leaf-1 # Device details
|
|
216
|
+
nterm-cli credentials --unlock # List credentials
|
|
217
|
+
nterm-cli --json devices # JSON output for scripting
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Python Scripts
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from nterm.scripting import NTermAPI
|
|
224
|
+
|
|
225
|
+
api = NTermAPI()
|
|
226
|
+
|
|
227
|
+
# Query devices
|
|
228
|
+
for device in api.devices("*spine*"):
|
|
229
|
+
print(f"{device.name}: {device.hostname}")
|
|
230
|
+
|
|
231
|
+
# Work with credentials
|
|
232
|
+
api.unlock("vault-password")
|
|
233
|
+
cred = api.credential("lab-admin")
|
|
234
|
+
print(f"Username: {cred.username}")
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Roadmap
|
|
238
|
+
|
|
239
|
+
The scripting API is the foundation for:
|
|
240
|
+
|
|
241
|
+
- **Command execution** — `api.connect()` and `api.send()` for programmatic device interaction
|
|
242
|
+
- **Batch operations** — Fan out commands across device groups
|
|
243
|
+
- **MCP tool integration** — Expose nterm capabilities to AI agents
|
|
244
|
+
|
|
245
|
+
See [scripting/README.md](nterm/scripting/README.md) for full API documentation.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
146
249
|
## Quick Start
|
|
147
250
|
|
|
148
251
|
### As a Widget
|
|
@@ -289,6 +392,7 @@ nterm/
|
|
|
289
392
|
├── session/
|
|
290
393
|
│ ├── ssh.py # SSHSession (Paramiko) with legacy fallback
|
|
291
394
|
│ ├── interactive_ssh.py # Native SSH + PTY
|
|
395
|
+
│ ├── local_terminal.py # Local shell/IPython sessions
|
|
292
396
|
│ └── pty_transport.py # Cross-platform PTY
|
|
293
397
|
├── terminal/
|
|
294
398
|
│ ├── widget.py # TerminalWidget (PyQt6 + xterm.js)
|
|
@@ -300,7 +404,10 @@ nterm/
|
|
|
300
404
|
│ ├── store.py # Encrypted credential storage
|
|
301
405
|
│ ├── resolver.py # Pattern-based resolution
|
|
302
406
|
│ └── manager_ui.py # PyQt6 credential manager
|
|
303
|
-
|
|
407
|
+
├── manager/ # Session tree, connection dialogs
|
|
408
|
+
└── scripting/ # API, CLI, automation support
|
|
409
|
+
├── api.py # NTermAPI class
|
|
410
|
+
└── cli.py # nterm-cli entry point
|
|
304
411
|
```
|
|
305
412
|
|
|
306
413
|
---
|
|
@@ -38,6 +38,12 @@ Built for managing hundreds of devices through bastion hosts with hardware secur
|
|
|
38
38
|
- Cross-platform keychain: macOS Keychain, Windows Credential Locker, Linux Secret Service
|
|
39
39
|
- Full PyQt6 management UI
|
|
40
40
|
|
|
41
|
+
**Scripting API** *(Experimental)*
|
|
42
|
+
- Query device inventory and credentials programmatically
|
|
43
|
+
- Built-in IPython console with API pre-loaded
|
|
44
|
+
- CLI for shell scripts and automation
|
|
45
|
+
- Foundation for MCP tools and agentic workflows
|
|
46
|
+
|
|
41
47
|
---
|
|
42
48
|
|
|
43
49
|
## Screenshots
|
|
@@ -52,15 +58,48 @@ Built for managing hundreds of devices through bastion hosts with hardware secur
|
|
|
52
58
|
|
|
53
59
|
---
|
|
54
60
|
|
|
61
|
+
## Dev Console
|
|
62
|
+
|
|
63
|
+
nterm includes a built-in development console accessible via **Dev → IPython** or **Dev → Shell**. Open in a tab alongside your SSH sessions, or pop out to a separate window.
|
|
64
|
+
|
|
65
|
+

|
|
66
|
+
|
|
67
|
+
The IPython console runs in the same Python environment as nterm, with the scripting API pre-loaded. Query your device inventory, inspect credentials, and prototype automation workflows without leaving the app.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# Available immediately when IPython opens
|
|
71
|
+
api.devices() # List all saved devices
|
|
72
|
+
api.search("leaf") # Search by name/hostname
|
|
73
|
+
api.credentials() # List credentials (after api.unlock())
|
|
74
|
+
api.help() # Show all commands
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Use cases:**
|
|
78
|
+
- Debug connection issues with live access to session objects
|
|
79
|
+
- Prototype automation scripts against your real device inventory
|
|
80
|
+
- Test credential resolution patterns
|
|
81
|
+
- Build and test MCP tools interactively
|
|
82
|
+
|
|
83
|
+
Requires the `scripting` extra: `pip install ntermqt[scripting]`
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
55
87
|
## Installation
|
|
56
88
|
|
|
89
|
+
### Be aware due to a naming conflict, the pypi package is actually "ntermqt"
|
|
90
|
+
|
|
57
91
|
### From PyPI
|
|
58
92
|
|
|
93
|
+
https://pypi.org/project/ntermqt/
|
|
94
|
+
|
|
59
95
|
```bash
|
|
60
|
-
pip install
|
|
96
|
+
pip install ntermqt
|
|
61
97
|
|
|
62
|
-
#
|
|
63
|
-
pip install
|
|
98
|
+
# With optional scripting support (IPython)
|
|
99
|
+
pip install ntermqt[scripting]
|
|
100
|
+
|
|
101
|
+
# With all optional features
|
|
102
|
+
pip install ntermqt[all]
|
|
64
103
|
|
|
65
104
|
# Run
|
|
66
105
|
nterm
|
|
@@ -78,10 +117,7 @@ source .venv/bin/activate # Linux/macOS
|
|
|
78
117
|
# .venv\Scripts\activate # Windows
|
|
79
118
|
|
|
80
119
|
# Install in development mode
|
|
81
|
-
pip install -e .
|
|
82
|
-
|
|
83
|
-
# Optional: system keychain support
|
|
84
|
-
pip install keyring
|
|
120
|
+
pip install -e ".[all]"
|
|
85
121
|
|
|
86
122
|
# Run
|
|
87
123
|
nterm
|
|
@@ -101,12 +137,70 @@ python -m nterm
|
|
|
101
137
|
|
|
102
138
|
| Platform | PTY | Keychain |
|
|
103
139
|
|----------|-----|----------|
|
|
104
|
-
| Linux | ✅
|
|
105
|
-
| macOS | ✅
|
|
140
|
+
| Linux | ✅ pexpect | Secret Service |
|
|
141
|
+
| macOS | ✅ pexpect | macOS Keychain |
|
|
106
142
|
| Windows 10+ | ✅ pywinpty | Credential Locker |
|
|
107
143
|
|
|
108
144
|
---
|
|
109
145
|
|
|
146
|
+
## Scripting API *(Experimental)*
|
|
147
|
+
|
|
148
|
+
nterm includes a scripting API for programmatic access to your device inventory and credential vault. Use it from IPython, CLI, or Python scripts.
|
|
149
|
+
|
|
150
|
+
### IPython Console
|
|
151
|
+
|
|
152
|
+
Open **Dev → IPython → Open in Tab** to get an interactive console with the API pre-loaded:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
api.devices() # List all saved devices
|
|
156
|
+
api.search("leaf") # Search by name/hostname
|
|
157
|
+
api.devices("eng-*") # Glob pattern filter
|
|
158
|
+
|
|
159
|
+
api.unlock("vault-password") # Unlock credential vault
|
|
160
|
+
api.credentials() # List credentials (metadata only)
|
|
161
|
+
|
|
162
|
+
api.help() # Show all commands
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### CLI
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
nterm-cli devices # List all devices
|
|
169
|
+
nterm-cli search leaf # Search devices
|
|
170
|
+
nterm-cli device eng-leaf-1 # Device details
|
|
171
|
+
nterm-cli credentials --unlock # List credentials
|
|
172
|
+
nterm-cli --json devices # JSON output for scripting
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Python Scripts
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from nterm.scripting import NTermAPI
|
|
179
|
+
|
|
180
|
+
api = NTermAPI()
|
|
181
|
+
|
|
182
|
+
# Query devices
|
|
183
|
+
for device in api.devices("*spine*"):
|
|
184
|
+
print(f"{device.name}: {device.hostname}")
|
|
185
|
+
|
|
186
|
+
# Work with credentials
|
|
187
|
+
api.unlock("vault-password")
|
|
188
|
+
cred = api.credential("lab-admin")
|
|
189
|
+
print(f"Username: {cred.username}")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Roadmap
|
|
193
|
+
|
|
194
|
+
The scripting API is the foundation for:
|
|
195
|
+
|
|
196
|
+
- **Command execution** — `api.connect()` and `api.send()` for programmatic device interaction
|
|
197
|
+
- **Batch operations** — Fan out commands across device groups
|
|
198
|
+
- **MCP tool integration** — Expose nterm capabilities to AI agents
|
|
199
|
+
|
|
200
|
+
See [scripting/README.md](nterm/scripting/README.md) for full API documentation.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
110
204
|
## Quick Start
|
|
111
205
|
|
|
112
206
|
### As a Widget
|
|
@@ -253,6 +347,7 @@ nterm/
|
|
|
253
347
|
├── session/
|
|
254
348
|
│ ├── ssh.py # SSHSession (Paramiko) with legacy fallback
|
|
255
349
|
│ ├── interactive_ssh.py # Native SSH + PTY
|
|
350
|
+
│ ├── local_terminal.py # Local shell/IPython sessions
|
|
256
351
|
│ └── pty_transport.py # Cross-platform PTY
|
|
257
352
|
├── terminal/
|
|
258
353
|
│ ├── widget.py # TerminalWidget (PyQt6 + xterm.js)
|
|
@@ -264,7 +359,10 @@ nterm/
|
|
|
264
359
|
│ ├── store.py # Encrypted credential storage
|
|
265
360
|
│ ├── resolver.py # Pattern-based resolution
|
|
266
361
|
│ └── manager_ui.py # PyQt6 credential manager
|
|
267
|
-
|
|
362
|
+
├── manager/ # Session tree, connection dialogs
|
|
363
|
+
└── scripting/ # API, CLI, automation support
|
|
364
|
+
├── api.py # NTermAPI class
|
|
365
|
+
└── cli.py # nterm-cli entry point
|
|
268
366
|
```
|
|
269
367
|
|
|
270
368
|
---
|
|
@@ -22,6 +22,7 @@ from nterm.manager import (
|
|
|
22
22
|
)
|
|
23
23
|
from nterm.terminal.widget import TerminalWidget
|
|
24
24
|
from nterm.session.ssh import SSHSession
|
|
25
|
+
from nterm.session.local_terminal import LocalTerminal
|
|
25
26
|
from nterm.connection.profile import ConnectionProfile, AuthConfig
|
|
26
27
|
from nterm.vault import CredentialManagerWidget
|
|
27
28
|
from nterm.vault.resolver import CredentialResolver
|
|
@@ -225,6 +226,35 @@ class TerminalTab(QWidget):
|
|
|
225
226
|
return True
|
|
226
227
|
|
|
227
228
|
|
|
229
|
+
class LocalTerminalTab(QWidget):
|
|
230
|
+
"""A terminal tab for local processes (shell, IPython, etc.)."""
|
|
231
|
+
|
|
232
|
+
def __init__(self, name: str, session: LocalTerminal, parent=None):
|
|
233
|
+
super().__init__(parent)
|
|
234
|
+
self.name = name
|
|
235
|
+
self.local_session = session
|
|
236
|
+
|
|
237
|
+
layout = QVBoxLayout(self)
|
|
238
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
239
|
+
|
|
240
|
+
self.terminal = TerminalWidget()
|
|
241
|
+
layout.addWidget(self.terminal)
|
|
242
|
+
|
|
243
|
+
self.terminal.attach_session(self.local_session)
|
|
244
|
+
|
|
245
|
+
def connect(self):
|
|
246
|
+
"""Start the local process."""
|
|
247
|
+
self.local_session.connect()
|
|
248
|
+
|
|
249
|
+
def disconnect(self):
|
|
250
|
+
"""Terminate the local process."""
|
|
251
|
+
self.local_session.disconnect()
|
|
252
|
+
|
|
253
|
+
def is_connected(self) -> bool:
|
|
254
|
+
"""Check if the process is still running."""
|
|
255
|
+
return self.local_session.is_connected
|
|
256
|
+
|
|
257
|
+
|
|
228
258
|
class MainWindow(QMainWindow):
|
|
229
259
|
"""
|
|
230
260
|
Main application window with session tree and tabbed terminals.
|
|
@@ -274,7 +304,7 @@ class MainWindow(QMainWindow):
|
|
|
274
304
|
# Apply multiline threshold to all open terminals
|
|
275
305
|
for i in range(self.tab_widget.count()):
|
|
276
306
|
tab = self.tab_widget.widget(i)
|
|
277
|
-
if isinstance(tab, TerminalTab):
|
|
307
|
+
if isinstance(tab, (TerminalTab, LocalTerminalTab)):
|
|
278
308
|
tab.terminal.set_multiline_threshold(settings.multiline_paste_threshold)
|
|
279
309
|
|
|
280
310
|
|
|
@@ -401,6 +431,32 @@ class MainWindow(QMainWindow):
|
|
|
401
431
|
action.triggered.connect(lambda checked, n=theme_name: self._apply_theme_by_name(n))
|
|
402
432
|
theme_menu.addAction(action)
|
|
403
433
|
|
|
434
|
+
# Dev menu
|
|
435
|
+
dev_menu = menubar.addMenu("&Dev")
|
|
436
|
+
|
|
437
|
+
# IPython submenu
|
|
438
|
+
ipython_menu = dev_menu.addMenu("&IPython")
|
|
439
|
+
|
|
440
|
+
ipython_tab_action = QAction("Open in &Tab", self)
|
|
441
|
+
ipython_tab_action.setShortcut(QKeySequence("Ctrl+Shift+I"))
|
|
442
|
+
ipython_tab_action.triggered.connect(lambda: self._open_local("IPython", LocalTerminal.ipython(), "tab"))
|
|
443
|
+
ipython_menu.addAction(ipython_tab_action)
|
|
444
|
+
|
|
445
|
+
ipython_window_action = QAction("Open in &Window", self)
|
|
446
|
+
ipython_window_action.triggered.connect(lambda: self._open_local("IPython", LocalTerminal.ipython(), "window"))
|
|
447
|
+
ipython_menu.addAction(ipython_window_action)
|
|
448
|
+
|
|
449
|
+
# Shell submenu
|
|
450
|
+
shell_menu = dev_menu.addMenu("&Shell")
|
|
451
|
+
|
|
452
|
+
shell_tab_action = QAction("Open in &Tab", self)
|
|
453
|
+
shell_tab_action.triggered.connect(lambda: self._open_local("Shell", LocalTerminal(), "tab"))
|
|
454
|
+
shell_menu.addAction(shell_tab_action)
|
|
455
|
+
|
|
456
|
+
shell_window_action = QAction("Open in &Window", self)
|
|
457
|
+
shell_window_action.triggered.connect(lambda: self._open_local("Shell", LocalTerminal(), "window"))
|
|
458
|
+
shell_menu.addAction(shell_window_action)
|
|
459
|
+
|
|
404
460
|
def _on_import_sessions(self):
|
|
405
461
|
"""Show import dialog."""
|
|
406
462
|
dialog = ImportDialog(self.session_store, self)
|
|
@@ -445,7 +501,7 @@ class MainWindow(QMainWindow):
|
|
|
445
501
|
# Update all open terminal tabs
|
|
446
502
|
for i in range(self.tab_widget.count()):
|
|
447
503
|
tab = self.tab_widget.widget(i)
|
|
448
|
-
if isinstance(tab, TerminalTab):
|
|
504
|
+
if isinstance(tab, (TerminalTab, LocalTerminalTab)):
|
|
449
505
|
tab.terminal.set_theme(theme)
|
|
450
506
|
|
|
451
507
|
def _apply_qt_theme(self, theme: Theme):
|
|
@@ -560,6 +616,23 @@ class MainWindow(QMainWindow):
|
|
|
560
616
|
self._child_windows.append(window)
|
|
561
617
|
window.destroyed.connect(lambda: self._child_windows.remove(window))
|
|
562
618
|
|
|
619
|
+
def _open_local(self, name: str, session: LocalTerminal, mode: str):
|
|
620
|
+
"""Open a local terminal session (IPython, shell, etc.)."""
|
|
621
|
+
if mode == "tab":
|
|
622
|
+
tab = LocalTerminalTab(name, session)
|
|
623
|
+
tab.terminal.set_theme(self.current_theme)
|
|
624
|
+
idx = self.tab_widget.addTab(tab, name)
|
|
625
|
+
self.tab_widget.setCurrentIndex(idx)
|
|
626
|
+
self.tab_widget.setTabToolTip(idx, f"{name} (local)")
|
|
627
|
+
tab.connect()
|
|
628
|
+
else:
|
|
629
|
+
window = LocalTerminalWindow(name, session, self.current_theme)
|
|
630
|
+
window.show()
|
|
631
|
+
if not hasattr(self, '_child_windows'):
|
|
632
|
+
self._child_windows = []
|
|
633
|
+
self._child_windows.append(window)
|
|
634
|
+
window.destroyed.connect(lambda: self._child_windows.remove(window))
|
|
635
|
+
|
|
563
636
|
# -------------------------------------------------------------------------
|
|
564
637
|
# Tab Management (NEW)
|
|
565
638
|
# -------------------------------------------------------------------------
|
|
@@ -569,7 +642,7 @@ class MainWindow(QMainWindow):
|
|
|
569
642
|
count = 0
|
|
570
643
|
for i in range(self.tab_widget.count()):
|
|
571
644
|
tab = self.tab_widget.widget(i)
|
|
572
|
-
if isinstance(tab, TerminalTab) and tab.is_connected():
|
|
645
|
+
if isinstance(tab, (TerminalTab, LocalTerminalTab)) and tab.is_connected():
|
|
573
646
|
count += 1
|
|
574
647
|
return count
|
|
575
648
|
|
|
@@ -619,13 +692,29 @@ class MainWindow(QMainWindow):
|
|
|
619
692
|
|
|
620
693
|
tab.disconnect()
|
|
621
694
|
|
|
695
|
+
elif isinstance(tab, LocalTerminalTab):
|
|
696
|
+
# Check if process is still running
|
|
697
|
+
if tab.is_connected():
|
|
698
|
+
reply = QMessageBox.question(
|
|
699
|
+
self,
|
|
700
|
+
"Close Tab",
|
|
701
|
+
f"'{tab.name}' is still running.\n\n"
|
|
702
|
+
"Terminate and close this tab?",
|
|
703
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
704
|
+
QMessageBox.StandardButton.No
|
|
705
|
+
)
|
|
706
|
+
if reply != QMessageBox.StandardButton.Yes:
|
|
707
|
+
return
|
|
708
|
+
|
|
709
|
+
tab.disconnect()
|
|
710
|
+
|
|
622
711
|
self.tab_widget.removeTab(index)
|
|
623
712
|
|
|
624
713
|
def _close_other_tabs(self, keep_index: int):
|
|
625
714
|
"""Close all tabs except the specified one."""
|
|
626
715
|
active_count = self._get_active_session_count()
|
|
627
716
|
tab_to_keep = self.tab_widget.widget(keep_index)
|
|
628
|
-
keep_is_active = isinstance(tab_to_keep, TerminalTab) and tab_to_keep.is_connected()
|
|
717
|
+
keep_is_active = isinstance(tab_to_keep, (TerminalTab, LocalTerminalTab)) and tab_to_keep.is_connected()
|
|
629
718
|
other_active = active_count - (1 if keep_is_active else 0)
|
|
630
719
|
|
|
631
720
|
if other_active > 0:
|
|
@@ -644,7 +733,7 @@ class MainWindow(QMainWindow):
|
|
|
644
733
|
for i in range(self.tab_widget.count() - 1, -1, -1):
|
|
645
734
|
if i != keep_index:
|
|
646
735
|
tab = self.tab_widget.widget(i)
|
|
647
|
-
if isinstance(tab, TerminalTab):
|
|
736
|
+
if isinstance(tab, (TerminalTab, LocalTerminalTab)):
|
|
648
737
|
tab.disconnect()
|
|
649
738
|
self.tab_widget.removeTab(i)
|
|
650
739
|
|
|
@@ -658,7 +747,7 @@ class MainWindow(QMainWindow):
|
|
|
658
747
|
active_count = 0
|
|
659
748
|
for i in range(index + 1, self.tab_widget.count()):
|
|
660
749
|
tab = self.tab_widget.widget(i)
|
|
661
|
-
if isinstance(tab, TerminalTab) and tab.is_connected():
|
|
750
|
+
if isinstance(tab, (TerminalTab, LocalTerminalTab)) and tab.is_connected():
|
|
662
751
|
active_count += 1
|
|
663
752
|
|
|
664
753
|
if active_count > 0:
|
|
@@ -676,7 +765,7 @@ class MainWindow(QMainWindow):
|
|
|
676
765
|
# Close from end to avoid index shifting
|
|
677
766
|
for i in range(self.tab_widget.count() - 1, index, -1):
|
|
678
767
|
tab = self.tab_widget.widget(i)
|
|
679
|
-
if isinstance(tab, TerminalTab):
|
|
768
|
+
if isinstance(tab, (TerminalTab, LocalTerminalTab)):
|
|
680
769
|
tab.disconnect()
|
|
681
770
|
self.tab_widget.removeTab(i)
|
|
682
771
|
|
|
@@ -702,7 +791,7 @@ class MainWindow(QMainWindow):
|
|
|
702
791
|
# Close all tabs
|
|
703
792
|
while self.tab_widget.count() > 0:
|
|
704
793
|
tab = self.tab_widget.widget(0)
|
|
705
|
-
if isinstance(tab, TerminalTab):
|
|
794
|
+
if isinstance(tab, (TerminalTab, LocalTerminalTab)):
|
|
706
795
|
tab.disconnect()
|
|
707
796
|
self.tab_widget.removeTab(0)
|
|
708
797
|
|
|
@@ -741,7 +830,7 @@ class MainWindow(QMainWindow):
|
|
|
741
830
|
# Disconnect all tabs
|
|
742
831
|
for i in range(self.tab_widget.count()):
|
|
743
832
|
tab = self.tab_widget.widget(i)
|
|
744
|
-
if isinstance(tab, TerminalTab):
|
|
833
|
+
if isinstance(tab, (TerminalTab, LocalTerminalTab)):
|
|
745
834
|
tab.disconnect()
|
|
746
835
|
|
|
747
836
|
self.session_store.close()
|
|
@@ -790,6 +879,40 @@ class TerminalWindow(QMainWindow):
|
|
|
790
879
|
event.accept()
|
|
791
880
|
|
|
792
881
|
|
|
882
|
+
class LocalTerminalWindow(QMainWindow):
|
|
883
|
+
"""Standalone window for local terminal sessions."""
|
|
884
|
+
|
|
885
|
+
def __init__(self, name: str, session: LocalTerminal, theme: Theme):
|
|
886
|
+
super().__init__()
|
|
887
|
+
self.setWindowTitle(f"{name} - Local")
|
|
888
|
+
self.resize(1000, 700)
|
|
889
|
+
|
|
890
|
+
self.setStyleSheet(generate_stylesheet(theme))
|
|
891
|
+
|
|
892
|
+
self.tab = LocalTerminalTab(name, session)
|
|
893
|
+
self.tab.terminal.set_theme(theme)
|
|
894
|
+
self.setCentralWidget(self.tab)
|
|
895
|
+
|
|
896
|
+
self.tab.connect()
|
|
897
|
+
|
|
898
|
+
def closeEvent(self, event):
|
|
899
|
+
"""Terminate on close with confirmation."""
|
|
900
|
+
if self.tab.is_connected():
|
|
901
|
+
reply = QMessageBox.question(
|
|
902
|
+
self,
|
|
903
|
+
"Close Window",
|
|
904
|
+
f"'{self.tab.name}' is still running.\n\nTerminate and close?",
|
|
905
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
906
|
+
QMessageBox.StandardButton.No
|
|
907
|
+
)
|
|
908
|
+
if reply != QMessageBox.StandardButton.Yes:
|
|
909
|
+
event.ignore()
|
|
910
|
+
return
|
|
911
|
+
|
|
912
|
+
self.tab.disconnect()
|
|
913
|
+
event.accept()
|
|
914
|
+
|
|
915
|
+
|
|
793
916
|
def main():
|
|
794
917
|
app = QApplication(sys.argv)
|
|
795
918
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nterm.scripting - Scripting API for nterm
|
|
3
|
+
|
|
4
|
+
Provides programmatic access to nterm's device sessions and credential vault.
|
|
5
|
+
Usable from IPython, CLI, scripts, or as foundation for MCP tools.
|
|
6
|
+
|
|
7
|
+
Quick Start (IPython):
|
|
8
|
+
from nterm.scripting import api
|
|
9
|
+
|
|
10
|
+
api.devices() # List all saved devices
|
|
11
|
+
api.search("leaf") # Search devices
|
|
12
|
+
api.device("eng-leaf-1") # Get specific device
|
|
13
|
+
|
|
14
|
+
api.unlock("vault-password") # Unlock credential vault
|
|
15
|
+
api.credentials() # List credentials
|
|
16
|
+
|
|
17
|
+
api.help() # Show all commands
|
|
18
|
+
|
|
19
|
+
Quick Start (CLI):
|
|
20
|
+
nterm-cli devices
|
|
21
|
+
nterm-cli search leaf
|
|
22
|
+
nterm-cli credentials --unlock
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from .api import (
|
|
26
|
+
NTermAPI,
|
|
27
|
+
DeviceInfo,
|
|
28
|
+
CredentialInfo,
|
|
29
|
+
get_api,
|
|
30
|
+
reset_api,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Convenience: pre-instantiated API
|
|
34
|
+
api = get_api()
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"NTermAPI",
|
|
38
|
+
"DeviceInfo",
|
|
39
|
+
"CredentialInfo",
|
|
40
|
+
"get_api",
|
|
41
|
+
"reset_api",
|
|
42
|
+
"api",
|
|
43
|
+
]
|