Ports Minas's PR #172 (against pre-rename `da` CLI on main) and applies the principle to the post-rename `agnes` CLI. Two distinct failure modes on Windows consoles whose default codepage is cp1250 (cs-CZ) / cp1252 (en-US): 1. `agnes pull` and other Rich-progress codepaths UnicodeEncodeError on Braille spinner glyphs. Fix: `cli/main.py` reconfigures stdout/stderr to UTF-8 with errors='replace' at import time on `sys.platform == 'win32'` so Rich's legacy-Windows render path emits decodable bytes. Wrapped in try/except so pytest's captured streams (which aren't TextIOWrapper) don't break. 2. `agnes skills list` and `agnes skills show` UnicodeDecodeError when reading skill markdown containing em-dashes / accented chars. Default `Path.read_text()` uses locale.getpreferredencoding(False), which is the broken codepage on Windows. Fix: every call site passes encoding='utf-8' explicitly. Broader scope than #172 because: - The bootstrap rewrite renamed/removed several files Minas's PR patched (`cli/commands/analyst.py` -> rolled into init.py; `cli/commands/sync.py` -> split into pull/push). Those targets no longer exist; the equivalent code lives in init.py. - Other call sites Minas didn't touch (still bare in his branch) are patched here too — config.py / update_check.py / snapshot_meta.py / setup.py / skills.py — so the codebase has zero locale-default text I/O in cli/. Side cleanup: stale `Run `da`` reference in snapshot_meta.py:88 fixed to `agnes` while touching the file.
78 lines
2.4 KiB
Python
78 lines
2.4 KiB
Python
"""CLI configuration — token storage, server URL, sync state."""
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
|
|
def _config_dir() -> Path:
|
|
d = Path(os.environ.get("AGNES_CONFIG_DIR", os.path.expanduser("~/.config/agnes")))
|
|
d.mkdir(parents=True, exist_ok=True)
|
|
return d
|
|
|
|
|
|
def get_server_url() -> str:
|
|
config = load_config()
|
|
return os.environ.get("AGNES_SERVER", config.get("server", "http://localhost:8000"))
|
|
|
|
|
|
def get_token() -> Optional[str]:
|
|
token_file = _config_dir() / "token.json"
|
|
if token_file.exists():
|
|
data = json.loads(token_file.read_text(encoding="utf-8"))
|
|
return data.get("access_token")
|
|
return os.environ.get("AGNES_TOKEN")
|
|
|
|
|
|
def save_token(token: str, email: str, role: Optional[str] = None):
|
|
"""Persist token + email to ~/.config/agnes/token.json.
|
|
|
|
The ``role`` parameter is accepted for back-compat with older callers
|
|
but is no longer written — authorization derives from group memberships
|
|
server-side, not from a CLI-cached label. Old token.json files with a
|
|
``role`` field are still readable; the field is simply ignored.
|
|
"""
|
|
token_file = _config_dir() / "token.json"
|
|
token_file.write_text(json.dumps({
|
|
"access_token": token,
|
|
"email": email,
|
|
}, indent=2), encoding="utf-8")
|
|
|
|
|
|
def clear_token():
|
|
token_file = _config_dir() / "token.json"
|
|
if token_file.exists():
|
|
token_file.unlink()
|
|
|
|
|
|
def load_config() -> dict:
|
|
config_file = _config_dir() / "config.yaml"
|
|
if config_file.exists():
|
|
import yaml
|
|
return yaml.safe_load(config_file.read_text(encoding="utf-8")) or {}
|
|
return {}
|
|
|
|
|
|
def get_sync_state() -> dict:
|
|
state_file = _config_dir() / "sync_state.json"
|
|
if state_file.exists():
|
|
return json.loads(state_file.read_text(encoding="utf-8"))
|
|
return {}
|
|
|
|
|
|
def save_sync_state(state: dict):
|
|
state_file = _config_dir() / "sync_state.json"
|
|
state_file.write_text(json.dumps(state, indent=2), encoding="utf-8")
|
|
|
|
|
|
def save_config(data: dict):
|
|
"""Persist server URL and other config to config.yaml."""
|
|
import yaml
|
|
|
|
config_file = _config_dir() / "config.yaml"
|
|
existing = {}
|
|
if config_file.exists():
|
|
existing = yaml.safe_load(config_file.read_text(encoding="utf-8")) or {}
|
|
existing.update(data)
|
|
config_file.write_text(yaml.dump(existing, default_flow_style=False), encoding="utf-8")
|