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 --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: Install as a user command:
```bash ```bash
uv tool install git+https://gitea.shujk.top/shujakuin/shusub2.git 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`: Configure the default public API URL for `shusub2`:
@@ -58,6 +68,8 @@ Configuration:
- `--api-url` / `SUB2API_QUOTA_TUI_API_URL` - `--api-url` / `SUB2API_QUOTA_TUI_API_URL`
- `--status-url` / `SHUSUB2_STATUS_URL` - `--status-url` / `SHUSUB2_STATUS_URL`
- `--save-config`
- `--install`
- `--refresh-seconds` / `SUB2API_QUOTA_TUI_REFRESH_SECONDS` - `--refresh-seconds` / `SUB2API_QUOTA_TUI_REFRESH_SECONDS`
- `--timeout` / `SUB2API_QUOTA_TUI_TIMEOUT` - `--timeout` / `SUB2API_QUOTA_TUI_TIMEOUT`
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "shusub2" name = "shusub2"
version = "0.1.6" version = "0.1.7"
description = "Terminal UI for Sub2API account quota and daily usage" description = "Terminal UI for Sub2API account quota and daily usage"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
+41 -1
View File
@@ -8,6 +8,7 @@ import importlib.metadata
import json import json
import os import os
import re import re
import subprocess
import sys import sys
import urllib.parse import urllib.parse
import urllib.request import urllib.request
@@ -16,7 +17,7 @@ from typing import Any
APP_NAME = "shusub2" 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_API_URL = "http://127.0.0.1:18318/api/tui/accounts"
DEFAULT_CONFIG_FILE = "~/.config/shusub2/api-url" DEFAULT_CONFIG_FILE = "~/.config/shusub2/api-url"
DEFAULT_STATUS_CONFIG_FILE = "~/.config/shusub2/status-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_FAILED_STATUSES = {"error", "failed", "failure"}
MONITOR_STOPWORDS = {"response", "responses", "monitor"} MONITOR_STOPWORDS = {"response", "responses", "monitor"}
INSTALL_COMMAND = "uv tool install --force git+https://gitea.shujk.top/shujakuin/shusub2.git" 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: 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: def inferred_status_url(api_url: str) -> str:
parsed = urllib.parse.urlparse(str(api_url or "").strip()) parsed = urllib.parse.urlparse(str(api_url or "").strip())
if not parsed.scheme or not parsed.netloc: 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 = argparse.ArgumentParser(description="Sub2API quota and daily usage TUI")
parser.add_argument("--api-url", default=default_api_url()) 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("--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-url", default=os.environ.get("SHUSUB2_VERSION_CHECK_URL", DEFAULT_VERSION_CHECK_URL))
parser.add_argument( parser.add_argument(
"--version-check-timeout", "--version-check-timeout",
@@ -645,6 +679,12 @@ def build_parser() -> argparse.ArgumentParser:
def main(argv: list[str] | None = None) -> int: def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv) args = build_parser().parse_args(argv)
status_url = str(args.status_url or "").strip() or inferred_status_url(args.api_url) 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( version_message = check_version_update(
args.version_check_url, args.version_check_url,
max(1, args.version_check_timeout), max(1, args.version_check_timeout),
+23 -4
View File
@@ -2,9 +2,11 @@ from __future__ import annotations
import contextlib import contextlib
import io import io
import os
from pathlib import Path from pathlib import Path
import importlib.util import importlib.util
import sys import sys
import tempfile
import unittest import unittest
@@ -98,10 +100,27 @@ class Sub2APIQuotaTUITests(unittest.TestCase):
def test_version_update_message_only_for_newer_versions(self) -> None: def test_version_update_message_only_for_newer_versions(self) -> None:
mod = load_module() mod = load_module()
self.assertEqual(mod.latest_version_from_text('name = "shusub2"\nversion = "0.1.7"\n'), "0.1.7") self.assertEqual(mod.latest_version_from_text('name = "shusub2"\nversion = "0.1.8"\n'), "0.1.8")
self.assertIn("0.1.6 -> 0.1.7", mod.version_update_message("0.1.7", "0.1.6")) 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.6", "0.1.6"), "") self.assertEqual(mod.version_update_message("0.1.7", "0.1.7"), "")
self.assertEqual(mod.version_update_message("0.1.5", "0.1.6"), "") 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: def test_once_output_is_name_first_and_includes_daily_quota(self) -> None:
mod = load_module() mod = load_module()
Generated
+1 -1
View File
@@ -85,7 +85,7 @@ wheels = [
[[package]] [[package]]
name = "shusub2" name = "shusub2"
version = "0.1.6" version = "0.1.7"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "textual" }, { name = "textual" },