"""Interactive (``setup``) + non-interactive (``init``) opt-in flow. Both end up doing the same thing — write a fresh sync.json + token to ``/.cloud-sync/``. Difference is just how the URL + token get collected: setup Qt login dialog for token; URL prompted at stdin if not supplied via ``--url`` flag. Falls back to all-stdin when headless / Qt missing. init Both URL + token come from flags. No prompting. Suitable for scripted CI runs or external launcher hooks (frazclient, discord-bot ``/cloud register`` integration). This is the only path that creates sync.json; pull/push never auto-mint. That makes opt-in explicit — a player intentionally enables sync rather than being silently signed up the first time they launch an instance. """ from __future__ import annotations import sys from pathlib import Path from . import config as cfgmod from .cli import Args def run( args: Args, url: str | None, token: str | None, interactive: bool, ) -> int: """Drive the opt-in flow. Returns CLI exit code (0/1/2).""" pack = args.pack_folder cfg_path = cfgmod.config_path(pack) if cfgmod.exists(pack): existing = cfgmod.read(pack) if existing is not None: print( f"instance-sync: this instance is already sync-enabled\n" f" sync.json: {cfg_path}\n" f" url: {existing.url}\n" f" instance_id: {existing.instance_id}\n" f" created_at: {existing.created_at}\n" "Run with `disable` first if you want to re-enroll.", file=sys.stderr, ) return 2 if cfgmod.disabled_path(pack).exists(): print( f"instance-sync: a disabled sync.json is present at " f"{cfgmod.disabled_path(pack)}\n" "Use `instance-sync enable` to resume the SAME instance_id " "(snapshots continue), or `instance-sync disable` again to " "discard it before a fresh setup.", file=sys.stderr, ) return 2 if url is None: if not interactive: print( "instance-sync: --url is required for `init`", file=sys.stderr, ) return 2 url = _prompt_url() if not url: return 1 if token is None: token = _prompt_token(headless=args.headless or not interactive) if token is None: print("instance-sync: setup cancelled (no token)", file=sys.stderr) return 1 if ":" not in token: print( "instance-sync: token must be discord_id:password", file=sys.stderr, ) return 2 head, _, tail = token.partition(":") if not head.strip().isdigit() or not tail.strip(): print( "instance-sync: token malformed (discord_id must be numeric, " "password must be non-empty)", file=sys.stderr, ) return 2 cfg = cfgmod.mint(url=url.strip()) cfgmod.write(pack, cfg) args.token_file.parent.mkdir(parents=True, exist_ok=True) args.token_file.write_text(token.strip() + "\n", encoding="utf-8") args.token_file.chmod(0o600) print( f"instance-sync: enabled\n" f" sync.json: {cfg_path}\n" f" token: {args.token_file}\n" f" url: {cfg.url}\n" f" instance_id: {cfg.instance_id}" ) return 0 def _prompt_url() -> str: """stdin prompt for the network endpoint.""" try: v = input("Timemachine Network URL (e.g. https://cloud.tm.center): ").strip() return v except (EOFError, KeyboardInterrupt): return "" def _prompt_token(headless: bool) -> str | None: """Returns the discord_id:password token string, or None if user cancelled.""" if not headless: try: from .ui_qt import prompt_login_qt except ImportError: pass else: return prompt_login_qt() # Headless fallback. try: return input("Paste token (discord_id:password): ").strip() except (EOFError, KeyboardInterrupt): return None