From 76cb6a470e2bd4ae7bf2a473ca39492007d86b92 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 31 Mar 2026 11:20:15 -0700 Subject: [PATCH] tangled: add knot deployment + migrate to aesthetic.computer/core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-hosted Tangled knot at knot.aesthetic.computer on the PDS droplet. Dual-push configured — origin now pushes to both Tangled and GitHub. SCORE.md updated to reflect new home. Co-Authored-By: Claude Opus 4.6 (1M context) --- SCORE.md | 5 +- at/knot/README.md | 86 +++++++ at/knot/deployment/.droplet_ip | 1 + at/knot/deployment/configure-dns.mjs | 140 +++++++++++ at/knot/deployment/deploy.fish | 59 +++++ at/knot/deployment/deploy.sh | 356 +++++++++++++++++++++++++++ at/knot/infra/Caddyfile | 13 + at/knot/infra/knotserver.service | 23 ++ at/knot/unify-repo-history.sh | 244 ++++++++++++++++++ 9 files changed, 926 insertions(+), 1 deletion(-) create mode 100644 at/knot/README.md create mode 100644 at/knot/deployment/.droplet_ip create mode 100644 at/knot/deployment/configure-dns.mjs create mode 100755 at/knot/deployment/deploy.fish create mode 100755 at/knot/deployment/deploy.sh create mode 100644 at/knot/infra/Caddyfile create mode 100644 at/knot/infra/knotserver.service create mode 100755 at/knot/unify-repo-history.sh diff --git a/SCORE.md b/SCORE.md index 5cd1d6abcf..3b7b37d62b 100644 --- a/SCORE.md +++ b/SCORE.md @@ -35,10 +35,13 @@ Every piece is URL addressable (e.g. https://aesthetic.computer/notepat). Genera **Recipes:** See [USER-GUIDE.md](USER-GUIDE.md) for making paintings, playing melodies, and joining the community. **Links:** -- **GitHub**: https://github.com/whistlegraph/aesthetic-computer +- **Tangled** (new home): https://tangled.org/aesthetic.computer/core +- **GitHub** (deprecating): https://github.com/whistlegraph/aesthetic-computer - **No Paint (predecessor)**: https://nopaint.art ([HN 2020](https://news.ycombinator.com/item?id=23546706)) - **Notepat on HN**: https://news.ycombinator.com/item?id=41526754 +> We are migrating from GitHub to [Tangled](https://tangled.org), a decentralized code hosting platform built on AT Protocol. Our repo now lives on a self-hosted knot at `knot.aesthetic.computer` under the same ATProto identity (`did:plc:k3k3wknzkcnekbnyde4dbatz`) that powers our PDS, user handles, and federated content. GitHub will be maintained as a read-only mirror during the transition. + --- ## Back Door diff --git a/at/knot/README.md b/at/knot/README.md new file mode 100644 index 0000000000..41dbf53965 --- /dev/null +++ b/at/knot/README.md @@ -0,0 +1,86 @@ +# Tangled Knot — knot.aesthetic.computer + +Self-hosted [Tangled](https://tangled.org) knot server co-located on the PDS +droplet (`at.aesthetic.computer`). Provides decentralized git hosting under +AC's ATProto identity. + +## Prerequisites + +1. PDS droplet running at `at.aesthetic.computer` (165.227.120.137) +2. Vault file `aesthetic-computer-vault/at/knot.env` with: + ``` + KNOT_OWNER_DID=did:plc:your-did-here + ``` + Find your DID at https://tangled.org/settings +3. Tangled account with SSH key added at https://tangled.org/settings/keys + +## Deploy + +```fish +cd at/knot/deployment +fish deploy.fish +``` + +This will: +- Install Go + build deps on the droplet +- Create `git` user with SSH AuthorizedKeysCommand +- Build the knot binary from source +- Deploy systemd service + environment +- Configure Caddy reverse proxy (TLS auto via Let's Encrypt) +- Create `knot.aesthetic.computer` DNS via Cloudflare + +## After Deploy + +1. Verify: `curl https://knot.aesthetic.computer/` +2. Register knot at https://tangled.org/settings/knots — click verify +3. Create repo on Tangled, selecting `knot.aesthetic.computer` as host +4. Push: `git remote add tangled git@knot.aesthetic.computer:aesthetic-computer.git && git push tangled main` + +## Unify Repo History + +Stitch the four predecessor repos into one continuous timeline: + +```bash +# Dry run first +./unify-repo-history.sh --dry-run + +# For real +./unify-repo-history.sh +``` + +Requires `git-filter-repo` (`pip install git-filter-repo`). + +## Architecture + +``` +knot.aesthetic.computer (Caddy :443) + └─ reverse proxy → localhost:5555 (knot public API) + └─ websocket → localhost:5555/events + +SSH :22 + └─ Match User git → knot keys (AuthorizedKeysCommand) + └─ git push/pull via SSH + +/home/git/ + ├─ .knot.env # config + ├─ repositories/ # bare git repos + ├─ database/ # knotserver.db (SQLite) + └─ log/ # knot.log +``` + +Co-hosts with PDS — same droplet, separate subdomain. + +## Files + +``` +at/knot/ +├─ README.md +├─ unify-repo-history.sh # graft 4 repos → one history +├─ deployment/ +│ ├─ deploy.sh # main deployment script +│ ├─ deploy.fish # fish wrapper (loads vault env) +│ └─ configure-dns.mjs # Cloudflare A record setup +└─ infra/ + ├─ Caddyfile # reverse proxy config + └─ knotserver.service # systemd unit +``` diff --git a/at/knot/deployment/.droplet_ip b/at/knot/deployment/.droplet_ip new file mode 100644 index 0000000000..6361baabd5 --- /dev/null +++ b/at/knot/deployment/.droplet_ip @@ -0,0 +1 @@ +165.227.120.137 diff --git a/at/knot/deployment/configure-dns.mjs b/at/knot/deployment/configure-dns.mjs new file mode 100644 index 0000000000..a06f5854ab --- /dev/null +++ b/at/knot/deployment/configure-dns.mjs @@ -0,0 +1,140 @@ +#!/usr/bin/env node +// Configure Cloudflare DNS for Tangled Knot deployment +// Creates A record for knot.aesthetic.computer + +import { readFileSync } from "fs"; +import { resolve } from "path"; + +const vaultEnvPath = resolve( + process.env.HOME, + "aesthetic-computer/aesthetic-computer-vault/.devcontainer/envs/devcontainer.env", +); + +const envContent = readFileSync(vaultEnvPath, "utf-8"); +const env = {}; +envContent.split("\n").forEach((line) => { + const match = line.match(/^([^#=]+)=(.*)$/); + if (match) { + const key = match[1].trim(); + let value = match[2].trim(); + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + env[key] = value; + } +}); + +const CLOUDFLARE_EMAIL = env.CLOUDFLARE_EMAIL; +const CLOUDFLARE_API_KEY = env.CLOUDFLARE_API_KEY; +const CLOUDFLARE_BASE_URL = "https://api.cloudflare.com/client/v4"; + +if (!CLOUDFLARE_EMAIL || !CLOUDFLARE_API_KEY) { + console.error("x Missing Cloudflare credentials in vault"); + console.error(" Expected in:", vaultEnvPath); + process.exit(1); +} + +const headers = { + "X-Auth-Email": CLOUDFLARE_EMAIL, + "X-Auth-Key": CLOUDFLARE_API_KEY, + "Content-Type": "application/json", +}; + +const dropletIP = readFileSync( + resolve(import.meta.dirname, ".droplet_ip"), + "utf-8", +).trim(); + +console.log("Configuring DNS for Tangled Knot"); +console.log(` Domain: knot.aesthetic.computer`); +console.log(` IP: ${dropletIP}`); +console.log(""); + +async function fetchZone(zoneName) { + const response = await fetch(`${CLOUDFLARE_BASE_URL}/zones`, { headers }); + const zones = await response.json(); + return zones.result?.find((zone) => zone.name === zoneName); +} + +async function fetchARecord(zoneId, recordName) { + const response = await fetch( + `${CLOUDFLARE_BASE_URL}/zones/${zoneId}/dns_records?type=A&name=${recordName}`, + { headers }, + ); + return response.json(); +} + +async function updateDNSRecord(zoneId, recordId, data) { + const response = await fetch( + `${CLOUDFLARE_BASE_URL}/zones/${zoneId}/dns_records/${recordId}`, + { method: "PUT", headers, body: JSON.stringify(data) }, + ); + return response.json(); +} + +async function createDNSRecord(zoneId, data) { + const response = await fetch( + `${CLOUDFLARE_BASE_URL}/zones/${zoneId}/dns_records`, + { method: "POST", headers, body: JSON.stringify(data) }, + ); + return response.json(); +} + +async function createOrUpdateARecord(subdomain, ip) { + const rootDomain = "aesthetic.computer"; + const zoneId = (await fetchZone(rootDomain))?.id; + + if (!zoneId) { + console.error(`x Zone ID not found for ${rootDomain}`); + return false; + } + + const recordResponse = await fetchARecord(zoneId, subdomain); + const record = recordResponse.result?.[0]; + + const data = { + type: "A", + name: subdomain, + content: ip, + ttl: 600, + proxied: false, // Direct connection for git SSH + }; + + if (record) { + if (record.content === ip) { + console.log(`✓ ${subdomain} already points to ${ip}`); + return true; + } + console.log(`i Updating ${subdomain}: ${record.content} -> ${ip}`); + const result = await updateDNSRecord(zoneId, record.id, data); + if (result.success) { + console.log(`✓ Updated ${subdomain} -> ${ip}`); + return true; + } else { + console.error(`x Failed to update ${subdomain}:`, result.errors); + return false; + } + } else { + console.log(`i Creating A record: ${subdomain} -> ${ip}`); + const result = await createDNSRecord(zoneId, data); + if (result.success) { + console.log(`✓ Created ${subdomain} -> ${ip}`); + return true; + } else { + console.error(`x Failed to create ${subdomain}:`, result.errors); + return false; + } + } +} + +const success = await createOrUpdateARecord("knot.aesthetic.computer", dropletIP); +if (success) { + console.log(""); + console.log("DNS configured: knot.aesthetic.computer -> " + dropletIP); +} else { + console.error("DNS configuration failed"); +} +process.exit(success ? 0 : 1); diff --git a/at/knot/deployment/deploy.fish b/at/knot/deployment/deploy.fish new file mode 100755 index 0000000000..e48001a2a0 --- /dev/null +++ b/at/knot/deployment/deploy.fish @@ -0,0 +1,59 @@ +#!/usr/bin/env fish +# Deploy Aesthetic Computer Tangled Knot +# Wraps deploy.sh with vault environment loading + +set SCRIPT_DIR (dirname (status --current-filename)) +set VAULT_DIR (cd "$SCRIPT_DIR/../../../aesthetic-computer-vault" 2>/dev/null && pwd; or echo "") + +echo "╔════════════════════════════════════════════════════════╗" +echo "║ Aesthetic Computer Knot Deployment (Tangled) ║" +echo "╚════════════════════════════════════════════════════════╝" +echo "" + +# Load knot-specific vault env +if test -f "$VAULT_DIR/at/knot.env" + echo "✓ Loading knot config from vault..." + for line in (cat "$VAULT_DIR/at/knot.env" | grep -v '^#' | grep -v '^$') + set -l parts (string split '=' -- $line) + if test (count $parts) -ge 2 + set -gx $parts[1] (string join '=' -- $parts[2..-1]) + end + end +else + echo "! Vault knot config not found: $VAULT_DIR/at/knot.env" + echo "" + echo " Create it with:" + echo " KNOT_OWNER_DID=did:plc:your-did-here" + echo "" + echo " Find your DID at https://tangled.org/settings" + echo "" + + # Default to @aesthetic.computer DID (jeffrey) + if test -z "$KNOT_OWNER_DID" + set -gx KNOT_OWNER_DID "did:plc:k3k3wknzkcnekbnyde4dbatz" + echo " Using default DID: $KNOT_OWNER_DID" + end +end + +# Load shared deploy env (for Cloudflare creds, DO token, etc.) +if test -f "$VAULT_DIR/at/deploy.env" + echo "✓ Loading shared deploy config..." + for line in (cat "$VAULT_DIR/at/deploy.env" | grep -v '^#' | grep -v '^$') + set -l parts (string split '=' -- $line) + if test (count $parts) -ge 2 + set -gx $parts[1] (string join '=' -- $parts[2..-1]) + end + end +end + +# Export for bash script +export KNOT_OWNER_DID +export DROPLET_IP + +echo "" +echo "→ Starting knot deployment..." +echo "" + +set -x AUTO_CONFIRM yes + +bash "$SCRIPT_DIR/deploy.sh" $argv diff --git a/at/knot/deployment/deploy.sh b/at/knot/deployment/deploy.sh new file mode 100755 index 0000000000..8892d72f7d --- /dev/null +++ b/at/knot/deployment/deploy.sh @@ -0,0 +1,356 @@ +#!/usr/bin/env bash +# Aesthetic Computer Knot Deployment Script +# Installs a Tangled knot server on the existing PDS droplet (at.aesthetic.computer) +# Target: knot.aesthetic.computer + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +VAULT_DIR="$PROJECT_ROOT/../aesthetic-computer-vault" +INFRA_DIR="$SCRIPT_DIR/../infra" + +# Knot configuration +KNOT_DOMAIN="knot.aesthetic.computer" +KNOT_PUBLIC_PORT=5555 +KNOT_INTERNAL_PORT=5444 +APPVIEW_ENDPOINT="https://tangled.org" + +# PDS droplet — knot co-hosts here +PDS_DEPLOY_DIR="$SCRIPT_DIR/../../pds/deployment/digitalocean" +DROPLET_IP_FILE="$PDS_DEPLOY_DIR/.droplet_ip" + +info() { echo -e "${BLUE}i${NC} $1"; } +success() { echo -e "${GREEN}✓${NC} $1"; } +warning() { echo -e "${YELLOW}!${NC} $1"; } +error() { echo -e "${RED}x${NC} $1"; exit 1; } + +get_droplet_ip() { + if [ -f "$DROPLET_IP_FILE" ]; then + DROPLET_IP=$(cat "$DROPLET_IP_FILE") + elif [ -n "${DROPLET_IP:-}" ]; then + : # already set via env + else + error "Droplet IP not found. Set DROPLET_IP or ensure $DROPLET_IP_FILE exists." + fi + info "Target: $DROPLET_IP ($KNOT_DOMAIN)" +} + +get_ssh_key() { + # Try PDS key first, then default + if [ -f ~/.ssh/aesthetic_pds ]; then + SSH_KEY=~/.ssh/aesthetic_pds + elif [ -f ~/.ssh/id_ed25519 ]; then + SSH_KEY=~/.ssh/id_ed25519 + else + error "No SSH key found. Expected ~/.ssh/aesthetic_pds or ~/.ssh/id_ed25519" + fi + info "SSH key: $SSH_KEY" +} + +get_owner_did() { + if [ -n "${KNOT_OWNER_DID:-}" ]; then + info "Owner DID: $KNOT_OWNER_DID" + return + fi + + # Try to load from vault + if [ -f "$VAULT_DIR/at/knot.env" ]; then + source "$VAULT_DIR/at/knot.env" + if [ -n "${KNOT_OWNER_DID:-}" ]; then + info "Owner DID (from vault): $KNOT_OWNER_DID" + return + fi + fi + + # Default: @aesthetic.computer DID (jeffrey) + KNOT_OWNER_DID="did:plc:k3k3wknzkcnekbnyde4dbatz" + info "Owner DID (aesthetic.computer): $KNOT_OWNER_DID" +} + +wait_for_ssh() { + info "Waiting for SSH..." + for i in {1..20}; do + if ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -i "$SSH_KEY" "root@$DROPLET_IP" "echo ok" &>/dev/null; then + success "SSH ready" + return + fi + sleep 3 + done + error "SSH timeout" +} + +remote() { + ssh -i "$SSH_KEY" "root@$DROPLET_IP" "$@" +} + +remote_script() { + ssh -i "$SSH_KEY" "root@$DROPLET_IP" 'bash -s' < "$1" +} + +install_go() { + info "Checking Go on remote..." + if remote "go version" &>/dev/null; then + success "Go already installed" + return + fi + + info "Installing Go..." + remote << 'ENDSSH' +set -euo pipefail +cd /tmp +curl -sLO https://go.dev/dl/go1.24.1.linux-amd64.tar.gz +rm -rf /usr/local/go +tar -C /usr/local -xzf go1.24.1.linux-amd64.tar.gz +rm go1.24.1.linux-amd64.tar.gz + +# Add to PATH for all users +echo 'export PATH=$PATH:/usr/local/go/bin' > /etc/profile.d/go.sh +export PATH=$PATH:/usr/local/go/bin +go version +ENDSSH + success "Go installed" +} + +install_build_deps() { + info "Installing build dependencies..." + remote << 'ENDSSH' +set -euo pipefail +apt-get update -qq +apt-get install -y -qq gcc git make sqlite3 > /dev/null +ENDSSH + success "Build dependencies installed" +} + +create_git_user() { + info "Creating git user..." + if remote "id git" &>/dev/null; then + success "git user already exists" + return + fi + + remote << 'ENDSSH' +set -euo pipefail +adduser --disabled-password --gecos "" git +mkdir -p /home/git/.ssh +chown -R git:git /home/git +ENDSSH + success "git user created" +} + +build_knot() { + info "Building knot binary..." + remote << 'ENDSSH' +set -euo pipefail +export PATH=$PATH:/usr/local/go/bin + +cd /tmp +rm -rf tangled-core + +# Clone from Tangled's own hosting +git clone https://tangled.org/@tangled.org/core tangled-core +cd tangled-core + +CGO_ENABLED=1 go build -o knot ./cmd/knot + +mv knot /usr/local/bin/knot +chown root:root /usr/local/bin/knot +chmod 755 /usr/local/bin/knot + +rm -rf /tmp/tangled-core + +/usr/local/bin/knot --help || true +ENDSSH + success "Knot binary built and installed" +} + +configure_ssh_knot() { + info "Configuring SSH for knot..." + remote << 'ENDSSH' +set -euo pipefail + +# Configure SSH AuthorizedKeysCommand for git user +cat > /etc/ssh/sshd_config.d/tangled-knot.conf << 'SSHEOF' +Match User git + AuthorizedKeysCommand /usr/local/bin/knot keys -o authorized-keys + AuthorizedKeysCommandUser nobody +SSHEOF + +# Reload SSH +systemctl reload ssh || systemctl reload sshd + +echo "SSH configured for knot" +ENDSSH + success "SSH configured for git user" +} + +deploy_knot_env() { + info "Deploying knot environment..." + remote "cat > /home/git/.knot.env" << EOF +KNOT_REPO_SCAN_PATH=/home/git/repositories +KNOT_SERVER_HOSTNAME=$KNOT_DOMAIN +APPVIEW_ENDPOINT=$APPVIEW_ENDPOINT +KNOT_SERVER_OWNER=$KNOT_OWNER_DID +KNOT_SERVER_INTERNAL_LISTEN_ADDR=127.0.0.1:$KNOT_INTERNAL_PORT +KNOT_SERVER_LISTEN_ADDR=127.0.0.1:$KNOT_PUBLIC_PORT +KNOT_SERVER_DB_PATH=/home/git/database/knotserver.db +EOF + + remote << 'ENDSSH' +set -euo pipefail +chown git:git /home/git/.knot.env +chmod 600 /home/git/.knot.env +mkdir -p /home/git/repositories /home/git/database /home/git/log +chown -R git:git /home/git/repositories /home/git/database /home/git/log + +# Optional MOTD +printf "aesthetic computer knot\n" > /home/git/motd +chown git:git /home/git/motd +ENDSSH + success "Knot environment deployed" +} + +deploy_systemd_service() { + info "Deploying systemd service..." + cat "$INFRA_DIR/knotserver.service" | remote "cat > /etc/systemd/system/knotserver.service" + remote << 'ENDSSH' +set -euo pipefail +systemctl daemon-reload +systemctl enable knotserver +systemctl start knotserver +sleep 2 +systemctl status knotserver --no-pager || true +ENDSSH + success "Knot service running" +} + +configure_caddy() { + info "Configuring Caddy reverse proxy..." + + # Check if Caddy is managing PDS + if remote "systemctl is-active caddy" &>/dev/null; then + info "Caddy already running (PDS), adding knot block..." + + # Append knot config to existing Caddyfile + remote "cat >> /etc/caddy/Caddyfile" << EOF + +# Tangled Knot Server +$KNOT_DOMAIN { + reverse_proxy localhost:$KNOT_PUBLIC_PORT + + @websocket { + header Connection *Upgrade* + header Upgrade websocket + } + reverse_proxy @websocket localhost:$KNOT_PUBLIC_PORT +} +EOF + remote "systemctl reload caddy" + else + # PDS might use its own TLS (common with bluesky PDS installer) + # Install Caddy for knot only + info "Installing Caddy..." + remote << 'ENDSSH' +set -euo pipefail +apt-get install -y -qq debian-keyring debian-archive-keyring apt-transport-https curl > /dev/null +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg 2>/dev/null +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list > /dev/null +apt-get update -qq +apt-get install -y -qq caddy > /dev/null +ENDSSH + + cat "$INFRA_DIR/Caddyfile" | remote "cat > /etc/caddy/Caddyfile" + remote "systemctl enable caddy && systemctl restart caddy" + fi + + success "Caddy configured for $KNOT_DOMAIN" +} + +configure_dns() { + info "Configuring DNS via Cloudflare..." + + # Write droplet IP for the DNS script + echo "$DROPLET_IP" > "$SCRIPT_DIR/.droplet_ip" + + node "$SCRIPT_DIR/configure-dns.mjs" || error "DNS configuration failed" + + success "DNS configured" +} + +verify_deployment() { + info "Verifying deployment..." + echo "" + + # Check systemd + if remote "systemctl is-active knotserver" &>/dev/null; then + success "knotserver service: active" + else + warning "knotserver service: not running" + remote "journalctl -u knotserver --no-pager -n 20" || true + fi + + # Check local port + if remote "curl -sf http://localhost:$KNOT_PUBLIC_PORT/ > /dev/null 2>&1"; then + success "Knot responding on localhost:$KNOT_PUBLIC_PORT" + else + warning "Knot not responding on localhost:$KNOT_PUBLIC_PORT yet" + fi + + echo "" + info "Next steps:" + echo " 1. Verify DNS: dig $KNOT_DOMAIN" + echo " 2. Check HTTPS: curl https://$KNOT_DOMAIN/" + echo " 3. Register knot at https://tangled.org/settings/knots" + echo " 4. Add SSH key at https://tangled.org/settings/keys" + echo " 5. Push repos: git remote add tangled git@$KNOT_DOMAIN:aesthetic-computer.git" + echo "" +} + +main() { + echo "" + echo "╔════════════════════════════════════════════════════════╗" + echo "║ Aesthetic Computer Knot Deployment (Tangled) ║" + echo "╚════════════════════════════════════════════════════════╝" + echo "" + + get_droplet_ip + get_ssh_key + get_owner_did + echo "" + + if [[ "${AUTO_CONFIRM:-no}" != "yes" ]]; then + read -p "Deploy knot to $DROPLET_IP as $KNOT_DOMAIN? (y/N) " -n 1 -r + echo "" + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + error "Cancelled" + fi + fi + + echo "" + wait_for_ssh + install_build_deps + install_go + create_git_user + build_knot + configure_ssh_knot + deploy_knot_env + deploy_systemd_service + configure_dns + configure_caddy + echo "" + verify_deployment + + success "Knot deployment complete!" +} + +if [ $# -gt 0 ]; then + "$@" +else + main +fi diff --git a/at/knot/infra/Caddyfile b/at/knot/infra/Caddyfile new file mode 100644 index 0000000000..0f9bb7585e --- /dev/null +++ b/at/knot/infra/Caddyfile @@ -0,0 +1,13 @@ +# Tangled Knot Server — knot.aesthetic.computer +# Only used if Caddy is not already running for PDS. +# If Caddy is already active, the deploy script appends to the existing Caddyfile. + +knot.aesthetic.computer { + reverse_proxy localhost:5555 + + @websocket { + header Connection *Upgrade* + header Upgrade websocket + } + reverse_proxy @websocket localhost:5555 +} diff --git a/at/knot/infra/knotserver.service b/at/knot/infra/knotserver.service new file mode 100644 index 0000000000..73aa90d2bb --- /dev/null +++ b/at/knot/infra/knotserver.service @@ -0,0 +1,23 @@ +[Unit] +Description=Tangled Knot Server (aesthetic computer) +After=network.target + +[Service] +Type=simple +User=git +Group=git +WorkingDirectory=/home/git +EnvironmentFile=/home/git/.knot.env +ExecStart=/usr/local/bin/knot server +Restart=on-failure +RestartSec=5 +StandardOutput=append:/home/git/log/knot.log +StandardError=append:/home/git/log/knot.log + +# Hardening +NoNewPrivileges=true +ProtectSystem=strict +ReadWritePaths=/home/git + +[Install] +WantedBy=multi-user.target diff --git a/at/knot/unify-repo-history.sh b/at/knot/unify-repo-history.sh new file mode 100755 index 0000000000..f68b7011c3 --- /dev/null +++ b/at/knot/unify-repo-history.sh @@ -0,0 +1,244 @@ +#!/usr/bin/env bash +# Unify Aesthetic Computer repo history +# +# Grafts the four successive repositories into one continuous git history: +# 1. whistlegraph/system-ac (Aug-Dec 2021, 38 commits) +# 2. whistlegraph/disks-ac (Oct-Dec 2021, companion) +# 3. whistlegraph/2022.aesthetic.computer (Dec 2021-Dec 2022, ~500 commits) +# 4. whistlegraph/aesthetic-computer (Dec 2022-present, 11k+ commits) +# +# After grafting, the unified history is pushed to the Tangled knot. +# +# Usage: +# ./unify-repo-history.sh [--dry-run] [--work-dir /path/to/workdir] +# +# Requirements: +# - git-filter-repo (pip install git-filter-repo) +# - Access to all four GitHub repos + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +info() { echo -e "${BLUE}i${NC} $1"; } +success() { echo -e "${GREEN}✓${NC} $1"; } +warning() { echo -e "${YELLOW}!${NC} $1"; } +error() { echo -e "${RED}x${NC} $1"; exit 1; } + +# Defaults +DRY_RUN=false +WORK_DIR="/tmp/ac-unified-history" +KNOT_REMOTE="git@knot.aesthetic.computer" + +# The four repos in chronological order +REPO_1="https://github.com/whistlegraph/system-ac.git" +REPO_2="https://github.com/whistlegraph/disks-ac.git" +REPO_3="https://github.com/whistlegraph/2022.aesthetic.computer.git" +REPO_4="https://github.com/whistlegraph/aesthetic-computer.git" + +# Parse args +while [[ $# -gt 0 ]]; do + case $1 in + --dry-run) DRY_RUN=true; shift ;; + --work-dir) WORK_DIR="$2"; shift 2 ;; + --knot-remote) KNOT_REMOTE="$2"; shift 2 ;; + *) error "Unknown option: $1" ;; + esac +done + +check_requirements() { + if ! command -v git-filter-repo &>/dev/null; then + error "git-filter-repo not found. Install: pip install git-filter-repo" + fi + info "Requirements OK" +} + +clone_repos() { + mkdir -p "$WORK_DIR" + cd "$WORK_DIR" + + info "Cloning all four repositories..." + + if [ ! -d "system-ac" ]; then + git clone "$REPO_1" system-ac + success "Cloned system-ac" + else + warning "system-ac already cloned" + fi + + if [ ! -d "disks-ac" ]; then + git clone "$REPO_2" disks-ac + success "Cloned disks-ac" + else + warning "disks-ac already cloned" + fi + + if [ ! -d "2022.aesthetic.computer" ]; then + git clone "$REPO_3" 2022.aesthetic.computer + success "Cloned 2022.aesthetic.computer" + else + warning "2022.aesthetic.computer already cloned" + fi + + if [ ! -d "aesthetic-computer" ]; then + git clone --mirror "$REPO_4" aesthetic-computer.git + # Also make a working copy for grafting + git clone aesthetic-computer.git aesthetic-computer + success "Cloned aesthetic-computer" + else + warning "aesthetic-computer already cloned" + fi +} + +find_boundary_commits() { + info "Finding boundary commits..." + echo "" + + # Last commit of each predecessor repo + cd "$WORK_DIR/system-ac" + SYSTEM_AC_LAST=$(git log --format="%H" -1) + SYSTEM_AC_LAST_DATE=$(git log --format="%ci" -1) + info "system-ac last: $SYSTEM_AC_LAST ($SYSTEM_AC_LAST_DATE)" + + cd "$WORK_DIR/disks-ac" + DISKS_AC_LAST=$(git log --format="%H" -1) + DISKS_AC_LAST_DATE=$(git log --format="%ci" -1) + info "disks-ac last: $DISKS_AC_LAST ($DISKS_AC_LAST_DATE)" + + cd "$WORK_DIR/2022.aesthetic.computer" + REPO_2022_LAST=$(git log --format="%H" -1) + REPO_2022_LAST_DATE=$(git log --format="%ci" -1) + info "2022.aesthetic.computer last: $REPO_2022_LAST ($REPO_2022_LAST_DATE)" + + # First (root) commit of each successor repo + cd "$WORK_DIR/2022.aesthetic.computer" + REPO_2022_FIRST=$(git rev-list --max-parents=0 HEAD | tail -1) + REPO_2022_FIRST_DATE=$(git log --format="%ci" "$REPO_2022_FIRST" -1) + info "2022.aesthetic.computer first: $REPO_2022_FIRST ($REPO_2022_FIRST_DATE)" + + cd "$WORK_DIR/aesthetic-computer" + CURRENT_FIRST=$(git rev-list --max-parents=0 HEAD | tail -1) + CURRENT_FIRST_DATE=$(git log --format="%ci" "$CURRENT_FIRST" -1) + info "aesthetic-computer first: $CURRENT_FIRST ($CURRENT_FIRST_DATE)" + + echo "" + info "Graft plan:" + echo " system-ac ($SYSTEM_AC_LAST_DATE)" + echo " + disks-ac ($DISKS_AC_LAST_DATE)" + echo " -> 2022.aesthetic.computer root ($REPO_2022_FIRST_DATE)" + echo " -> aesthetic-computer root ($CURRENT_FIRST_DATE)" + echo "" +} + +build_unified_repo() { + info "Building unified repository..." + + cd "$WORK_DIR/aesthetic-computer" + + # Add predecessor repos as remotes + git remote add system-ac "$WORK_DIR/system-ac" 2>/dev/null || true + git remote add disks-ac "$WORK_DIR/disks-ac" 2>/dev/null || true + git remote add repo-2022 "$WORK_DIR/2022.aesthetic.computer" 2>/dev/null || true + + git fetch system-ac + git fetch disks-ac + git fetch repo-2022 + + success "Fetched all predecessor histories" + + # Graft: 2022 repo's root commit gets system-ac and disks-ac as parents + info "Grafting 2022.aesthetic.computer onto system-ac + disks-ac..." + git replace --graft "$REPO_2022_FIRST" "$SYSTEM_AC_LAST" "$DISKS_AC_LAST" + + # Graft: current repo's root commit gets 2022 repo's last commit as parent + info "Grafting aesthetic-computer onto 2022.aesthetic.computer..." + git replace --graft "$CURRENT_FIRST" "$REPO_2022_LAST" + + success "Grafts applied" + + # Verify the chain + info "Verifying unified history..." + TOTAL_COMMITS=$(git log --oneline | wc -l) + EARLIEST=$(git log --format="%ci %s" --reverse | head -1) + LATEST=$(git log --format="%ci %s" -1) + echo " Total commits: $TOTAL_COMMITS" + echo " Earliest: $EARLIEST" + echo " Latest: $LATEST" + echo "" + + if [ "$DRY_RUN" = true ]; then + warning "Dry run — grafts are temporary (git replace refs only)" + warning "Run without --dry-run to make permanent with filter-repo" + return + fi + + # Make grafts permanent + info "Making grafts permanent with filter-repo..." + git filter-repo --force + + success "Unified history is permanent" + + # Clean up remotes + git remote remove system-ac 2>/dev/null || true + git remote remove disks-ac 2>/dev/null || true + git remote remove repo-2022 2>/dev/null || true +} + +push_to_knot() { + if [ "$DRY_RUN" = true ]; then + info "[dry-run] Would push to $KNOT_REMOTE:aesthetic-computer.git" + return + fi + + cd "$WORK_DIR/aesthetic-computer" + + info "Adding Tangled knot remote..." + git remote add tangled "$KNOT_REMOTE:aesthetic-computer.git" 2>/dev/null || \ + git remote set-url tangled "$KNOT_REMOTE:aesthetic-computer.git" + + info "Pushing unified history to knot..." + git push tangled --all --force + git push tangled --tags + + success "Pushed to $KNOT_REMOTE" +} + +main() { + echo "" + echo "╔════════════════════════════════════════════════════════╗" + echo "║ Aesthetic Computer — Unify Repository History ║" + echo "╚════════════════════════════════════════════════════════╝" + echo "" + + if [ "$DRY_RUN" = true ]; then + warning "DRY RUN MODE — no permanent changes" + echo "" + fi + + info "Work directory: $WORK_DIR" + echo "" + + check_requirements + clone_repos + find_boundary_commits + build_unified_repo + push_to_knot + + echo "" + success "Done!" + echo "" + info "Unified history at: $WORK_DIR/aesthetic-computer" + echo "" + echo "To use in your working copy:" + echo " cd /workspaces/aesthetic-computer" + echo " git remote add tangled $KNOT_REMOTE:aesthetic-computer.git" + echo " git fetch tangled" + echo " git push tangled main" + echo "" +} + +main -- knotmirror 0.0.1