devconfig-gen-singbox 2.1.2__tar.gz → 2.1.4__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.
- {devconfig_gen_singbox-2.1.2/src/devconfig_gen_singbox.egg-info → devconfig_gen_singbox-2.1.4}/PKG-INFO +2 -2
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/README.md +1 -1
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/pyproject.toml +1 -1
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/__init__.py +1 -1
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4/src/devconfig_gen_singbox.egg-info}/PKG-INFO +2 -2
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen_singbox.egg-info/SOURCES.txt +2 -0
- devconfig_gen_singbox-2.1.4/src/singbox_ops/adapters/acme/__init__.py +8 -0
- devconfig_gen_singbox-2.1.4/src/singbox_ops/adapters/acme/prune.py +56 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/export/local_files.py +6 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/runtime/__init__.py +3 -0
- devconfig_gen_singbox-2.1.4/src/singbox_ops/adapters/runtime/auto_update.py +47 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/runtime/nftables.py +4 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/runtime/warp_watchdog.py +11 -4
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/cli.py +26 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/core/command.py +11 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/core/orchestrator.py +7 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/core/plan.py +1 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_singbox_ops_acme.py +30 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_singbox_ops_cli.py +1 -1
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_singbox_ops_runtime.py +29 -3
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_singbox_ops_state_export.py +10 -0
- devconfig_gen_singbox-2.1.2/src/singbox_ops/adapters/acme/__init__.py +0 -7
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/LICENSE +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/setup.cfg +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/cli.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/engine.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/formats.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/interactive.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/models.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/providers/__init__.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/providers/custom.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/providers/env_provider.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/providers/json_provider.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/providers/singbox/__init__.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/providers/singbox/data/rules.json +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/providers/singbox/links.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/providers/singbox/models.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/providers/singbox/plugins/__init__.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/providers/singbox/plugins/anytls.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/providers/singbox/plugins/base.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/providers/singbox/plugins/hysteria2.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/providers/singbox/plugins/tuic.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/providers/singbox/provider.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/providers/singbox/route.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/providers/singbox/schema.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/py.typed +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/registry.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/validation.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/web_ui.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen_singbox.egg-info/dependency_links.txt +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen_singbox.egg-info/entry_points.txt +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen_singbox.egg-info/requires.txt +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen_singbox.egg-info/top_level.txt +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/__init__.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/__init__.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/acme/base.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/acme/cloudflare_dns01.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/base.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/dns/__init__.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/dns/base.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/dns/cloudflare.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/export/__init__.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/runtime/packages.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/runtime/systemd.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/secrets/__init__.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/secrets/base.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/secrets/python_secrets.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/secrets/singbox_subprocess.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/secrets/static.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/state/__init__.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/state/local_json.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/core/__init__.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/core/context_builder.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/core/exceptions.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/core/network.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/core/protocols.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/templates/singbox_auto_update.py.template +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/templates/warp_watchdog.sh.template +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_api_parity.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_cli_e2e.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_custom_provider.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_devconfig_core.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_env_provider.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_formats.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_interactive.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_merge.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_provider_metadata.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_singbox_ops_context_builder.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_singbox_ops_dns.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_singbox_ops_orchestrator.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_singbox_ops_plan.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_singbox_ops_secrets.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_singbox_provider.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_validation.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_web_ui.py +0 -0
- {devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_web_ui_widgets.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devconfig_gen_singbox
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.4
|
|
4
4
|
Summary: Provider-based developer configuration generation engine and CLI
|
|
5
5
|
Author-email: henryliu443 <henryliu443@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -28,7 +28,7 @@ Dynamic: license-file
|
|
|
28
28
|
> **A sing-box domain implementation built on top of DevConfig-Gen.**
|
|
29
29
|
> **构建在 `DevConfig-Gen` 之上的 sing-box 领域实现(child / fork)。**
|
|
30
30
|
|
|
31
|
-
[](CHANGELOG.md)
|
|
32
32
|
[](LICENSE)
|
|
33
33
|

|
|
34
34
|

|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> **A sing-box domain implementation built on top of DevConfig-Gen.**
|
|
4
4
|
> **构建在 `DevConfig-Gen` 之上的 sing-box 领域实现(child / fork)。**
|
|
5
5
|
|
|
6
|
-
[](CHANGELOG.md)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|

|
|
9
9
|

|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devconfig_gen_singbox
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.4
|
|
4
4
|
Summary: Provider-based developer configuration generation engine and CLI
|
|
5
5
|
Author-email: henryliu443 <henryliu443@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -28,7 +28,7 @@ Dynamic: license-file
|
|
|
28
28
|
> **A sing-box domain implementation built on top of DevConfig-Gen.**
|
|
29
29
|
> **构建在 `DevConfig-Gen` 之上的 sing-box 领域实现(child / fork)。**
|
|
30
30
|
|
|
31
|
-
[](CHANGELOG.md)
|
|
32
32
|
[](LICENSE)
|
|
33
33
|

|
|
34
34
|

|
|
@@ -40,12 +40,14 @@ src/singbox_ops/adapters/base.py
|
|
|
40
40
|
src/singbox_ops/adapters/acme/__init__.py
|
|
41
41
|
src/singbox_ops/adapters/acme/base.py
|
|
42
42
|
src/singbox_ops/adapters/acme/cloudflare_dns01.py
|
|
43
|
+
src/singbox_ops/adapters/acme/prune.py
|
|
43
44
|
src/singbox_ops/adapters/dns/__init__.py
|
|
44
45
|
src/singbox_ops/adapters/dns/base.py
|
|
45
46
|
src/singbox_ops/adapters/dns/cloudflare.py
|
|
46
47
|
src/singbox_ops/adapters/export/__init__.py
|
|
47
48
|
src/singbox_ops/adapters/export/local_files.py
|
|
48
49
|
src/singbox_ops/adapters/runtime/__init__.py
|
|
50
|
+
src/singbox_ops/adapters/runtime/auto_update.py
|
|
49
51
|
src/singbox_ops/adapters/runtime/nftables.py
|
|
50
52
|
src/singbox_ops/adapters/runtime/packages.py
|
|
51
53
|
src/singbox_ops/adapters/runtime/systemd.py
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""ACME certificate adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .cloudflare_dns01 import CloudflareDNS01ACME
|
|
6
|
+
from .prune import DEFAULT_ACME_HOME, plan_prune, prune_certs
|
|
7
|
+
|
|
8
|
+
__all__ = ["CloudflareDNS01ACME", "DEFAULT_ACME_HOME", "plan_prune", "prune_certs"]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Prune stale acme.sh certificate directories.
|
|
2
|
+
|
|
3
|
+
Only ``<acme_home>/<host>_ecc`` directories are ever considered. Anything whose
|
|
4
|
+
host is not in the keep set is removed. The operation refuses to run with an
|
|
5
|
+
empty keep set, and defaults to a dry run, so a slip can never wipe every
|
|
6
|
+
certificate (the failure mode of hand-rolled shell).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from typing import Mapping, Sequence
|
|
13
|
+
|
|
14
|
+
from ...core.command import CommandRunner, LocalCommandRunner
|
|
15
|
+
from ...core.exceptions import AdapterError
|
|
16
|
+
|
|
17
|
+
DEFAULT_ACME_HOME = "/root/.acme.sh"
|
|
18
|
+
SUFFIX = "_ecc"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def plan_prune(
|
|
22
|
+
runner: CommandRunner,
|
|
23
|
+
acme_home: str,
|
|
24
|
+
keep_hosts: Sequence[str],
|
|
25
|
+
) -> "tuple[list, list]":
|
|
26
|
+
keep_set = {str(item).strip() for item in keep_hosts if str(item).strip()}
|
|
27
|
+
if not keep_set:
|
|
28
|
+
raise AdapterError("refusing to prune: no hosts to keep")
|
|
29
|
+
try:
|
|
30
|
+
entries = runner.listdir(acme_home)
|
|
31
|
+
except OSError as exc:
|
|
32
|
+
raise AdapterError(f"cannot list {acme_home}: {exc}") from exc
|
|
33
|
+
|
|
34
|
+
keep: list = []
|
|
35
|
+
remove: list = []
|
|
36
|
+
for entry in entries:
|
|
37
|
+
if not entry.endswith(SUFFIX):
|
|
38
|
+
continue
|
|
39
|
+
domain = entry[: -len(SUFFIX)]
|
|
40
|
+
(keep if domain in keep_set else remove).append(entry)
|
|
41
|
+
return keep, remove
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def prune_certs(
|
|
45
|
+
runner: CommandRunner,
|
|
46
|
+
acme_home: str = DEFAULT_ACME_HOME,
|
|
47
|
+
keep_hosts: Sequence[str] = (),
|
|
48
|
+
dry_run: bool = True,
|
|
49
|
+
) -> Mapping[str, object]:
|
|
50
|
+
keep, remove = plan_prune(runner, acme_home, keep_hosts)
|
|
51
|
+
result = {"acme_home": acme_home, "keep": sorted(keep), "remove": sorted(remove), "dry_run": dry_run}
|
|
52
|
+
if dry_run:
|
|
53
|
+
return result
|
|
54
|
+
for entry in remove:
|
|
55
|
+
runner.run(["rm", "-rf", os.path.join(acme_home, entry)], check=False)
|
|
56
|
+
return result
|
|
@@ -56,6 +56,12 @@ class LocalFilesExport:
|
|
|
56
56
|
destination = outputs.get(key)
|
|
57
57
|
if not destination:
|
|
58
58
|
continue
|
|
59
|
+
# Single-file model: never write *through* a legacy symlink (for
|
|
60
|
+
# example config.json -> profiles/config.direct.json). Replace the
|
|
61
|
+
# link with a real file so the old profile-switching scheme cannot
|
|
62
|
+
# silently redirect our write.
|
|
63
|
+
if not dry_run and self.runner.islink(destination):
|
|
64
|
+
self.runner.unlink(destination)
|
|
59
65
|
text = serialize_artifact(artifact)
|
|
60
66
|
self.runner.write_text(destination, text, mode=0o600)
|
|
61
67
|
written[key] = destination
|
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from .auto_update import AutoUpdateRuntime, build_auto_update_script
|
|
5
6
|
from .nftables import NftablesFirewall, build_nftables_conf, detect_ssh_port
|
|
6
7
|
from .packages import DebianPackagesRuntime
|
|
7
8
|
from .systemd import SystemdRuntime
|
|
8
9
|
from .warp_watchdog import WarpWatchdogRuntime
|
|
9
10
|
|
|
10
11
|
__all__ = [
|
|
12
|
+
"AutoUpdateRuntime",
|
|
11
13
|
"DebianPackagesRuntime",
|
|
12
14
|
"NftablesFirewall",
|
|
13
15
|
"SystemdRuntime",
|
|
14
16
|
"WarpWatchdogRuntime",
|
|
17
|
+
"build_auto_update_script",
|
|
15
18
|
"build_nftables_conf",
|
|
16
19
|
"detect_ssh_port",
|
|
17
20
|
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Daily sing-box auto-update job (copied verbatim from the legacy deployment).
|
|
2
|
+
|
|
3
|
+
The script is shipped as a template and installed as-is, then scheduled through
|
|
4
|
+
``/etc/cron.d`` so no interactive crontab editing is needed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Mapping, Optional
|
|
11
|
+
|
|
12
|
+
from ...core.command import CommandRunner, LocalCommandRunner
|
|
13
|
+
|
|
14
|
+
SCRIPT_PATH = "/usr/local/bin/singbox_auto_update.py"
|
|
15
|
+
CRON_PATH = "/etc/cron.d/singbox-auto-update"
|
|
16
|
+
LOG_PATH = "/var/log/singbox-auto-update.log"
|
|
17
|
+
CRON_SCHEDULE = "17 4 * * *"
|
|
18
|
+
|
|
19
|
+
_TEMPLATE = Path(__file__).resolve().parents[2] / "templates" / "singbox_auto_update.py.template"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_auto_update_script() -> str:
|
|
23
|
+
return _TEMPLATE.read_text(encoding="utf-8")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_cron_entry(script_path: str = SCRIPT_PATH, log_path: str = LOG_PATH) -> str:
|
|
27
|
+
return f"{CRON_SCHEDULE} root /usr/bin/env python3 {script_path} >> {log_path} 2>&1\n"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AutoUpdateRuntime:
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
runner: Optional[CommandRunner] = None,
|
|
34
|
+
script_path: str = SCRIPT_PATH,
|
|
35
|
+
cron_path: str = CRON_PATH,
|
|
36
|
+
):
|
|
37
|
+
self.runner = runner or LocalCommandRunner()
|
|
38
|
+
self.script_path = script_path
|
|
39
|
+
self.cron_path = cron_path
|
|
40
|
+
|
|
41
|
+
def apply(self, plan, hosts: Mapping[str, str], *, dry_run: bool = False) -> None:
|
|
42
|
+
self.runner.write_text(self.script_path, build_auto_update_script(), mode=0o755)
|
|
43
|
+
self.runner.write_text(self.cron_path, build_cron_entry(self.script_path), mode=0o644)
|
|
44
|
+
|
|
45
|
+
def destroy(self, plan, hosts: Mapping[str, str], *, dry_run: bool = False) -> None:
|
|
46
|
+
self.runner.unlink(self.cron_path, missing_ok=True)
|
|
47
|
+
self.runner.unlink(self.script_path, missing_ok=True)
|
|
@@ -90,6 +90,10 @@ class NftablesFirewall:
|
|
|
90
90
|
ports = protocol_ports(plan.protocols)
|
|
91
91
|
ssh_port = detect_ssh_port(self.runner)
|
|
92
92
|
conf = build_nftables_conf(ports["tcp"], ports["udp"], ssh_port)
|
|
93
|
+
# Takeover: drop any previous table (legacy included) before applying
|
|
94
|
+
# ours, so chains never stack up and ``destroy`` cannot half-remove a
|
|
95
|
+
# foreign ruleset.
|
|
96
|
+
self.runner.run(["nft", "delete", "table", "inet", "singbox_guard"], check=False)
|
|
93
97
|
self.runner.mkdir("/etc/nftables.d", mode=0o755)
|
|
94
98
|
self.runner.write_text(self.conf_path, conf, mode=0o644)
|
|
95
99
|
self.runner.run(["nft", "-f", self.conf_path], check=True)
|
|
@@ -62,11 +62,18 @@ class WarpWatchdogRuntime:
|
|
|
62
62
|
self.cron_path = cron_path
|
|
63
63
|
|
|
64
64
|
def apply(self, plan, hosts: Mapping[str, str], *, dry_run: bool = False) -> None:
|
|
65
|
-
if plan.tunnel_mode
|
|
65
|
+
if plan.tunnel_mode in ("proxy", "tun"):
|
|
66
|
+
script = build_watchdog_script(plan.tunnel_mode)
|
|
67
|
+
self.runner.write_text(self.script_path, script, mode=0o755)
|
|
68
|
+
self.runner.write_text(self.cron_path, build_cron_entry(self.script_path), mode=0o644)
|
|
66
69
|
return
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
|
|
71
|
+
# ``direct`` / ``none``: the server does not use WARP, so cancel the
|
|
72
|
+
# watchdog and stop the WARP daemon instead of babysitting it forever.
|
|
73
|
+
self.runner.unlink(self.cron_path, missing_ok=True)
|
|
74
|
+
self.runner.unlink(self.script_path, missing_ok=True)
|
|
75
|
+
self.runner.run(["pkill", "-9", "-x", "warp-svc"], check=False)
|
|
76
|
+
self.runner.run(["systemctl", "disable", "warp-svc"], check=False)
|
|
70
77
|
|
|
71
78
|
def destroy(self, plan, hosts: Mapping[str, str], *, dry_run: bool = False) -> None:
|
|
72
79
|
self.runner.unlink(self.cron_path, missing_ok=True)
|
|
@@ -33,6 +33,7 @@ SUBCOMMANDS = (
|
|
|
33
33
|
("deploy", "Deploy sing-box (interactive wizard when --plan is omitted)"),
|
|
34
34
|
("redeploy", "Regenerate credentials and redeploy"),
|
|
35
35
|
("destroy", "Tear down what a previous deploy created"),
|
|
36
|
+
("certs", "Prune stale acme.sh certificate dirs (dry-run unless --apply)"),
|
|
36
37
|
)
|
|
37
38
|
|
|
38
39
|
|
|
@@ -46,6 +47,14 @@ def add_subparsers(sub) -> None:
|
|
|
46
47
|
command.add_argument("--protocols", help="Comma-separated protocols (skips that prompt)")
|
|
47
48
|
if name == "plan":
|
|
48
49
|
command.add_argument("--output", default="plan.yaml", help="Where to write the plan")
|
|
50
|
+
if name == "certs":
|
|
51
|
+
command.add_argument("--keep", required=True, help="Comma-separated hosts to keep")
|
|
52
|
+
command.add_argument("--acme-home", default="/root/.acme.sh", help="acme.sh home dir")
|
|
53
|
+
command.add_argument(
|
|
54
|
+
"--apply",
|
|
55
|
+
action="store_true",
|
|
56
|
+
help="Actually delete (default is a dry run)",
|
|
57
|
+
)
|
|
49
58
|
if name == "context":
|
|
50
59
|
command.add_argument("--format", choices=("json", "yaml"), help="Output format")
|
|
51
60
|
if name in ("deploy", "redeploy", "destroy"):
|
|
@@ -120,6 +129,7 @@ def interactive_plan(domain=None, protocols=None, reader=None, writer=None) -> d
|
|
|
120
129
|
"systemd": "systemd" if use_runtime else None,
|
|
121
130
|
"firewall": "nftables-basic" if use_runtime else None,
|
|
122
131
|
"watchdog": "warp" if use_runtime else None,
|
|
132
|
+
"auto_update": "auto-update" if use_runtime else None,
|
|
123
133
|
},
|
|
124
134
|
},
|
|
125
135
|
"outputs": {
|
|
@@ -141,6 +151,22 @@ def _detect_or_placeholder(plan) -> str:
|
|
|
141
151
|
|
|
142
152
|
def _run(args) -> int:
|
|
143
153
|
try:
|
|
154
|
+
if getattr(args, "command", None) == "certs":
|
|
155
|
+
from .adapters.acme.prune import DEFAULT_ACME_HOME, prune_certs
|
|
156
|
+
from .core.command import LocalCommandRunner, RecordingRunner
|
|
157
|
+
|
|
158
|
+
keep = [item for item in (getattr(args, "keep", "") or "").split(",") if item.strip()]
|
|
159
|
+
dry_run = not bool(getattr(args, "apply", False))
|
|
160
|
+
runner = RecordingRunner() if dry_run else LocalCommandRunner()
|
|
161
|
+
result = prune_certs(
|
|
162
|
+
runner,
|
|
163
|
+
acme_home=getattr(args, "acme_home", DEFAULT_ACME_HOME),
|
|
164
|
+
keep_hosts=keep,
|
|
165
|
+
dry_run=dry_run,
|
|
166
|
+
)
|
|
167
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
168
|
+
return 0
|
|
169
|
+
|
|
144
170
|
if getattr(args, "plan", None):
|
|
145
171
|
plan = load_plan(args.plan)
|
|
146
172
|
elif args.command == "destroy":
|
|
@@ -55,6 +55,9 @@ class CommandRunner:
|
|
|
55
55
|
def islink(self, path) -> bool:
|
|
56
56
|
raise NotImplementedError
|
|
57
57
|
|
|
58
|
+
def listdir(self, path) -> List[str]:
|
|
59
|
+
raise NotImplementedError
|
|
60
|
+
|
|
58
61
|
def mkdir(self, path, *, mode: Optional[int] = None) -> None:
|
|
59
62
|
raise NotImplementedError
|
|
60
63
|
|
|
@@ -113,6 +116,9 @@ class LocalCommandRunner(CommandRunner):
|
|
|
113
116
|
def islink(self, path) -> bool:
|
|
114
117
|
return Path(path).is_symlink()
|
|
115
118
|
|
|
119
|
+
def listdir(self, path) -> List[str]:
|
|
120
|
+
return sorted(os.listdir(str(path)))
|
|
121
|
+
|
|
116
122
|
def mkdir(self, path, *, mode: Optional[int] = None) -> None:
|
|
117
123
|
target = Path(path)
|
|
118
124
|
target.mkdir(parents=True, exist_ok=True)
|
|
@@ -149,6 +155,7 @@ class RecordingRunner(CommandRunner):
|
|
|
149
155
|
self.links: List[Tuple[str, str]] = []
|
|
150
156
|
self._existing = set(str(item) for item in (existing or ()))
|
|
151
157
|
self._files = {}
|
|
158
|
+
self._dirs = {}
|
|
152
159
|
|
|
153
160
|
def run(self, args: Sequence[str], *, check: bool = True, env=None) -> CommandResult:
|
|
154
161
|
self.commands.append(("run", tuple(str(item) for item in args), dict(env or {})))
|
|
@@ -173,9 +180,13 @@ class RecordingRunner(CommandRunner):
|
|
|
173
180
|
def islink(self, path) -> bool:
|
|
174
181
|
return False
|
|
175
182
|
|
|
183
|
+
def listdir(self, path) -> List[str]:
|
|
184
|
+
return sorted(self._dirs.get(str(path), set()))
|
|
185
|
+
|
|
176
186
|
def mkdir(self, path, *, mode: Optional[int] = None) -> None:
|
|
177
187
|
self.directories.append((str(path), mode))
|
|
178
188
|
self._existing.add(str(path))
|
|
189
|
+
self._dirs.setdefault(str(path), set())
|
|
179
190
|
|
|
180
191
|
def unlink(self, path, *, missing_ok: bool = True) -> None:
|
|
181
192
|
self.removed.append(str(path))
|
{devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/core/orchestrator.py
RENAMED
|
@@ -15,6 +15,7 @@ from devconfig_gen import formats
|
|
|
15
15
|
from ..adapters.acme.cloudflare_dns01 import CloudflareDNS01ACME
|
|
16
16
|
from ..adapters.dns.cloudflare import CloudflareDNS
|
|
17
17
|
from ..adapters.export.local_files import LocalFilesExport
|
|
18
|
+
from ..adapters.runtime.auto_update import AutoUpdateRuntime
|
|
18
19
|
from ..adapters.runtime.nftables import NftablesFirewall
|
|
19
20
|
from ..adapters.runtime.packages import DebianPackagesRuntime
|
|
20
21
|
from ..adapters.runtime.systemd import SystemdRuntime
|
|
@@ -44,6 +45,7 @@ RUNTIME_FACTORIES = {
|
|
|
44
45
|
"systemd": SystemdRuntime,
|
|
45
46
|
"nftables-basic": NftablesFirewall,
|
|
46
47
|
"warp": WarpWatchdogRuntime,
|
|
48
|
+
"auto-update": AutoUpdateRuntime,
|
|
47
49
|
}
|
|
48
50
|
STATE_FACTORIES = {"local-json": LocalJsonState}
|
|
49
51
|
|
|
@@ -59,6 +61,7 @@ class AdapterSuite:
|
|
|
59
61
|
systemd: Optional[Any] = None
|
|
60
62
|
firewall: Optional[Any] = None
|
|
61
63
|
watchdog: Optional[Any] = None
|
|
64
|
+
auto_update: Optional[Any] = None
|
|
62
65
|
|
|
63
66
|
|
|
64
67
|
@dataclass
|
|
@@ -126,6 +129,7 @@ def build_suite(plan: DeployPlan, runner: CommandRunner) -> AdapterSuite:
|
|
|
126
129
|
systemd = _runtime(plan, "systemd", runtime_kwargs)
|
|
127
130
|
firewall = _runtime(plan, "firewall", runtime_kwargs)
|
|
128
131
|
watchdog = _runtime(plan, "watchdog", runtime_kwargs)
|
|
132
|
+
auto_update = _runtime(plan, "auto_update", runtime_kwargs)
|
|
129
133
|
|
|
130
134
|
return AdapterSuite(
|
|
131
135
|
secrets=secrets,
|
|
@@ -137,6 +141,7 @@ def build_suite(plan: DeployPlan, runner: CommandRunner) -> AdapterSuite:
|
|
|
137
141
|
systemd=systemd,
|
|
138
142
|
firewall=firewall,
|
|
139
143
|
watchdog=watchdog,
|
|
144
|
+
auto_update=auto_update,
|
|
140
145
|
)
|
|
141
146
|
|
|
142
147
|
|
|
@@ -204,6 +209,7 @@ def deploy(
|
|
|
204
209
|
("systemd", suite.systemd),
|
|
205
210
|
("firewall", suite.firewall),
|
|
206
211
|
("watchdog", suite.watchdog),
|
|
212
|
+
("auto_update", suite.auto_update),
|
|
207
213
|
):
|
|
208
214
|
if adapter is None:
|
|
209
215
|
continue
|
|
@@ -257,6 +263,7 @@ def destroy(
|
|
|
257
263
|
|
|
258
264
|
for key, adapter in (
|
|
259
265
|
("watchdog", suite.watchdog),
|
|
266
|
+
("auto_update", suite.auto_update),
|
|
260
267
|
("firewall", suite.firewall),
|
|
261
268
|
("systemd", suite.systemd),
|
|
262
269
|
("packages", suite.packages),
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import unittest
|
|
2
2
|
|
|
3
3
|
from singbox_ops.adapters.acme.cloudflare_dns01 import CloudflareDNS01ACME
|
|
4
|
+
from singbox_ops.adapters.acme.prune import plan_prune, prune_certs
|
|
4
5
|
from singbox_ops.core.command import CommandResult
|
|
6
|
+
from singbox_ops.core.exceptions import AdapterError
|
|
5
7
|
from tests._ops_support import FakeRunner
|
|
6
8
|
|
|
7
9
|
HOSTS = {"tuic": "c3d4.example.com", "hysteria2": "e5f6.example.com"}
|
|
@@ -62,5 +64,33 @@ class ACMETests(unittest.TestCase):
|
|
|
62
64
|
self.assertEqual(runner.commands, [])
|
|
63
65
|
|
|
64
66
|
|
|
67
|
+
class PruneTests(unittest.TestCase):
|
|
68
|
+
def _runner(self, entries):
|
|
69
|
+
return FakeRunner(dirs={"/root/.acme.sh": entries})
|
|
70
|
+
|
|
71
|
+
def test_plan_keep_and_remove(self):
|
|
72
|
+
runner = self._runner(["a.example.com_ecc", "b.example.com_ecc", "ca", "account.conf"])
|
|
73
|
+
keep, remove = plan_prune(runner, "/root/.acme.sh", ["a.example.com"])
|
|
74
|
+
self.assertEqual(keep, ["a.example.com_ecc"])
|
|
75
|
+
self.assertEqual(remove, ["b.example.com_ecc"])
|
|
76
|
+
|
|
77
|
+
def test_refuses_empty_keep(self):
|
|
78
|
+
runner = self._runner(["a.example.com_ecc"])
|
|
79
|
+
with self.assertRaises(AdapterError):
|
|
80
|
+
plan_prune(runner, "/root/.acme.sh", [])
|
|
81
|
+
|
|
82
|
+
def test_dry_run_does_not_delete(self):
|
|
83
|
+
runner = self._runner(["a.example.com_ecc", "b.example.com_ecc"])
|
|
84
|
+
result = prune_certs(runner, "/root/.acme.sh", ["a.example.com"], dry_run=True)
|
|
85
|
+
self.assertEqual(result["remove"], ["b.example.com_ecc"])
|
|
86
|
+
self.assertEqual(runner.commands, [])
|
|
87
|
+
|
|
88
|
+
def test_apply_deletes_only_stale(self):
|
|
89
|
+
runner = self._runner(["a.example.com_ecc", "b.example.com_ecc"])
|
|
90
|
+
prune_certs(runner, "/root/.acme.sh", ["a.example.com"], dry_run=False)
|
|
91
|
+
runs = [cmd[1] for cmd in runner.commands if cmd[0] == "run"]
|
|
92
|
+
self.assertEqual(runs, [("rm", "-rf", "/root/.acme.sh/b.example.com_ecc")])
|
|
93
|
+
|
|
94
|
+
|
|
65
95
|
if __name__ == "__main__":
|
|
66
96
|
unittest.main()
|
|
@@ -14,7 +14,7 @@ class SingleCliNameTests(unittest.TestCase):
|
|
|
14
14
|
result = run_cli("--help")
|
|
15
15
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
16
16
|
self.assertIn("devconfig_gen_singbox", result.stdout)
|
|
17
|
-
for name in ("generate", "init", "ui", "context", "plan", "deploy", "redeploy", "destroy"):
|
|
17
|
+
for name in ("generate", "init", "ui", "context", "plan", "deploy", "redeploy", "destroy", "certs"):
|
|
18
18
|
self.assertIn(name, result.stdout)
|
|
19
19
|
self.assertNotIn("singbox-ops", result.stdout)
|
|
20
20
|
|
{devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_singbox_ops_runtime.py
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import unittest
|
|
2
2
|
|
|
3
|
+
from singbox_ops.adapters.runtime.auto_update import AutoUpdateRuntime
|
|
3
4
|
from singbox_ops.adapters.runtime.nftables import (
|
|
4
5
|
NftablesFirewall,
|
|
5
6
|
build_nftables_conf,
|
|
@@ -44,10 +45,12 @@ class NftablesTests(unittest.TestCase):
|
|
|
44
45
|
def test_detect_ssh_port_default(self):
|
|
45
46
|
self.assertEqual(detect_ssh_port(FakeRunner()), 22)
|
|
46
47
|
|
|
47
|
-
def
|
|
48
|
+
def test_apply_deletes_legacy_table_then_loads(self):
|
|
48
49
|
runner = FakeRunner()
|
|
49
50
|
NftablesFirewall(runner=runner).apply(plan_for(), {})
|
|
50
|
-
|
|
51
|
+
runs = [cmd[1] for cmd in runner.commands if cmd[0] == "run"]
|
|
52
|
+
self.assertIn(("nft", "delete", "table", "inet", "singbox_guard"), runs)
|
|
53
|
+
self.assertTrue(any(item[0] == "nft" and item[1] == "-f" for item in runs))
|
|
51
54
|
self.assertTrue(runner.writes)
|
|
52
55
|
|
|
53
56
|
|
|
@@ -80,10 +83,33 @@ class WatchdogTests(unittest.TestCase):
|
|
|
80
83
|
self.assertIn("/root/warp_lazy_watchdog.sh", paths)
|
|
81
84
|
self.assertIn("/etc/cron.d/singbox-warp-watchdog", paths)
|
|
82
85
|
|
|
83
|
-
def
|
|
86
|
+
def test_direct_mode_cancels_watchdog_and_kills_warp(self):
|
|
84
87
|
runner = FakeRunner()
|
|
85
88
|
WarpWatchdogRuntime(runner=runner).apply(plan_for("none"), {})
|
|
86
89
|
self.assertEqual(runner.writes, [])
|
|
90
|
+
self.assertIn("/etc/cron.d/singbox-warp-watchdog", runner.removed)
|
|
91
|
+
self.assertIn("/root/warp_lazy_watchdog.sh", runner.removed)
|
|
92
|
+
runs = [cmd[1] for cmd in runner.commands if cmd[0] == "run"]
|
|
93
|
+
self.assertIn(("pkill", "-9", "-x", "warp-svc"), runs)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class AutoUpdateTests(unittest.TestCase):
|
|
97
|
+
def test_apply_writes_script_and_cron(self):
|
|
98
|
+
runner = FakeRunner()
|
|
99
|
+
AutoUpdateRuntime(runner=runner).apply(plan_for("none"), {})
|
|
100
|
+
paths = [path for path, _, _ in runner.writes]
|
|
101
|
+
self.assertIn("/usr/local/bin/singbox_auto_update.py", paths)
|
|
102
|
+
self.assertIn("/etc/cron.d/singbox-auto-update", paths)
|
|
103
|
+
cron = next(text for path, text, _ in runner.writes if path.endswith("singbox-auto-update"))
|
|
104
|
+
self.assertIn("17 4 * * *", cron)
|
|
105
|
+
script = next(text for path, text, _ in runner.writes if path.endswith("singbox_auto_update.py"))
|
|
106
|
+
self.assertIn("SagerNet/sing-box", script)
|
|
107
|
+
|
|
108
|
+
def test_destroy_removes_both(self):
|
|
109
|
+
runner = FakeRunner()
|
|
110
|
+
AutoUpdateRuntime(runner=runner).destroy(plan_for("none"), {})
|
|
111
|
+
self.assertIn("/etc/cron.d/singbox-auto-update", runner.removed)
|
|
112
|
+
self.assertIn("/usr/local/bin/singbox_auto_update.py", runner.removed)
|
|
87
113
|
|
|
88
114
|
|
|
89
115
|
class PackagesTests(unittest.TestCase):
|
{devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_singbox_ops_state_export.py
RENAMED
|
@@ -42,6 +42,16 @@ class ExportTests(unittest.TestCase):
|
|
|
42
42
|
self.assertEqual(match_output_key("sing-box-links.txt"), "links")
|
|
43
43
|
self.assertIsNone(match_output_key("unknown.txt"))
|
|
44
44
|
|
|
45
|
+
def test_replaces_symlink_destination(self):
|
|
46
|
+
runner = FakeRunner(links=["/etc/sing-box/config.json"])
|
|
47
|
+
artifacts = [
|
|
48
|
+
GeneratedArtifact("sing-box.server.json", {"a": 1}, "application/json"),
|
|
49
|
+
]
|
|
50
|
+
LocalFilesExport(runner=runner).write(artifacts, {"server_config": "/etc/sing-box/config.json"})
|
|
51
|
+
# the legacy symlink is removed, then a real file is written
|
|
52
|
+
self.assertIn("/etc/sing-box/config.json", runner.removed)
|
|
53
|
+
self.assertTrue(any(path == "/etc/sing-box/config.json" for path, _, _ in runner.writes))
|
|
54
|
+
|
|
45
55
|
def test_writes_artifacts(self):
|
|
46
56
|
runner = FakeRunner()
|
|
47
57
|
artifacts = [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/interactive.py
RENAMED
|
File without changes
|
|
File without changes
|
{devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/providers/__init__.py
RENAMED
|
File without changes
|
{devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/devconfig_gen/providers/custom.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/__init__.py
RENAMED
|
File without changes
|
{devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/acme/base.py
RENAMED
|
File without changes
|
|
File without changes
|
{devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/base.py
RENAMED
|
File without changes
|
{devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/dns/__init__.py
RENAMED
|
File without changes
|
{devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/dns/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/adapters/secrets/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/core/__init__.py
RENAMED
|
File without changes
|
{devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/core/context_builder.py
RENAMED
|
File without changes
|
{devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/core/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
{devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/src/singbox_ops/core/protocols.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_singbox_ops_orchestrator.py
RENAMED
|
File without changes
|
|
File without changes
|
{devconfig_gen_singbox-2.1.2 → devconfig_gen_singbox-2.1.4}/tests/test_singbox_ops_secrets.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|