feat: persist config from uvx bootstrap

This commit is contained in:
2026-06-09 17:20:57 +08:00
parent 01aa4a6012
commit 6e7831da7c
5 changed files with 79 additions and 8 deletions
+13 -1
View File
@@ -12,11 +12,21 @@ uvx --from git+https://gitea.shujk.top/shujakuin/shusub2.git shusub2 \
--api-url https://codex.server2.shujk.top/1232131231313123/api/tui/accounts
```
Bootstrap a new machine from `uvx`: save the API URL, install `shusub2` as a
user command, then run it later as `shusub2`:
```bash
uvx --from git+https://gitea.shujk.top/shujakuin/shusub2.git shusub2 \
--api-url https://codex.server2.shujk.top/1232131231313123/api/tui/accounts \
--install
shusub2
```
Install as a user command:
```bash
uv tool install git+https://gitea.shujk.top/shujakuin/shusub2.git
shusub2 --api-url https://codex.server2.shujk.top/1232131231313123/api/tui/accounts
shusub2 --api-url https://codex.server2.shujk.top/1232131231313123/api/tui/accounts --save-config
```
Configure the default public API URL for `shusub2`:
@@ -58,6 +68,8 @@ Configuration:
- `--api-url` / `SUB2API_QUOTA_TUI_API_URL`
- `--status-url` / `SHUSUB2_STATUS_URL`
- `--save-config`
- `--install`
- `--refresh-seconds` / `SUB2API_QUOTA_TUI_REFRESH_SECONDS`
- `--timeout` / `SUB2API_QUOTA_TUI_TIMEOUT`
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "shusub2"
version = "0.1.6"
version = "0.1.7"
description = "Terminal UI for Sub2API account quota and daily usage"
readme = "README.md"
requires-python = ">=3.11"
+41 -1
View File
@@ -8,6 +8,7 @@ import importlib.metadata
import json
import os
import re
import subprocess
import sys
import urllib.parse
import urllib.request
@@ -16,7 +17,7 @@ from typing import Any
APP_NAME = "shusub2"
FALLBACK_VERSION = "0.1.6"
FALLBACK_VERSION = "0.1.7"
DEFAULT_API_URL = "http://127.0.0.1:18318/api/tui/accounts"
DEFAULT_CONFIG_FILE = "~/.config/shusub2/api-url"
DEFAULT_STATUS_CONFIG_FILE = "~/.config/shusub2/status-url"
@@ -28,6 +29,7 @@ MONITOR_OK_STATUSES = {"operational", "ok", "success"}
MONITOR_FAILED_STATUSES = {"error", "failed", "failure"}
MONITOR_STOPWORDS = {"response", "responses", "monitor"}
INSTALL_COMMAND = "uv tool install --force git+https://gitea.shujk.top/shujakuin/shusub2.git"
INSTALL_COMMAND_ARGS = ["uv", "tool", "install", "--force", "git+https://gitea.shujk.top/shujakuin/shusub2.git"]
def env_int(name: str, default: int, *, minimum: int = 1) -> int:
@@ -68,6 +70,36 @@ def default_status_url() -> str:
)
def config_file_path() -> Path:
return Path(os.environ.get("SHUSUB2_API_URL_FILE", DEFAULT_CONFIG_FILE)).expanduser()
def write_api_url_config(api_url: str) -> Path:
value = str(api_url or "").strip()
if not value:
raise ValueError("api url is empty")
path = config_file_path()
path.parent.mkdir(parents=True, exist_ok=True)
try:
path.parent.chmod(0o700)
except OSError:
pass
path.write_text(value + "\n", encoding="utf-8")
try:
path.chmod(0o600)
except OSError:
pass
return path
def run_install_command() -> int:
try:
return subprocess.run(INSTALL_COMMAND_ARGS, check=False).returncode
except FileNotFoundError:
print("uv command not found; install uv first, then run:", INSTALL_COMMAND, file=sys.stderr)
return 127
def inferred_status_url(api_url: str) -> str:
parsed = urllib.parse.urlparse(str(api_url or "").strip())
if not parsed.scheme or not parsed.netloc:
@@ -624,6 +656,8 @@ def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Sub2API quota and daily usage TUI")
parser.add_argument("--api-url", default=default_api_url())
parser.add_argument("--status-url", default=default_status_url(), help="optional sub2api-status /api/status URL for channel monitor health")
parser.add_argument("--save-config", action="store_true", help="persist --api-url to ~/.config/shusub2/api-url before running")
parser.add_argument("--install", action="store_true", help="persist --api-url, install shusub2 as a uv tool, then exit")
parser.add_argument("--version-check-url", default=os.environ.get("SHUSUB2_VERSION_CHECK_URL", DEFAULT_VERSION_CHECK_URL))
parser.add_argument(
"--version-check-timeout",
@@ -645,6 +679,12 @@ def build_parser() -> argparse.ArgumentParser:
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
status_url = str(args.status_url or "").strip() or inferred_status_url(args.api_url)
if args.save_config or args.install:
config_path = write_api_url_config(args.api_url)
print(f"saved api url to {config_path}")
if args.install:
print("installing shusub2 with uv tool...")
return run_install_command()
version_message = check_version_update(
args.version_check_url,
max(1, args.version_check_timeout),
+23 -4
View File
@@ -2,9 +2,11 @@ from __future__ import annotations
import contextlib
import io
import os
from pathlib import Path
import importlib.util
import sys
import tempfile
import unittest
@@ -98,10 +100,27 @@ class Sub2APIQuotaTUITests(unittest.TestCase):
def test_version_update_message_only_for_newer_versions(self) -> None:
mod = load_module()
self.assertEqual(mod.latest_version_from_text('name = "shusub2"\nversion = "0.1.7"\n'), "0.1.7")
self.assertIn("0.1.6 -> 0.1.7", mod.version_update_message("0.1.7", "0.1.6"))
self.assertEqual(mod.version_update_message("0.1.6", "0.1.6"), "")
self.assertEqual(mod.version_update_message("0.1.5", "0.1.6"), "")
self.assertEqual(mod.latest_version_from_text('name = "shusub2"\nversion = "0.1.8"\n'), "0.1.8")
self.assertIn("0.1.7 -> 0.1.8", mod.version_update_message("0.1.8", "0.1.7"))
self.assertEqual(mod.version_update_message("0.1.7", "0.1.7"), "")
self.assertEqual(mod.version_update_message("0.1.6", "0.1.7"), "")
def test_write_api_url_config_uses_config_file_env(self) -> None:
mod = load_module()
with tempfile.TemporaryDirectory() as tmp:
config_file = Path(tmp) / "shusub2" / "api-url"
old_value = os.environ.get("SHUSUB2_API_URL_FILE")
os.environ["SHUSUB2_API_URL_FILE"] = str(config_file)
try:
written = mod.write_api_url_config("https://example.com/api/tui/accounts")
self.assertEqual(written, config_file)
self.assertEqual(config_file.read_text(encoding="utf-8"), "https://example.com/api/tui/accounts\n")
finally:
if old_value is None:
os.environ.pop("SHUSUB2_API_URL_FILE", None)
else:
os.environ["SHUSUB2_API_URL_FILE"] = old_value
def test_once_output_is_name_first_and_includes_daily_quota(self) -> None:
mod = load_module()
Generated
+1 -1
View File
@@ -85,7 +85,7 @@ wheels = [
[[package]]
name = "shusub2"
version = "0.1.6"
version = "0.1.7"
source = { editable = "." }
dependencies = [
{ name = "textual" },