From 2af97160c40dac30e154efcac21c5538ba38d45d Mon Sep 17 00:00:00 2001 From: notplants-bot Date: Thu, 16 Jul 2026 09:36:54 +0000 Subject: [PATCH] tangled_repo.py: create a Tangled repo as the bot (POST /repo/new); sibling of tangled_pr.py Co-Authored-By: Claude Opus 4.8 --- tangled_repo.py | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100755 tangled_repo.py diff --git a/tangled_repo.py b/tangled_repo.py new file mode 100755 index 0000000..32d35ab --- /dev/null +++ b/tangled_repo.py @@ -0,0 +1,60 @@ +#!/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): + if not os.path.exists(path): + sys.exit(f"no cookie file at {path} — refresh it with scripts/get-tangled-cookie.py") + for line in open(path): + line = line.strip() + if line.startswith("TANGLED_COOKIE="): + return line[len("TANGLED_COOKIE="):] + sys.exit("cookie file present but has no TANGLED_COOKIE= line") + +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=os.path.join(os.path.dirname(os.path.abspath(__file__)), ".tangled-session")) + 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()