proxy-tuner 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. proxy_tuner-0.1.0/PKG-INFO +122 -0
  2. proxy_tuner-0.1.0/README.md +88 -0
  3. proxy_tuner-0.1.0/pyproject.toml +72 -0
  4. proxy_tuner-0.1.0/setup.cfg +4 -0
  5. proxy_tuner-0.1.0/src/proxy_tuner/__init__.py +3 -0
  6. proxy_tuner-0.1.0/src/proxy_tuner/cli.py +109 -0
  7. proxy_tuner-0.1.0/src/proxy_tuner/cli_config.py +152 -0
  8. proxy_tuner-0.1.0/src/proxy_tuner/cli_logs.py +58 -0
  9. proxy_tuner-0.1.0/src/proxy_tuner/cli_monitor.py +84 -0
  10. proxy_tuner-0.1.0/src/proxy_tuner/cli_outbound.py +146 -0
  11. proxy_tuner-0.1.0/src/proxy_tuner/cli_rule.py +251 -0
  12. proxy_tuner-0.1.0/src/proxy_tuner/cli_start.py +277 -0
  13. proxy_tuner-0.1.0/src/proxy_tuner/cli_stats.py +71 -0
  14. proxy_tuner-0.1.0/src/proxy_tuner/completions.py +85 -0
  15. proxy_tuner-0.1.0/src/proxy_tuner/config.py +391 -0
  16. proxy_tuner-0.1.0/src/proxy_tuner/dns.py +258 -0
  17. proxy_tuner-0.1.0/src/proxy_tuner/doctor.py +207 -0
  18. proxy_tuner-0.1.0/src/proxy_tuner/firewall.py +172 -0
  19. proxy_tuner-0.1.0/src/proxy_tuner/forwarder.py +449 -0
  20. proxy_tuner-0.1.0/src/proxy_tuner/http_proxy.py +142 -0
  21. proxy_tuner-0.1.0/src/proxy_tuner/logging.py +96 -0
  22. proxy_tuner-0.1.0/src/proxy_tuner/outbounds.py +186 -0
  23. proxy_tuner-0.1.0/src/proxy_tuner/platform/__init__.py +40 -0
  24. proxy_tuner-0.1.0/src/proxy_tuner/platform/base.py +54 -0
  25. proxy_tuner-0.1.0/src/proxy_tuner/platform/linux.py +196 -0
  26. proxy_tuner-0.1.0/src/proxy_tuner/platform/macos.py +46 -0
  27. proxy_tuner-0.1.0/src/proxy_tuner/platform/windows.py +244 -0
  28. proxy_tuner-0.1.0/src/proxy_tuner/pool.py +157 -0
  29. proxy_tuner-0.1.0/src/proxy_tuner/rules.py +255 -0
  30. proxy_tuner-0.1.0/src/proxy_tuner/setup_wizard.py +157 -0
  31. proxy_tuner-0.1.0/src/proxy_tuner/socks5.py +241 -0
  32. proxy_tuner-0.1.0/src/proxy_tuner/tun.py +240 -0
  33. proxy_tuner-0.1.0/src/proxy_tuner.egg-info/PKG-INFO +122 -0
  34. proxy_tuner-0.1.0/src/proxy_tuner.egg-info/SOURCES.txt +47 -0
  35. proxy_tuner-0.1.0/src/proxy_tuner.egg-info/dependency_links.txt +1 -0
  36. proxy_tuner-0.1.0/src/proxy_tuner.egg-info/entry_points.txt +2 -0
  37. proxy_tuner-0.1.0/src/proxy_tuner.egg-info/requires.txt +11 -0
  38. proxy_tuner-0.1.0/src/proxy_tuner.egg-info/top_level.txt +1 -0
  39. proxy_tuner-0.1.0/tests/test_cli.py +297 -0
  40. proxy_tuner-0.1.0/tests/test_completions.py +56 -0
  41. proxy_tuner-0.1.0/tests/test_config.py +419 -0
  42. proxy_tuner-0.1.0/tests/test_dns.py +126 -0
  43. proxy_tuner-0.1.0/tests/test_firewall.py +95 -0
  44. proxy_tuner-0.1.0/tests/test_forwarder.py +226 -0
  45. proxy_tuner-0.1.0/tests/test_http_proxy.py +40 -0
  46. proxy_tuner-0.1.0/tests/test_pool.py +122 -0
  47. proxy_tuner-0.1.0/tests/test_rules.py +628 -0
  48. proxy_tuner-0.1.0/tests/test_socks5.py +83 -0
  49. proxy_tuner-0.1.0/tests/test_tun.py +173 -0
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: proxy-tuner
3
+ Version: 0.1.0
4
+ Summary: Cross-platform CLI tool for routing traffic through multiple proxy outbounds with flexible rule-based routing by process, domain, IP, and port
5
+ Author-email: HoomanJ <Hooman.Jalalpoor@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/HoomanJ/proxy-tuner
8
+ Project-URL: Documentation, https://github.com/HoomanJ/proxy-tuner/tree/master/docs
9
+ Project-URL: Issues, https://github.com/HoomanJ/proxy-tuner/issues
10
+ Keywords: proxy,tunnel,vpn,routing,socks5,http-proxy
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: System :: Networking
22
+ Classifier: Topic :: System :: Monitoring
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: click>=8.0
26
+ Requires-Dist: rich>=13.0
27
+ Provides-Extra: linux
28
+ Requires-Dist: pyroute2>=0.7; extra == "linux"
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7.0; extra == "dev"
31
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
32
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
33
+ Requires-Dist: mypy>=1.0; extra == "dev"
34
+
35
+ # ProxyTuner
36
+
37
+ > **⚠ Warning: This software is in early development (alpha). Do not use in production or sensitive workspaces.**
38
+ > It may contain bugs, insecure defaults, or incomplete security measures.
39
+ > Use only for personal testing and experimentation.
40
+
41
+ A cross-platform (Windows & Linux) CLI tool for tuning and routing network traffic through multiple proxy outbounds with flexible rule-based routing.
42
+
43
+ ## What It Does
44
+
45
+ ProxyTuner lets you:
46
+
47
+ - **Manage proxy outbounds** — add/remove SOCKS5 or HTTP proxies as named outbounds
48
+ - **Define routing rules** — direct traffic to specific outbounds based on:
49
+ - Process name (e.g., `firefox`, `chrome`, `curl`)
50
+ - Domain / hostname patterns (regex)
51
+ - IP addresses and CIDR ranges
52
+ - URL patterns (regex)
53
+ - Port ranges
54
+ - **Start/stop tuning** — activate or deactivate traffic redirection at any time
55
+ - **Per-platform optimization** — uses native OS mechanisms:
56
+ - **Linux**: nftables/iptables TPROXY + TUN interface
57
+ - **Windows**: WinDivert packet interception
58
+
59
+ ## Quick Start
60
+
61
+ ```bash
62
+ # Install
63
+ pip install proxy-tuner
64
+
65
+ # Add a SOCKS5 proxy outbound
66
+ proxy-tuner outbound add my-vpn --type socks5 --host 127.0.0.1 --port 1080
67
+
68
+ # Add an HTTP proxy outbound
69
+ proxy-tuner outbound add my-http --type http --host 127.0.0.1 --port 8080
70
+
71
+ # Add a routing rule: send Firefox traffic through my-vpn
72
+ proxy-tuner rule add firefox-vpn --process firefox --outbound my-vpn
73
+
74
+ # Add a rule: send *.cn domains through a specific proxy
75
+ proxy-tuner rule add china-traffic --domain "*.cn" --outbound my-http
76
+
77
+ # Start tuning
78
+ proxy-tuner start
79
+
80
+ # Stop tuning
81
+ proxy-tuner stop
82
+
83
+ # Show status
84
+ proxy-tuner status
85
+ ```
86
+
87
+ ## How It Works
88
+
89
+ ProxyTuner creates a local transparent proxy that intercepts traffic based on your rules and forwards it through the configured outbound proxies.
90
+
91
+ ```
92
+ Application traffic
93
+
94
+
95
+ ┌──────────────────┐
96
+ │ ProxyTuner │
97
+ │ (rule engine) │
98
+ └──────────────────┘
99
+
100
+ ┌────┴────┐
101
+ ▼ ▼
102
+ Outbound1 Outbound2
103
+ (SOCKS5) (HTTP)
104
+ ```
105
+
106
+ On Linux, traffic is redirected to ProxyTuner via nftables/iptables with per-process (`--uid-owner`) matching. On Windows, WinDivert captures packets from specific processes.
107
+
108
+ ## Requirements
109
+
110
+ - Python 3.10+
111
+ - Linux: `nftables` or `iptables` (root/sudo required)
112
+ - Windows: [WinDivert](https://reqrypt.org/windivert.html) driver (admin required)
113
+
114
+ ## Documentation
115
+
116
+ - [Architecture](docs/architecture.md) — technical design and internals
117
+ - [Configuration](docs/configuration.md) — config file reference
118
+ - [Usage Guide](docs/usage.md) — CLI commands and examples
119
+
120
+ ## License
121
+
122
+ MIT
@@ -0,0 +1,88 @@
1
+ # ProxyTuner
2
+
3
+ > **⚠ Warning: This software is in early development (alpha). Do not use in production or sensitive workspaces.**
4
+ > It may contain bugs, insecure defaults, or incomplete security measures.
5
+ > Use only for personal testing and experimentation.
6
+
7
+ A cross-platform (Windows & Linux) CLI tool for tuning and routing network traffic through multiple proxy outbounds with flexible rule-based routing.
8
+
9
+ ## What It Does
10
+
11
+ ProxyTuner lets you:
12
+
13
+ - **Manage proxy outbounds** — add/remove SOCKS5 or HTTP proxies as named outbounds
14
+ - **Define routing rules** — direct traffic to specific outbounds based on:
15
+ - Process name (e.g., `firefox`, `chrome`, `curl`)
16
+ - Domain / hostname patterns (regex)
17
+ - IP addresses and CIDR ranges
18
+ - URL patterns (regex)
19
+ - Port ranges
20
+ - **Start/stop tuning** — activate or deactivate traffic redirection at any time
21
+ - **Per-platform optimization** — uses native OS mechanisms:
22
+ - **Linux**: nftables/iptables TPROXY + TUN interface
23
+ - **Windows**: WinDivert packet interception
24
+
25
+ ## Quick Start
26
+
27
+ ```bash
28
+ # Install
29
+ pip install proxy-tuner
30
+
31
+ # Add a SOCKS5 proxy outbound
32
+ proxy-tuner outbound add my-vpn --type socks5 --host 127.0.0.1 --port 1080
33
+
34
+ # Add an HTTP proxy outbound
35
+ proxy-tuner outbound add my-http --type http --host 127.0.0.1 --port 8080
36
+
37
+ # Add a routing rule: send Firefox traffic through my-vpn
38
+ proxy-tuner rule add firefox-vpn --process firefox --outbound my-vpn
39
+
40
+ # Add a rule: send *.cn domains through a specific proxy
41
+ proxy-tuner rule add china-traffic --domain "*.cn" --outbound my-http
42
+
43
+ # Start tuning
44
+ proxy-tuner start
45
+
46
+ # Stop tuning
47
+ proxy-tuner stop
48
+
49
+ # Show status
50
+ proxy-tuner status
51
+ ```
52
+
53
+ ## How It Works
54
+
55
+ ProxyTuner creates a local transparent proxy that intercepts traffic based on your rules and forwards it through the configured outbound proxies.
56
+
57
+ ```
58
+ Application traffic
59
+
60
+
61
+ ┌──────────────────┐
62
+ │ ProxyTuner │
63
+ │ (rule engine) │
64
+ └──────────────────┘
65
+
66
+ ┌────┴────┐
67
+ ▼ ▼
68
+ Outbound1 Outbound2
69
+ (SOCKS5) (HTTP)
70
+ ```
71
+
72
+ On Linux, traffic is redirected to ProxyTuner via nftables/iptables with per-process (`--uid-owner`) matching. On Windows, WinDivert captures packets from specific processes.
73
+
74
+ ## Requirements
75
+
76
+ - Python 3.10+
77
+ - Linux: `nftables` or `iptables` (root/sudo required)
78
+ - Windows: [WinDivert](https://reqrypt.org/windivert.html) driver (admin required)
79
+
80
+ ## Documentation
81
+
82
+ - [Architecture](docs/architecture.md) — technical design and internals
83
+ - [Configuration](docs/configuration.md) — config file reference
84
+ - [Usage Guide](docs/usage.md) — CLI commands and examples
85
+
86
+ ## License
87
+
88
+ MIT
@@ -0,0 +1,72 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "proxy-tuner"
7
+ version = "0.1.0"
8
+ description = "Cross-platform CLI tool for routing traffic through multiple proxy outbounds with flexible rule-based routing by process, domain, IP, and port"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "HoomanJ", email = "Hooman.Jalalpoor@gmail.com" },
14
+ ]
15
+ keywords = ["proxy", "tunnel", "vpn", "routing", "socks5", "http-proxy"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Environment :: Console",
19
+ "Intended Audience :: System Administrators",
20
+ "Operating System :: POSIX :: Linux",
21
+ "Operating System :: Microsoft :: Windows",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: System :: Networking",
28
+ "Topic :: System :: Monitoring",
29
+ ]
30
+ dependencies = [
31
+ "click>=8.0",
32
+ "rich>=13.0",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ linux = [
37
+ "pyroute2>=0.7",
38
+ ]
39
+ dev = [
40
+ "pytest>=7.0",
41
+ "pytest-asyncio>=0.21",
42
+ "ruff>=0.1.0",
43
+ "mypy>=1.0",
44
+ ]
45
+
46
+ [project.scripts]
47
+ proxy-tuner = "proxy_tuner.cli:main"
48
+
49
+ [project.urls]
50
+ Repository = "https://github.com/HoomanJ/proxy-tuner"
51
+ Documentation = "https://github.com/HoomanJ/proxy-tuner/tree/master/docs"
52
+ Issues = "https://github.com/HoomanJ/proxy-tuner/issues"
53
+
54
+ [tool.setuptools.packages.find]
55
+ where = ["src"]
56
+
57
+ [tool.ruff]
58
+ target-version = "py310"
59
+ line-length = 100
60
+
61
+ [tool.ruff.lint]
62
+ select = ["E", "F", "W", "I", "N", "UP", "B", "SIM"]
63
+
64
+ [tool.mypy]
65
+ python_version = "3.10"
66
+ strict = true
67
+ warn_return_any = true
68
+ warn_unused_configs = true
69
+
70
+ [tool.pytest.ini_options]
71
+ testpaths = ["tests"]
72
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """ProxyTuner — cross-platform proxy routing CLI."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,109 @@
1
+ """Main CLI entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import click
8
+ from rich.console import Console
9
+
10
+ from proxy_tuner import __version__
11
+ from proxy_tuner.config import ConfigManager
12
+
13
+ console = Console()
14
+
15
+
16
+ @click.group()
17
+ @click.version_option(version=__version__, prog_name="proxy-tuner")
18
+ @click.option(
19
+ "--config",
20
+ "config_path",
21
+ type=click.Path(path_type=Path),
22
+ default=None,
23
+ help="Path to config file (default: platform default)",
24
+ )
25
+ @click.pass_context
26
+ def main(ctx: click.Context, config_path: Path | None) -> None:
27
+ """ProxyTuner — cross-platform proxy routing CLI.
28
+
29
+ Route network traffic through multiple proxy outbounds with
30
+ flexible rule-based routing.
31
+ """
32
+ ctx.ensure_object(dict)
33
+ ctx.obj["config_manager"] = ConfigManager(config_path)
34
+
35
+
36
+ # Register subcommands
37
+ from proxy_tuner.cli_config import config_group # noqa: E402
38
+ from proxy_tuner.cli_logs import logs # noqa: E402
39
+ from proxy_tuner.cli_monitor import monitor # noqa: E402
40
+ from proxy_tuner.cli_outbound import outbound_group # noqa: E402
41
+ from proxy_tuner.cli_rule import rule_group # noqa: E402
42
+ from proxy_tuner.cli_start import start, status, stop # noqa: E402
43
+ from proxy_tuner.cli_stats import stats # noqa: E402
44
+ from proxy_tuner.completions import completions_group # noqa: E402
45
+ from proxy_tuner.doctor import doctor # noqa: E402
46
+ from proxy_tuner.setup_wizard import setup_wizard # noqa: E402
47
+
48
+ main.add_command(outbound_group)
49
+ main.add_command(rule_group)
50
+ main.add_command(config_group)
51
+ main.add_command(completions_group)
52
+ main.add_command(setup_wizard)
53
+ main.add_command(doctor)
54
+ main.add_command(logs)
55
+ main.add_command(stats)
56
+ main.add_command(monitor)
57
+ main.add_command(start)
58
+ main.add_command(stop)
59
+ main.add_command(status)
60
+
61
+
62
+ @main.command("version")
63
+ def version_cmd() -> None:
64
+ """Show version information."""
65
+ from rich.console import Console
66
+
67
+ Console().print(f"proxy-tuner {__version__}")
68
+
69
+
70
+ @main.command("reload")
71
+ @click.pass_context
72
+ def reload(ctx: click.Context) -> None:
73
+ """Reload configuration (hot-reload while running)."""
74
+ import os
75
+ import signal
76
+
77
+ manager: ConfigManager = ctx.obj["config_manager"]
78
+
79
+ from proxy_tuner.cli_start import _read_pid
80
+
81
+ pid = _read_pid(manager)
82
+ if pid is None:
83
+ console.print("[yellow]ProxyTuner is not running.[/yellow]")
84
+ raise click.Abort()
85
+
86
+ # Validate new config
87
+ try:
88
+ config = manager.load()
89
+ except Exception as e:
90
+ console.print(f"[red]Config error:[/red] {e}")
91
+ raise click.Abort() from e
92
+
93
+ errors = config.validate_references()
94
+ if errors:
95
+ console.print("[red]Config validation errors:[/red]")
96
+ for err in errors:
97
+ console.print(f" • {err}")
98
+ raise click.Abort()
99
+
100
+ # Send SIGHUP to trigger reload
101
+ try:
102
+ os.kill(pid, signal.SIGHUP)
103
+ console.print(f"[green]✓[/green] Sent reload signal to PID {pid}")
104
+ except OSError as e:
105
+ console.print(f"[red]Error:[/red] {e}")
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()
@@ -0,0 +1,152 @@
1
+ """Config management CLI subcommands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+ from rich.console import Console
7
+ from rich.syntax import Syntax
8
+
9
+ from proxy_tuner.config import ConfigManager
10
+
11
+ console = Console()
12
+
13
+
14
+ @click.group("config")
15
+ def config_group() -> None:
16
+ """Manage configuration."""
17
+
18
+
19
+ @config_group.command("show")
20
+ @click.pass_context
21
+ def show_config(ctx: click.Context) -> None:
22
+ """Display the current configuration."""
23
+ import json
24
+
25
+ manager: ConfigManager = ctx.obj["config_manager"]
26
+ config = manager.get()
27
+
28
+ from proxy_tuner.config import _serialize_config
29
+
30
+ output = json.dumps(_serialize_config(config), indent=2, ensure_ascii=False)
31
+ console.print(Syntax(output, "json", theme="monokai"))
32
+
33
+
34
+ @config_group.command("path")
35
+ @click.pass_context
36
+ def config_path(ctx: click.Context) -> None:
37
+ """Show the config file path."""
38
+ manager: ConfigManager = ctx.obj["config_manager"]
39
+ console.print(str(manager.path))
40
+
41
+
42
+ @config_group.command("edit")
43
+ @click.pass_context
44
+ def edit_config(ctx: click.Context) -> None:
45
+ """Open the config file in the default editor."""
46
+ import os
47
+ import subprocess
48
+
49
+ manager: ConfigManager = ctx.obj["config_manager"]
50
+
51
+ # Ensure file exists
52
+ if not manager.path.exists():
53
+ manager.save(manager.get())
54
+
55
+ editor = os.environ.get("EDITOR", "vi")
56
+ try:
57
+ subprocess.run([editor, str(manager.path)], check=True)
58
+ console.print(f"[green]✓[/green] Config edited at {manager.path}")
59
+ except FileNotFoundError:
60
+ msg = f"[red]Error:[/red] Editor '{editor}' not found."
61
+ console.print(msg + " Set $EDITOR or use 'config show'.")
62
+ except subprocess.CalledProcessError as e:
63
+ console.print(f"[red]Error:[/red] Editor exited with code {e.returncode}")
64
+
65
+
66
+ @config_group.command("validate")
67
+ @click.pass_context
68
+ def validate_config(ctx: click.Context) -> None:
69
+ """Validate the configuration file."""
70
+ manager: ConfigManager = ctx.obj["config_manager"]
71
+
72
+ try:
73
+ config = manager.load()
74
+ except Exception as e:
75
+ console.print(f"[red]Parse error:[/red] {e}")
76
+ raise click.Abort() from e
77
+
78
+ errors = config.validate_references()
79
+ if errors:
80
+ console.print("[red]Validation errors:[/red]")
81
+ for err in errors:
82
+ console.print(f" • {err}")
83
+ raise click.Abort()
84
+
85
+ console.print("[green]✓[/green] Configuration is valid")
86
+
87
+
88
+ @config_group.command("init")
89
+ @click.pass_context
90
+ def init_config(ctx: click.Context) -> None:
91
+ """Create a default config file."""
92
+ from proxy_tuner.config import Config
93
+
94
+ manager: ConfigManager = ctx.obj["config_manager"]
95
+
96
+ if manager.path.exists():
97
+ console.print(f"[yellow]Config already exists at {manager.path}[/yellow]")
98
+ if not click.confirm("Overwrite?"):
99
+ return
100
+
101
+ manager.save(Config())
102
+ console.print(f"[green]✓[/green] Created default config at {manager.path}")
103
+
104
+
105
+ @config_group.command("set")
106
+ @click.argument("key")
107
+ @click.argument("value")
108
+ @click.pass_context
109
+ def set_config(ctx: click.Context, key: str, value: str) -> None:
110
+ """Set a configuration value.
111
+
112
+ Examples:
113
+ proxy-tuner config set settings.listen_port 9090
114
+ proxy-tuner config set settings.log_level debug
115
+ proxy-tuner config set settings.dns_server 1.1.1.1
116
+ """
117
+ manager: ConfigManager = ctx.obj["config_manager"]
118
+ config = manager.get()
119
+
120
+ # Parse the key path
121
+ parts = key.split(".")
122
+ if len(parts) < 2:
123
+ msg = "[red]Error:[/red] Key must be in format 'section.field'"
124
+ console.print(f"{msg} (e.g., settings.listen_port)")
125
+ raise click.Abort()
126
+
127
+ section = parts[0]
128
+ field_name = parts[1]
129
+
130
+ # Type coercion
131
+ if value.lower() == "true":
132
+ coerced: object = True
133
+ elif value.lower() == "false":
134
+ coerced = False
135
+ elif value.lower() == "null" or value.lower() == "none":
136
+ coerced = None
137
+ else:
138
+ try:
139
+ coerced = int(value)
140
+ except ValueError:
141
+ try:
142
+ coerced = float(value)
143
+ except ValueError:
144
+ coerced = value
145
+
146
+ if section == "settings" and hasattr(config.settings, field_name):
147
+ setattr(config.settings, field_name, coerced)
148
+ manager.save(config)
149
+ console.print(f"[green]✓[/green] Set {key} = {coerced}")
150
+ else:
151
+ console.print(f"[red]Error:[/red] Unknown setting '{key}'. Valid sections: settings")
152
+ raise click.Abort()
@@ -0,0 +1,58 @@
1
+ """Logs viewing CLI command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+ from rich.console import Console
7
+
8
+ console = Console()
9
+
10
+
11
+ @click.command("logs")
12
+ @click.option("-n", "--lines", default=50, type=int, help="Number of lines to show")
13
+ @click.option("-f", "--follow", is_flag=True, help="Follow log output (tail -f)")
14
+ @click.pass_context
15
+ def logs(ctx: click.Context, lines: int, follow: bool) -> None:
16
+ """View ProxyTuner logs."""
17
+
18
+ from proxy_tuner.config import get_config_dir
19
+
20
+ log_file = get_config_dir() / "proxy-tuner.log"
21
+
22
+ if follow:
23
+ # Tail -f mode
24
+ if not log_file.exists():
25
+ console.print(f"[yellow]Log file not found: {log_file}[/yellow]")
26
+ return
27
+
28
+ console.print(f"[dim]Following {log_file} (Ctrl+C to stop)[/dim]")
29
+ try:
30
+ with open(log_file) as f:
31
+ # Seek to end minus some bytes
32
+ f.seek(0, 2)
33
+ size = f.tell()
34
+ f.seek(max(0, size - 4096))
35
+ f.readline() # Skip partial line
36
+
37
+ while True:
38
+ line = f.readline()
39
+ if line:
40
+ console.print(line.rstrip())
41
+ else:
42
+ import time
43
+ time.sleep(0.1)
44
+ except KeyboardInterrupt:
45
+ pass
46
+ else:
47
+ # Show last N lines
48
+ if not log_file.exists():
49
+ console.print(f"[yellow]No log file found at {log_file}[/yellow]")
50
+ msg = "[dim]Logs are written when running with "
51
+ console.print(msg + "--log-file or in daemon mode[/dim]")
52
+ return
53
+
54
+ with open(log_file) as f:
55
+ all_lines = f.readlines()
56
+ recent = all_lines[-lines:]
57
+ for line in recent:
58
+ console.print(line.rstrip())
@@ -0,0 +1,84 @@
1
+ """Monitor command — live view of connections and activity."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+ import click
8
+ from rich.console import Console
9
+ from rich.live import Live
10
+ from rich.table import Table
11
+
12
+ console = Console()
13
+
14
+
15
+ def _format_bytes(n: int) -> str:
16
+ for unit in ("B", "KB", "MB", "GB"):
17
+ if abs(n) < 1024:
18
+ return f"{n:.1f} {unit}"
19
+ n /= 1024
20
+ return f"{n:.1f} TB"
21
+
22
+
23
+ @click.command("monitor")
24
+ @click.option("--interval", "-i", default=2, type=int, help="Refresh interval in seconds")
25
+ @click.pass_context
26
+ def monitor(ctx: click.Context, interval: int) -> None:
27
+ """Live monitoring dashboard."""
28
+ import json
29
+
30
+ from proxy_tuner.config import get_config_dir
31
+
32
+ stats_file = get_config_dir() / "stats.json"
33
+ pid_file = get_config_dir() / "proxytuner.pid"
34
+
35
+ def _build_table() -> Table:
36
+ table = Table(title="ProxyTuner Monitor", show_header=True)
37
+ table.add_column("Metric", style="bold")
38
+ table.add_column("Value", justify="right")
39
+
40
+ # Status
41
+ running = False
42
+ if pid_file.exists():
43
+ try:
44
+ import os
45
+ pid = int(pid_file.read_text().strip())
46
+ os.kill(pid, 0)
47
+ running = True
48
+ except (ValueError, OSError):
49
+ pass
50
+
51
+ status = "[green]Running[/green]" if running else "[red]Stopped[/red]"
52
+ table.add_row("Status", status)
53
+
54
+ # Load stats
55
+ data = {}
56
+ if stats_file.exists():
57
+ try:
58
+ with open(stats_file) as f:
59
+ data = json.load(f)
60
+ except Exception:
61
+ pass
62
+
63
+ # Aggregate stats
64
+ total_conns = sum(s.get("connections", 0) for s in data.values())
65
+ total_sent = sum(s.get("bytes_sent", 0) for s in data.values())
66
+ total_recv = sum(s.get("bytes_received", 0) for s in data.values())
67
+ total_errors = sum(s.get("errors", 0) for s in data.values())
68
+
69
+ table.add_row("Total connections", str(total_conns))
70
+ table.add_row("Bytes sent", _format_bytes(total_sent))
71
+ table.add_row("Bytes received", _format_bytes(total_recv))
72
+ table.add_row("Errors", str(total_errors))
73
+ table.add_row("Last update", time.strftime("%H:%M:%S"))
74
+
75
+ return table
76
+
77
+ try:
78
+ refresh = 1 // max(interval, 1)
79
+ with Live(_build_table(), refresh_per_second=refresh, console=console) as live:
80
+ while True:
81
+ time.sleep(interval)
82
+ live.update(_build_table())
83
+ except KeyboardInterrupt:
84
+ pass