#!/usr/bin/env bash
set -euo pipefail

usage() {
  cat <<'EOF'
Start the Hypersnap Lite local Farcaster builder stack.

Usage:
  bash start-hypersnap-lite.sh [start] [--yes] [--reset] [--rebuild-hypersnap] [--no-open]
  bash start-hypersnap-lite.sh status
  bash start-hypersnap-lite.sh hello-world [signer_uuid]

What it starts:
  - A tiny local Node command center on http://127.0.0.1:8890
  - Hypersnap Lite on http://127.0.0.1:3381
  - JSON signer storage under ~/.hypersnap-lite/data

No Anky monorepo is cloned or required.

Environment overrides:
  HYPERSNAP_LITE_HOME=$HOME/.hypersnap-lite
  HYPERSNAP_PORT=3381
  HYPERSNAP_UI_PORT=8890
  HYPERSNAP_DIR=$HOME/.hypersnap-lite/hypersnap
  HYPERSNAP_REPO=https://github.com/jpfraneto/hypersnap.git
  HYPERSNAP_BRANCH=hypersnap-lite
  FARCASTER_APP_FID=123
  FARCASTER_APP_PRIVATE_KEY=0x...
EOF
}

COMMAND="start"
RESET=0
REBUILD_HYPERSNAP=0
OPEN_BROWSER=1
YES=0
HELLO_WORLD_SIGNER_UUID=""

for arg in "$@"; do
  case "$arg" in
    start) COMMAND="start" ;;
    status) COMMAND="status" ;;
    hello-world) COMMAND="hello-world" ;;
    --reset) RESET=1 ;;
    --rebuild-hypersnap) REBUILD_HYPERSNAP=1 ;;
    --no-open) OPEN_BROWSER=0 ;;
    -y|--yes) YES=1 ;;
    -h|--help)
      usage
      exit 0
      ;;
    *)
      if [ "$COMMAND" = "hello-world" ] && [ -z "$HELLO_WORLD_SIGNER_UUID" ]; then
        HELLO_WORLD_SIGNER_UUID="$arg"
      else
        echo "Unknown argument: $arg" >&2
        usage >&2
        exit 1
      fi
      ;;
  esac
done

log() {
  printf '[hypersnap-lite] %s\n' "$*" >&2
}

need_cmd() {
  if ! command -v "$1" >/dev/null 2>&1; then
    echo "Missing required command: $1" >&2
    exit 1
  fi
}

runtime_dir() {
  printf '%s\n' "${HYPERSNAP_LITE_HOME:-$HOME/.hypersnap-lite}"
}

ui_port() {
  printf '%s\n' "${HYPERSNAP_UI_PORT:-8890}"
}

hypersnap_port() {
  printf '%s\n' "${HYPERSNAP_PORT:-3381}"
}

tty_available() {
  [ -r /dev/tty ] && [ -w /dev/tty ] && { [ -t 0 ] || [ -t 1 ]; }
}

read_tty() {
  local prompt="$1"
  local var_name="$2"
  if ! tty_available; then
    echo "Interactive input is required for: $prompt" >&2
    echo "Rerun in a terminal or provide env vars." >&2
    exit 1
  fi
  printf '%s' "$prompt" > /dev/tty
  IFS= read -r "$var_name" < /dev/tty
}

read_secret_tty() {
  local prompt="$1"
  local var_name="$2"
  if ! tty_available; then
    echo "Interactive hidden input is required." >&2
    exit 1
  fi
  printf '%s' "$prompt" > /dev/tty
  IFS= read -rs "$var_name" < /dev/tty
  printf '\n' > /dev/tty
}

confirm_tty() {
  local prompt="$1"
  local answer
  if [ "$YES" -eq 1 ]; then
    return 0
  fi
  read_tty "$prompt [y/N]: " answer
  case "$answer" in
    y|Y|yes|YES|Yes) return 0 ;;
    *) return 1 ;;
  esac
}

wait_for_url() {
  local url="$1"
  local label="$2"
  local tries="${3:-60}"

  for _ in $(seq 1 "$tries"); do
    if curl -fsS -m 2 "$url" >/dev/null 2>&1; then
      log "$label is ready"
      return 0
    fi
    sleep 1
  done

  echo "$label did not become ready at $url" >&2
  return 1
}

open_url() {
  local url="$1"
  if [ "$OPEN_BROWSER" -eq 0 ]; then
    return 0
  fi
  if command -v xdg-open >/dev/null 2>&1; then
    xdg-open "$url" >/dev/null 2>&1 || true
  elif command -v open >/dev/null 2>&1; then
    open "$url" >/dev/null 2>&1 || true
  elif command -v python3 >/dev/null 2>&1; then
    python3 -m webbrowser "$url" >/dev/null 2>&1 || true
  fi
}

print_intro() {
  local root out
  root="$(runtime_dir)"
  out="/dev/stdout"
  if tty_available; then
    out="/dev/tty"
  fi
  cat > "$out" <<EOF

hypersnap * hypersnap * hypersnap * hypersnap
        hypersnap                 hypersnap
     hypersnap                       hypersnap
   hypersnap                           hypersnap
  hypersnap                             hypersnap
  hypersnap                             hypersnap
   hypersnap                           hypersnap
     hypersnap                       hypersnap
        hypersnap                 hypersnap
hypersnap * hypersnap * hypersnap * hypersnap
        hypersnap                 hypersnap
     hypersnap                       hypersnap
   hypersnap                           hypersnap
  hypersnap                             hypersnap

Hypersnap Lite local installer

This sets up the smallest local Farcaster posting stack:

  browser command center
    -> tiny local Node server
    -> signed Farcaster message
    -> local Hypersnap Lite relay
    -> Farcaster/Snapchain network

What will happen:
  1. Create local runtime folder:
     $root
  2. Write a tiny command center app into:
     $root/app
  3. Install only the JS packages needed for signing, QR codes, and protobuf.
  4. Clone/build Hypersnap Lite if the Docker image is not already present:
     https://github.com/jpfraneto/hypersnap/tree/hypersnap-lite
  5. Start Hypersnap Lite:
     http://127.0.0.1:$(hypersnap_port)/v1/info
  6. Start the command center:
     http://127.0.0.1:$(ui_port)

Code and agent context:
  Installer: https://lite.hypersnap.lat/install.sh
  AGENTS.md: https://lite.hypersnap.lat/AGENTS.md
  Hypersnap Lite source: https://github.com/jpfraneto/hypersnap/tree/hypersnap-lite

Important:
  - No Anky monorepo is downloaded.
  - Runtime data stays local under $root.
  - Your posting Farcaster account connects later by QR code.
  - Do not paste the mnemonic for the account that will post casts.
  - Current developer mode still needs a client/app custody key to create signer requests.

EOF
}

write_agents_md() {
  local app_dir="$1"
  cat > "$app_dir/AGENTS.md" <<'EOF'
# Hypersnap Lite Agent Instructions

You are helping a builder use or extend Hypersnap Lite as a local Farcaster posting client.

## What This Is

Hypersnap Lite is a minimal local stack for posting to Farcaster without syncing the full network state.

Runtime flow:

```text
browser command center
  -> tiny local Node backend on http://127.0.0.1:8890
  -> signed Farcaster message
  -> local Hypersnap Lite relay on http://127.0.0.1:3381
  -> Farcaster/Snapchain network
```

The browser does not hold signer private keys. The local backend stores approved signer keys encrypted and signs Farcaster messages locally.

## User Data Location

All runtime data should stay under:

```text
~/.hypersnap-lite
```

Important paths:

```text
~/.hypersnap-lite/env
~/.hypersnap-lite/data/signers.json
~/.hypersnap-lite/rocks-lite
~/.hypersnap-lite/logs/server.log
~/.hypersnap-lite/app
~/.hypersnap-lite/hypersnap
```

Do not print or commit `~/.hypersnap-lite/env` or signer data.

## Launch

Preferred public command:

```bash
curl -fsSL https://lite.hypersnap.lat | bash
```

Status:

```bash
bash ~/.hypersnap-lite/app/start.sh status
```

Post a test cast after signer approval:

```bash
bash ~/.hypersnap-lite/app/start.sh hello-world
```

## Extension Rules

- Keep this independent from unrelated monorepos.
- Prefer local-first defaults.
- Keep signer material on the user's machine.
- Store generated runtime data under `~/.hypersnap-lite`.
- Treat Hypersnap Lite as the relay for signed messages, not as a full Farcaster indexer.
- If adding read features, use a separate read provider or make it optional.
- If removing the developer custody mnemonic requirement, add a hosted signer-request relay so users only approve by QR.
EOF
}

write_package_json() {
  local app_dir="$1"
  cat > "$app_dir/package.json" <<'EOF'
{
  "name": "hypersnap-lite-command-center",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "node server.mjs"
  },
  "dependencies": {
    "@noble/hashes": "^1.8.0",
    "express": "^4.19.2",
    "ethers": "^6.15.0",
    "protobufjs": "^7.4.0",
    "qrcode": "^1.5.4",
    "tweetnacl": "^1.0.3",
    "uuid": "^11.1.0"
  },
  "overrides": {
    "ws": "^8.20.1"
  }
}
EOF
}

write_html() {
  local app_dir="$1"
  mkdir -p "$app_dir/public"
  cat > "$app_dir/public/index.html" <<'EOF'
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Hypersnap Lite Command Center</title>
  <style>
    :root { color-scheme: light; --bg: #f7f5f0; --panel: #fff; --text: #171717; --muted: #66615a; --line: #ded8cf; --accent: #4f46e5; --accent-dark: #3930b4; --ok: #0f7a4f; --bad: #b42318; }
    * { box-sizing: border-box; }
    body { margin: 0; background: var(--bg); color: var(--text); font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; line-height: 1.45; }
    main { width: min(920px, calc(100% - 32px)); margin: 32px auto; }
    header { display: flex; justify-content: space-between; align-items: flex-end; gap: 16px; margin-bottom: 20px; border-bottom: 1px solid var(--line); padding-bottom: 16px; }
    h1 { margin: 0; font-size: clamp(28px, 4vw, 44px); line-height: 1; letter-spacing: 0; }
    .status-pill { display: inline-flex; align-items: center; min-height: 34px; padding: 0 12px; border: 1px solid var(--line); border-radius: 999px; background: var(--panel); color: var(--muted); font-size: 14px; white-space: nowrap; }
    .grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; }
    section { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 16px; min-width: 0; }
    section.wide { grid-column: 1 / -1; }
    h2 { margin: 0 0 12px; font-size: 17px; letter-spacing: 0; }
    label { display: block; margin: 12px 0 6px; font-size: 13px; color: var(--muted); }
    input, textarea { width: 100%; min-width: 0; border: 1px solid var(--line); border-radius: 6px; padding: 10px 11px; color: var(--text); background: #fff; font: inherit; }
    textarea { min-height: 104px; resize: vertical; }
    button, a.button { display: inline-flex; align-items: center; justify-content: center; min-height: 40px; border: 0; border-radius: 6px; padding: 0 14px; background: var(--accent); color: white; font: inherit; font-weight: 650; text-decoration: none; cursor: pointer; }
    button:hover, a.button:hover { background: var(--accent-dark); }
    button.secondary { background: #ede9e1; color: var(--text); border: 1px solid var(--line); }
    button.secondary:hover { background: #e3ddd3; }
    .row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-top: 14px; }
    .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; overflow-wrap: anywhere; }
    .hint { color: var(--muted); font-size: 13px; margin: 8px 0 0; }
    .result { margin-top: 16px; padding: 12px; border: 1px solid var(--line); border-radius: 6px; background: #fbfaf8; min-height: 46px; overflow: auto; max-height: 320px; white-space: pre-wrap; }
    .qr-box { display: grid; place-items: center; width: 100%; min-height: 220px; margin-top: 12px; border: 1px solid var(--line); border-radius: 6px; background: #fff; overflow: hidden; }
    .qr-box img { width: min(240px, 100%); height: auto; display: none; }
    .ok { color: var(--ok); }
    .bad { color: var(--bad); }
    @media (max-width: 820px) { header { display: block; } .status-pill { margin-top: 12px; } .grid { grid-template-columns: 1fr; } }
  </style>
</head>
<body>
  <main>
    <header>
      <div>
        <h1>Hypersnap Lite Command Center</h1>
        <p class="hint">Connect a Farcaster account, sign locally, and relay through Hypersnap Lite.</p>
      </div>
      <div id="health" class="status-pill">checking localhost</div>
    </header>

    <div class="grid">
      <section>
        <h2>1. Local Stack</h2>
        <label for="apiBase">API base</label>
        <input id="apiBase" value="http://127.0.0.1:8890">
        <div class="row">
          <button id="checkHealth" class="secondary">Check</button>
          <button id="copyAgents" class="secondary">Copy AGENTS.md</button>
        </div>
        <p class="hint mono">Node: http://127.0.0.1:3381/v1/info</p>
      </section>

      <section>
        <h2>2. Signer</h2>
        <label for="userId">Local label</label>
        <input id="userId" value="builder">
        <label for="redirectUrl">Redirect URL</label>
        <input id="redirectUrl" placeholder="optional">
        <div class="row">
          <button id="startSigner">Create signer</button>
          <button id="pollSigner" class="secondary">Poll</button>
        </div>
      </section>

      <section>
        <h2>3. Approval</h2>
        <label for="signerUuid">Signer UUID</label>
        <input id="signerUuid" placeholder="created signer UUID">
        <div class="qr-box">
          <img id="approvalQr" alt="Farcaster signer approval QR">
          <span id="qrEmpty" class="hint">Create a signer to show QR.</span>
        </div>
        <div class="row">
          <a id="approvalLink" class="button" href="#" target="_blank" rel="noreferrer">Open approval</a>
          <button id="refreshQr" class="secondary">Refresh QR</button>
        </div>
        <p id="approvalHint" class="hint">Create a signer first.</p>
      </section>

      <section class="wide">
        <h2>4. Cast</h2>
        <label for="castText">Text</label>
        <textarea id="castText" maxlength="320" placeholder="Write a cast">hello world from hypersnap lite</textarea>
        <label for="embedUrl">Embed URL</label>
        <input id="embedUrl" placeholder="optional">
        <div class="row">
          <button id="postCast">Post cast</button>
          <button id="clearLog" class="secondary">Clear log</button>
        </div>
        <pre id="result" class="result mono">Ready.</pre>
      </section>
    </div>
  </main>

  <script>
    const $ = (id) => document.getElementById(id);
    const storage = window.localStorage;
    const apiBase = $("apiBase");
    const signerUuid = $("signerUuid");
    const approvalLink = $("approvalLink");
    const approvalHint = $("approvalHint");
    const approvalQr = $("approvalQr");
    const qrEmpty = $("qrEmpty");
    const result = $("result");
    const health = $("health");
    apiBase.value = storage.getItem("hsl_api_base") || apiBase.value;
    signerUuid.value = storage.getItem("hsl_signer_uuid") || "";
    $("userId").value = storage.getItem("hsl_user_id") || $("userId").value;
    $("castText").value = storage.getItem("hsl_cast_text") || $("castText").value;
    setApproval(storage.getItem("hsl_approval_url") || "");
    setQrFromSigner();

    function base() {
      const value = apiBase.value.trim().replace(/\/$/, "");
      storage.setItem("hsl_api_base", value);
      return value;
    }
    function log(title, value, isError) {
      const payload = typeof value === "string" ? value : JSON.stringify(value, null, 2);
      result.textContent = `${title}\n${payload}`;
      result.className = `result mono ${isError ? "bad" : ""}`;
    }
    function setApproval(url) {
      if (url) {
        approvalLink.href = url;
        approvalHint.textContent = url;
        storage.setItem("hsl_approval_url", url);
      } else {
        approvalLink.href = "#";
        approvalHint.textContent = "Create a signer first.";
        storage.removeItem("hsl_approval_url");
      }
    }
    function setQrFromSigner() {
      const uuid = signerUuid.value.trim();
      if (!uuid) {
        approvalQr.removeAttribute("src");
        approvalQr.style.display = "none";
        qrEmpty.style.display = "block";
        return;
      }
      approvalQr.src = `${base()}/api/farcaster/signer/${encodeURIComponent(uuid)}/qr.svg?t=${Date.now()}`;
      approvalQr.style.display = "block";
      qrEmpty.style.display = "none";
    }
    async function request(path, options = {}) {
      const response = await fetch(`${base()}${path}`, {
        ...options,
        headers: { "content-type": "application/json", ...(options.headers || {}) }
      });
      const text = await response.text();
      let data;
      try { data = text ? JSON.parse(text) : {}; } catch { data = text; }
      if (!response.ok) {
        const message = typeof data === "object" && data.error ? data.error : text;
        throw new Error(message || `HTTP ${response.status}`);
      }
      return data;
    }
    async function checkHealth() {
      try {
        const data = await request("/api/health", { method: "GET", headers: {} });
        health.textContent = "localhost ready";
        health.className = "status-pill ok";
        log("Health", data, false);
      } catch (error) {
        health.textContent = "localhost offline";
        health.className = "status-pill bad";
        log("Health error", error.message, true);
      }
    }
    $("checkHealth").addEventListener("click", checkHealth);
    $("copyAgents").addEventListener("click", async () => {
      try {
        const response = await fetch("/AGENTS.md", { cache: "no-store" });
        if (!response.ok) throw new Error(`AGENTS.md unavailable: HTTP ${response.status}`);
        const text = await response.text();
        if (navigator.clipboard && window.isSecureContext) {
          await navigator.clipboard.writeText(text);
        } else {
          const area = document.createElement("textarea");
          area.value = text;
          area.style.position = "fixed";
          area.style.left = "-9999px";
          document.body.appendChild(area);
          area.focus();
          area.select();
          document.execCommand("copy");
          area.remove();
        }
        log("AGENTS.md copied", "Paste it into Claude Code, Codex, or another agent so it understands this local Hypersnap Lite stack.", false);
      } catch (error) {
        log("Copy AGENTS.md error", error.message, true);
      }
    });
    $("startSigner").addEventListener("click", async () => {
      try {
        const userId = $("userId").value.trim();
        storage.setItem("hsl_user_id", userId);
        const redirectUrl = $("redirectUrl").value.trim();
        const body = { user_id: userId || null };
        if (redirectUrl) body.redirect_url = redirectUrl;
        const data = await request("/api/farcaster/signer/start", { method: "POST", body: JSON.stringify(body) });
        signerUuid.value = data.signer_uuid;
        storage.setItem("hsl_signer_uuid", data.signer_uuid);
        setApproval(data.approval_url || data.deeplink_url);
        setQrFromSigner();
        log("Signer created. Scan QR, approve, then poll.", data, false);
      } catch (error) {
        log("Create signer error", error.message, true);
      }
    });
    $("pollSigner").addEventListener("click", async () => {
      try {
        const uuid = signerUuid.value.trim();
        if (!uuid) throw new Error("Missing signer UUID");
        const data = await request(`/api/farcaster/signer/${encodeURIComponent(uuid)}`, { method: "GET", headers: {} });
        setApproval(data.approval_url || data.deeplink_url);
        setQrFromSigner();
        log(data.approved ? "Signer approved" : "Signer status", data, false);
      } catch (error) {
        log("Signer status error", error.message, true);
      }
    });
    $("postCast").addEventListener("click", async () => {
      try {
        const uuid = signerUuid.value.trim();
        const text = $("castText").value.trim();
        const embed = $("embedUrl").value.trim();
        if (!uuid) throw new Error("Missing signer UUID");
        if (!text) throw new Error("Missing cast text");
        storage.setItem("hsl_cast_text", text);
        const body = { signer_uuid: uuid, text, embeds: embed ? [embed] : [] };
        const data = await request("/api/farcaster/cast", { method: "POST", body: JSON.stringify(body) });
        log("Cast submitted", data, false);
      } catch (error) {
        log("Post cast error", error.message, true);
      }
    });
    $("clearLog").addEventListener("click", () => { result.textContent = "Ready."; result.className = "result mono"; });
    $("refreshQr").addEventListener("click", setQrFromSigner);
    apiBase.addEventListener("change", () => storage.setItem("hsl_api_base", base()));
    signerUuid.addEventListener("change", () => { storage.setItem("hsl_signer_uuid", signerUuid.value.trim()); setQrFromSigner(); });
    checkHealth();
  </script>
</body>
</html>
EOF
}

write_server() {
  local app_dir="$1"
  cat > "$app_dir/server.mjs" <<'EOF'
import express from "express";
import fs from "node:fs/promises";
import fssync from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { fileURLToPath } from "node:url";
import { Wallet } from "ethers";
import nacl from "tweetnacl";
import { blake3 } from "@noble/hashes/blake3";
import protobuf from "protobufjs";
import QRCode from "qrcode";
import { v4 as uuidv4 } from "uuid";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = process.env.HYPERSNAP_LITE_HOME || path.join(process.env.HOME, ".hypersnap-lite");
const DATA_DIR = path.join(ROOT, "data");
const SIGNERS_FILE = path.join(DATA_DIR, "signers.json");
const PORT = Number(process.env.HYPERSNAP_UI_PORT || 8890);
const HYPERSNAP_URL = process.env.HYPERSNAP_LITE_URL || "http://127.0.0.1:3381";
const FARCASTER_CLIENT_API = process.env.FARCASTER_CLIENT_API || "https://api.farcaster.xyz";
const FARCASTER_EPOCH_SECONDS = 1609459200;
const SIGNED_KEY_REQUEST_VALIDATOR = "0x00000000fc700472606ed4fa22623acf62c60553";

const PROTO = `
syntax = "proto3";
message Message {
  MessageData data = 1;
  bytes hash = 2;
  HashScheme hash_scheme = 3;
  bytes signature = 4;
  SignatureScheme signature_scheme = 5;
  bytes signer = 6;
  optional bytes data_bytes = 7;
}
message MessageData {
  MessageType type = 1;
  uint64 fid = 2;
  uint32 timestamp = 3;
  FarcasterNetwork network = 4;
  oneof body { CastAddBody cast_add_body = 5; }
}
message CastAddBody {
  repeated string embeds_deprecated = 1;
  repeated uint64 mentions = 2;
  oneof parent { CastId parent_cast_id = 3; string parent_url = 7; }
  string text = 4;
  repeated uint32 mentions_positions = 5;
  repeated Embed embeds = 6;
  CastType type = 8;
}
message Embed {
  oneof embed { string url = 1; CastId cast_id = 2; }
}
message CastId {
  uint64 fid = 1;
  bytes hash = 2;
}
enum HashScheme { HASH_SCHEME_NONE = 0; HASH_SCHEME_BLAKE3 = 1; }
enum SignatureScheme { SIGNATURE_SCHEME_NONE = 0; SIGNATURE_SCHEME_ED25519 = 1; }
enum MessageType { MESSAGE_TYPE_NONE = 0; MESSAGE_TYPE_CAST_ADD = 1; }
enum FarcasterNetwork { FARCASTER_NETWORK_NONE = 0; FARCASTER_NETWORK_MAINNET = 1; }
enum CastType { CAST = 0; LONG_CAST = 1; TEN_K_CAST = 2; }
`;
const root = protobuf.parse(PROTO).root;
const Message = root.lookupType("Message");
const MessageData = root.lookupType("MessageData");

await fs.mkdir(DATA_DIR, { recursive: true });
if (!fssync.existsSync(SIGNERS_FILE)) {
  await fs.writeFile(SIGNERS_FILE, JSON.stringify({ signers: [] }, null, 2));
}

const app = express();
app.use(express.json({ limit: "1mb" }));
app.use(express.static(path.join(__dirname, "public")));

app.get("/AGENTS.md", (_req, res) => {
  res.type("text/markdown").send(fssync.readFileSync(path.join(__dirname, "AGENTS.md"), "utf8"));
});

app.get("/api/health", async (_req, res) => {
  res.json({
    status: "ok",
    runtime_dir: ROOT,
    hypersnap_lite_url: HYPERSNAP_URL,
    app_fid_configured: Boolean(process.env.FARCASTER_APP_FID),
  });
});

app.get("/api/hypersnap/info", async (_req, res, next) => {
  try {
    const response = await fetch(`${HYPERSNAP_URL.replace(/\/$/, "")}/v1/info`);
    res.status(response.status).type("application/json").send(await response.text());
  } catch (error) {
    next(error);
  }
});

app.post("/api/farcaster/signer/start", async (req, res, next) => {
  try {
    ensureConfig();
    const signerUuid = uuidv4();
    const signer = generateSigner();
    const deadline = Math.floor(Date.now() / 1000) + 24 * 60 * 60;
    const signature = await signedKeyRequestSignature(
      Number(process.env.FARCASTER_APP_FID),
      signer.publicKeyHex,
      deadline,
      process.env.FARCASTER_APP_PRIVATE_KEY,
    );
    const request = await createSignedKeyRequest(signer.publicKeyHex, signature, deadline, req.body?.redirect_url);
    const db = await readDb();
    const record = {
      signer_uuid: signerUuid,
      user_id: req.body?.user_id || null,
      fid: request.userFid || null,
      public_key: signer.publicKeyHex,
      encrypted_private_key: encrypt(signer.privateKey),
      request_token: request.token,
      approval_url: request.deeplinkUrl,
      state: request.state,
      created_at: new Date().toISOString(),
      approved_at: null,
      last_used_at: null,
    };
    db.signers.push(record);
    await writeDb(db);
    res.json(toSignerResponse(record));
  } catch (error) {
    next(error);
  }
});

app.get("/api/farcaster/signer/:uuid", async (req, res, next) => {
  try {
    ensureConfig();
    const record = await loadSigner(req.params.uuid);
    await refreshSigner(record);
    res.json(toSignerStatus(record));
  } catch (error) {
    next(error);
  }
});

app.get("/api/farcaster/signer/:uuid/qr.svg", async (req, res, next) => {
  try {
    const record = await loadSigner(req.params.uuid);
    const svg = await QRCode.toString(record.approval_url, { type: "svg", margin: 1, width: 240 });
    res.type("image/svg+xml").send(svg);
  } catch (error) {
    next(error);
  }
});

app.post("/api/farcaster/cast", async (req, res, next) => {
  try {
    ensureConfig();
    const record = await loadSigner(req.body?.signer_uuid);
    if (!isApproved(record.state)) {
      await refreshSigner(record);
    }
    if (!isApproved(record.state)) {
      throw badRequest(`signer is not approved yet; current state is ${record.state}`);
    }
    if (!record.fid) throw badRequest("approved signer is missing fid");
    const text = String(req.body?.text || "").trim();
    const embeds = Array.isArray(req.body?.embeds) ? req.body.embeds : [];
    const messageBytes = buildCastAddMessage({
      fid: Number(record.fid),
      signerPrivateKey: decrypt(record.encrypted_private_key),
      text,
      embeds,
      parentUrl: req.body?.parent_url || null,
    });
    const submit = await submitMessage(messageBytes);
    record.last_used_at = new Date().toISOString();
    await saveSigner(record);
    res.json({ success: true, signer_uuid: record.signer_uuid, fid: record.fid, hash: submit.hash || null, hub_response: submit });
  } catch (error) {
    next(error);
  }
});

app.use((err, _req, res, _next) => {
  const status = err.status || 500;
  res.status(status).json({ error: err.message || "internal error" });
});

app.listen(PORT, "0.0.0.0", () => {
  console.log(`Hypersnap Lite command center listening on http://127.0.0.1:${PORT}`);
});

function ensureConfig() {
  if (!process.env.FARCASTER_APP_FID || process.env.FARCASTER_APP_FID === "0") throw badRequest("FARCASTER_APP_FID must be configured");
  if (!process.env.FARCASTER_APP_PRIVATE_KEY) throw badRequest("FARCASTER_APP_PRIVATE_KEY must be configured");
  if (!process.env.SIGNER_ENCRYPTION_KEY) throw badRequest("SIGNER_ENCRYPTION_KEY must be configured");
}

function generateSigner() {
  const seed = crypto.randomBytes(32);
  const kp = nacl.sign.keyPair.fromSeed(seed);
  return { privateKey: seed, publicKeyHex: `0x${Buffer.from(kp.publicKey).toString("hex")}` };
}

async function signedKeyRequestSignature(appFid, publicKeyHex, deadline, privateKey) {
  const wallet = new Wallet(privateKey);
  const domain = {
    name: "Farcaster SignedKeyRequestValidator",
    version: "1",
    chainId: 10,
    verifyingContract: SIGNED_KEY_REQUEST_VALIDATOR,
  };
  const types = {
    SignedKeyRequest: [
      { name: "requestFid", type: "uint256" },
      { name: "key", type: "bytes" },
      { name: "deadline", type: "uint256" },
    ],
  };
  return wallet.signTypedData(domain, types, { requestFid: appFid, key: publicKeyHex, deadline });
}

async function createSignedKeyRequest(publicKeyHex, signature, deadline, redirectUrl) {
  const body = {
    key: publicKeyHex,
    requestFid: Number(process.env.FARCASTER_APP_FID),
    signature,
    deadline,
  };
  if (redirectUrl) body.redirectUrl = redirectUrl;
  const response = await fetch(`${FARCASTER_CLIENT_API.replace(/\/$/, "")}/v2/signed-key-requests`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body),
  });
  const json = await response.json().catch(async () => ({ error: await response.text() }));
  if (!response.ok) throw new Error(`Farcaster signed key request failed ${response.status}: ${JSON.stringify(json)}`);
  return json.result.signedKeyRequest;
}

async function getSignedKeyRequest(token) {
  const url = new URL(`${FARCASTER_CLIENT_API.replace(/\/$/, "")}/v2/signed-key-request`);
  url.searchParams.set("token", token);
  const response = await fetch(url);
  const json = await response.json().catch(async () => ({ error: await response.text() }));
  if (!response.ok) throw new Error(`Farcaster signed key request status failed ${response.status}: ${JSON.stringify(json)}`);
  return json.result.signedKeyRequest;
}

function buildCastAddMessage({ fid, signerPrivateKey, text, embeds, parentUrl }) {
  if (!text) throw badRequest("text is required");
  if (Buffer.byteLength(text, "utf8") > 320) throw badRequest("cast text must be 320 bytes or fewer");
  const keyPair = nacl.sign.keyPair.fromSeed(Buffer.from(signerPrivateKey));
  const castAddBody = {
    embedsDeprecated: [],
    mentions: [],
    text,
    mentionsPositions: [],
    embeds: embeds.filter(Boolean).map((url) => ({ url })),
    type: 0,
  };
  if (parentUrl) castAddBody.parentUrl = parentUrl;
  const data = MessageData.create({
    type: 1,
    fid,
    timestamp: farcasterTimeNow(),
    network: 1,
    castAddBody,
  });
  const dataBytes = MessageData.encode(data).finish();
  const hash = Buffer.from(blake3(dataBytes).slice(0, 20));
  const signature = Buffer.from(nacl.sign.detached(hash, keyPair.secretKey));
  const message = Message.create({
    data,
    hash,
    hashScheme: 1,
    signature,
    signatureScheme: 1,
    signer: Buffer.from(keyPair.publicKey),
    dataBytes,
  });
  return Buffer.from(Message.encode(message).finish());
}

async function submitMessage(messageBytes) {
  const response = await fetch(`${HYPERSNAP_URL.replace(/\/$/, "")}/v1/submitMessage`, {
    method: "POST",
    headers: { "content-type": "application/octet-stream" },
    body: messageBytes,
  });
  const json = await response.json().catch(async () => ({ error: await response.text() }));
  if (!response.ok) throw new Error(`Hypersnap submitMessage failed ${response.status}: ${JSON.stringify(json)}`);
  return json;
}

function farcasterTimeNow() {
  const now = Math.floor(Date.now() / 1000);
  return now - FARCASTER_EPOCH_SECONDS;
}

async function refreshSigner(record) {
  const request = await getSignedKeyRequest(record.request_token);
  record.state = request.state;
  record.fid = request.userFid || record.fid || null;
  record.approval_url = request.deeplinkUrl || record.approval_url;
  if (isApproved(record.state) && !record.approved_at) record.approved_at = new Date().toISOString();
  await saveSigner(record);
}

function toSignerResponse(record) {
  return {
    signer_uuid: record.signer_uuid,
    public_key: record.public_key,
    request_token: record.request_token,
    approval_url: record.approval_url,
    deeplink_url: record.approval_url,
    state: record.state,
    fid: record.fid,
  };
}

function toSignerStatus(record) {
  return { ...toSignerResponse(record), approved: isApproved(record.state) };
}

function isApproved(state) {
  return ["approved", "completed", "complete"].includes(String(state || "").toLowerCase());
}

async function readDb() {
  return JSON.parse(await fs.readFile(SIGNERS_FILE, "utf8"));
}

async function writeDb(db) {
  await fs.writeFile(SIGNERS_FILE, JSON.stringify(db, null, 2));
}

async function loadSigner(uuid) {
  if (!uuid) throw badRequest("missing signer UUID");
  const db = await readDb();
  const record = db.signers.find((s) => s.signer_uuid === uuid);
  if (!record) {
    const err = new Error("farcaster signer not found");
    err.status = 404;
    throw err;
  }
  return record;
}

async function saveSigner(record) {
  const db = await readDb();
  const idx = db.signers.findIndex((s) => s.signer_uuid === record.signer_uuid);
  if (idx >= 0) db.signers[idx] = record;
  await writeDb(db);
}

function encryptionKey() {
  const secret = process.env.SIGNER_ENCRYPTION_KEY || "";
  const stripped = secret.replace(/^0x/, "");
  if (/^[0-9a-fA-F]{64}$/.test(stripped)) return Buffer.from(stripped, "hex");
  return crypto.createHash("sha256").update(secret).digest();
}

function encrypt(bytes) {
  const nonce = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv("aes-256-gcm", encryptionKey(), nonce);
  const ciphertext = Buffer.concat([cipher.update(Buffer.from(bytes)), cipher.final()]);
  const tag = cipher.getAuthTag();
  return Buffer.concat([nonce, tag, ciphertext]).toString("base64");
}

function decrypt(encoded) {
  const payload = Buffer.from(encoded, "base64");
  const nonce = payload.subarray(0, 12);
  const tag = payload.subarray(12, 28);
  const ciphertext = payload.subarray(28);
  const decipher = crypto.createDecipheriv("aes-256-gcm", encryptionKey(), nonce);
  decipher.setAuthTag(tag);
  return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
}

function badRequest(message) {
  const err = new Error(message);
  err.status = 400;
  return err;
}
EOF
}

write_start_sh() {
  local app_dir="$1"
  local root
  root="$(runtime_dir)"
  cat > "$app_dir/start.sh" <<EOF
#!/usr/bin/env bash
set -euo pipefail

INSTALLER="$root/install.sh"
INSTALLER_URL="\${HYPERSNAP_INSTALLER_URL:-https://lite.hypersnap.lat/install.sh}"

if [ -f "\$INSTALLER" ] && head -n 1 "\$INSTALLER" 2>/dev/null | grep -q "bash"; then
  exec bash "\$INSTALLER" "\$@"
fi

curl -fsSL "\$INSTALLER_URL" | bash -s -- "\$@"
EOF
  chmod +x "$app_dir/start.sh"
}

write_local_app() {
  local root app_dir
  root="$(runtime_dir)"
  app_dir="$root/app"
  mkdir -p "$app_dir" "$root/data" "$root/logs"
  write_package_json "$app_dir"
  write_agents_md "$app_dir"
  write_html "$app_dir"
  write_server "$app_dir"
  write_start_sh "$app_dir"
  local source_script
  source_script="${BASH_SOURCE[0]:-$0}"
  if [ -f "$source_script" ] && grep -q "Hypersnap Lite local installer" "$source_script" 2>/dev/null; then
    cp "$source_script" "$root/install.sh"
    chmod +x "$root/install.sh"
  fi
}

install_node_deps() {
  local app_dir="$1"
  if [ ! -d "$app_dir/node_modules" ] || [ ! -f "$app_dir/package-lock.json" ] || [ "$app_dir/package.json" -nt "$app_dir/package-lock.json" ] || [ "$RESET" -eq 1 ]; then
    log "installing command center dependencies in $app_dir"
    (cd "$app_dir" && npm install --omit=dev)
  else
    log "command center dependencies already installed"
  fi
}

derive_private_key_from_mnemonic() {
  local mnemonic="$1"
  local derivation_path="${DERIVATION_PATH:-}"
  local tmp
  if [ -z "$derivation_path" ]; then
    derivation_path="m/44'/60'/0'/0/0"
  fi
  tmp="$(mktemp -d)"
  npm --prefix "$tmp" install ethers@6 >/dev/null 2>&1
  FARCASTER_MNEMONIC="$mnemonic" DERIVATION_PATH="$derivation_path" NODE_PATH="$tmp/node_modules" node - <<'NODE'
const { HDNodeWallet } = require("ethers");
const phrase = process.env.FARCASTER_MNEMONIC;
const path = process.env.DERIVATION_PATH || "m/44'/60'/0'/0/0";
if (!phrase || !phrase.trim()) throw new Error("mnemonic is empty");
const wallet = HDNodeWallet.fromPhrase(phrase.trim(), undefined, path);
process.stdout.write(`${wallet.address}\t${wallet.privateKey}`);
NODE
  rm -rf "$tmp"
}

latest_signer_uuid() {
  local file
  file="$(runtime_dir)/data/signers.json"
  [ -f "$file" ] || return 1
  node - "$file" <<'NODE'
const fs = require("fs");
const db = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const approved = (db.signers || []).filter((s) => s.fid && ["approved", "completed", "complete"].includes(String(s.state).toLowerCase()));
approved.sort((a, b) => String(b.approved_at || b.created_at).localeCompare(String(a.approved_at || a.created_at)));
if (approved[0]) process.stdout.write(approved[0].signer_uuid);
NODE
}

print_status() {
  local root port hport signer_uuid
  root="$(runtime_dir)"
  port="$(ui_port)"
  hport="$(hypersnap_port)"
  printf '\nHypersnap Lite local status\n'
  printf '  Runtime folder: %s\n' "$root"
  printf '  Command center: http://127.0.0.1:%s\n' "$port"
  printf '  Node status:    http://127.0.0.1:%s/v1/info\n' "$hport"
  if curl -fsS -m 2 "http://127.0.0.1:${hport}/v1/info" >/dev/null 2>&1; then
    printf '  Hypersnap Lite: reachable\n'
  else
    printf '  Hypersnap Lite: not reachable\n'
  fi
  if curl -fsS -m 2 "http://127.0.0.1:${port}/api/health" >/dev/null 2>&1; then
    printf '  Command center: reachable\n'
  else
    printf '  Command center: not reachable\n'
  fi
  signer_uuid="$(latest_signer_uuid || true)"
  if [ -n "$signer_uuid" ]; then
    printf '  Latest signer:  %s\n' "$signer_uuid"
    printf '\nPost a test cast:\n'
    printf '  bash %s/app/start.sh hello-world\n' "$root"
  else
    printf '  Latest signer:  none approved yet\n'
    printf '\nOpen the command center, create a signer, scan the QR, and approve it first.\n'
  fi
  printf '\n'
}

post_hello_world() {
  local port signer_uuid
  port="$(ui_port)"
  signer_uuid="${HELLO_WORLD_SIGNER_UUID:-}"
  if [ -z "$signer_uuid" ]; then
    signer_uuid="$(latest_signer_uuid || true)"
  fi
  if [ -z "$signer_uuid" ]; then
    echo "No approved signer found. Open http://127.0.0.1:${port} and approve a signer first." >&2
    exit 1
  fi
  curl -fsS -X POST "http://127.0.0.1:${port}/api/farcaster/cast" \
    -H 'content-type: application/json' \
    -d "{\"signer_uuid\":\"${signer_uuid}\",\"text\":\"hello world from hypersnap lite\"}"
  printf '\n'
}

write_hypersnap_compose() {
  local compose_path="$1"
  local hypersnap_dir="$2"
  local port root
  port="$(hypersnap_port)"
  root="$(runtime_dir)"
  cat > "$compose_path" <<EOF
services:
  hypersnap:
    image: hypersnap-lite-local:latest
    pull_policy: never
    init: true
    environment:
      RUST_BACKTRACE: "full"
    entrypoint:
      - "/bin/bash"
      - "-c"
      - |
        set -euo pipefail
        cat > config.toml <<'CONFIG'
        rpc_address="0.0.0.0:3383"
        http_address="0.0.0.0:3381"
        rocksdb_dir=".rocks-lite"
        fc_network="Mainnet"
        read_node=true
        lite_node=true

        [statsd]
        prefix="snapchain"
        addr="statsd:8125"
        use_tags=true

        [gossip]
        address="/ip4/0.0.0.0/udp/3382/quic-v1"
        bootstrap_peers = "/ip4/54.236.164.51/udp/3382/quic-v1, /ip4/54.87.204.167/udp/3382/quic-v1, /ip4/44.197.255.20/udp/3382/quic-v1, /ip4/54.157.62.17/udp/3382/quic-v1, /ip4/34.195.157.114/udp/3382/quic-v1, /ip4/107.20.169.236/udp/3382/quic-v1, /ip4/34.4.32.36/udp/3382/quic-v1"
        enable_autodiscovery=true

        [consensus]
        shard_ids = [1,2]
        num_shards = 2

        [hyper]
        enabled=false

        [api]
        enabled=false

        [replication]
        enable=false
        snapshot_interval=28800
        snapshot_max_age="24h"

        [snapshot]
        load_db_from_snapshot=false
        force_load_db_from_snapshot=false
        CONFIG
        cat /app/validators.toml >> config.toml
        exec "\$0" "\$@"
    command: [ "./hypersnap", "--config-path", "config.toml" ]
    restart: unless-stopped
    ports:
      - "${port}:3381/tcp"
      - "3382:3382/udp"
      - "3383:3383/tcp"
    volumes:
      - ${root}/rocks-lite:/app/.rocks-lite
      - ${hypersnap_dir}/validators.toml:/app/validators.toml:ro
    networks:
      - hypersnap
    ulimits:
      nofile:
        soft: 65535
        hard: 65535

  statsd:
    image: graphiteapp/graphite-statsd:1.1.10-5
    restart: unless-stopped
    ports:
      - '8125:8125/udp'
      - '8126:8126'
    networks:
      - hypersnap

networks:
  hypersnap:
    driver: bridge
EOF
}

ensure_hypersnap_image() {
  local hypersnap_dir="$1"
  local repo="${HYPERSNAP_REPO:-https://github.com/jpfraneto/hypersnap.git}"
  local branch="${HYPERSNAP_BRANCH:-hypersnap-lite}"
  local root build_log
  root="$(runtime_dir)"
  build_log="$root/logs/hypersnap-build.log"
  if [ "$REBUILD_HYPERSNAP" -eq 0 ] && docker image inspect hypersnap-lite-local:latest >/dev/null 2>&1; then
    log "Hypersnap Lite image already exists"
    return 0
  fi
  if [ ! -d "$hypersnap_dir/.git" ]; then
    log "downloading Hypersnap Lite source into $hypersnap_dir"
    mkdir -p "$(dirname "$hypersnap_dir")"
    git clone --quiet --branch "$branch" "$repo" "$hypersnap_dir"
  fi
  log "building Hypersnap Lite Docker image; log: $build_log"
  if ! docker build -t hypersnap-lite-local:latest "$hypersnap_dir" >"$build_log" 2>&1; then
    echo "Hypersnap Lite Docker build failed. Last log lines:" >&2
    tail -80 "$build_log" >&2 || true
    exit 1
  fi
}

ensure_hypersnap() {
  local root hypersnap_dir compose_path port
  root="$(runtime_dir)"
  hypersnap_dir="${HYPERSNAP_DIR:-$root/hypersnap}"
  compose_path="$root/docker-compose.hypersnap.yml"
  port="$(hypersnap_port)"
  if curl -fsS -m 2 "http://127.0.0.1:${port}/v1/info" >/dev/null 2>&1; then
    log "Hypersnap Lite is already reachable on port $port"
    return 0
  fi
  ensure_hypersnap_image "$hypersnap_dir"
  mkdir -p "$root/rocks-lite"
  write_hypersnap_compose "$compose_path" "$hypersnap_dir"
  log "starting Hypersnap Lite on port $port"
  docker compose -p hypersnap-lite -f "$compose_path" up -d statsd hypersnap
  wait_for_url "http://127.0.0.1:${port}/v1/info" "Hypersnap Lite" 90
}

start_server() {
  local root app_dir env_file port pid_file log_file
  root="$(runtime_dir)"
  app_dir="$root/app"
  env_file="$root/env"
  port="$(ui_port)"
  pid_file="$root/server.pid"
  log_file="$root/logs/server.log"
  if [ -f "$pid_file" ]; then
    local old_pid
    old_pid="$(cat "$pid_file" 2>/dev/null || true)"
    if [ -n "$old_pid" ] && kill -0 "$old_pid" >/dev/null 2>&1; then
      log "restarting existing command center"
      kill "$old_pid" >/dev/null 2>&1 || true
      sleep 1
    fi
  fi
  if curl -fsS -m 2 "http://127.0.0.1:${port}/api/health" >/dev/null 2>&1; then
    log "command center is already reachable on port $port"
    return 0
  fi
  log "starting command center on port $port"
  setsid bash -c '
    set -euo pipefail
    cd "$1"
    set -a
    . "$2"
    set +a
    exec npm start
  ' bash "$app_dir" "$env_file" </dev/null >"$log_file" 2>&1 &
  echo $! > "$pid_file"
  wait_for_url "http://127.0.0.1:${port}/api/health" "Command center" 60
}

write_env() {
  local env_file="$1"
  cat > "$env_file" <<EOF
HYPERSNAP_LITE_HOME=$(runtime_dir)
HYPERSNAP_UI_PORT=$(ui_port)
HYPERSNAP_LITE_URL=http://127.0.0.1:$(hypersnap_port)
FARCASTER_CLIENT_API=https://api.farcaster.xyz
FARCASTER_APP_FID=$FARCASTER_APP_FID_VALUE
FARCASTER_APP_PRIVATE_KEY=$FARCASTER_APP_PRIVATE_KEY_VALUE
SIGNER_ENCRYPTION_KEY=$SIGNER_ENCRYPTION_KEY_VALUE
FARCASTER_ACCOUNT=$FARCASTER_ACCOUNT_VALUE
EOF
  chmod 600 "$env_file"
}

main() {
  need_cmd curl
  need_cmd docker
  need_cmd git
  need_cmd node
  need_cmd npm

  local root app_dir env_file url hurl first_run
  root="$(runtime_dir)"
  app_dir="$root/app"
  env_file="$root/env"
  url="http://127.0.0.1:$(ui_port)"
  hurl="http://127.0.0.1:$(hypersnap_port)/v1/info"
  first_run=0

  mkdir -p "$root" "$root/logs" "$root/data"
  chmod 700 "$root"

  if [ "$COMMAND" = "status" ]; then
    print_status
    exit 0
  fi
  if [ "$COMMAND" = "hello-world" ]; then
    print_status
    post_hello_world
    exit 0
  fi

  if [ "$RESET" -eq 1 ] || [ ! -f "$env_file" ] || [ ! -f "$app_dir/server.mjs" ]; then
    first_run=1
  fi

  if [ "$first_run" -eq 1 ]; then
    print_intro
    if ! confirm_tty "Continue with this local install?"; then
      echo "Install cancelled." >&2
      exit 0
    fi
  fi

  FARCASTER_APP_FID_VALUE="${FARCASTER_APP_FID:-}"
  FARCASTER_APP_PRIVATE_KEY_VALUE="${FARCASTER_APP_PRIVATE_KEY:-}"
  SIGNER_ENCRYPTION_KEY_VALUE="${SIGNER_ENCRYPTION_KEY:-}"
  FARCASTER_ACCOUNT_VALUE="${FARCASTER_ACCOUNT:-}"
  if [ "$RESET" -eq 0 ] && [ -f "$env_file" ]; then
    # shellcheck disable=SC1090
    . "$env_file"
    FARCASTER_APP_FID_VALUE="${FARCASTER_APP_FID:-$FARCASTER_APP_FID_VALUE}"
    FARCASTER_APP_PRIVATE_KEY_VALUE="${FARCASTER_APP_PRIVATE_KEY:-$FARCASTER_APP_PRIVATE_KEY_VALUE}"
    SIGNER_ENCRYPTION_KEY_VALUE="${SIGNER_ENCRYPTION_KEY:-$SIGNER_ENCRYPTION_KEY_VALUE}"
    FARCASTER_ACCOUNT_VALUE="${FARCASTER_ACCOUNT:-$FARCASTER_ACCOUNT_VALUE}"
  fi

  if [ -z "$FARCASTER_ACCOUNT_VALUE" ]; then
    if tty_available; then
      read_tty "Farcaster account you plan to connect later by QR (username or FID, optional): " FARCASTER_ACCOUNT_VALUE
    else
      FARCASTER_ACCOUNT_VALUE=""
    fi
  fi
  if [ -z "$FARCASTER_APP_FID_VALUE" ]; then
    cat > /dev/tty <<'EOF'

Developer app identity

To create a Farcaster signer request, the local app currently needs a client/app FID and custody key.
This is not the user account that will post. The posting account connects later by QR in the browser.

EOF
    read_tty "Client/app Farcaster FID: " FARCASTER_APP_FID_VALUE
  fi
  if ! [[ "$FARCASTER_APP_FID_VALUE" =~ ^[0-9]+$ ]] || [ "$FARCASTER_APP_FID_VALUE" = "0" ]; then
    echo "Farcaster app FID must be a non-zero number." >&2
    exit 1
  fi
  if [ -z "$FARCASTER_APP_PRIVATE_KEY_VALUE" ]; then
    cat > /dev/tty <<EOF

Current setup needs the custody mnemonic for client/app FID $FARCASTER_APP_FID_VALUE.
Do not paste the mnemonic for the Farcaster account that will post casts.
That account will approve a signer by QR in the browser.

EOF
    if ! confirm_tty "I understand this is the client/app custody mnemonic, not the posting account mnemonic"; then
      echo "Install paused before mnemonic entry." >&2
      echo "Rerun with FARCASTER_APP_PRIVATE_KEY=0x... if you already have the custody private key." >&2
      exit 1
    fi
    local mnemonic derived derived_address
    read_secret_tty "Paste client/app custody mnemonic for FID $FARCASTER_APP_FID_VALUE. Input is hidden: " mnemonic
    derived="$(derive_private_key_from_mnemonic "$mnemonic")"
    unset mnemonic
    derived_address="${derived%%$'\t'*}"
    FARCASTER_APP_PRIVATE_KEY_VALUE="${derived#*$'\t'}"
    log "derived client/app custody address $derived_address"
  fi
  if [ -z "$SIGNER_ENCRYPTION_KEY_VALUE" ]; then
    if command -v openssl >/dev/null 2>&1; then
      SIGNER_ENCRYPTION_KEY_VALUE="$(openssl rand -hex 32)"
    else
      SIGNER_ENCRYPTION_KEY_VALUE="$(node -e 'console.log(require("crypto").randomBytes(32).toString("hex"))')"
    fi
  fi

  write_local_app
  install_node_deps "$app_dir"
  write_env "$env_file"
  log "wrote local config to $env_file"
  ensure_hypersnap
  start_server
  open_url "$url"

  cat <<EOF

Hypersnap Lite is running.

What just happened:
  - No app monorepo was cloned.
  - Tiny command center was written to:
    $app_dir
  - Runtime data is under:
    $root
  - Signers are stored locally in:
    $root/data/signers.json
  - Hypersnap Lite node data is in:
    $root/rocks-lite

Open:
  $url

Node status:
  $hurl

Next:
  1. Create signer
  2. Scan the QR with the Farcaster account you want to connect${FARCASTER_ACCOUNT_VALUE:+: $FARCASTER_ACCOUNT_VALUE}
  3. Poll until approved
  4. Post cast
  5. Click "Copy AGENTS.md" to hand this context to an LLM

Check status:
  bash $app_dir/start.sh status

Post hello world after signer approval:
  bash $app_dir/start.sh hello-world

Logs:
  $root/logs/server.log
EOF
}

main "$@"
