sing-box-cli 0.0.3__tar.gz → 0.0.5__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 (26) hide show
  1. sing_box_cli-0.0.5/CHANGELOG.md +38 -0
  2. {sing_box_cli-0.0.3 → sing_box_cli-0.0.5}/PKG-INFO +21 -5
  3. sing_box_cli-0.0.5/README.md +46 -0
  4. sing_box_cli-0.0.5/assets/image.png +0 -0
  5. {sing_box_cli-0.0.3 → sing_box_cli-0.0.5}/pyproject.toml +3 -3
  6. {sing_box_cli-0.0.3 → sing_box_cli-0.0.5}/src/sing_box_service/__init__.py +1 -1
  7. {sing_box_cli-0.0.3 → sing_box_cli-0.0.5}/src/sing_box_service/config.py +3 -2
  8. {sing_box_cli-0.0.3 → sing_box_cli-0.0.5}/src/sing_box_service/main.py +6 -8
  9. sing_box_cli-0.0.5/src/sing_box_service/service.py +210 -0
  10. {sing_box_cli-0.0.3 → sing_box_cli-0.0.5}/uv.lock +117 -58
  11. sing_box_cli-0.0.3/CHANGELOG.md +0 -15
  12. sing_box_cli-0.0.3/README.md +0 -30
  13. sing_box_cli-0.0.3/assets/image.png +0 -0
  14. sing_box_cli-0.0.3/mkdocs.yml +0 -183
  15. sing_box_cli-0.0.3/src/sing_box_service/service.py +0 -172
  16. {sing_box_cli-0.0.3 → sing_box_cli-0.0.5}/.github/dependabot.yml +0 -0
  17. {sing_box_cli-0.0.3 → sing_box_cli-0.0.5}/.github/workflows/main.yml +0 -0
  18. {sing_box_cli-0.0.3 → sing_box_cli-0.0.5}/.gitignore +0 -0
  19. {sing_box_cli-0.0.3 → sing_box_cli-0.0.5}/.pre-commit-config.yaml +0 -0
  20. {sing_box_cli-0.0.3 → sing_box_cli-0.0.5}/.vscode/PythonImportHelper-v2-Completion.json +0 -0
  21. {sing_box_cli-0.0.3 → sing_box_cli-0.0.5}/.vscode/settings.json +0 -0
  22. {sing_box_cli-0.0.3 → sing_box_cli-0.0.5}/LICENSE +0 -0
  23. {sing_box_cli-0.0.3 → sing_box_cli-0.0.5}/scripts/bump.sh +0 -0
  24. {sing_box_cli-0.0.3 → sing_box_cli-0.0.5}/scripts/format.sh +0 -0
  25. {sing_box_cli-0.0.3 → sing_box_cli-0.0.5}/scripts/lint.sh +0 -0
  26. {sing_box_cli-0.0.3 → sing_box_cli-0.0.5}/src/sing_box_service/py.typed +0 -0
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## 0.0.5 - 2025-01-07
6
+
7
+ ### 🚀 Features
8
+
9
+ - Enable windows logs
10
+
11
+ ### 🐛 Bug Fixes
12
+
13
+ - Show and update config encode error
14
+ - Windows sys_platform
15
+ - Remove init_service
16
+ - Ignore mypy warn_unused_ignores
17
+ - Replace schtask with raw pwsh
18
+
19
+ ### 📚 Documentation
20
+
21
+ - Update README.md
22
+
23
+ ### ⚙️ Miscellaneous Tasks
24
+
25
+ - Type ignore
26
+ - Debug bin path
27
+
28
+ ## 0.0.3 - 2025-01-07
29
+
30
+ ### 🚀 Features
31
+
32
+ - Sing-box service,config,logs
33
+
34
+ ### 🐛 Bug Fixes
35
+
36
+ - Cli name
37
+
38
+ <!-- generated by git-cliff -->
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sing-box-cli
3
- Version: 0.0.3
3
+ Version: 0.0.5
4
4
  Summary: cross-platform sing-box-service manager.
5
5
  Project-URL: Homepage, https://github.com/aittcuszz/sing-box-service
6
6
  Project-URL: Issues, https://github.com/atticuszz/sing-box-service/issues
@@ -51,7 +51,15 @@ cross-platform sing-box service manager
51
51
 
52
52
  ## Install
53
53
 
54
- ### init UV
54
+ ### uv
55
+
56
+ windows
57
+
58
+ ```powershell
59
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
60
+ ```
61
+
62
+ linux
55
63
 
56
64
  ```bash
57
65
  # root
@@ -62,14 +70,22 @@ curl -LsSf https://astral.sh/uv/install.sh | sh
62
70
  ln -s $(which uv) /usr/local/bin/uv
63
71
  ```
64
72
 
65
- ### add sing-box-service
73
+ ### sing-box
66
74
 
67
75
  ```bash
68
- sudo uv tool install sing-box-cli
76
+ uv tool install sing-box-cli
69
77
  ```
70
78
 
71
79
  ## Run
72
80
 
81
+ Windows in Admin powershell
82
+
83
+ ```powershell
84
+ sing-box-cli --help
85
+ ```
86
+
87
+ Linux
88
+
73
89
  ```bash
74
- sudo uvx sing-box --help
90
+ sudo uv tool run sing-box-cli --help
75
91
  ```
@@ -0,0 +1,46 @@
1
+ # Sing-Box CLI
2
+
3
+ cross-platform sing-box service manager
4
+
5
+ ![](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
+ # root
21
+ sudo su
22
+ # install as root
23
+ curl -LsSf https://astral.sh/uv/install.sh | sh
24
+ # create link
25
+ ln -s $(which uv) /usr/local/bin/uv
26
+ ```
27
+
28
+ ### sing-box
29
+
30
+ ```bash
31
+ uv tool install sing-box-cli
32
+ ```
33
+
34
+ ## Run
35
+
36
+ Windows in Admin powershell
37
+
38
+ ```powershell
39
+ sing-box-cli --help
40
+ ```
41
+
42
+ Linux
43
+
44
+ ```bash
45
+ sudo uv tool run sing-box-cli --help
46
+ ```
Binary file
@@ -41,7 +41,7 @@ sing-box-cli = "sing_box_service:main" # equal to from python import main; then
41
41
 
42
42
  [tool.uv]
43
43
  environments = [
44
- "sys_platform == 'windows'",
44
+ "sys_platform == 'win32'",
45
45
  "sys_platform == 'linux'",
46
46
  ]
47
47
 
@@ -73,7 +73,7 @@ packages = ["src/sing_box_service"]
73
73
  [tool.mypy]
74
74
  strict = true
75
75
  exclude = ["venv", ".venv"]
76
-
76
+ warn_unused_ignores=false
77
77
 
78
78
  ## Linter and formatter
79
79
  [tool.ruff]
@@ -209,7 +209,7 @@ sort_commits = "oldest"
209
209
 
210
210
 
211
211
  [tool.bumpversion]
212
- current_version = "0.0.3"
212
+ current_version = "0.0.5"
213
213
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
214
214
  serialize = ["{major}.{minor}.{patch}"]
215
215
  search = "{current_version}"
@@ -2,4 +2,4 @@ from .main import main
2
2
 
3
3
  __all__ = ["main"]
4
4
 
5
- __version__ = "0.0.3"
5
+ __version__ = "0.0.5"
@@ -25,6 +25,7 @@ class Config:
25
25
  raise FileNotFoundError(f"❌ {bin_filename} not found in PATH")
26
26
 
27
27
  self.bin_path = Path(bin_path)
28
+ print(f"🔧 Using binary: {self.bin_path}")
28
29
 
29
30
  if self.is_windows:
30
31
  self.install_dir = Path(os.environ["ProgramFiles"]) / "sing-box"
@@ -65,7 +66,7 @@ class Config:
65
66
  try:
66
67
  response = requests.get(self.sub_url)
67
68
  response.raise_for_status()
68
- self.config_file.write_text(response.text)
69
+ self.config_file.write_text(response.text, encoding="utf-8")
69
70
  if not self.is_windows:
70
71
  shutil.chown(self.config_file, user=self.user, group=self.user)
71
72
  print("✅ Configuration updated successfully.")
@@ -84,7 +85,7 @@ class Config:
84
85
 
85
86
  def show_config(self) -> None:
86
87
  print(f"📄 Configuration file: {self.config_file}")
87
- print(self.config_file.read_text())
88
+ print(self.config_file.read_text(encoding="utf-8"))
88
89
 
89
90
  def show_subscription(self) -> None:
90
91
  if self.sub_url:
@@ -1,5 +1,4 @@
1
1
  import os
2
- import subprocess
3
2
  import sys
4
3
 
5
4
  import typer
@@ -29,7 +28,7 @@ class SingBoxCLI:
29
28
  def ensure_root(self) -> None:
30
29
  """https://gist.github.com/RDCH106/fdd419ef7dd803932b16056aab1d2300"""
31
30
  try:
32
- if os.geteuid() != 0:
31
+ if os.geteuid() != 0: # type: ignore
33
32
  print("⚠️ This script must be run as root.")
34
33
  sys.exit(1)
35
34
  except AttributeError:
@@ -46,7 +45,7 @@ def service_start() -> None:
46
45
  cli = SingBoxCLI()
47
46
  cli.ensure_root()
48
47
  cli.config.init_directories()
49
- cli.service.init_service()
48
+ cli.service.create_service()
50
49
  cli.service.start()
51
50
  print("✅ Service started.")
52
51
  print("🔗 Dashboard URL: https://metacubexd.pages.dev/")
@@ -67,7 +66,9 @@ def service_restart() -> None:
67
66
  """Restart sing-box service"""
68
67
  cli = SingBoxCLI()
69
68
  cli.ensure_root()
70
- cli.service.init_service()
69
+ if not cli.service.check_service():
70
+ cli.service.create_service()
71
+ print("⌛ Service created successfully.")
71
72
  cli.service.restart()
72
73
  print("✅ Service restarted.")
73
74
  print("🔗 Dashboard URL: https://metacubexd.pages.dev/")
@@ -130,10 +131,7 @@ def config_show() -> None:
130
131
  def logs() -> None:
131
132
  """Show sing-box service logs"""
132
133
  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"])
134
+ cli.service.logs()
137
135
 
138
136
 
139
137
  def main() -> None:
@@ -0,0 +1,210 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+ from .config import Config
5
+
6
+
7
+ class ServiceManager:
8
+ def __init__(self, config: Config):
9
+ self.config = config
10
+
11
+ def create_service(self) -> None:
12
+ raise NotImplementedError()
13
+
14
+ def check_service(self) -> bool:
15
+ raise NotImplementedError()
16
+
17
+ def start(self) -> None:
18
+ raise NotImplementedError()
19
+
20
+ def stop(self) -> None:
21
+ raise NotImplementedError()
22
+
23
+ def restart(self) -> None:
24
+ raise NotImplementedError()
25
+
26
+ def status(self) -> str:
27
+ raise NotImplementedError()
28
+
29
+ def logs(self) -> None:
30
+ raise NotImplementedError()
31
+
32
+ def disable(self) -> None:
33
+ raise NotImplementedError()
34
+
35
+
36
+ class WindowsServiceManager(ServiceManager):
37
+ def __init__(self, config: Config):
38
+ super().__init__(config)
39
+ self.task_name = "sing-box"
40
+ self.log_file = self.config.install_dir / "sing-box.log"
41
+
42
+ def create_service(self) -> None:
43
+ start_script = self.config.install_dir / "start-singbox.ps1"
44
+ script_content = f"""
45
+ Add-Type -Name Window -Namespace Console -MemberDefinition '
46
+ [DllImport("Kernel32.dll")]
47
+ public static extern IntPtr GetConsoleWindow();
48
+ [DllImport("user32.dll")]
49
+ public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow);
50
+ '
51
+ $console = [Console.Window]::GetConsoleWindow()
52
+ [Console.Window]::ShowWindow($console, 0)
53
+
54
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
55
+ $OutputEncoding = [System.Text.Encoding]::UTF8
56
+ chcp 65001 | Out-Null
57
+
58
+ Set-Location "{self.config.install_dir}"
59
+
60
+ if (Test-Path "{self.log_file}") {{
61
+ Clear-Content "{self.log_file}"
62
+ }}
63
+
64
+ try {{
65
+ & "{self.config.bin_path}" tools synctime -w -C "{self.config.install_dir}" *>&1 | Tee-Object -FilePath "{self.log_file}" -Append
66
+ & "{self.config.bin_path}" run -C "{self.config.install_dir}" *>&1 | Tee-Object -FilePath "{self.log_file}" -Append
67
+ }} catch {{
68
+ $_ | Out-File -FilePath "{self.log_file}" -Append
69
+ }}
70
+ """
71
+ start_script.write_text(script_content, encoding="utf-8")
72
+
73
+ ps_command = f"""
74
+ $action = New-ScheduledTaskAction `
75
+ -Execute "pwsh.exe" `
76
+ -Argument "-ExecutionPolicy Bypass -NoLogo -WindowStyle Hidden -File `"{start_script}`""
77
+
78
+ $trigger = New-ScheduledTaskTrigger -AtLogon
79
+ $principal = New-ScheduledTaskPrincipal -UserId "{self.config.user}" -RunLevel Highest
80
+ $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
81
+
82
+ Register-ScheduledTask -TaskName "{self.task_name}" `
83
+ -Action $action `
84
+ -Trigger $trigger `
85
+ -Principal $principal `
86
+ -Settings $settings `
87
+ -Force
88
+ """
89
+ subprocess.run(["pwsh", "-Command", ps_command], check=True)
90
+
91
+ def check_service(self) -> bool:
92
+ ps_command = f"Get-ScheduledTask -TaskName '{self.task_name}' -ErrorAction SilentlyContinue"
93
+ result = subprocess.run(["pwsh", "-Command", ps_command], capture_output=True)
94
+ return result.returncode == 0
95
+
96
+ def start(self) -> None:
97
+ subprocess.run(
98
+ ["pwsh", "-Command", f"Start-ScheduledTask -TaskName '{self.task_name}'"]
99
+ )
100
+
101
+ def stop(self) -> None:
102
+ subprocess.run(
103
+ ["pwsh", "-Command", f"Stop-ScheduledTask -TaskName '{self.task_name}'"]
104
+ )
105
+ ps_command = "Get-Process | Where-Object { $_.ProcessName -eq 'sing-box' } | Stop-Process -Force"
106
+ subprocess.run(["pwsh", "-Command", ps_command])
107
+
108
+ def restart(self) -> None:
109
+ self.stop()
110
+ self.start()
111
+
112
+ def status(self) -> str:
113
+ ps_command = f"""
114
+ $task = Get-ScheduledTask -TaskName '{self.task_name}' -ErrorAction SilentlyContinue
115
+ if ($task) {{
116
+ $process = Get-Process | Where-Object {{ $_.Path -eq '{self.config.bin_path}' }}
117
+ if ($process) {{
118
+ "Running (PID: $($process.Id))"
119
+ }} else {{
120
+ "Stopped"
121
+ }}
122
+ }} else {{
123
+ "Not installed"
124
+ }}
125
+ """
126
+ result = subprocess.run(
127
+ ["pwsh", "-Command", ps_command], capture_output=True, text=True
128
+ )
129
+ return result.stdout.strip()
130
+
131
+ def logs(self) -> None:
132
+ if not self.log_file.exists():
133
+ print("⚠️ Log file not found")
134
+ return
135
+ try:
136
+ print("⌛ Showing real-time logs (Press Ctrl+C to exit)")
137
+ ps_command = (
138
+ f"Get-Content -Path '{self.log_file}' -Wait -Tail 50 -Encoding UTF8"
139
+ )
140
+ subprocess.run(["pwsh", "-Command", ps_command])
141
+ except KeyboardInterrupt:
142
+ return
143
+
144
+ def disable(self) -> None:
145
+ self.stop()
146
+ subprocess.run(
147
+ [
148
+ "pwsh",
149
+ "-Command",
150
+ f"Unregister-ScheduledTask -TaskName '{self.task_name}' -Confirm:$false",
151
+ ]
152
+ )
153
+
154
+
155
+ class LinuxServiceManager(ServiceManager):
156
+ def __init__(self, config: Config):
157
+ super().__init__(config)
158
+ self.service_name = "sing-box"
159
+ self.service_file = Path("/etc/systemd/system/sing-box.service")
160
+
161
+ def create_service(self) -> None:
162
+ service_content = f"""
163
+ [Unit]
164
+ Description=sing-box service
165
+ Documentation=https://sing-box.sagernet.org
166
+ After=network.target nss-lookup.target
167
+
168
+ [Service]
169
+ Type=simple
170
+ LimitNPROC=500
171
+ LimitNOFILE=1000000
172
+ CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
173
+ AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
174
+ Restart=always
175
+ ExecStartPre={self.config.bin_path} tools synctime -w -C {self.config.install_dir}
176
+ ExecStart={self.config.bin_path} run -C {self.config.install_dir}
177
+ ExecReload=/bin/kill -HUP $MAINPID
178
+
179
+ [Install]
180
+ WantedBy=multi-user.target
181
+ """
182
+ self.service_file.write_text(service_content)
183
+ subprocess.run(["systemctl", "daemon-reload"])
184
+ subprocess.run(["systemctl", "enable", self.service_name])
185
+
186
+ def check_service(self) -> bool:
187
+ return self.service_file.exists()
188
+
189
+ def start(self) -> None:
190
+ subprocess.run(["systemctl", "start", self.service_name])
191
+
192
+ def stop(self) -> None:
193
+ subprocess.run(["systemctl", "stop", self.service_name])
194
+
195
+ def restart(self) -> None:
196
+ subprocess.run(["systemctl", "restart", self.service_name])
197
+
198
+ def status(self) -> str:
199
+ try:
200
+ subprocess.check_call(["systemctl", "is-active", self.service_name])
201
+ return "Running"
202
+ except Exception:
203
+ return "Stopped"
204
+
205
+ def logs(self) -> None:
206
+ subprocess.run(["journalctl", "-u", "sing-box", "-o", "cat", "-f"])
207
+
208
+ def disable(self) -> None:
209
+ self.stop()
210
+ subprocess.run(["systemctl", "disable", self.service_name])