Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

238 lines
7.8 KiB
Python

"""Persistent storage for quickstart wizard selections."""
from __future__ import annotations
import json
from copy import deepcopy
from datetime import UTC, datetime
from pathlib import Path
from typing import Any
from config.constants import get_store_path as _get_store_path_from_config
from config.remote_store import (
load_named_remotes as _load_named_remotes_from_config,
)
from config.remote_store import (
load_remote_ops_config as _load_remote_ops_config_from_config,
)
_VERSION = 1
_EMPTY_CONFIG = {"version": _VERSION, "wizard": {}, "targets": {}, "probes": {}}
def get_store_path() -> Path:
"""Default path to the wizard config file.
Re-exports ``config.constants.get_store_path`` so layers below
``surfaces/`` can import the path without crossing the surfaces
boundary. The function lives in ``config/`` because that's where
``OPENSRE_HOME_DIR`` is defined; this module preserves the legacy
``from surfaces.cli.wizard.store import get_store_path`` import
path that callers already use.
"""
return _get_store_path_from_config()
def _load_raw(path: Path | None = None) -> dict[str, Any]:
store_path = path or get_store_path()
if not store_path.exists():
return deepcopy(_EMPTY_CONFIG)
try:
data = json.loads(store_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return deepcopy(_EMPTY_CONFIG)
if not isinstance(data, dict):
return deepcopy(_EMPTY_CONFIG)
return data
def load_local_config(path: Path | None = None) -> dict[str, Any]:
"""Return the persisted wizard payload for the current user."""
return _load_raw(path)
def save_local_config(
*,
wizard_mode: str,
provider: str,
model: str,
api_key_env: str,
model_env: str,
probes: dict[str, dict[str, object]],
auth_method: str | None = None,
path: Path | None = None,
) -> Path:
"""Persist the local wizard configuration to disk."""
store_path = path or get_store_path()
data = _load_raw(store_path)
timestamp = datetime.now(UTC).isoformat()
data["version"] = _VERSION
data["wizard"] = {
"mode": wizard_mode,
"configured_target": "local",
"updated_at": timestamp,
}
targets = data.setdefault("targets", {})
targets["local"] = {
"provider": provider,
"model": model,
"api_key_env": api_key_env,
"model_env": model_env,
"updated_at": timestamp,
}
if auth_method:
targets["local"]["auth_method"] = auth_method
data["probes"] = probes
store_path.parent.mkdir(parents=True, exist_ok=True)
store_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
return store_path
def update_local_llm_selection(
*,
provider: str,
model: str,
api_key_env: str = "",
model_env: str = "",
auth_method: str | None = None,
path: Path | None = None,
) -> Path:
"""Merge LLM provider/model into the wizard store without resetting other fields."""
store_path = path or get_store_path()
data = _load_raw(store_path)
timestamp = datetime.now(UTC).isoformat()
targets = data.setdefault("targets", {})
local = targets.setdefault("local", {})
local["provider"] = provider
local["model"] = model
local["api_key_env"] = api_key_env
local["model_env"] = model_env
if auth_method:
local["auth_method"] = auth_method
else:
local.pop("auth_method", None)
local["updated_at"] = timestamp
store_path.parent.mkdir(parents=True, exist_ok=True)
store_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
return store_path
def load_remote_url(path: Path | None = None) -> str | None:
"""Return the persisted remote agent URL, or ``None`` if not configured."""
data = _load_raw(path)
url: str | None = data.get("remote", {}).get("url") or None
return url
def save_remote_url(url: str, path: Path | None = None) -> None:
"""Persist the remote agent URL to the store."""
store_path = path or get_store_path()
data = _load_raw(store_path)
data.setdefault("remote", {})["url"] = url
store_path.parent.mkdir(parents=True, exist_ok=True)
store_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
# Re-exported from ``config.remote_store`` so layers below ``surfaces/`` can read this
# without crossing the surfaces boundary. Existing callers + test mocks that target
# ``surfaces.cli.wizard.store.load_named_remotes`` keep working unchanged.
load_named_remotes = _load_named_remotes_from_config
def save_named_remote(
name: str,
url: str,
*,
set_active: bool = False,
source: str = "manual",
path: Path | None = None,
) -> None:
"""Save a named remote endpoint."""
store_path = path or get_store_path()
data = _load_raw(store_path)
remote_section = data.setdefault("remote", {})
remotes = remote_section.setdefault("remotes", {})
remotes[name] = {
"url": url,
"source": source,
"updated_at": datetime.now(UTC).isoformat(),
}
if set_active:
remote_section["url"] = url
remote_section["active_name"] = name
store_path.parent.mkdir(parents=True, exist_ok=True)
store_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
def delete_named_remote(name: str, path: Path | None = None) -> None:
"""Remove a named remote entry and clear the active URL if it was the active one."""
store_path = path or get_store_path()
data = _load_raw(store_path)
remote_section = data.get("remote", {})
remotes: dict[str, Any] = remote_section.get("remotes", {})
if name not in remotes:
return
removed_url = remotes.pop(name, {}).get("url")
if removed_url and remote_section.get("url") == removed_url:
remote_section.pop("url", None)
remote_section.pop("active_name", None)
store_path.parent.mkdir(parents=True, exist_ok=True)
store_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
def set_active_remote(name: str, path: Path | None = None) -> str:
"""Switch the active remote to *name*. Returns the URL."""
store_path = path or get_store_path()
data = _load_raw(store_path)
remotes: dict[str, Any] = data.get("remote", {}).get("remotes", {})
entry = remotes.get(name)
if not entry or not entry.get("url"):
raise KeyError(f"No remote named '{name}'")
url: str = str(entry["url"])
remote_section = data.setdefault("remote", {})
remote_section["url"] = url
remote_section["active_name"] = name
store_path.parent.mkdir(parents=True, exist_ok=True)
store_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
return url
def load_active_remote_name(path: Path | None = None) -> str | None:
"""Return the name of the currently active remote, or ``None``."""
data = _load_raw(path)
name: str | None = data.get("remote", {}).get("active_name") or None
return name
# Re-exported from ``config.remote_store`` — see the comment above
# ``load_named_remotes`` for why this name resolves through ``config/``.
load_remote_ops_config = _load_remote_ops_config_from_config
def save_remote_ops_config(
*,
provider: str,
project: str | None,
service: str | None,
path: Path | None = None,
) -> None:
"""Persist remote ops provider scope to the store."""
store_path = path or get_store_path()
data = _load_raw(store_path)
remote_data = data.setdefault("remote", {})
remote_data["provider"] = provider
if project:
remote_data["project"] = project
else:
remote_data.pop("project", None)
if service:
remote_data["service"] = service
else:
remote_data.pop("service", None)
store_path.parent.mkdir(parents=True, exist_ok=True)
store_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")