#!/usr/bin/env python3 """Create a Tangled repo via a reused session cookie (sibling of tangled_pr.py). Tangled repo creation is an htmx POST to /repo/new (the web "New repository" form), authenticated by the bot's appview session cookie. There is no separate git-create; you create the repo here, then push to git@tangled.org:/. Cookie: engine/.tangled-session (refresh with scripts/get-tangled-cookie.py). python3 engine/tangled_repo.py --name lichen.page.backup --description "..." # then: git remote add backup git@tangled.org:notplants-bot.bsky.social/lichen.page.backup # git push --force backup 'refs/remotes//*:refs/heads/*' && git push --force backup --tags """ import argparse, os, sys, urllib.parse, urllib.request BASE = "https://tangled.org" def load_cookie(path=None): """The cookie lives in the encrypted store (tangled.cookie) — see engine/README.md (Secrets). `path` is a legacy escape hatch: a TANGLED_COOKIE=... file, used only if explicitly passed.""" if path: for line in open(path): if line.strip().startswith("TANGLED_COOKIE="): return line.strip()[len("TANGLED_COOKIE="):] sys.exit(f"{path} has no TANGLED_COOKIE= line") sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import secrets as _store c = _store.get("tangled.cookie") if not c: sys.exit("no tangled.cookie in the secret store — add it with: sops /secrets/store.yaml") return c def main(): ap = argparse.ArgumentParser(description="create a Tangled repo via a reused session cookie") ap.add_argument("--name", required=True, help="repo name, e.g. lichen.page.backup") ap.add_argument("--description", default="") ap.add_argument("--branch", default="main", help="default branch (form default: main)") ap.add_argument("--domain", default="knot1.tangled.sh", help="knot to host on (radio value)") ap.add_argument("--cookie-file", default=None, help="legacy: read the cookie from this file instead of the store") a = ap.parse_args() cookie = load_cookie(a.cookie_file) # The form only processes the create when it sees HX-Request (otherwise it re-renders the page — a 200 # that creates nothing). Success = HTTP 200 with an HX-Location header pointing at the owner/repo. form = {"name": a.name, "description": a.description, "branch": a.branch, "domain": a.domain} data = urllib.parse.urlencode(form).encode() req = urllib.request.Request(f"{BASE}/repo/new", data=data, method="POST", headers={ "Cookie": cookie, "HX-Request": "true", "HX-Current-URL": f"{BASE}/repo/new", "Content-Type": "application/x-www-form-urlencoded", }) try: resp = urllib.request.urlopen(req, timeout=30) body = resp.read().decode("utf-8", "replace") loc = resp.headers.get("HX-Location") or resp.headers.get("HX-Redirect") or "" except urllib.error.HTTPError as e: sys.exit(f"create failed: HTTP {e.code}\n{e.read().decode('utf-8','replace')[:500]}") if resp.status == 200 and loc: print(f"OK: repo created -> {loc} (push to git@tangled.org:/{a.name})") elif "already exists" in body.lower(): sys.exit(f"repo {a.name!r} already exists") else: sys.exit(f"unexpected response (status {resp.status}, no HX-Location). Body head:\n{body[:500]}") if __name__ == "__main__": main()