#!/usr/bin/env python3 """cfg — CLI client for the config service. Usage: cfg get Read a key (personal > shared) cfg set Write to personal scope cfg set --shared Write to shared scope (admin) cfg list [--scope shared|personal] List keys cfg sync Sync all to local cache cfg delete [--shared] Delete a key cfg token Save auth token Config: ~/.config/cfg/config.json Cache: ~/.config/cfg/cache.json """ import json import os import sys import urllib.request import urllib.error from pathlib import Path CONFIG_DIR = Path.home() / ".config" / "cfg" CONFIG_FILE = CONFIG_DIR / "config.json" CACHE_FILE = CONFIG_DIR / "cache.json" DEFAULT_ENDPOINT = "https://config-service.oc-fleet.workers.dev" def load_config() -> dict: if CONFIG_FILE.exists(): return json.loads(CONFIG_FILE.read_text()) return {} def save_config(cfg: dict): CONFIG_DIR.mkdir(parents=True, exist_ok=True) CONFIG_FILE.write_text(json.dumps(cfg, indent=2)) def get_token() -> str: cfg = load_config() token = cfg.get("token") or os.environ.get("CFG_TOKEN") if not token: print("No token configured. Run: cfg token ", file=sys.stderr) sys.exit(1) return token def get_endpoint() -> str: cfg = load_config() return cfg.get("endpoint", DEFAULT_ENDPOINT) def api(method: str, path: str, body: dict | None = None) -> dict: url = f"{get_endpoint()}{path}" data = json.dumps(body).encode() if body else None req = urllib.request.Request(url, data=data, method=method) req.add_header("Authorization", f"Bearer {get_token()}") req.add_header("Content-Type", "application/json") try: with urllib.request.urlopen(req) as resp: return json.loads(resp.read()) except urllib.error.HTTPError as e: result = json.loads(e.read()) print(f"Error: {result.get('error', 'unknown')}", file=sys.stderr) sys.exit(1) def cmd_get(args: list[str]): if not args: print("Usage: cfg get ", file=sys.stderr) sys.exit(1) result = api("GET", f"/config/{args[0]}") print(result["value"]) def cmd_set(args: list[str]): shared = "--shared" in args args = [a for a in args if a != "--shared"] if len(args) < 2: print("Usage: cfg set [--shared] ", file=sys.stderr) sys.exit(1) key, value = args[0], args[1] scope = "shared" if shared else "personal" api("PUT", f"/config/{key}?scope={scope}", {"value": value}) print(f"✓ Set {key} ({scope})") def cmd_delete(args: list[str]): shared = "--shared" in args args = [a for a in args if a != "--shared"] if not args: print("Usage: cfg delete [--shared]", file=sys.stderr) sys.exit(1) scope = "shared" if shared else "personal" api("DELETE", f"/config/{args[0]}?scope={scope}") print(f"✓ Deleted {args[0]} ({scope})") def cmd_list(args: list[str]): scope = None if "--scope" in args: idx = args.index("--scope") if idx + 1 < len(args): scope = args[idx + 1] path = "/config" + (f"?scope={scope}" if scope else "") result = api("GET", path) if "keys" in result: for k in sorted(result["keys"]): print(f" {k}") elif "secrets" in result: for k, v in sorted(result["secrets"].items()): print(f" {k:<40} ({v['scope']})") def cmd_sync(args: list[str]): result = api("POST", "/config/sync") # Save to cache CONFIG_DIR.mkdir(parents=True, exist_ok=True) cache = { "secrets": {k: {"value": v["value"], "updatedAt": v["updated_at"]} for k, v in result["secrets"].items()}, "lastSync": result.get("agent_id", ""), } CACHE_FILE.write_text(json.dumps(cache, indent=2)) print(f"✓ Synced {len(result['secrets'])} keys") def cmd_token(args: list[str]): if not args: print("Usage: cfg token ", file=sys.stderr) sys.exit(1) cfg = load_config() cfg["token"] = args[0] save_config(cfg) print("✓ Token saved") def main(): if len(sys.argv) < 2: print(__doc__) sys.exit(0) cmd = sys.argv[1] args = sys.argv[2:] commands = { "get": cmd_get, "set": cmd_set, "delete": cmd_delete, "list": cmd_list, "sync": cmd_sync, "token": cmd_token, } if cmd not in commands: print(f"Unknown command: {cmd}", file=sys.stderr) print(__doc__) sys.exit(1) commands[cmd](args) if __name__ == "__main__": main()