sing-box-cli 0.1.1__tar.gz → 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. sing_box_cli-0.1.3/PKG-INFO +186 -0
  2. sing_box_cli-0.1.3/README.md +137 -0
  3. sing_box_cli-0.1.3/docs/config-serve.md +49 -0
  4. sing_box_cli-0.1.3/docs/traefik-config-serve.yml +19 -0
  5. sing_box_cli-0.1.3/hatch_build.py +47 -0
  6. {sing_box_cli-0.1.1 → sing_box_cli-0.1.3}/pyproject.toml +7 -1
  7. {sing_box_cli-0.1.1 → sing_box_cli-0.1.3}/src/sing_box_cli/__init__.py +1 -1
  8. {sing_box_cli-0.1.1 → sing_box_cli-0.1.3}/src/sing_box_cli/api/__init__.py +27 -17
  9. {sing_box_cli-0.1.1 → sing_box_cli-0.1.3}/src/sing_box_cli/api/client.py +14 -6
  10. {sing_box_cli-0.1.1 → sing_box_cli-0.1.3}/src/sing_box_cli/api/connections.py +22 -21
  11. sing_box_cli-0.1.3/src/sing_box_cli/api/logs.py +93 -0
  12. {sing_box_cli-0.1.1 → sing_box_cli-0.1.3}/src/sing_box_cli/api/monitor.py +45 -56
  13. {sing_box_cli-0.1.1 → sing_box_cli-0.1.3}/src/sing_box_cli/api/policy.py +18 -16
  14. {sing_box_cli-0.1.1 → sing_box_cli-0.1.3}/src/sing_box_cli/config/__init__.py +12 -1
  15. sing_box_cli-0.1.3/src/sing_box_cli/config/_serve_worker.py +56 -0
  16. {sing_box_cli-0.1.1 → sing_box_cli-0.1.3}/src/sing_box_cli/config/config.py +12 -0
  17. sing_box_cli-0.1.3/src/sing_box_cli/config/serve.py +282 -0
  18. sing_box_cli-0.1.3/src/sing_box_cli/config/serve_backend.py +340 -0
  19. {sing_box_cli-0.1.1 → sing_box_cli-0.1.3}/src/sing_box_cli/config/utils.py +3 -1
  20. sing_box_cli-0.1.3/src/sing_box_cli/installation.py +67 -0
  21. {sing_box_cli-0.1.1 → sing_box_cli-0.1.3}/src/sing_box_cli/main.py +17 -2
  22. {sing_box_cli-0.1.1 → sing_box_cli-0.1.3}/src/sing_box_cli/service/manager.py +54 -110
  23. sing_box_cli-0.1.3/windows-service/common.go +121 -0
  24. sing_box_cli-0.1.3/windows-service/common_test.go +66 -0
  25. sing_box_cli-0.1.3/windows-service/go.mod +5 -0
  26. sing_box_cli-0.1.3/windows-service/go.sum +2 -0
  27. sing_box_cli-0.1.3/windows-service/integration_windows_test.go +141 -0
  28. sing_box_cli-0.1.3/windows-service/main_other.go +13 -0
  29. sing_box_cli-0.1.3/windows-service/main_windows.go +385 -0
  30. sing_box_cli-0.1.3/windows-service/testdata/core.go +30 -0
  31. sing_box_cli-0.1.1/.github/dependabot.yml +0 -16
  32. sing_box_cli-0.1.1/.github/workflows/main.yml +0 -99
  33. sing_box_cli-0.1.1/.pre-commit-config.yaml +0 -50
  34. sing_box_cli-0.1.1/CHANGELOG.md +0 -551
  35. sing_box_cli-0.1.1/PKG-INFO +0 -99
  36. sing_box_cli-0.1.1/README.md +0 -50
  37. sing_box_cli-0.1.1/assets/image.png +0 -0
  38. sing_box_cli-0.1.1/scripts/bump.sh +0 -8
  39. sing_box_cli-0.1.1/scripts/format.sh +0 -5
  40. sing_box_cli-0.1.1/scripts/lint.sh +0 -8
  41. sing_box_cli-0.1.1/scripts/pre-commit.sh +0 -9
  42. sing_box_cli-0.1.1/scripts/test.sh +0 -8
  43. sing_box_cli-0.1.1/src/sing_box_cli/api/logs.py +0 -93
  44. sing_box_cli-0.1.1/src/sing_box_cli/bin/nssm.exe +0 -0
  45. sing_box_cli-0.1.1/tests/config/test_utils.py +0 -177
  46. sing_box_cli-0.1.1/tests/conftest.py +0 -11
  47. sing_box_cli-0.1.1/tests/data/config.json +0 -243
  48. sing_box_cli-0.1.1/uv.lock +0 -1036
  49. {sing_box_cli-0.1.1 → sing_box_cli-0.1.3}/.gitignore +0 -0
  50. {sing_box_cli-0.1.1 → sing_box_cli-0.1.3}/LICENSE +0 -0
  51. {sing_box_cli-0.1.1 → sing_box_cli-0.1.3}/src/sing_box_cli/api/widget.py +0 -0
  52. {sing_box_cli-0.1.1 → sing_box_cli-0.1.3}/src/sing_box_cli/common.py +0 -0
  53. {sing_box_cli-0.1.1 → sing_box_cli-0.1.3}/src/sing_box_cli/py.typed +0 -0
  54. {sing_box_cli-0.1.1 → sing_box_cli-0.1.3}/src/sing_box_cli/service/__init__.py +0 -0
@@ -0,0 +1,186 @@
1
+ Metadata-Version: 2.5
2
+ Name: sing-box-cli
3
+ Version: 0.1.3
4
+ Summary: cross-platform sing-box manager.
5
+ Project-URL: Homepage, https://github.com/aittcuszz/sing-box-service
6
+ Project-URL: Issues, https://github.com/atticuszz/sing-box-service/issues
7
+ Project-URL: documentation, https://atticuszz.github.io/sing-box-service/
8
+ Project-URL: Changelog, https://github.com/atticuszz/sing-box-service/blob/main/CHANGELOG.md
9
+ Author-email: "A.J.Zeller" <hello@atticux.me>
10
+ Maintainer-email: "A.J.Zeller" <hello@atticux.me>
11
+ License: MIT License
12
+
13
+ Copyright (c) 2024 A.J.Zeller
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a copy
16
+ of this software and associated documentation files (the "Software"), to deal
17
+ in the Software without restriction, including without limitation the rights
18
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
+ copies of the Software, and to permit persons to whom the Software is
20
+ furnished to do so, subject to the following conditions:
21
+
22
+ The above copyright notice and this permission notice shall be included in all
23
+ copies or substantial portions of the Software.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ SOFTWARE.
32
+ License-File: LICENSE
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python
35
+ Classifier: Programming Language :: Python :: 3 :: Only
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Programming Language :: Python :: 3.13
39
+ Classifier: Programming Language :: Python :: 3.14
40
+ Requires-Python: <3.15,>=3.11
41
+ Requires-Dist: httpx>=0.28.1
42
+ Requires-Dist: plotext>=5.3.2
43
+ Requires-Dist: prompt-toolkit>=3.0.50
44
+ Requires-Dist: psutil>=6.1.1
45
+ Requires-Dist: pydantic>=2.10.6
46
+ Requires-Dist: sing-box-bin>=1.12.12
47
+ Requires-Dist: typer>=0.15.1
48
+ Description-Content-Type: text/markdown
49
+
50
+ # Sing-Box CLI
51
+
52
+ 🎤 Cross-platform sing-box service manager.
53
+
54
+ ![help](assets/image.png)
55
+
56
+ ## Install
57
+
58
+ ### uv
59
+
60
+ Windows
61
+
62
+ ```powershell
63
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
64
+ ```
65
+
66
+ Linux
67
+
68
+ ```bash
69
+ curl -LsSf https://astral.sh/uv/install.sh | sh
70
+ ```
71
+
72
+ ### sing-box-cli
73
+
74
+ Install
75
+ ```powershell
76
+ # windows
77
+ uv tool install sing-box-cli
78
+ ```
79
+
80
+ ```bash
81
+ # linux
82
+ uv tool install sing-box-cli
83
+ sbc install
84
+ ```
85
+
86
+ On Linux, `sbc install` installs both `sbc` and `sing-box-cli` into
87
+ `/usr/local/bin`, requesting sudo only to write those system launchers. It replaces
88
+ the old manual symlinks and can be run again safely. Use `--bin-dir PATH` to select
89
+ another existing executable directory. Unrelated files are preserved.
90
+
91
+ The system launchers use Python `-B`, so commands such as `sudo sbc service enable`
92
+ do not create root-owned `__pycache__` files in your uv environment. Run the install
93
+ command as your normal user after installing the tool. Do not run `uv tool install`,
94
+ `uv tool upgrade`, or `uv run` with sudo.
95
+
96
+ Install with specific sing-box version
97
+
98
+ ```bash
99
+ uv tool install sing-box-cli --with sing-box-bin==x.y.z
100
+ ```
101
+
102
+ Upgrade
103
+ ```bash
104
+ uv tool upgrade sing-box-cli
105
+ ```
106
+
107
+ Linux launchers continue to use the same environment after a normal uv upgrade.
108
+ Run `sbc install` again if you move or change the installation environment.
109
+
110
+ After upgrading on Windows, run the following in an administrator PowerShell:
111
+
112
+ ```powershell
113
+ sbc service enable
114
+ sbc service status
115
+ ```
116
+
117
+ This refreshes the installed service paths and migrates existing NSSM installations
118
+ to the native Go service host, keeping the same `sing-box-service` name and account.
119
+ An existing running service is stopped and restarted when its executable paths change.
120
+ If that update fails, the old service target is restored and the error is reported.
121
+
122
+ The native host and core are copied into versioned directories under
123
+ `%ProgramFiles%/sing-box-cli/sing-box-service`. The running service does not depend
124
+ on uv's environment, so future uv upgrades do not overwrite its running executables.
125
+ Configuration files stay in their existing location. Windows services run at boot;
126
+ closing the terminal does not stop them. Linux continues to use the existing systemd service.
127
+
128
+ No Go installation is required when installing a published wheel. Go is needed
129
+ only to build a wheel from source; the required version is in `windows-service/go.mod`.
130
+ Architecture support follows `sing-box-bin`: Windows distributes the amd64 core
131
+ and service helper; Linux distributes amd64 and arm64 cores. No additional CPU
132
+ architectures are introduced by the service helper.
133
+
134
+ ### Logs
135
+
136
+ The service uses the user's sing-box configuration without changing its log level.
137
+ Set `log.level` to `info` or `warn` in that configuration to reduce generated logs.
138
+ `sbc logs --log-level debug` selects the logs to view; it cannot recover debug entries
139
+ that the core did not generate. For diagnostics, explicitly change the core's log level
140
+ and restart, then restore it when finished.
141
+
142
+ The native Windows host writes only warnings/errors to the Windows Application event
143
+ log (source `sing-box-service-native`). Core stdout/stderr is kept as a bounded diagnostic
144
+ tail for failures, without an additional debug log file. To persist core logs, use
145
+ sing-box's `log.output` configuration; streaming `sbc logs` continues to use the core API.
146
+ Old versioned executables are retained for rollback. After disabling the service,
147
+ its directory under Program Files may be removed manually; user configuration is separate.
148
+
149
+ ## Run
150
+
151
+ Windows in Admin powershell
152
+
153
+ ```powershell
154
+ sbc --help
155
+ ```
156
+
157
+ Linux
158
+
159
+ ```bash
160
+ sudo sbc --help
161
+ ```
162
+
163
+ ## Serve private GitHub configurations
164
+
165
+ Set `GH_TOKEN` (or `GITHUB_TOKEN`) with **Contents: read** access to your private
166
+ repository, then run as your normal user:
167
+
168
+ ```bash
169
+ sbc config serve add https://github.com/owner/private-configs --domain sub.example.com
170
+ sbc config serve list
171
+ sbc config serve start
172
+ sbc config serve stop
173
+ ```
174
+
175
+ `list` prints URLs for valid root `.json` configurations. The server reads GitHub
176
+ through its API without cloning; `start` runs in the background at `127.0.0.1:8080`
177
+ by default. Use `--host` and `--port` for another listening address.
178
+
179
+ Point Traefik at the server using [this example](docs/traefik-config-serve.yml).
180
+ Clients continue to use the existing update command:
181
+
182
+ ```bash
183
+ sbc config update https://sub.example.com/alice.json --restart
184
+ ```
185
+
186
+ See [configuration server details](docs/config-serve.md) for settings, logs and limits.
@@ -0,0 +1,137 @@
1
+ # Sing-Box CLI
2
+
3
+ 🎤 Cross-platform sing-box service manager.
4
+
5
+ ![help](assets/image.png)
6
+
7
+ ## Install
8
+
9
+ ### uv
10
+
11
+ Windows
12
+
13
+ ```powershell
14
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
15
+ ```
16
+
17
+ Linux
18
+
19
+ ```bash
20
+ curl -LsSf https://astral.sh/uv/install.sh | sh
21
+ ```
22
+
23
+ ### sing-box-cli
24
+
25
+ Install
26
+ ```powershell
27
+ # windows
28
+ uv tool install sing-box-cli
29
+ ```
30
+
31
+ ```bash
32
+ # linux
33
+ uv tool install sing-box-cli
34
+ sbc install
35
+ ```
36
+
37
+ On Linux, `sbc install` installs both `sbc` and `sing-box-cli` into
38
+ `/usr/local/bin`, requesting sudo only to write those system launchers. It replaces
39
+ the old manual symlinks and can be run again safely. Use `--bin-dir PATH` to select
40
+ another existing executable directory. Unrelated files are preserved.
41
+
42
+ The system launchers use Python `-B`, so commands such as `sudo sbc service enable`
43
+ do not create root-owned `__pycache__` files in your uv environment. Run the install
44
+ command as your normal user after installing the tool. Do not run `uv tool install`,
45
+ `uv tool upgrade`, or `uv run` with sudo.
46
+
47
+ Install with specific sing-box version
48
+
49
+ ```bash
50
+ uv tool install sing-box-cli --with sing-box-bin==x.y.z
51
+ ```
52
+
53
+ Upgrade
54
+ ```bash
55
+ uv tool upgrade sing-box-cli
56
+ ```
57
+
58
+ Linux launchers continue to use the same environment after a normal uv upgrade.
59
+ Run `sbc install` again if you move or change the installation environment.
60
+
61
+ After upgrading on Windows, run the following in an administrator PowerShell:
62
+
63
+ ```powershell
64
+ sbc service enable
65
+ sbc service status
66
+ ```
67
+
68
+ This refreshes the installed service paths and migrates existing NSSM installations
69
+ to the native Go service host, keeping the same `sing-box-service` name and account.
70
+ An existing running service is stopped and restarted when its executable paths change.
71
+ If that update fails, the old service target is restored and the error is reported.
72
+
73
+ The native host and core are copied into versioned directories under
74
+ `%ProgramFiles%/sing-box-cli/sing-box-service`. The running service does not depend
75
+ on uv's environment, so future uv upgrades do not overwrite its running executables.
76
+ Configuration files stay in their existing location. Windows services run at boot;
77
+ closing the terminal does not stop them. Linux continues to use the existing systemd service.
78
+
79
+ No Go installation is required when installing a published wheel. Go is needed
80
+ only to build a wheel from source; the required version is in `windows-service/go.mod`.
81
+ Architecture support follows `sing-box-bin`: Windows distributes the amd64 core
82
+ and service helper; Linux distributes amd64 and arm64 cores. No additional CPU
83
+ architectures are introduced by the service helper.
84
+
85
+ ### Logs
86
+
87
+ The service uses the user's sing-box configuration without changing its log level.
88
+ Set `log.level` to `info` or `warn` in that configuration to reduce generated logs.
89
+ `sbc logs --log-level debug` selects the logs to view; it cannot recover debug entries
90
+ that the core did not generate. For diagnostics, explicitly change the core's log level
91
+ and restart, then restore it when finished.
92
+
93
+ The native Windows host writes only warnings/errors to the Windows Application event
94
+ log (source `sing-box-service-native`). Core stdout/stderr is kept as a bounded diagnostic
95
+ tail for failures, without an additional debug log file. To persist core logs, use
96
+ sing-box's `log.output` configuration; streaming `sbc logs` continues to use the core API.
97
+ Old versioned executables are retained for rollback. After disabling the service,
98
+ its directory under Program Files may be removed manually; user configuration is separate.
99
+
100
+ ## Run
101
+
102
+ Windows in Admin powershell
103
+
104
+ ```powershell
105
+ sbc --help
106
+ ```
107
+
108
+ Linux
109
+
110
+ ```bash
111
+ sudo sbc --help
112
+ ```
113
+
114
+ ## Serve private GitHub configurations
115
+
116
+ Set `GH_TOKEN` (or `GITHUB_TOKEN`) with **Contents: read** access to your private
117
+ repository, then run as your normal user:
118
+
119
+ ```bash
120
+ sbc config serve add https://github.com/owner/private-configs --domain sub.example.com
121
+ sbc config serve list
122
+ sbc config serve start
123
+ sbc config serve stop
124
+ ```
125
+
126
+ `list` prints URLs for valid root `.json` configurations. The server reads GitHub
127
+ through its API without cloning; `start` runs in the background at `127.0.0.1:8080`
128
+ by default. Use `--host` and `--port` for another listening address.
129
+
130
+ Point Traefik at the server using [this example](docs/traefik-config-serve.yml).
131
+ Clients continue to use the existing update command:
132
+
133
+ ```bash
134
+ sbc config update https://sub.example.com/alice.json --restart
135
+ ```
136
+
137
+ See [configuration server details](docs/config-serve.md) for settings, logs and limits.
@@ -0,0 +1,49 @@
1
+ # Configuration server
2
+
3
+ The server maps one domain to one private GitHub repository. Multiple domains can
4
+ share a server. Set `GH_TOKEN` or `GITHUB_TOKEN` to a fine-grained token with
5
+ **Contents: read** access to all registered repositories.
6
+
7
+ `config serve add` checks GitHub access and saves the binding; registering the same
8
+ binding again is safe. `config serve list` validates root `.json` files with the
9
+ sing-box core bundled with this package, prints valid URLs, and reports invalid
10
+ configurations on stderr.
11
+
12
+ ## Background process
13
+
14
+ `config serve start` waits until the socket is listening, then prints the PID,
15
+ address and log path. Closing the terminal leaves the server running. Repeated
16
+ starts use the existing process, and repeated stops are safe. Run `stop` as the
17
+ same user with the same state directory. Restart after changing the GitHub token
18
+ or upgrading the package. Boot-time startup is managed separately by deployment.
19
+
20
+ Settings, process identity and `serve.log` are stored in the platform's
21
+ `sing-box-cli/serve` application directory (`~/.config/sing-box-cli/serve` on Linux).
22
+ Set `SBC_SERVE_DIR` to use another directory for all commands. Tokens and client
23
+ configuration are not stored there. New registrations take effect while running.
24
+
25
+ ## GitHub reads and validation
26
+
27
+ Requests read the repository's default branch through the GitHub Contents API.
28
+ No Git installation, clone/pull, background synchronization or persistent config
29
+ cache is needed. File updates and deletions take effect on subsequent requests.
30
+
31
+ Each response is validated and preserves the original content. Missing files,
32
+ invalid configurations and GitHub errors return HTTP errors. Configurations are
33
+ limited to 5 MiB and must be compatible with the bundled core. Referenced sibling
34
+ files are not downloaded. Existing client `--token` subscriptions and saved
35
+ settings remain supported; server URLs need no client token.
36
+
37
+ ## Traefik
38
+
39
+ Merge [the example router and service](traefik-config-serve.yml) into the existing
40
+ `http.routers` and `http.services` sections. Traefik must preserve the `Host` header.
41
+ The example uses `websecure` and the host gateway from `digitalocean-public`'s
42
+ `~/devspace/traefik.yml`. Bind to that gateway for this deployment:
43
+
44
+ ```bash
45
+ sbc config serve start --host 172.18.0.1 --port 8080
46
+ ```
47
+
48
+ Replace `sub.example.com` with your domain and configure DNS and the TLS
49
+ certificate through your deployment. `--domain` only registers the binding.
@@ -0,0 +1,19 @@
1
+ # Merge these entries into the existing http.routers and http.services sections.
2
+ # Set the domain to match config serve add --domain.
3
+ http:
4
+ routers:
5
+ config-serve:
6
+ rule: "Host(`sub.example.com`)"
7
+ entryPoints:
8
+ - websecure
9
+ service: config-serve
10
+ tls: {}
11
+
12
+ services:
13
+ config-serve:
14
+ loadBalancer:
15
+ passHostHeader: true
16
+ servers:
17
+ # Host gateway used by the existing digitalocean-public Traefik config.
18
+ # Bind sbc config serve start --host 172.18.0.1 --port 8080 on the host.
19
+ - url: "http://172.18.0.1:8080"
@@ -0,0 +1,47 @@
1
+ """Bundle the small Windows SCM helper; the CLI remains Python."""
2
+
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
10
+
11
+
12
+ class CustomBuildHook(BuildHookInterface):
13
+ def initialize(self, version: str, build_data: dict[str, Any]) -> None:
14
+ if self.target_name != "wheel" or version == "editable":
15
+ return
16
+ go = shutil.which("go")
17
+ if go is None:
18
+ raise RuntimeError(
19
+ "Building a wheel requires Go (see windows-service/go.mod). Installing a published wheel does not require Go."
20
+ )
21
+ root = Path(self.root)
22
+ output = root / "build" / "windows-service"
23
+ output.mkdir(parents=True, exist_ok=True)
24
+ # Match sing-box-bin's single Windows amd64 resource.
25
+ filename = "sbc-service-windows-amd64.exe"
26
+ destination = output / filename
27
+ subprocess.run(
28
+ [
29
+ go,
30
+ "build",
31
+ "-mod=readonly",
32
+ "-trimpath",
33
+ "-ldflags=-s -w",
34
+ "-o",
35
+ str(destination),
36
+ ".",
37
+ ],
38
+ cwd=root / "windows-service",
39
+ env={
40
+ **os.environ,
41
+ "GOOS": "windows",
42
+ "GOARCH": "amd64",
43
+ "CGO_ENABLED": "0",
44
+ },
45
+ check=True,
46
+ )
47
+ build_data["force_include"][str(destination)] = f"sing_box_cli/bin/{filename}"
@@ -71,6 +71,12 @@ path = "src/sing_box_cli/__init__.py"
71
71
  packages = ["src/sing_box_cli"]
72
72
  # FROM: https://hatch.pypa.io/latest/build/
73
73
 
74
+ [tool.hatch.build.hooks.custom]
75
+ path = "hatch_build.py"
76
+
77
+ [tool.hatch.build.targets.sdist]
78
+ include = ["/src", "/windows-service", "/docs", "/hatch_build.py", "/pyproject.toml", "/README.md", "/LICENSE"]
79
+
74
80
  ## Test
75
81
  [tool.mypy]
76
82
  strict = true
@@ -209,7 +215,7 @@ topo_order = false
209
215
  sort_commits = "oldest"
210
216
 
211
217
  [tool.bumpversion]
212
- current_version = "0.1.1"
218
+ current_version = "0.1.3"
213
219
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
214
220
  serialize = ["{major}.{minor}.{patch}"]
215
221
  search = "{current_version}"
@@ -2,4 +2,4 @@ from .main import main
2
2
 
3
3
  __all__ = ["main"]
4
4
 
5
- __version__ = "0.1.1"
5
+ __version__ = "0.1.3"
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
- from typing import Annotated
2
+ from collections.abc import Callable, Coroutine
3
+ from typing import Annotated, Any
3
4
 
4
5
  import typer
5
6
 
@@ -53,16 +54,33 @@ def create_client(
53
54
  return SingBoxAPIClient(base_url, token)
54
55
 
55
56
 
57
+ def _run_tui(
58
+ ctx: typer.Context,
59
+ command: Callable[[SingBoxAPIClient], Coroutine[Any, Any, None]],
60
+ base_url: StrOrNone,
61
+ token: StrOrNone,
62
+ ) -> None:
63
+ """Run an interactive command and handle user-requested exits consistently."""
64
+ try:
65
+ ctx_obj = get_context_obj(ctx)
66
+ api_client = create_client(ctx_obj.config, base_url, token)
67
+ asyncio.run(command(api_client))
68
+ except (KeyboardInterrupt, EOFError):
69
+ pass
70
+ print("Exited.")
71
+
72
+
56
73
  @api.command()
57
74
  def stats(
58
75
  ctx: typer.Context, base_url: ApiUrlOption = None, token: ApiTokenOption = None
59
76
  ) -> None:
60
77
  """Show sing-box traffic, memory statistics and connections, requires API token(Optional)"""
61
- ctx_obj = get_context_obj(ctx)
62
- api_client = create_client(ctx_obj.config, base_url, token)
63
- visualizer = ResourceVisualizer()
64
- monitor = ResourceMonitor(api_client, visualizer)
65
- asyncio.run(monitor.start())
78
+ _run_tui(
79
+ ctx,
80
+ lambda client: ResourceMonitor(client, ResourceVisualizer()).start(),
81
+ base_url,
82
+ token,
83
+ )
66
84
 
67
85
 
68
86
  @api.command()
@@ -70,10 +88,7 @@ def conns(
70
88
  ctx: typer.Context, base_url: ApiUrlOption = None, token: ApiTokenOption = None
71
89
  ) -> None:
72
90
  """Manage sing-box connections, requires API token(Optional)"""
73
- ctx_obj = get_context_obj(ctx)
74
- api_client = create_client(ctx_obj.config, base_url, token)
75
- manager = ConnectionsManager(api_client)
76
- asyncio.run(manager.run())
91
+ _run_tui(ctx, lambda client: ConnectionsManager(client).run(), base_url, token)
77
92
 
78
93
 
79
94
  @api.command()
@@ -81,10 +96,7 @@ def proxy(
81
96
  ctx: typer.Context, base_url: ApiUrlOption = None, token: ApiTokenOption = None
82
97
  ) -> None:
83
98
  """Manage sing-box policy groups, requires API token(Optional)"""
84
- ctx_obj = get_context_obj(ctx)
85
- api_client = create_client(ctx_obj.config, base_url, token)
86
- manager = PolicyGroupManager(api_client)
87
- asyncio.run(manager.run())
99
+ _run_tui(ctx, lambda client: PolicyGroupManager(client).run(), base_url, token)
88
100
 
89
101
 
90
102
  @api.command()
@@ -95,7 +107,5 @@ def logs(
95
107
  token: ApiTokenOption = None,
96
108
  ) -> None:
97
109
  """Show sing-box logs, requires API token(Optional)"""
98
- ctx_obj = get_context_obj(ctx)
99
- api_client = create_client(ctx_obj.config, base_url, token)
100
110
  print("⌛ Showing real-time logs (Press Ctrl+C to exit)")
101
- asyncio.run(get_logs(api_client, log_level))
111
+ _run_tui(ctx, lambda client: get_logs(client, log_level), base_url, token)
@@ -5,6 +5,7 @@ Provides an async client for interacting with Sing-Box HTTP API.
5
5
 
6
6
  import json
7
7
  from collections.abc import AsyncGenerator
8
+ from contextlib import aclosing
8
9
  from typing import Any, TypeVar
9
10
 
10
11
  import httpx
@@ -224,8 +225,11 @@ class SingBoxAPIClient:
224
225
  Yields:
225
226
  TrafficData object containing traffic data (up/down in B/s)
226
227
  """
227
- async for data in self._make_stream_request("/traffic", TrafficData):
228
- yield data
228
+ async with aclosing(
229
+ self._make_stream_request("/traffic", TrafficData)
230
+ ) as stream:
231
+ async for data in stream:
232
+ yield data
229
233
 
230
234
  async def memory_stream(self) -> AsyncGenerator[MemoryData, None]:
231
235
  """
@@ -234,8 +238,9 @@ class SingBoxAPIClient:
234
238
  Yields:
235
239
  MemoryData object containing memory data (inuse/total in bytes)
236
240
  """
237
- async for data in self._make_stream_request("/memory", MemoryData):
238
- yield data
241
+ async with aclosing(self._make_stream_request("/memory", MemoryData)) as stream:
242
+ async for data in stream:
243
+ yield data
239
244
 
240
245
  async def log_stream(
241
246
  self, level: str | None = None
@@ -247,8 +252,11 @@ class SingBoxAPIClient:
247
252
  LogEntry object containing log data (type/payload)
248
253
  """
249
254
  params = {"level": level} if level else None
250
- async for data in self._make_stream_request("/logs", LogEntry, params=params):
251
- yield data
255
+ async with aclosing(
256
+ self._make_stream_request("/logs", LogEntry, params=params)
257
+ ) as stream:
258
+ async for data in stream:
259
+ yield data
252
260
 
253
261
  async def get_connections(self) -> ConnectionData:
254
262
  """