#!/usr/bin/env python3 """ocd-fill.py -- create ocd account stubs from oc-go account names. Reads the oc-go provider account map (a YAML file), takes the account names, and appends [[account]] stubs to the ocd config for any name not already present. cookie and default_workspace are left empty for you to fill in. Inputs source defaults to $OC_GO_YAML or ~/.pi/agent/oc-providers/accounts/oc-go.yaml (fallback: ~/.pi/agent/oc-providers/oc-go.yaml) config defaults to $OCD_CONFIG or $XDG_CONFIG_HOME/ocd/config.toml (~/.config/ocd/config.toml) Flags --source PATH override the oc-go YAML path --config PATH override the ocd config path --dry-run print the plan, do not write -h, --help print this help Report: added N / present N / to fill N added new stubs written this run present source names already in the config to fill config entries whose cookie or default_workspace is empty or still a placeholder """ from __future__ import annotations import argparse import os import sys from pathlib import Path import yaml try: import tomllib # Python 3.11+ except ModuleNotFoundError: # pragma: no cover sys.stderr.write("ocd-fill: python 3.11+ required for tomllib\n") sys.exit(1) HEADER = ( "# ocd config. Generated by ocd-fill; fill in cookie and\n" "# default_workspace for each account. ${VAR} expands from env.\n" ) def default_source() -> Path: if env := os.environ.get("OC_GO_YAML"): return Path(env) home = Path.home() primary = home / ".pi/agent/oc-providers/accounts/oc-go.yaml" if primary.is_file(): return primary return home / ".pi/agent/oc-providers/oc-go.yaml" def default_config() -> Path: if env := os.environ.get("OCD_CONFIG"): return Path(env) xdg = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config") return Path(xdg) / "ocd/config.toml" def toml_basic_string(s: str) -> str: """Encode a TOML basic string (double-quoted, minimal escapes).""" s = s.replace("\\", "\\\\").replace('"', '\\"') return f'"{s}"' def read_source_names(path: Path) -> list[str]: with path.open(encoding="utf-8") as f: data = yaml.safe_load(f) or {} accs = data.get("accounts") or {} if not isinstance(accs, dict): sys.stderr.write(f"ocd-fill: `accounts:` is not a map in {path}\n") sys.exit(1) return list(accs.keys()) def read_config(path: Path) -> tuple[list[str], int]: """Return (existing account names, count of entries still to fill).""" if not path.is_file(): return [], 0 try: with path.open("rb") as f: data = tomllib.load(f) except tomllib.TOMLDecodeError as e: sys.stderr.write(f"ocd-fill: config is not valid TOML: {e}\n") sys.exit(1) names: list[str] = [] to_fill = 0 for a in data.get("account", []): n = a.get("name") if n: names.append(str(n)) cookie = str(a.get("cookie", "") or "") ws = str(a.get("default_workspace", "") or "") needs = ( not cookie.strip() or not ws.strip() or "REPLACE_ME" in cookie or "REPLACE_ME" in ws ) if needs: to_fill += 1 return names, to_fill def append_stubs(path: Path, names: list[str]) -> None: path.parent.mkdir(parents=True, exist_ok=True) existing = path.read_text(encoding="utf-8") if path.is_file() else "" stub_text = "\n\n".join(stub_block(n) for n in names) + "\n" with path.open("a", encoding="utf-8") as f: if existing.strip(): # Keep one blank line before the first new stub. f.write(existing.rstrip("\n") + "\n\n") else: f.write(HEADER + "\n") f.write(stub_text) def stub_block(name: str) -> str: return ( f"[[account]]\n" f"name = {toml_basic_string(name)}\n" f'cookie = ""\n' f'default_workspace = ""' ) def main(argv: list[str]) -> int: p = argparse.ArgumentParser( prog="ocd-fill", description="Create ocd account stubs from oc-go account names.", add_help=False, ) p.add_argument("-h", "--help", action="help", help="print this help and exit") p.add_argument("--source", type=Path, default=default_source(), help="oc-go YAML path") p.add_argument("--config", type=Path, default=default_config(), help="ocd config path to write") p.add_argument("--dry-run", action="store_true", help="print plan, do not write") args = p.parse_args(argv) if not args.source.is_file(): sys.stderr.write(f"ocd-fill: source file not found: {args.source}\n") return 1 src_names = read_source_names(args.source) existing_names, to_fill = read_config(args.config) existing_set = set(existing_names) to_add = [n for n in src_names if n not in existing_set] present = [n for n in src_names if n in existing_set] n_added = len(to_add) n_present = len(present) print(f"ocd-fill: source={args.source}") print(f"ocd-fill: config={args.config}") if n_added == 0: print("ocd-fill: added 0") else: print(f"ocd-fill: added {n_added}: {' '.join(to_add)}") print(f"ocd-fill: present {n_present}") print(f"ocd-fill: to fill {to_fill}") if args.dry_run: print("ocd-fill: dry run, no write") return 0 if n_added == 0: return 0 append_stubs(args.config, to_add) print(f"ocd-fill: wrote {n_added} stub(s) to {args.config}") return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv[1:]))