ptn 0.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- porterminal/__init__.py +288 -0
- porterminal/__main__.py +8 -0
- porterminal/app.py +381 -0
- porterminal/application/__init__.py +1 -0
- porterminal/application/ports/__init__.py +7 -0
- porterminal/application/ports/connection_port.py +34 -0
- porterminal/application/services/__init__.py +13 -0
- porterminal/application/services/management_service.py +279 -0
- porterminal/application/services/session_service.py +249 -0
- porterminal/application/services/tab_service.py +286 -0
- porterminal/application/services/terminal_service.py +426 -0
- porterminal/asgi.py +38 -0
- porterminal/cli/__init__.py +19 -0
- porterminal/cli/args.py +91 -0
- porterminal/cli/display.py +157 -0
- porterminal/composition.py +208 -0
- porterminal/config.py +195 -0
- porterminal/container.py +65 -0
- porterminal/domain/__init__.py +91 -0
- porterminal/domain/entities/__init__.py +16 -0
- porterminal/domain/entities/output_buffer.py +73 -0
- porterminal/domain/entities/session.py +86 -0
- porterminal/domain/entities/tab.py +71 -0
- porterminal/domain/ports/__init__.py +12 -0
- porterminal/domain/ports/pty_port.py +80 -0
- porterminal/domain/ports/session_repository.py +58 -0
- porterminal/domain/ports/tab_repository.py +75 -0
- porterminal/domain/services/__init__.py +18 -0
- porterminal/domain/services/environment_sanitizer.py +61 -0
- porterminal/domain/services/rate_limiter.py +63 -0
- porterminal/domain/services/session_limits.py +104 -0
- porterminal/domain/services/tab_limits.py +54 -0
- porterminal/domain/values/__init__.py +25 -0
- porterminal/domain/values/environment_rules.py +156 -0
- porterminal/domain/values/rate_limit_config.py +21 -0
- porterminal/domain/values/session_id.py +20 -0
- porterminal/domain/values/shell_command.py +37 -0
- porterminal/domain/values/tab_id.py +24 -0
- porterminal/domain/values/terminal_dimensions.py +45 -0
- porterminal/domain/values/user_id.py +25 -0
- porterminal/infrastructure/__init__.py +20 -0
- porterminal/infrastructure/cloudflared.py +295 -0
- porterminal/infrastructure/config/__init__.py +9 -0
- porterminal/infrastructure/config/shell_detector.py +84 -0
- porterminal/infrastructure/config/yaml_loader.py +34 -0
- porterminal/infrastructure/network.py +43 -0
- porterminal/infrastructure/registry/__init__.py +5 -0
- porterminal/infrastructure/registry/user_connection_registry.py +104 -0
- porterminal/infrastructure/repositories/__init__.py +9 -0
- porterminal/infrastructure/repositories/in_memory_session.py +70 -0
- porterminal/infrastructure/repositories/in_memory_tab.py +124 -0
- porterminal/infrastructure/server.py +161 -0
- porterminal/infrastructure/web/__init__.py +7 -0
- porterminal/infrastructure/web/websocket_adapter.py +78 -0
- porterminal/logging_setup.py +48 -0
- porterminal/pty/__init__.py +46 -0
- porterminal/pty/env.py +97 -0
- porterminal/pty/manager.py +163 -0
- porterminal/pty/protocol.py +84 -0
- porterminal/pty/unix.py +162 -0
- porterminal/pty/windows.py +131 -0
- porterminal/static/assets/app-BQiuUo6Q.css +32 -0
- porterminal/static/assets/app-YNN_jEhv.js +71 -0
- porterminal/static/icon.svg +34 -0
- porterminal/static/index.html +139 -0
- porterminal/static/manifest.json +31 -0
- porterminal/static/sw.js +66 -0
- porterminal/updater.py +257 -0
- ptn-0.1.4.dist-info/METADATA +191 -0
- ptn-0.1.4.dist-info/RECORD +73 -0
- ptn-0.1.4.dist-info/WHEEL +4 -0
- ptn-0.1.4.dist-info/entry_points.txt +2 -0
- ptn-0.1.4.dist-info/licenses/LICENSE +661 -0
porterminal/asgi.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""ASGI application factory for uvicorn.
|
|
2
|
+
|
|
3
|
+
This module provides a factory function that uvicorn can use to create
|
|
4
|
+
the FastAPI application with proper dependency injection.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
uvicorn porterminal.asgi:create_app_from_env --factory
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from porterminal.composition import create_container
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_app_from_env():
|
|
16
|
+
"""Create FastAPI app from environment variables.
|
|
17
|
+
|
|
18
|
+
This is called by uvicorn when using the --factory flag.
|
|
19
|
+
Environment variables:
|
|
20
|
+
PORTERMINAL_CONFIG_PATH: Path to config file (default: config.yaml)
|
|
21
|
+
PORTERMINAL_CWD: Working directory for PTY sessions
|
|
22
|
+
"""
|
|
23
|
+
from porterminal.app import create_app
|
|
24
|
+
|
|
25
|
+
config_path = os.environ.get("PORTERMINAL_CONFIG_PATH", "config.yaml")
|
|
26
|
+
cwd = os.environ.get("PORTERMINAL_CWD")
|
|
27
|
+
|
|
28
|
+
container = create_container(config_path=config_path, cwd=cwd)
|
|
29
|
+
|
|
30
|
+
# Create app with container
|
|
31
|
+
# Note: The current app.py doesn't accept container yet,
|
|
32
|
+
# so we just create the default app and store container in state
|
|
33
|
+
app = create_app()
|
|
34
|
+
|
|
35
|
+
# Store container in app state for handlers to access
|
|
36
|
+
app.state.container = container
|
|
37
|
+
|
|
38
|
+
return app
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""CLI utilities for Porterminal."""
|
|
2
|
+
|
|
3
|
+
from .args import parse_args
|
|
4
|
+
from .display import (
|
|
5
|
+
LOGO,
|
|
6
|
+
TAGLINE,
|
|
7
|
+
display_startup_screen,
|
|
8
|
+
get_caution,
|
|
9
|
+
get_qr_code,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"parse_args",
|
|
14
|
+
"display_startup_screen",
|
|
15
|
+
"get_qr_code",
|
|
16
|
+
"get_caution",
|
|
17
|
+
"LOGO",
|
|
18
|
+
"TAGLINE",
|
|
19
|
+
]
|
porterminal/cli/args.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Command line argument parsing."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from porterminal import __version__
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_args() -> argparse.Namespace:
|
|
10
|
+
"""Parse command line arguments.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
Parsed arguments namespace with:
|
|
14
|
+
- path: Starting directory for the shell (optional)
|
|
15
|
+
- no_tunnel: Whether to skip Cloudflare tunnel
|
|
16
|
+
- verbose: Whether to show detailed logs
|
|
17
|
+
- update: Whether to update to latest version
|
|
18
|
+
- check_update: Whether to check for updates
|
|
19
|
+
"""
|
|
20
|
+
parser = argparse.ArgumentParser(
|
|
21
|
+
description="Porterminal - Web terminal via Cloudflare Tunnel",
|
|
22
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"-V",
|
|
26
|
+
"--version",
|
|
27
|
+
action="version",
|
|
28
|
+
version=f"%(prog)s {__version__}",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"path",
|
|
32
|
+
nargs="?",
|
|
33
|
+
default=None,
|
|
34
|
+
help="Starting directory for the shell (default: current directory)",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--no-tunnel",
|
|
38
|
+
action="store_true",
|
|
39
|
+
help="Start server only, without Cloudflare tunnel",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"-v",
|
|
43
|
+
"--verbose",
|
|
44
|
+
action="store_true",
|
|
45
|
+
help="Show detailed startup logs",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"-U",
|
|
49
|
+
"--update",
|
|
50
|
+
action="store_true",
|
|
51
|
+
help="Update to the latest version",
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--check-update",
|
|
55
|
+
action="store_true",
|
|
56
|
+
help="Check if a newer version is available",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"-b",
|
|
60
|
+
"--background",
|
|
61
|
+
action="store_true",
|
|
62
|
+
help="Run in background and return immediately",
|
|
63
|
+
)
|
|
64
|
+
# Internal argument for background mode communication
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--_url-file",
|
|
67
|
+
dest="url_file",
|
|
68
|
+
help=argparse.SUPPRESS,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
args = parser.parse_args()
|
|
72
|
+
|
|
73
|
+
# Handle update commands early (before main app starts)
|
|
74
|
+
if args.check_update:
|
|
75
|
+
from porterminal.updater import check_for_updates, get_upgrade_command
|
|
76
|
+
|
|
77
|
+
has_update, latest = check_for_updates(use_cache=False)
|
|
78
|
+
if has_update:
|
|
79
|
+
print(f"Update available: {__version__} → {latest}")
|
|
80
|
+
print(f"Run: {get_upgrade_command()}")
|
|
81
|
+
else:
|
|
82
|
+
print(f"Already at latest version ({__version__})")
|
|
83
|
+
sys.exit(0)
|
|
84
|
+
|
|
85
|
+
if args.update:
|
|
86
|
+
from porterminal.updater import update_package
|
|
87
|
+
|
|
88
|
+
success = update_package()
|
|
89
|
+
sys.exit(0 if success else 1)
|
|
90
|
+
|
|
91
|
+
return args
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Display utilities for the startup screen."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import random
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import qrcode
|
|
8
|
+
from rich.align import Align
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from porterminal import __version__
|
|
13
|
+
|
|
14
|
+
# Force UTF-8 for Windows console
|
|
15
|
+
if sys.platform == "win32":
|
|
16
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
17
|
+
|
|
18
|
+
console = Console(force_terminal=True)
|
|
19
|
+
|
|
20
|
+
LOGO = r"""
|
|
21
|
+
██████ ██████ ██████ ██████ ██████ ██████ ██ ██ ██ ██ ██ ████ ██
|
|
22
|
+
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███ ███ ██ ███ ██ ██ ██ ██
|
|
23
|
+
██████ ██ ██ ██████ ██ ████ ██████ ██ █ ██ ██ ██ █ ██ ██████ ██
|
|
24
|
+
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ███ ██ ██ ██
|
|
25
|
+
██ ██████ ██ ██ ██ ██████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██████
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
TAGLINE = r"""
|
|
29
|
+
█ █ █ █▄▄ █▀▀ █▀▀ █▀█ █▀▄ █▀▀ █▀▀ █▀█ █▀█ █▀▄▀█ ▄▀█ █▄ █ █▄█ █ █ █ █▀▀ █▀█ █▀▀
|
|
30
|
+
▀▄▀ █ █▄█ ██▄ █▄▄ █▄█ █▄▀ ██▄ █▀ █▀▄ █▄█ █ ▀ █ █▀█ █ ▀█ █ ▀▄▀▄▀ ██▄ █▀▄ ██▄
|
|
31
|
+
""".strip()
|
|
32
|
+
|
|
33
|
+
CAUTION_DEFAULT = "CAUTION: DO NOT VIBE CODE WHILE DRIVING"
|
|
34
|
+
|
|
35
|
+
CAUTION_EASTER_EGGS = [
|
|
36
|
+
"VIBE CODING ON THE TOILET IS FINE THO",
|
|
37
|
+
"DEPLOYING TO PROD FROM BED IS A LIFESTYLE",
|
|
38
|
+
"TOUCHING GRASS WHILE TOUCHING CODE",
|
|
39
|
+
"MOM SAID IT'S MY TURN ON THE SERVER",
|
|
40
|
+
"IT WORKS ON MY PHONE",
|
|
41
|
+
"404: WORK-LIFE BALANCE NOT FOUND",
|
|
42
|
+
"git commit -m 'fixed from toilet'",
|
|
43
|
+
"*HACKER VOICE* I'M IN (the bathroom)",
|
|
44
|
+
"THEY SAID REMOTE WORK. I DELIVERED.",
|
|
45
|
+
"TECHNICALLY THIS IS A STANDING DESK",
|
|
46
|
+
"SUDO MAKE ME A SANDWICH (I'M IN LINE)",
|
|
47
|
+
"MY OTHER TERMINAL IS A YACHT",
|
|
48
|
+
"REAL PROGRAMMERS CODE IN TRAFFIC JAMS",
|
|
49
|
+
"MERGE CONFLICTS RESOLVED AT 30,000 FT",
|
|
50
|
+
"PUSHED TO MAIN FROM THE CHECKOUT LINE",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_caution() -> str:
|
|
55
|
+
"""Get caution message with 1% chance of easter egg."""
|
|
56
|
+
if random.random() < 0.01:
|
|
57
|
+
return random.choice(CAUTION_EASTER_EGGS)
|
|
58
|
+
return CAUTION_DEFAULT
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_qr_code(url: str) -> str:
|
|
62
|
+
"""Generate QR code as ASCII string.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
url: URL to encode in the QR code.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
ASCII art representation of the QR code.
|
|
69
|
+
"""
|
|
70
|
+
qr = qrcode.QRCode(
|
|
71
|
+
version=1,
|
|
72
|
+
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
|
73
|
+
box_size=1,
|
|
74
|
+
border=1,
|
|
75
|
+
)
|
|
76
|
+
qr.add_data(url)
|
|
77
|
+
qr.make(fit=True)
|
|
78
|
+
|
|
79
|
+
buffer = io.StringIO()
|
|
80
|
+
qr.print_ascii(out=buffer, invert=True)
|
|
81
|
+
# Remove only truly empty lines (not whitespace-only lines which are part of QR)
|
|
82
|
+
lines = [line for line in buffer.getvalue().split("\n") if line]
|
|
83
|
+
# Strip trailing empty lines
|
|
84
|
+
while lines and not lines[-1]:
|
|
85
|
+
lines.pop()
|
|
86
|
+
return "\n".join(lines)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def display_startup_screen(
|
|
90
|
+
url: str,
|
|
91
|
+
is_tunnel: bool = True,
|
|
92
|
+
cwd: str | None = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Display the final startup screen with QR code.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
url: Primary URL to display and encode in QR.
|
|
98
|
+
is_tunnel: Whether tunnel mode is active.
|
|
99
|
+
cwd: Current working directory to display.
|
|
100
|
+
"""
|
|
101
|
+
console.clear()
|
|
102
|
+
|
|
103
|
+
# Build QR code
|
|
104
|
+
try:
|
|
105
|
+
qr_text = get_qr_code(url)
|
|
106
|
+
except Exception:
|
|
107
|
+
qr_text = "[QR code unavailable]"
|
|
108
|
+
|
|
109
|
+
# Status indicator
|
|
110
|
+
if is_tunnel:
|
|
111
|
+
status = "[green]●[/green] TUNNEL ACTIVE - SCAN THE QR CODE TO ACCESS YOUR TERMINAL"
|
|
112
|
+
else:
|
|
113
|
+
status = "[yellow]●[/yellow] LOCAL MODE"
|
|
114
|
+
|
|
115
|
+
# Build logo with gradient
|
|
116
|
+
logo_lines = LOGO.strip().split("\n")
|
|
117
|
+
colors = ["bold bright_cyan", "bright_cyan", "cyan", "bright_blue", "blue"]
|
|
118
|
+
logo_colored = []
|
|
119
|
+
for i, line in enumerate(logo_lines):
|
|
120
|
+
color = colors[i] if i < len(colors) else colors[-1]
|
|
121
|
+
logo_colored.append(f"[{color}]{line}[/{color}]")
|
|
122
|
+
|
|
123
|
+
# Build tagline with gradient
|
|
124
|
+
tagline_lines = TAGLINE.split("\n")
|
|
125
|
+
tagline_colors = ["bright_magenta", "magenta"]
|
|
126
|
+
tagline_colored = []
|
|
127
|
+
for i, line in enumerate(tagline_lines):
|
|
128
|
+
color = tagline_colors[i] if i < len(tagline_colors) else tagline_colors[-1]
|
|
129
|
+
tagline_colored.append(f"[{color}]{line}[/{color}]")
|
|
130
|
+
|
|
131
|
+
# Left side content
|
|
132
|
+
left_lines = [
|
|
133
|
+
*logo_colored,
|
|
134
|
+
f"[dim]v{__version__}[/dim]",
|
|
135
|
+
"",
|
|
136
|
+
*tagline_colored,
|
|
137
|
+
"",
|
|
138
|
+
f"[bold yellow]{get_caution()}[/bold yellow]",
|
|
139
|
+
"[bright_red]The URL is the only security. Use at your own risk.[/bright_red]",
|
|
140
|
+
status,
|
|
141
|
+
f"[bold cyan]{url}[/bold cyan]",
|
|
142
|
+
]
|
|
143
|
+
if cwd:
|
|
144
|
+
left_lines.append(f"[dim]{cwd}[/dim]")
|
|
145
|
+
left_lines.append("[dim]Ctrl+C to stop[/dim]")
|
|
146
|
+
|
|
147
|
+
left_content = "\n".join(left_lines)
|
|
148
|
+
|
|
149
|
+
# Create side-by-side layout (logo left, QR right)
|
|
150
|
+
table = Table.grid(padding=(0, 4))
|
|
151
|
+
table.add_column(justify="left", vertical="middle")
|
|
152
|
+
table.add_column(justify="left", vertical="middle")
|
|
153
|
+
table.add_row(left_content, qr_text)
|
|
154
|
+
|
|
155
|
+
console.print()
|
|
156
|
+
console.print(Align.center(table))
|
|
157
|
+
console.print()
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Composition root - the ONLY place where dependencies are wired."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from porterminal.application.services import (
|
|
7
|
+
ManagementService,
|
|
8
|
+
SessionService,
|
|
9
|
+
TabService,
|
|
10
|
+
TerminalService,
|
|
11
|
+
)
|
|
12
|
+
from porterminal.container import Container
|
|
13
|
+
from porterminal.domain import (
|
|
14
|
+
EnvironmentRules,
|
|
15
|
+
EnvironmentSanitizer,
|
|
16
|
+
PTYPort,
|
|
17
|
+
SessionLimitChecker,
|
|
18
|
+
ShellCommand,
|
|
19
|
+
TabLimitChecker,
|
|
20
|
+
TerminalDimensions,
|
|
21
|
+
)
|
|
22
|
+
from porterminal.infrastructure.config import ShellDetector, YAMLConfigLoader
|
|
23
|
+
from porterminal.infrastructure.registry import UserConnectionRegistry
|
|
24
|
+
from porterminal.infrastructure.repositories import InMemorySessionRepository, InMemoryTabRepository
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def create_pty_factory(
|
|
28
|
+
cwd: str | None = None,
|
|
29
|
+
) -> Callable[[ShellCommand, TerminalDimensions, dict[str, str], str | None], PTYPort]:
|
|
30
|
+
"""Create a PTY factory function.
|
|
31
|
+
|
|
32
|
+
This bridges the domain PTYPort interface with the existing
|
|
33
|
+
infrastructure PTY implementation.
|
|
34
|
+
"""
|
|
35
|
+
from porterminal.pty import SecurePTYManager, create_backend
|
|
36
|
+
|
|
37
|
+
def factory(
|
|
38
|
+
shell: ShellCommand,
|
|
39
|
+
dimensions: TerminalDimensions,
|
|
40
|
+
environment: dict[str, str],
|
|
41
|
+
working_directory: str | None = None,
|
|
42
|
+
) -> PTYPort:
|
|
43
|
+
# Use provided cwd or factory default
|
|
44
|
+
effective_cwd = working_directory or cwd
|
|
45
|
+
|
|
46
|
+
# Create backend
|
|
47
|
+
backend = create_backend()
|
|
48
|
+
|
|
49
|
+
# Create shell config compatible with existing infrastructure
|
|
50
|
+
from porterminal.config import ShellConfig as LegacyShellConfig
|
|
51
|
+
|
|
52
|
+
legacy_shell = LegacyShellConfig(
|
|
53
|
+
name=shell.name,
|
|
54
|
+
id=shell.id,
|
|
55
|
+
command=shell.command,
|
|
56
|
+
args=list(shell.args),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Create manager (which implements PTY operations)
|
|
60
|
+
manager = SecurePTYManager(
|
|
61
|
+
backend=backend,
|
|
62
|
+
shell_config=legacy_shell,
|
|
63
|
+
cols=dimensions.cols,
|
|
64
|
+
rows=dimensions.rows,
|
|
65
|
+
cwd=effective_cwd,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Spawn with environment (manager handles sanitization internally,
|
|
69
|
+
# but we pass our sanitized env to be safe)
|
|
70
|
+
manager.spawn()
|
|
71
|
+
|
|
72
|
+
return PTYManagerAdapter(manager, dimensions)
|
|
73
|
+
|
|
74
|
+
return factory
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class PTYManagerAdapter:
|
|
78
|
+
"""Adapts SecurePTYManager to PTYPort interface."""
|
|
79
|
+
|
|
80
|
+
def __init__(self, manager, dimensions: TerminalDimensions) -> None:
|
|
81
|
+
self._manager = manager
|
|
82
|
+
self._dimensions = dimensions
|
|
83
|
+
|
|
84
|
+
def spawn(self) -> None:
|
|
85
|
+
"""Already spawned in factory."""
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
def read(self, size: int = 4096) -> bytes:
|
|
89
|
+
return self._manager.read(size)
|
|
90
|
+
|
|
91
|
+
def write(self, data: bytes) -> None:
|
|
92
|
+
self._manager.write(data)
|
|
93
|
+
|
|
94
|
+
def resize(self, dimensions: TerminalDimensions) -> None:
|
|
95
|
+
self._manager.resize(dimensions.cols, dimensions.rows)
|
|
96
|
+
self._dimensions = dimensions
|
|
97
|
+
|
|
98
|
+
def is_alive(self) -> bool:
|
|
99
|
+
return self._manager.is_alive()
|
|
100
|
+
|
|
101
|
+
def close(self) -> None:
|
|
102
|
+
self._manager.close()
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def dimensions(self) -> TerminalDimensions:
|
|
106
|
+
return self._dimensions
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def create_container(
|
|
110
|
+
config_path: Path | str = "config.yaml",
|
|
111
|
+
cwd: str | None = None,
|
|
112
|
+
) -> Container:
|
|
113
|
+
"""Create the dependency container with all wired dependencies.
|
|
114
|
+
|
|
115
|
+
This is the composition root - the single place where all
|
|
116
|
+
dependencies are created and wired together.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
config_path: Path to config file.
|
|
120
|
+
cwd: Working directory for PTY sessions.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Fully wired dependency container.
|
|
124
|
+
"""
|
|
125
|
+
# Load configuration
|
|
126
|
+
loader = YAMLConfigLoader(config_path)
|
|
127
|
+
config_data = loader.load()
|
|
128
|
+
|
|
129
|
+
# Detect shells
|
|
130
|
+
detector = ShellDetector()
|
|
131
|
+
shells = detector.detect_shells()
|
|
132
|
+
|
|
133
|
+
# Get config values with defaults
|
|
134
|
+
server_data = config_data.get("server", {})
|
|
135
|
+
terminal_data = config_data.get("terminal", {})
|
|
136
|
+
|
|
137
|
+
server_host = server_data.get("host", "127.0.0.1")
|
|
138
|
+
server_port = server_data.get("port", 8000)
|
|
139
|
+
default_cols = terminal_data.get("cols", 120)
|
|
140
|
+
default_rows = terminal_data.get("rows", 30)
|
|
141
|
+
default_shell_id = terminal_data.get("default_shell") or detector.get_default_shell_id()
|
|
142
|
+
buttons = config_data.get("buttons", [])
|
|
143
|
+
|
|
144
|
+
# Use configured shells if provided, otherwise use detected
|
|
145
|
+
configured_shells = terminal_data.get("shells", [])
|
|
146
|
+
if configured_shells:
|
|
147
|
+
shells = [ShellCommand.from_dict(s) for s in configured_shells]
|
|
148
|
+
|
|
149
|
+
# Create repositories
|
|
150
|
+
session_repository = InMemorySessionRepository()
|
|
151
|
+
tab_repository = InMemoryTabRepository()
|
|
152
|
+
|
|
153
|
+
# Create connection registry for broadcasting
|
|
154
|
+
connection_registry = UserConnectionRegistry()
|
|
155
|
+
|
|
156
|
+
# Create PTY factory
|
|
157
|
+
pty_factory = create_pty_factory(cwd)
|
|
158
|
+
|
|
159
|
+
# Create services
|
|
160
|
+
session_service = SessionService(
|
|
161
|
+
repository=session_repository,
|
|
162
|
+
pty_factory=pty_factory,
|
|
163
|
+
limit_checker=SessionLimitChecker(),
|
|
164
|
+
environment_sanitizer=EnvironmentSanitizer(EnvironmentRules()),
|
|
165
|
+
working_directory=cwd,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
tab_service = TabService(
|
|
169
|
+
repository=tab_repository,
|
|
170
|
+
limit_checker=TabLimitChecker(),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
terminal_service = TerminalService()
|
|
174
|
+
|
|
175
|
+
# Create a shell provider closure for ManagementService
|
|
176
|
+
def get_shell(shell_id: str | None) -> ShellCommand | None:
|
|
177
|
+
target_id = shell_id or default_shell_id
|
|
178
|
+
for shell in shells:
|
|
179
|
+
if shell.id == target_id:
|
|
180
|
+
return shell
|
|
181
|
+
return shells[0] if shells else None
|
|
182
|
+
|
|
183
|
+
management_service = ManagementService(
|
|
184
|
+
session_service=session_service,
|
|
185
|
+
tab_service=tab_service,
|
|
186
|
+
connection_registry=connection_registry,
|
|
187
|
+
shell_provider=get_shell,
|
|
188
|
+
default_dimensions=TerminalDimensions(default_cols, default_rows),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return Container(
|
|
192
|
+
session_service=session_service,
|
|
193
|
+
tab_service=tab_service,
|
|
194
|
+
terminal_service=terminal_service,
|
|
195
|
+
management_service=management_service,
|
|
196
|
+
session_repository=session_repository,
|
|
197
|
+
tab_repository=tab_repository,
|
|
198
|
+
connection_registry=connection_registry,
|
|
199
|
+
pty_factory=pty_factory,
|
|
200
|
+
available_shells=shells,
|
|
201
|
+
default_shell_id=default_shell_id,
|
|
202
|
+
server_host=server_host,
|
|
203
|
+
server_port=server_port,
|
|
204
|
+
default_cols=default_cols,
|
|
205
|
+
default_rows=default_rows,
|
|
206
|
+
buttons=buttons,
|
|
207
|
+
cwd=cwd,
|
|
208
|
+
)
|