ssh-docs 0.1.0__tar.gz → 0.1.1__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.
- {ssh_docs-0.1.0 → ssh_docs-0.1.1}/PKG-INFO +64 -1
- {ssh_docs-0.1.0 → ssh_docs-0.1.1}/README.md +63 -0
- {ssh_docs-0.1.0 → ssh_docs-0.1.1}/pyproject.toml +1 -1
- {ssh_docs-0.1.0 → ssh_docs-0.1.1}/ssh_docs/cli.py +62 -0
- {ssh_docs-0.1.0 → ssh_docs-0.1.1}/ssh_docs/server.py +49 -56
- {ssh_docs-0.1.0 → ssh_docs-0.1.1}/ssh_docs/shell.py +175 -7
- {ssh_docs-0.1.0 → ssh_docs-0.1.1}/ssh_docs.egg-info/PKG-INFO +64 -1
- {ssh_docs-0.1.0 → ssh_docs-0.1.1}/setup.cfg +0 -0
- {ssh_docs-0.1.0 → ssh_docs-0.1.1}/ssh_docs/__init__.py +0 -0
- {ssh_docs-0.1.0 → ssh_docs-0.1.1}/ssh_docs/__main__.py +0 -0
- {ssh_docs-0.1.0 → ssh_docs-0.1.1}/ssh_docs/config.py +0 -0
- {ssh_docs-0.1.0 → ssh_docs-0.1.1}/ssh_docs.egg-info/SOURCES.txt +0 -0
- {ssh_docs-0.1.0 → ssh_docs-0.1.1}/ssh_docs.egg-info/dependency_links.txt +0 -0
- {ssh_docs-0.1.0 → ssh_docs-0.1.1}/ssh_docs.egg-info/entry_points.txt +0 -0
- {ssh_docs-0.1.0 → ssh_docs-0.1.1}/ssh_docs.egg-info/requires.txt +0 -0
- {ssh_docs-0.1.0 → ssh_docs-0.1.1}/ssh_docs.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ssh-docs
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Expose documentation via SSH - browse docs using familiar Unix commands
|
|
5
5
|
Author: SSH-Docs Contributors
|
|
6
6
|
License: MIT
|
|
@@ -33,8 +33,14 @@ Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
|
33
33
|
|
|
34
34
|
# SSH-Docs
|
|
35
35
|
|
|
36
|
+
[](https://badge.fury.io/py/ssh-docs)
|
|
37
|
+
[](https://www.python.org/downloads/)
|
|
38
|
+
[](https://opensource.org/licenses/MIT)
|
|
39
|
+
|
|
36
40
|
**Expose documentation via SSH** - Browse your documentation using familiar Unix commands over SSH.
|
|
37
41
|
|
|
42
|
+
**🎉 Now available on PyPI!** Install with: `pip install ssh-docs`
|
|
43
|
+
|
|
38
44
|
## Features
|
|
39
45
|
|
|
40
46
|
- 🔒 **Secure SSH Access** - Standard SSH protocol with authentication options
|
|
@@ -282,6 +288,63 @@ ssh-docs keygen
|
|
|
282
288
|
ssh-docs keygen --output-dir ./keys
|
|
283
289
|
```
|
|
284
290
|
|
|
291
|
+
## Shell Completion
|
|
292
|
+
|
|
293
|
+
SSH-Docs supports tab completion for commands, options, and file paths in Bash, Zsh, and Fish shells.
|
|
294
|
+
|
|
295
|
+
### Installation
|
|
296
|
+
|
|
297
|
+
**Bash:**
|
|
298
|
+
```bash
|
|
299
|
+
# Add to ~/.bashrc
|
|
300
|
+
ssh-docs completion --shell bash >> ~/.bashrc
|
|
301
|
+
source ~/.bashrc
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Zsh:**
|
|
305
|
+
```zsh
|
|
306
|
+
# Add to ~/.zshrc
|
|
307
|
+
ssh-docs completion --shell zsh >> ~/.zshrc
|
|
308
|
+
source ~/.zshrc
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Fish:**
|
|
312
|
+
```fish
|
|
313
|
+
# Add to ~/.config/fish/config.fish
|
|
314
|
+
ssh-docs completion --shell fish >> ~/.config/fish/config.fish
|
|
315
|
+
source ~/.config/fish/config.fish
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### What Gets Completed
|
|
319
|
+
|
|
320
|
+
Once enabled, tab completion works for:
|
|
321
|
+
|
|
322
|
+
- **Commands**: `serve`, `init`, `validate`, `keygen`, `completion`
|
|
323
|
+
- **Options**: `--port`, `--config`, `--auth`, etc.
|
|
324
|
+
- **Values**: Authentication types (`public`, `key`, `password`)
|
|
325
|
+
- **File Paths**: Config files, directories, and other paths
|
|
326
|
+
- **Config Files**: Automatically suggests `.yml` and `.yaml` files
|
|
327
|
+
|
|
328
|
+
### Usage Examples
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
# Press TAB to complete commands
|
|
332
|
+
ssh-docs <TAB>
|
|
333
|
+
# Shows: serve init validate keygen completion
|
|
334
|
+
|
|
335
|
+
# Press TAB to complete options
|
|
336
|
+
ssh-docs serve --<TAB>
|
|
337
|
+
# Shows: --port --config --auth --host ...
|
|
338
|
+
|
|
339
|
+
# Press TAB to complete auth types
|
|
340
|
+
ssh-docs serve --auth <TAB>
|
|
341
|
+
# Shows: public key password
|
|
342
|
+
|
|
343
|
+
# Press TAB to complete config files
|
|
344
|
+
ssh-docs serve --config <TAB>
|
|
345
|
+
# Shows: .ssh-docs.yml custom-config.yml ...
|
|
346
|
+
```
|
|
347
|
+
|
|
285
348
|
## Use Cases
|
|
286
349
|
|
|
287
350
|
### Local Development
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
# SSH-Docs
|
|
2
2
|
|
|
3
|
+
[](https://badge.fury.io/py/ssh-docs)
|
|
4
|
+
[](https://www.python.org/downloads/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
3
7
|
**Expose documentation via SSH** - Browse your documentation using familiar Unix commands over SSH.
|
|
4
8
|
|
|
9
|
+
**🎉 Now available on PyPI!** Install with: `pip install ssh-docs`
|
|
10
|
+
|
|
5
11
|
## Features
|
|
6
12
|
|
|
7
13
|
- 🔒 **Secure SSH Access** - Standard SSH protocol with authentication options
|
|
@@ -249,6 +255,63 @@ ssh-docs keygen
|
|
|
249
255
|
ssh-docs keygen --output-dir ./keys
|
|
250
256
|
```
|
|
251
257
|
|
|
258
|
+
## Shell Completion
|
|
259
|
+
|
|
260
|
+
SSH-Docs supports tab completion for commands, options, and file paths in Bash, Zsh, and Fish shells.
|
|
261
|
+
|
|
262
|
+
### Installation
|
|
263
|
+
|
|
264
|
+
**Bash:**
|
|
265
|
+
```bash
|
|
266
|
+
# Add to ~/.bashrc
|
|
267
|
+
ssh-docs completion --shell bash >> ~/.bashrc
|
|
268
|
+
source ~/.bashrc
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Zsh:**
|
|
272
|
+
```zsh
|
|
273
|
+
# Add to ~/.zshrc
|
|
274
|
+
ssh-docs completion --shell zsh >> ~/.zshrc
|
|
275
|
+
source ~/.zshrc
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Fish:**
|
|
279
|
+
```fish
|
|
280
|
+
# Add to ~/.config/fish/config.fish
|
|
281
|
+
ssh-docs completion --shell fish >> ~/.config/fish/config.fish
|
|
282
|
+
source ~/.config/fish/config.fish
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### What Gets Completed
|
|
286
|
+
|
|
287
|
+
Once enabled, tab completion works for:
|
|
288
|
+
|
|
289
|
+
- **Commands**: `serve`, `init`, `validate`, `keygen`, `completion`
|
|
290
|
+
- **Options**: `--port`, `--config`, `--auth`, etc.
|
|
291
|
+
- **Values**: Authentication types (`public`, `key`, `password`)
|
|
292
|
+
- **File Paths**: Config files, directories, and other paths
|
|
293
|
+
- **Config Files**: Automatically suggests `.yml` and `.yaml` files
|
|
294
|
+
|
|
295
|
+
### Usage Examples
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
# Press TAB to complete commands
|
|
299
|
+
ssh-docs <TAB>
|
|
300
|
+
# Shows: serve init validate keygen completion
|
|
301
|
+
|
|
302
|
+
# Press TAB to complete options
|
|
303
|
+
ssh-docs serve --<TAB>
|
|
304
|
+
# Shows: --port --config --auth --host ...
|
|
305
|
+
|
|
306
|
+
# Press TAB to complete auth types
|
|
307
|
+
ssh-docs serve --auth <TAB>
|
|
308
|
+
# Shows: public key password
|
|
309
|
+
|
|
310
|
+
# Press TAB to complete config files
|
|
311
|
+
ssh-docs serve --config <TAB>
|
|
312
|
+
# Shows: .ssh-docs.yml custom-config.yml ...
|
|
313
|
+
```
|
|
314
|
+
|
|
252
315
|
## Use Cases
|
|
253
316
|
|
|
254
317
|
### Local Development
|
|
@@ -8,6 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
from typing import Optional
|
|
9
9
|
|
|
10
10
|
import click
|
|
11
|
+
from click.shell_completion import CompletionItem
|
|
11
12
|
|
|
12
13
|
from .config import (
|
|
13
14
|
Config,
|
|
@@ -19,6 +20,19 @@ from .config import (
|
|
|
19
20
|
from .server import run_server
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def complete_config_files(ctx, param, incomplete):
|
|
26
|
+
"""Complete .yml and .yaml config files in current directory."""
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
configs = list(Path(".").glob("*.yml")) + list(Path(".").glob("*.yaml"))
|
|
29
|
+
return [
|
|
30
|
+
CompletionItem(str(c))
|
|
31
|
+
for c in configs
|
|
32
|
+
if str(c).startswith(incomplete)
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
22
36
|
@click.group()
|
|
23
37
|
@click.version_option(version="0.1.0", prog_name="ssh-docs")
|
|
24
38
|
def cli() -> None:
|
|
@@ -50,6 +64,7 @@ def cli() -> None:
|
|
|
50
64
|
"--config",
|
|
51
65
|
type=click.Path(exists=True),
|
|
52
66
|
default=None,
|
|
67
|
+
shell_complete=complete_config_files,
|
|
53
68
|
help="Config file path [default: .ssh-docs.yml]",
|
|
54
69
|
)
|
|
55
70
|
@click.option(
|
|
@@ -383,6 +398,53 @@ def keygen(output_dir: Optional[str], force: bool) -> None:
|
|
|
383
398
|
click.echo(f" host_key: {key_path}")
|
|
384
399
|
|
|
385
400
|
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@cli.command()
|
|
404
|
+
@click.option(
|
|
405
|
+
"--shell",
|
|
406
|
+
type=click.Choice(["bash", "zsh", "fish"]),
|
|
407
|
+
required=True,
|
|
408
|
+
help="Shell type",
|
|
409
|
+
)
|
|
410
|
+
def completion(shell: str) -> None:
|
|
411
|
+
"""Generate shell completion script.
|
|
412
|
+
|
|
413
|
+
Install tab completion for your shell to autocomplete commands,
|
|
414
|
+
options, and file paths.
|
|
415
|
+
|
|
416
|
+
Examples:
|
|
417
|
+
|
|
418
|
+
\b
|
|
419
|
+
# Install for bash
|
|
420
|
+
ssh-docs completion --shell bash >> ~/.bashrc
|
|
421
|
+
source ~/.bashrc
|
|
422
|
+
|
|
423
|
+
\b
|
|
424
|
+
# Install for zsh
|
|
425
|
+
ssh-docs completion --shell zsh >> ~/.zshrc
|
|
426
|
+
source ~/.zshrc
|
|
427
|
+
|
|
428
|
+
\b
|
|
429
|
+
# Install for fish
|
|
430
|
+
ssh-docs completion --shell fish >> ~/.config/fish/config.fish
|
|
431
|
+
source ~/.config/fish/config.fish
|
|
432
|
+
"""
|
|
433
|
+
shell_map = {
|
|
434
|
+
"bash": "bash_source",
|
|
435
|
+
"zsh": "zsh_source",
|
|
436
|
+
"fish": "fish_source",
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
prog_name = "ssh-docs"
|
|
440
|
+
complete_var = f"_{prog_name.upper().replace('-', '_')}_COMPLETE"
|
|
441
|
+
|
|
442
|
+
if shell == "fish":
|
|
443
|
+
click.echo(f"eval (env {complete_var}={shell_map[shell]} {prog_name})")
|
|
444
|
+
else:
|
|
445
|
+
click.echo(f'eval "$({complete_var}={shell_map[shell]} {prog_name})"')
|
|
446
|
+
|
|
447
|
+
|
|
386
448
|
def main() -> None:
|
|
387
449
|
"""Main entry point for CLI."""
|
|
388
450
|
cli()
|
|
@@ -38,9 +38,7 @@ class SSHDocsServer:
|
|
|
38
38
|
# Configure server options
|
|
39
39
|
server_options = {
|
|
40
40
|
"server_host_keys": [host_key],
|
|
41
|
-
"
|
|
42
|
-
"session_factory": None, # We handle sessions via process_factory
|
|
43
|
-
"encoding": None, # Handle encoding in shell
|
|
41
|
+
"encoding": "utf-8", # Use UTF-8 encoding for text
|
|
44
42
|
}
|
|
45
43
|
|
|
46
44
|
# Add authentication if configured
|
|
@@ -51,9 +49,9 @@ class SSHDocsServer:
|
|
|
51
49
|
server_options["authorized_client_keys"] = self.config.authorized_keys
|
|
52
50
|
|
|
53
51
|
else:
|
|
54
|
-
# Public access - accept any connection
|
|
55
|
-
|
|
56
|
-
server_options["
|
|
52
|
+
# Public access - accept any connection without authentication
|
|
53
|
+
# We enable password auth but accept any password
|
|
54
|
+
server_options["password_auth"] = True
|
|
57
55
|
|
|
58
56
|
logger.info(f"Starting SSH server on {self.config.host}:{self.config.port}")
|
|
59
57
|
logger.info(f"Content root: {self.content_root}")
|
|
@@ -81,9 +79,7 @@ class SSHDocsServer:
|
|
|
81
79
|
await self.server.wait_closed()
|
|
82
80
|
logger.info("SSH server stopped")
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
"""Factory method to create a new process for each connection."""
|
|
86
|
-
return SSHDocsProcess(self)
|
|
82
|
+
|
|
87
83
|
|
|
88
84
|
async def _get_host_key(self) -> str:
|
|
89
85
|
"""Get or generate SSH host key."""
|
|
@@ -131,18 +127,34 @@ class SSHDocsServerProtocol(asyncssh.SSHServer):
|
|
|
131
127
|
"""Begin authentication for a user."""
|
|
132
128
|
logger.debug(f"Authentication attempt for user: {username}")
|
|
133
129
|
|
|
134
|
-
# For public access,
|
|
130
|
+
# For public access, skip authentication entirely
|
|
135
131
|
if self.server.config.auth_type == "public":
|
|
136
|
-
|
|
132
|
+
# Returning False skips authentication
|
|
133
|
+
return False
|
|
137
134
|
|
|
138
135
|
return True
|
|
139
136
|
|
|
140
137
|
def password_auth_supported(self) -> bool:
|
|
141
138
|
"""Check if password authentication is supported."""
|
|
139
|
+
# Only support password auth for password mode, not public
|
|
142
140
|
return self.server.config.auth_type == "password"
|
|
141
|
+
|
|
142
|
+
def public_key_auth_supported(self) -> bool:
|
|
143
|
+
"""Check if public key authentication is supported."""
|
|
144
|
+
# Support public key auth for key mode
|
|
145
|
+
return self.server.config.auth_type == "key"
|
|
146
|
+
|
|
147
|
+
def kbdint_auth_supported(self) -> bool:
|
|
148
|
+
"""Check if keyboard-interactive authentication is supported."""
|
|
149
|
+
# Not supported
|
|
150
|
+
return False
|
|
143
151
|
|
|
144
152
|
def validate_password(self, username: str, password: str) -> bool:
|
|
145
153
|
"""Validate password for a user."""
|
|
154
|
+
# For public access, accept any password
|
|
155
|
+
if self.server.config.auth_type == "public":
|
|
156
|
+
return True
|
|
157
|
+
|
|
146
158
|
if self.server.config.auth_type != "password":
|
|
147
159
|
return False
|
|
148
160
|
|
|
@@ -150,54 +162,35 @@ class SSHDocsServerProtocol(asyncssh.SSHServer):
|
|
|
150
162
|
return False
|
|
151
163
|
|
|
152
164
|
return password == self.server.config.password
|
|
165
|
+
|
|
166
|
+
def session_requested(self):
|
|
167
|
+
"""Handle session request by returning a handler function."""
|
|
168
|
+
async def handle_session(stdin, stdout, stderr):
|
|
169
|
+
"""Handler function that receives stream objects."""
|
|
170
|
+
shell = SSHDocsShell(
|
|
171
|
+
stdin=stdin,
|
|
172
|
+
stdout=stdout,
|
|
173
|
+
stderr=stderr,
|
|
174
|
+
content_root=self.server.content_root,
|
|
175
|
+
site_name=self.server.config.site_name,
|
|
176
|
+
banner=self.server.config.banner,
|
|
177
|
+
)
|
|
178
|
+
try:
|
|
179
|
+
logger.info("Starting shell session")
|
|
180
|
+
await shell.run()
|
|
181
|
+
logger.info("Shell session ended normally")
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.error(f"Shell error: {e}", exc_info=True)
|
|
184
|
+
finally:
|
|
185
|
+
# Close streams to signal session completion
|
|
186
|
+
stdout.close()
|
|
187
|
+
stderr.close()
|
|
188
|
+
logger.info("Streams closed")
|
|
189
|
+
|
|
190
|
+
return handle_session
|
|
153
191
|
|
|
154
192
|
|
|
155
|
-
class SSHDocsProcess(asyncssh.SSHServerProcess):
|
|
156
|
-
"""Process handler for SSH sessions."""
|
|
157
|
-
|
|
158
|
-
def __init__(self, server: SSHDocsServer) -> None:
|
|
159
|
-
self.server = server
|
|
160
|
-
self.shell: Optional[SSHDocsShell] = None
|
|
161
|
-
|
|
162
|
-
def connection_made(self, chan: asyncssh.SSHServerChannel) -> None:
|
|
163
|
-
"""Called when channel is opened."""
|
|
164
|
-
self.chan = chan
|
|
165
|
-
|
|
166
|
-
def shell_requested(self) -> bool:
|
|
167
|
-
"""Handle shell request."""
|
|
168
|
-
return True
|
|
169
|
-
|
|
170
|
-
def session_started(self) -> None:
|
|
171
|
-
"""Called when session starts."""
|
|
172
|
-
# Create shell instance
|
|
173
|
-
self.shell = SSHDocsShell(
|
|
174
|
-
stdin=self.stdin,
|
|
175
|
-
stdout=self.stdout,
|
|
176
|
-
stderr=self.stderr,
|
|
177
|
-
content_root=self.server.content_root,
|
|
178
|
-
site_name=self.server.config.site_name,
|
|
179
|
-
banner=self.server.config.banner,
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
# Start shell in background task
|
|
183
|
-
asyncio.create_task(self._run_shell())
|
|
184
|
-
|
|
185
|
-
async def _run_shell(self) -> None:
|
|
186
|
-
"""Run the shell session."""
|
|
187
|
-
try:
|
|
188
|
-
await self.shell.run()
|
|
189
|
-
except Exception as e:
|
|
190
|
-
logger.error(f"Shell error: {e}")
|
|
191
|
-
finally:
|
|
192
|
-
self.exit(0)
|
|
193
|
-
|
|
194
|
-
def break_received(self, msec: int) -> bool:
|
|
195
|
-
"""Handle break signal."""
|
|
196
|
-
return True
|
|
197
193
|
|
|
198
|
-
def signal_received(self, signal: str) -> None:
|
|
199
|
-
"""Handle signal."""
|
|
200
|
-
logger.debug(f"Received signal: {signal}")
|
|
201
194
|
|
|
202
195
|
|
|
203
196
|
async def run_server(config: Config) -> None:
|
|
@@ -8,6 +8,8 @@ import shlex
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any, Callable, Optional
|
|
10
10
|
|
|
11
|
+
import asyncssh
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
class SSHDocsShell:
|
|
13
15
|
"""Interactive shell session that provides Unix-like commands for browsing documentation."""
|
|
@@ -28,6 +30,99 @@ class SSHDocsShell:
|
|
|
28
30
|
self.site_name = site_name
|
|
29
31
|
self.cwd = "/site"
|
|
30
32
|
self.banner = banner or self._default_banner()
|
|
33
|
+
self.commands = ["help", "pwd", "ls", "cd", "cat", "head", "tail", "find", "grep", "exit", "quit"]
|
|
34
|
+
|
|
35
|
+
def _get_completions(self, text: str, state: int) -> Optional[str]:
|
|
36
|
+
"""Generate completions for tab completion."""
|
|
37
|
+
# Parse the current line to understand context
|
|
38
|
+
line_before_cursor = text
|
|
39
|
+
parts = line_before_cursor.split()
|
|
40
|
+
|
|
41
|
+
# If empty or just whitespace, suggest commands
|
|
42
|
+
if not parts or (len(parts) == 1 and not line_before_cursor.endswith(' ')):
|
|
43
|
+
prefix = parts[0] if parts else ""
|
|
44
|
+
matches = [cmd for cmd in self.commands if cmd.startswith(prefix)]
|
|
45
|
+
return matches[state] if state < len(matches) else None
|
|
46
|
+
|
|
47
|
+
# If we have a command, complete paths/files
|
|
48
|
+
command = parts[0]
|
|
49
|
+
if command in self.commands:
|
|
50
|
+
# Get the last argument being typed
|
|
51
|
+
if line_before_cursor.endswith(' '):
|
|
52
|
+
prefix = ""
|
|
53
|
+
else:
|
|
54
|
+
prefix = parts[-1] if len(parts) > 1 else ""
|
|
55
|
+
|
|
56
|
+
# Complete file/directory paths
|
|
57
|
+
matches = self._complete_path(prefix)
|
|
58
|
+
return matches[state] if state < len(matches) else None
|
|
59
|
+
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
def _complete_path(self, prefix: str) -> list[str]:
|
|
63
|
+
"""Complete file and directory paths."""
|
|
64
|
+
# Determine the directory to search and the prefix to match
|
|
65
|
+
if prefix.startswith('/'):
|
|
66
|
+
# Absolute path - extract directory and filename parts
|
|
67
|
+
if prefix.endswith('/'):
|
|
68
|
+
# Path ends with /, list contents of that directory
|
|
69
|
+
dir_part = prefix.rstrip('/')
|
|
70
|
+
file_part = ''
|
|
71
|
+
elif '/' in prefix.rstrip('/')[1:]: # Has more than just /
|
|
72
|
+
dir_part = prefix.rsplit('/', 1)[0] or '/'
|
|
73
|
+
file_part = prefix.rsplit('/', 1)[1]
|
|
74
|
+
else:
|
|
75
|
+
dir_part = '/site'
|
|
76
|
+
file_part = prefix[1:] if len(prefix) > 1 else ''
|
|
77
|
+
virtual_path = dir_part
|
|
78
|
+
else:
|
|
79
|
+
# Relative path
|
|
80
|
+
if '/' in prefix:
|
|
81
|
+
dir_part = prefix.rsplit('/', 1)[0]
|
|
82
|
+
file_part = prefix.rsplit('/', 1)[1]
|
|
83
|
+
virtual_path = self._resolve_virtual_path(dir_part)
|
|
84
|
+
else:
|
|
85
|
+
# Just a filename in current directory
|
|
86
|
+
dir_part = ''
|
|
87
|
+
file_part = prefix
|
|
88
|
+
virtual_path = self.cwd
|
|
89
|
+
|
|
90
|
+
real_path = self._to_real_path(virtual_path)
|
|
91
|
+
if not real_path or not real_path.exists() or not real_path.is_dir():
|
|
92
|
+
return []
|
|
93
|
+
|
|
94
|
+
# Get matching entries
|
|
95
|
+
try:
|
|
96
|
+
entries = []
|
|
97
|
+
for child in sorted(real_path.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower())):
|
|
98
|
+
name = child.name
|
|
99
|
+
|
|
100
|
+
# Skip if doesn't match prefix
|
|
101
|
+
if not name.startswith(file_part):
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
# Build the completion string
|
|
105
|
+
if prefix.startswith('/'):
|
|
106
|
+
# Absolute path - return full virtual path
|
|
107
|
+
full_name = self._to_virtual_path(child)
|
|
108
|
+
elif dir_part:
|
|
109
|
+
# Relative path with directory component
|
|
110
|
+
full_name = f"{dir_part}/{name}"
|
|
111
|
+
else:
|
|
112
|
+
# Just filename
|
|
113
|
+
full_name = name
|
|
114
|
+
|
|
115
|
+
# Add trailing slash for directories
|
|
116
|
+
if child.is_dir():
|
|
117
|
+
full_name += "/"
|
|
118
|
+
|
|
119
|
+
entries.append(full_name)
|
|
120
|
+
|
|
121
|
+
return entries
|
|
122
|
+
except (PermissionError, OSError):
|
|
123
|
+
return []
|
|
124
|
+
|
|
125
|
+
|
|
31
126
|
|
|
32
127
|
def _default_banner(self) -> str:
|
|
33
128
|
return f"""Connected to {self.site_name}.ssh-docs
|
|
@@ -39,17 +134,90 @@ Readonly session
|
|
|
39
134
|
"""
|
|
40
135
|
|
|
41
136
|
async def run(self) -> None:
|
|
42
|
-
"""Main command loop."""
|
|
137
|
+
"""Main command loop with tab completion support."""
|
|
138
|
+
import logging
|
|
139
|
+
logger = logging.getLogger(__name__)
|
|
140
|
+
|
|
141
|
+
logger.info("Shell run() started")
|
|
43
142
|
self.stdout.write(self.banner)
|
|
143
|
+
logger.info("Banner written")
|
|
44
144
|
|
|
145
|
+
# Buffer for current line being edited
|
|
146
|
+
current_line = ""
|
|
147
|
+
|
|
45
148
|
while True:
|
|
46
149
|
try:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
150
|
+
prompt = f"{self.cwd}$ "
|
|
151
|
+
self.stdout.write(prompt)
|
|
152
|
+
|
|
153
|
+
# Read input character by character to handle tab completion
|
|
154
|
+
current_line = ""
|
|
155
|
+
while True:
|
|
156
|
+
char = await self.stdin.read(1)
|
|
157
|
+
|
|
158
|
+
if not char:
|
|
159
|
+
logger.info("Connection closed")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
# Handle tab completion
|
|
163
|
+
if char == '\t':
|
|
164
|
+
completions = self._complete_path(current_line.split()[-1] if current_line.split() else "")
|
|
165
|
+
|
|
166
|
+
if len(completions) == 1:
|
|
167
|
+
# Single match - complete it
|
|
168
|
+
parts = current_line.split()
|
|
169
|
+
if parts:
|
|
170
|
+
# Replace last part with completion
|
|
171
|
+
parts[-1] = completions[0]
|
|
172
|
+
current_line = ' '.join(parts)
|
|
173
|
+
else:
|
|
174
|
+
current_line = completions[0]
|
|
175
|
+
|
|
176
|
+
# Clear line and rewrite
|
|
177
|
+
self.stdout.write('\r' + ' ' * (len(prompt) + len(current_line) + 10) + '\r')
|
|
178
|
+
self.stdout.write(prompt + current_line)
|
|
179
|
+
|
|
180
|
+
elif len(completions) > 1:
|
|
181
|
+
# Multiple matches - show them
|
|
182
|
+
self.stdout.write('\n')
|
|
183
|
+
for comp in completions:
|
|
184
|
+
self.stdout.write(f"{comp} ")
|
|
185
|
+
self.stdout.write('\n')
|
|
186
|
+
self.stdout.write(prompt + current_line)
|
|
187
|
+
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
# Handle backspace
|
|
191
|
+
elif char in ('\x7f', '\x08'):
|
|
192
|
+
if current_line:
|
|
193
|
+
current_line = current_line[:-1]
|
|
194
|
+
self.stdout.write('\b \b')
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
# Handle newline
|
|
198
|
+
elif char in ('\n', '\r'):
|
|
199
|
+
self.stdout.write('\n')
|
|
200
|
+
break
|
|
201
|
+
|
|
202
|
+
# Handle Ctrl+C
|
|
203
|
+
elif char == '\x03':
|
|
204
|
+
self.stdout.write('^C\n')
|
|
205
|
+
current_line = ""
|
|
206
|
+
break
|
|
207
|
+
|
|
208
|
+
# Handle Ctrl+D
|
|
209
|
+
elif char == '\x04':
|
|
210
|
+
if not current_line:
|
|
211
|
+
self.stdout.write('\nSession closed\n')
|
|
212
|
+
return
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
# Regular character
|
|
216
|
+
else:
|
|
217
|
+
current_line += char
|
|
218
|
+
self.stdout.write(char)
|
|
219
|
+
|
|
220
|
+
raw = current_line.strip()
|
|
53
221
|
if not raw:
|
|
54
222
|
continue
|
|
55
223
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ssh-docs
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Expose documentation via SSH - browse docs using familiar Unix commands
|
|
5
5
|
Author: SSH-Docs Contributors
|
|
6
6
|
License: MIT
|
|
@@ -33,8 +33,14 @@ Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
|
33
33
|
|
|
34
34
|
# SSH-Docs
|
|
35
35
|
|
|
36
|
+
[](https://badge.fury.io/py/ssh-docs)
|
|
37
|
+
[](https://www.python.org/downloads/)
|
|
38
|
+
[](https://opensource.org/licenses/MIT)
|
|
39
|
+
|
|
36
40
|
**Expose documentation via SSH** - Browse your documentation using familiar Unix commands over SSH.
|
|
37
41
|
|
|
42
|
+
**🎉 Now available on PyPI!** Install with: `pip install ssh-docs`
|
|
43
|
+
|
|
38
44
|
## Features
|
|
39
45
|
|
|
40
46
|
- 🔒 **Secure SSH Access** - Standard SSH protocol with authentication options
|
|
@@ -282,6 +288,63 @@ ssh-docs keygen
|
|
|
282
288
|
ssh-docs keygen --output-dir ./keys
|
|
283
289
|
```
|
|
284
290
|
|
|
291
|
+
## Shell Completion
|
|
292
|
+
|
|
293
|
+
SSH-Docs supports tab completion for commands, options, and file paths in Bash, Zsh, and Fish shells.
|
|
294
|
+
|
|
295
|
+
### Installation
|
|
296
|
+
|
|
297
|
+
**Bash:**
|
|
298
|
+
```bash
|
|
299
|
+
# Add to ~/.bashrc
|
|
300
|
+
ssh-docs completion --shell bash >> ~/.bashrc
|
|
301
|
+
source ~/.bashrc
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Zsh:**
|
|
305
|
+
```zsh
|
|
306
|
+
# Add to ~/.zshrc
|
|
307
|
+
ssh-docs completion --shell zsh >> ~/.zshrc
|
|
308
|
+
source ~/.zshrc
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Fish:**
|
|
312
|
+
```fish
|
|
313
|
+
# Add to ~/.config/fish/config.fish
|
|
314
|
+
ssh-docs completion --shell fish >> ~/.config/fish/config.fish
|
|
315
|
+
source ~/.config/fish/config.fish
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### What Gets Completed
|
|
319
|
+
|
|
320
|
+
Once enabled, tab completion works for:
|
|
321
|
+
|
|
322
|
+
- **Commands**: `serve`, `init`, `validate`, `keygen`, `completion`
|
|
323
|
+
- **Options**: `--port`, `--config`, `--auth`, etc.
|
|
324
|
+
- **Values**: Authentication types (`public`, `key`, `password`)
|
|
325
|
+
- **File Paths**: Config files, directories, and other paths
|
|
326
|
+
- **Config Files**: Automatically suggests `.yml` and `.yaml` files
|
|
327
|
+
|
|
328
|
+
### Usage Examples
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
# Press TAB to complete commands
|
|
332
|
+
ssh-docs <TAB>
|
|
333
|
+
# Shows: serve init validate keygen completion
|
|
334
|
+
|
|
335
|
+
# Press TAB to complete options
|
|
336
|
+
ssh-docs serve --<TAB>
|
|
337
|
+
# Shows: --port --config --auth --host ...
|
|
338
|
+
|
|
339
|
+
# Press TAB to complete auth types
|
|
340
|
+
ssh-docs serve --auth <TAB>
|
|
341
|
+
# Shows: public key password
|
|
342
|
+
|
|
343
|
+
# Press TAB to complete config files
|
|
344
|
+
ssh-docs serve --config <TAB>
|
|
345
|
+
# Shows: .ssh-docs.yml custom-config.yml ...
|
|
346
|
+
```
|
|
347
|
+
|
|
285
348
|
## Use Cases
|
|
286
349
|
|
|
287
350
|
### Local Development
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|