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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ssh-docs
3
- Version: 0.1.0
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
+ [![PyPI version](https://badge.fury.io/py/ssh-docs.svg)](https://badge.fury.io/py/ssh-docs)
37
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![PyPI version](https://badge.fury.io/py/ssh-docs.svg)](https://badge.fury.io/py/ssh-docs)
4
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ssh-docs"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "Expose documentation via SSH - browse docs using familiar Unix commands"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -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
- "process_factory": self._create_process,
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
- server_options["password_auth"] = False
56
- server_options["public_key_auth"] = False
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
- def _create_process(self) -> SSHDocsProcess:
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, accept any username
130
+ # For public access, skip authentication entirely
135
131
  if self.server.config.auth_type == "public":
136
- return True
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
- self.stdout.write(f"{self.cwd}$ ")
48
- line = await self.stdin.readline()
49
- if not line:
50
- break
51
-
52
- raw = line.strip()
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.0
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
+ [![PyPI version](https://badge.fury.io/py/ssh-docs.svg)](https://badge.fury.io/py/ssh-docs)
37
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
38
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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