sing-box-cli 0.0.3__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.
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: sing-box-cli
3
+ Version: 0.0.3
4
+ Summary: cross-platform sing-box-service 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" <zhouge1831@gmail.com>
10
+ Maintainer-email: "A.J.Zeller" <zhouge1831@gmail.com>
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.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Requires-Python: >=3.10
41
+ Requires-Dist: psutil>=6.1.1
42
+ Requires-Dist: requests>=2.32.3
43
+ Requires-Dist: typer>=0.15.1
44
+ Description-Content-Type: text/markdown
45
+
46
+ # Sing-Box CLI
47
+
48
+ cross-platform sing-box service manager
49
+
50
+ ![](assets/image.png)
51
+
52
+ ## Install
53
+
54
+ ### init UV
55
+
56
+ ```bash
57
+ # root
58
+ sudo su
59
+ # install as root
60
+ curl -LsSf https://astral.sh/uv/install.sh | sh
61
+ # create link
62
+ ln -s $(which uv) /usr/local/bin/uv
63
+ ```
64
+
65
+ ### add sing-box-service
66
+
67
+ ```bash
68
+ sudo uv tool install sing-box-cli
69
+ ```
70
+
71
+ ## Run
72
+
73
+ ```bash
74
+ sudo uvx sing-box --help
75
+ ```
@@ -0,0 +1,10 @@
1
+ sing_box_service/__init__.py,sha256=eYKke1kgc4teyR5wOBo7lsh1KjHpliVV1XpEbnK9nYE,66
2
+ sing_box_service/config.py,sha256=oTEzYRHQvRC3kPvxtaaVm-QgEzJUnPcaoBWJyTQqve8,3340
3
+ sing_box_service/main.py,sha256=c0KBPnd4dYVkyolZ9rUivOCwIRMQzM1lct3wST4R9C8,3636
4
+ sing_box_service/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ sing_box_service/service.py,sha256=SXB6lz0oNMgVkRNXWeo-zqRoxAefqLbcX-ZuspR5DvQ,5236
6
+ sing_box_cli-0.0.3.dist-info/METADATA,sha256=Gfu9arQeZ9LUg5msbKsbGNJBKOqIU1l7QGxIhspUUJA,2654
7
+ sing_box_cli-0.0.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ sing_box_cli-0.0.3.dist-info/entry_points.txt,sha256=30dsIhKb2r4vXsdW9j6F4qyDZF0YL-5wV74iKYyYhv0,55
9
+ sing_box_cli-0.0.3.dist-info/licenses/LICENSE,sha256=QeGTPA1ps6IxVztxQ62u7-x-oEe1S_fvleYlm5VlBc8,1067
10
+ sing_box_cli-0.0.3.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sing-box-cli = sing_box_service:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 A.J.Zeller
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ from .main import main
2
+
3
+ __all__ = ["main"]
4
+
5
+ __version__ = "0.0.3"
@@ -0,0 +1,94 @@
1
+ import os
2
+ import platform
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ import requests
7
+ from rich import print
8
+
9
+
10
+ class Config:
11
+ def __init__(self) -> None:
12
+ self.is_windows = platform.system() == "Windows"
13
+ user = (
14
+ os.environ.get("SUDO_USER")
15
+ or os.environ.get("USER")
16
+ or os.environ.get("USERNAME")
17
+ )
18
+ if not user:
19
+ raise ValueError("❌ Unable to detect user name")
20
+
21
+ self.user = user
22
+ bin_filename = "sing-box.exe" if self.is_windows else "sing-box"
23
+ bin_path = shutil.which(bin_filename)
24
+ if not bin_path:
25
+ raise FileNotFoundError(f"❌ {bin_filename} not found in PATH")
26
+
27
+ self.bin_path = Path(bin_path)
28
+
29
+ if self.is_windows:
30
+ self.install_dir = Path(os.environ["ProgramFiles"]) / "sing-box"
31
+ else:
32
+ self.user_home = Path(os.path.expanduser(f"~{self.user}"))
33
+ self.install_dir = self.user_home / "proxy/sing-box"
34
+
35
+ self.config_file = self.install_dir / "config.json"
36
+ self.subscription_file = self.install_dir / "subscription.txt"
37
+
38
+ def init_directories(self) -> None:
39
+ self.install_dir.mkdir(parents=True, exist_ok=True)
40
+ if not self.config_file.exists():
41
+ self.config_file.write_text("{}")
42
+ print(f"📁 Created empty config file: {self.config_file}")
43
+
44
+ if not self.subscription_file.exists():
45
+ self.subscription_file.touch()
46
+ print(f"📁 Created subscription file: {self.subscription_file}")
47
+
48
+ if not self.is_windows:
49
+ shutil.chown(self.install_dir, user=self.user, group=self.user)
50
+ shutil.chown(self.config_file, user=self.user, group=self.user)
51
+ shutil.chown(self.subscription_file, user=self.user, group=self.user)
52
+
53
+ @property
54
+ def sub_url(self) -> str:
55
+ if not self.subscription_file.exists():
56
+ return ""
57
+ return self.subscription_file.read_text().strip()
58
+
59
+ def update_config(self) -> bool:
60
+ if not self.sub_url:
61
+ print("❌ No valid subscription URL found.")
62
+ return False
63
+
64
+ print(f"⌛ Updating configuration from {self.sub_url}")
65
+ try:
66
+ response = requests.get(self.sub_url)
67
+ response.raise_for_status()
68
+ self.config_file.write_text(response.text)
69
+ if not self.is_windows:
70
+ shutil.chown(self.config_file, user=self.user, group=self.user)
71
+ print("✅ Configuration updated successfully.")
72
+ return True
73
+ except Exception as e:
74
+ print(f"❌ Failed to update configuration: {e}")
75
+ return False
76
+
77
+ def add_subscription(self, url: str) -> bool:
78
+ if not url.startswith(("http://", "https://")):
79
+ print("❌ Invalid URL format.")
80
+ return False
81
+ self.subscription_file.write_text(url)
82
+ print("📁 Subscription added successfully.")
83
+ return True
84
+
85
+ def show_config(self) -> None:
86
+ print(f"📄 Configuration file: {self.config_file}")
87
+ print(self.config_file.read_text())
88
+
89
+ def show_subscription(self) -> None:
90
+ if self.sub_url:
91
+ print("🔗 Current subscription URL:")
92
+ print(self.sub_url)
93
+ else:
94
+ print("❌ No subscription URL found.")
@@ -0,0 +1,144 @@
1
+ import os
2
+ import subprocess
3
+ import sys
4
+
5
+ import typer
6
+ from rich import print
7
+
8
+ from .config import Config
9
+ from .service import LinuxServiceManager, WindowsServiceManager
10
+
11
+ app = typer.Typer(help="sing-box service manager.")
12
+ service = typer.Typer(help="Service management commands")
13
+ config = typer.Typer(help="Configuration management commands")
14
+
15
+
16
+ app.add_typer(service, name="service")
17
+ app.add_typer(config, name="config")
18
+
19
+
20
+ class SingBoxCLI:
21
+ def __init__(self) -> None:
22
+ self.config = Config()
23
+ self.service = (
24
+ WindowsServiceManager(self.config)
25
+ if self.config.is_windows
26
+ else LinuxServiceManager(self.config)
27
+ )
28
+
29
+ def ensure_root(self) -> None:
30
+ """https://gist.github.com/RDCH106/fdd419ef7dd803932b16056aab1d2300"""
31
+ try:
32
+ if os.geteuid() != 0:
33
+ print("⚠️ This script must be run as root.")
34
+ sys.exit(1)
35
+ except AttributeError:
36
+ import ctypes
37
+
38
+ if not ctypes.windll.shell32.IsUserAnAdmin(): # type: ignore
39
+ print("⚠️ This script must be run as Administrator.")
40
+ sys.exit(1)
41
+
42
+
43
+ @service.command("start")
44
+ def service_start() -> None:
45
+ """Start sing-box service"""
46
+ cli = SingBoxCLI()
47
+ cli.ensure_root()
48
+ cli.config.init_directories()
49
+ cli.service.init_service()
50
+ cli.service.start()
51
+ print("✅ Service started.")
52
+ print("🔗 Dashboard URL: https://metacubexd.pages.dev/")
53
+ print("🔌 Default API: http://127.0.0.1:9090")
54
+
55
+
56
+ @service.command("stop")
57
+ def service_stop() -> None:
58
+ """Stop sing-box service"""
59
+ cli = SingBoxCLI()
60
+ cli.ensure_root()
61
+ cli.service.stop()
62
+ print("✋ Service stopped.")
63
+
64
+
65
+ @service.command("restart")
66
+ def service_restart() -> None:
67
+ """Restart sing-box service"""
68
+ cli = SingBoxCLI()
69
+ cli.ensure_root()
70
+ cli.service.init_service()
71
+ cli.service.restart()
72
+ print("✅ Service restarted.")
73
+ print("🔗 Dashboard URL: https://metacubexd.pages.dev/")
74
+ print("🔌 Default API: http://127.0.0.1:9090")
75
+
76
+
77
+ @service.command("status")
78
+ def service_status() -> None:
79
+ """Check service status"""
80
+ cli = SingBoxCLI()
81
+ cli.ensure_root()
82
+ status = cli.service.status()
83
+ print(f"🏃 Service status: {status}")
84
+
85
+
86
+ @service.command("disable")
87
+ def service_disable() -> None:
88
+ """Disable sing-box service autostart"""
89
+ cli = SingBoxCLI()
90
+ cli.ensure_root()
91
+ cli.service.stop()
92
+ cli.service.disable()
93
+ print("✋ Autostart disabled.")
94
+
95
+
96
+ @config.command("add-sub")
97
+ def config_add_sub(url: str) -> None:
98
+ """Add subscription URL"""
99
+ cli = SingBoxCLI()
100
+ cli.ensure_root()
101
+ if cli.config.add_subscription(url):
102
+ if cli.config.update_config():
103
+ cli.service.restart()
104
+
105
+
106
+ @config.command("update")
107
+ def config_update() -> None:
108
+ """Update configuration from subscription URL"""
109
+ cli = SingBoxCLI()
110
+ cli.ensure_root()
111
+ if cli.config.update_config():
112
+ cli.service.restart()
113
+
114
+
115
+ @config.command("show-sub")
116
+ def config_show_sub() -> None:
117
+ """Show subscription URL"""
118
+ cli = SingBoxCLI()
119
+ cli.config.show_subscription()
120
+
121
+
122
+ @config.command("show")
123
+ def config_show() -> None:
124
+ """Show configuration"""
125
+ cli = SingBoxCLI()
126
+ cli.config.show_config()
127
+
128
+
129
+ @app.command()
130
+ def logs() -> None:
131
+ """Show sing-box service logs"""
132
+ cli = SingBoxCLI()
133
+ if cli.config.is_windows:
134
+ print("⚠️ Log viewing is not supported on Windows")
135
+ else:
136
+ subprocess.run(["journalctl", "-u", "sing-box", "-o", "cat", "-f"])
137
+
138
+
139
+ def main() -> None:
140
+ app()
141
+
142
+
143
+ if __name__ == "__main__":
144
+ main()
File without changes
@@ -0,0 +1,172 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+ import psutil
5
+ from rich import print
6
+
7
+ from .config import Config
8
+
9
+
10
+ class ServiceManager:
11
+ def __init__(self, config: Config):
12
+ self.config = config
13
+
14
+ def create_service(self) -> None:
15
+ raise NotImplementedError()
16
+
17
+ def check_service(self) -> bool:
18
+ raise NotImplementedError()
19
+
20
+ def init_service(self) -> None:
21
+ if not self.check_service():
22
+ print("⌛ Creating service...")
23
+ self.create_service()
24
+
25
+ def start(self) -> None:
26
+ raise NotImplementedError()
27
+
28
+ def stop(self) -> None:
29
+ raise NotImplementedError()
30
+
31
+ def restart(self) -> None:
32
+ raise NotImplementedError()
33
+
34
+ def status(self) -> str:
35
+ raise NotImplementedError()
36
+
37
+ def disable(self) -> None:
38
+ raise NotImplementedError()
39
+
40
+
41
+ class WindowsServiceManager(ServiceManager):
42
+ def __init__(self, config: Config):
43
+ super().__init__(config)
44
+ self.task_name = "sing-box"
45
+
46
+ def create_service(self) -> None:
47
+ start_script = self.config.install_dir / "start-singbox.ps1"
48
+ script_content = f"""
49
+ Add-Type -Name Window -Namespace Console -MemberDefinition '
50
+ [DllImport("Kernel32.dll")]
51
+ public static extern IntPtr GetConsoleWindow();
52
+ [DllImport("user32.dll")]
53
+ public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow);
54
+ '
55
+ $console = [Console.Window]::GetConsoleWindow()
56
+ [Console.Window]::ShowWindow($console, 0)
57
+
58
+ Set-Location "{self.config.install_dir}"
59
+ & "{self.config.bin_path}" tools synctime -w -C "{self.config.install_dir}"
60
+ & "{self.config.bin_path}" run -C "{self.config.install_dir}"
61
+ """
62
+ start_script.write_text(script_content)
63
+
64
+ subprocess.run(
65
+ [
66
+ "schtasks",
67
+ "/create",
68
+ "/tn",
69
+ self.task_name,
70
+ "/tr",
71
+ f"powershell -ExecutionPolicy Bypass -File {start_script}",
72
+ "/sc",
73
+ "onlogon",
74
+ "/ru",
75
+ self.config.user,
76
+ "/rl",
77
+ "HIGHEST",
78
+ "/f",
79
+ ]
80
+ )
81
+
82
+ def check_service(self) -> bool:
83
+ try:
84
+ subprocess.run(
85
+ ["schtasks", "/query", "/tn", self.task_name],
86
+ capture_output=True,
87
+ check=True,
88
+ )
89
+ return True
90
+ except subprocess.CalledProcessError:
91
+ return False
92
+
93
+ def start(self) -> None:
94
+ subprocess.run(["schtasks", "/run", "/tn", self.task_name])
95
+
96
+ def stop(self) -> None:
97
+ subprocess.run(["schtasks", "/end", "/tn", self.task_name])
98
+ for proc in psutil.process_iter(["name"]):
99
+ if proc.info["name"] == "sing-box.exe":
100
+ proc.kill()
101
+
102
+ def restart(self) -> None:
103
+ self.stop()
104
+ self.start()
105
+
106
+ def status(self) -> str:
107
+ try:
108
+ output = subprocess.check_output(
109
+ ["schtasks", "/query", "/tn", self.task_name]
110
+ )
111
+ return "Running" if "Running" in output.decode() else "Stopped"
112
+ except Exception:
113
+ return "Not installed"
114
+
115
+ def disable(self) -> None:
116
+ self.stop()
117
+ subprocess.run(["schtasks", "/delete", "/tn", self.task_name, "/f"])
118
+
119
+
120
+ class LinuxServiceManager(ServiceManager):
121
+ def __init__(self, config: Config):
122
+ super().__init__(config)
123
+ self.service_name = "sing-box"
124
+ self.service_file = Path("/etc/systemd/system/sing-box.service")
125
+
126
+ def create_service(self) -> None:
127
+ service_content = f"""
128
+ [Unit]
129
+ Description=sing-box service
130
+ Documentation=https://sing-box.sagernet.org
131
+ After=network.target nss-lookup.target
132
+
133
+ [Service]
134
+ Type=simple
135
+ LimitNPROC=500
136
+ LimitNOFILE=1000000
137
+ CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
138
+ AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
139
+ Restart=always
140
+ ExecStartPre={self.config.bin_path} tools synctime -w -C {self.config.install_dir}
141
+ ExecStart={self.config.bin_path} run -C {self.config.install_dir}
142
+ ExecReload=/bin/kill -HUP $MAINPID
143
+
144
+ [Install]
145
+ WantedBy=multi-user.target
146
+ """
147
+ self.service_file.write_text(service_content)
148
+ subprocess.run(["systemctl", "daemon-reload"])
149
+ subprocess.run(["systemctl", "enable", self.service_name])
150
+
151
+ def check_service(self) -> bool:
152
+ return self.service_file.exists()
153
+
154
+ def start(self) -> None:
155
+ subprocess.run(["systemctl", "start", self.service_name])
156
+
157
+ def stop(self) -> None:
158
+ subprocess.run(["systemctl", "stop", self.service_name])
159
+
160
+ def restart(self) -> None:
161
+ subprocess.run(["systemctl", "restart", self.service_name])
162
+
163
+ def status(self) -> str:
164
+ try:
165
+ subprocess.check_call(["systemctl", "is-active", self.service_name])
166
+ return "Running"
167
+ except Exception:
168
+ return "Stopped"
169
+
170
+ def disable(self) -> None:
171
+ self.stop()
172
+ subprocess.run(["systemctl", "disable", self.service_name])