#!/usr/bin/env python3 """ sync-client.py — Push config changes to your MC server's HuggingFace dataset from your local machine. The server's background watcher will detect the changes and apply them within 60 seconds. Usage: # Push a single config file python sync-client.py push server.properties # Push multiple files python sync-client.py push server.properties ops.json # Push an entire directory (e.g. plugin configs) python sync-client.py push plugins/ # Pull all server files to local directory python sync-client.py pull [output_dir] # List what's in the dataset python sync-client.py list # Watch for changes locally and auto-push python sync-client.py watch [directory] Environment variables: HF_DATASET_ID — Your HuggingFace dataset ID HF_TOKEN — Your HuggingFace write token """ import os import sys import shutil import tempfile import subprocess import time import json from pathlib import Path DATASET_ID = os.environ.get("HF_DATASET_ID", "applepie69/MCP-Server") HF_TOKEN = os.environ.get("HF_TOKEN", "") BRANCH = os.environ.get("SYNC_BRANCH", "main") REPO_URL = f"https://x-access-token:{HF_TOKEN}@huggingface.co/datasets/{DATASET_ID}" # The files that are considered "config" and safe to push CONFIG_FILES = { "server.properties", "ops.json", "whitelist.json", "banned-players.json", "banned-ips.json", "bukkit.yml", "spigot.yml", "paper-global.yml", "paper-world-defaults.yml", "commands.yml", "help.yml", } CONFIG_DIRS = {"plugins"} def run(cmd, **kwargs): """Run a command and return the result.""" return subprocess.run(cmd, shell=True, capture_output=True, text=True, **kwargs) def clone_repo(dest): """Clone the dataset repo to dest directory.""" if os.path.exists(dest): shutil.rmtree(dest) os.makedirs(dest, exist_ok=True) result = run(f'git clone --depth 1 --branch {BRANCH} "{REPO_URL}" "{dest}"') if result.returncode != 0: # Try without branch result = run(f'git clone --depth 1 "{REPO_URL}" "{dest}"') if result.returncode != 0: print(f"ERROR: Failed to clone dataset. Check HF_DATASET_ID and HF_TOKEN.") print(f" stderr: {result.stderr}") sys.exit(1) # Configure git run(f'cd "{dest}" && git config user.email "mc-sync@local" && git config user.name "MC Sync Client"') def push_to_repo(repo_dir, message="Config update from local"): """Commit and push changes in repo_dir.""" run(f'cd "{repo_dir}" && git add -A') result = run(f'cd "{repo_dir}" && git diff --cached --quiet') if result.returncode == 0: print("No changes to push.") return run(f'cd "{repo_dir}" && git commit -m "{message}"') result = run(f'cd "{repo_dir}" && git push origin HEAD:{BRANCH}') if result.returncode != 0: print(f"ERROR: Push failed: {result.stderr}") sys.exit(1) print(f"Pushed: {message}") def cmd_push(files): """Push local files to the dataset.""" if not DATASET_ID or not HF_TOKEN: print("ERROR: Set HF_DATASET_ID and HF_TOKEN environment variables.") sys.exit(1) with tempfile.TemporaryDirectory() as tmp: print(f"Cloning dataset {DATASET_ID}...") clone_repo(tmp) for f in files: src = Path(f) if not src.exists(): print(f"WARNING: {f} not found, skipping.") continue dst = Path(tmp) / f if src.is_dir(): # Copy directory contents if dst.exists(): shutil.rmtree(dst) shutil.copytree(src, dst) print(f" Staged directory: {f}/") else: dst.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(src, dst) print(f" Staged file: {f}") push_to_repo(tmp, f"Config push — {', '.join(files)}") print("Done! Server will pick up changes within 60 seconds.") def cmd_pull(output_dir=None): """Pull all files from the dataset to a local directory.""" if not DATASET_ID or not HF_TOKEN: print("ERROR: Set HF_DATASET_ID and HF_TOKEN environment variables.") sys.exit(1) out = Path(output_dir or "./mc-server-files") out.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory() as tmp: print(f"Cloning dataset {DATASET_ID}...") clone_repo(tmp) # Copy everything except .git for item in Path(tmp).iterdir(): if item.name == ".git": continue dst = out / item.name if item.is_dir(): if dst.exists(): shutil.rmtree(dst) shutil.copytree(item, dst) else: shutil.copy2(item, dst) print(f"Pulled all files to {out}/") def cmd_list(): """List files in the dataset.""" if not DATASET_ID or not HF_TOKEN: print("ERROR: Set HF_DATASET_ID and HF_TOKEN environment variables.") sys.exit(1) with tempfile.TemporaryDirectory() as tmp: clone_repo(tmp) result = run(f'cd "{tmp}" && git ls-files') if result.stdout.strip(): print("Files in dataset:") for line in result.stdout.strip().split("\n"): print(f" {line}") else: print("Dataset is empty (no tracked files).") def cmd_watch(directory=None): """Watch a local directory for changes and auto-push.""" import hashlib watch_dir = Path(directory or ".") if not watch_dir.exists(): print(f"ERROR: Directory {watch_dir} does not exist.") sys.exit(1) print(f"Watching {watch_dir}/ for changes... (Ctrl+C to stop)") file_hashes = {} def get_hashes(): hashes = {} for f in watch_dir.rglob("*"): if f.is_file() and ".git" not in f.parts: rel = f.relative_to(watch_dir) try: hashes[str(rel)] = hashlib.md5(f.read_bytes()).hexdigest() except: pass return hashes file_hashes = get_hashes() while True: time.sleep(5) new_hashes = get_hashes() if new_hashes != file_hashes: changed = [] for k, v in new_hashes.items(): if file_hashes.get(k) != v: changed.append(k) for k in list(file_hashes.keys()): if k not in new_hashes: changed.append(f"{k} (deleted)") print(f"\nChanges detected: {', '.join(changed)}") print("Pushing to dataset...") try: with tempfile.TemporaryDirectory() as tmp: clone_repo(tmp) # Copy changed files for f in watch_dir.rglob("*"): if f.is_file() and ".git" not in f.parts: rel = f.relative_to(watch_dir) dst = Path(tmp) / rel dst.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(f, dst) push_to_repo(tmp, f"Auto-push: {', '.join(changed[:3])}{'...' if len(changed) > 3 else ''}") except Exception as e: print(f"Push failed: {e}") file_hashes = new_hashes def main(): if len(sys.argv) < 2: print(__doc__) sys.exit(0) cmd = sys.argv[1] if cmd == "push": if len(sys.argv) < 3: print("Usage: sync-client.py push [file2] ...") sys.exit(1) cmd_push(sys.argv[2:]) elif cmd == "pull": output = sys.argv[2] if len(sys.argv) > 2 else None cmd_pull(output) elif cmd == "list": cmd_list() elif cmd == "watch": directory = sys.argv[2] if len(sys.argv) > 2 else None cmd_watch(directory) else: print(f"Unknown command: {cmd}") print("Commands: push, pull, list, watch") sys.exit(1) if __name__ == "__main__": main()