#!/usr/bin/env python3
"""Claude Code Bridge — client agent.

Runs on your own machine and does the actual work there: it reads and writes
your files, runs your shell commands, and serves the chat UI on localhost. The
only thing it sends elsewhere is the model call, which goes to your bridge —
the machine holding the Claude subscription — where usage caps and the model
allowlist are enforced.

Python 3.8+, standard library only. Works on Linux, macOS and Windows.

    python3 agent.py --bridge http://192.168.1.20:8787 --token <token>

Then open http://localhost:8600 in a browser.
"""

import argparse
import base64
import http.client
import fnmatch
import json
import mimetypes
import os
import platform
import queue
import re
import shutil
import socket
import subprocess
import sys
import threading
import time
import traceback
import urllib.error
import urllib.parse
import urllib.request
import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

VERSION = "1.1.0"
IS_WINDOWS = os.name == "nt"

AGENT_PATH = os.path.abspath(__file__)
SERVER = None
RESTART = threading.Event()

# Populated when the bridge serves this file as a single-file download.
EMBEDDED_UI = {"api.js":"ZXhwb3J0IGNsYXNzIEh0dHBFcnJvciBleHRlbmRzIEVycm9yIHsKICBjb25zdHJ1Y3RvcihzdGF0dXMsIG1lc3NhZ2UpIHsKICAgIHN1cGVyKG1lc3NhZ2UpOwogICAgdGhpcy5zdGF0dXMgPSBzdGF0dXM7CiAgfQp9Cgphc3luYyBmdW5jdGlvbiByZXF1ZXN0KG1ldGhvZCwgdXJsLCBib2R5KSB7CiAgY29uc3QgcmVzID0gYXdhaXQgZmV0Y2godXJsLCB7CiAgICBtZXRob2QsCiAgICBoZWFkZXJzOiBib2R5ID09PSB1bmRlZmluZWQgPyB7fSA6IHsgJ2NvbnRlbnQtdHlwZSc6ICdhcHBsaWNhdGlvbi9qc29uJyB9LAogICAgYm9keTogYm9keSA9PT0gdW5kZWZpbmVkID8gdW5kZWZpbmVkIDogSlNPTi5zdHJpbmdpZnkoYm9keSksCiAgfSk7CiAgY29uc3QgdGV4dCA9IGF3YWl0IHJlcy50ZXh0KCk7CiAgY29uc3QgZGF0YSA9IHRleHQgPyBKU09OLnBhcnNlKHRleHQpIDoge307CiAgaWYgKCFyZXMub2spIHRocm93IG5ldyBIdHRwRXJyb3IocmVzLnN0YXR1cywgZGF0YS5lcnJvciA/PyBgUmVxdWVzdCBmYWlsZWQgKCR7cmVzLnN0YXR1c30pYCk7CiAgcmV0dXJuIGRhdGE7Cn0KCmV4cG9ydCBjb25zdCBhcGkgPSB7CiAgYm9vdHN0cmFwOiAoKSA9PiByZXF1ZXN0KCdHRVQnLCAnL2FwaS9ib290c3RyYXAnKSwKICBzYXZlU2V0dGluZ3M6IChwYXlsb2FkKSA9PiByZXF1ZXN0KCdQT1NUJywgJy9hcGkvY29uZmlnJywgcGF5bG9hZCksCiAgYWRkUm9vdDogKHBhdGgpID0+IHJlcXVlc3QoJ1BPU1QnLCAnL2FwaS9yb290cycsIHsgcGF0aCB9KSwKICB1c2FnZTogKCkgPT4gcmVxdWVzdCgnR0VUJywgJy9hcGkvdXNhZ2UnKSwKICBjaGVja1VwZGF0ZTogKCkgPT4gcmVxdWVzdCgnR0VUJywgJy9hcGkvdXBkYXRlJyksCiAgYXBwbHlVcGRhdGU6ICgpID0+IHJlcXVlc3QoJ1BPU1QnLCAnL2FwaS91cGRhdGUnLCB7fSksCiAgc2Vzc2lvbnM6ICgpID0+IHJlcXVlc3QoJ0dFVCcsICcvYXBpL3Nlc3Npb25zJyksCiAgY3JlYXRlU2Vzc2lvbjogKHBheWxvYWQpID0+IHJlcXVlc3QoJ1BPU1QnLCAnL2FwaS9zZXNzaW9ucycsIHBheWxvYWQpLAogIGRlbGV0ZVNlc3Npb246IChpZCkgPT4gcmVxdWVzdCgnREVMRVRFJywgYC9hcGkvc2Vzc2lvbnMvJHtpZH1gKSwKICBoaXN0b3J5OiAoaWQpID0+IHJlcXVlc3QoJ0dFVCcsIGAvYXBpL3Nlc3Npb25zLyR7aWR9L2hpc3RvcnlgKSwKICBzZW5kTWVzc2FnZTogKGlkLCBwYXlsb2FkKSA9PiByZXF1ZXN0KCdQT1NUJywgYC9hcGkvc2Vzc2lvbnMvJHtpZH0vbWVzc2FnZWAsIHBheWxvYWQpLAogIGludGVycnVwdDogKGlkKSA9PiByZXF1ZXN0KCdQT1NUJywgYC9hcGkvc2Vzc2lvbnMvJHtpZH0vaW50ZXJydXB0YCwge30pLAogIHNldE1vZGVsOiAoaWQsIHBheWxvYWQpID0+IHJlcXVlc3QoJ1BPU1QnLCBgL2FwaS9zZXNzaW9ucy8ke2lkfS9tb2RlbGAsIHBheWxvYWQpLAogIGxpc3REaXI6IChwcm9qZWN0LCBwYXRoKSA9PgogICAgcmVxdWVzdCgnR0VUJywgYC9hcGkvZmlsZXM/cHJvamVjdD0ke2VuY29kZVVSSUNvbXBvbmVudChwcm9qZWN0KX0mcGF0aD0ke2VuY29kZVVSSUNvbXBvbmVudChwYXRoKX1gKSwKICByZWFkRmlsZTogKHByb2plY3QsIHBhdGgpID0+CiAgICByZXF1ZXN0KCdHRVQnLCBgL2FwaS9maWxlP3Byb2plY3Q9JHtlbmNvZGVVUklDb21wb25lbnQocHJvamVjdCl9JnBhdGg9JHtlbmNvZGVVUklDb21wb25lbnQocGF0aCl9YCksCn07CgovKioKICogU3Vic2NyaWJlIHRvIGEgc2Vzc2lvbidzIGV2ZW50IHN0cmVhbS4gRXZlbnRTb3VyY2UgaGFuZGxlcyByZWNvbm5lY3Rpb24gYW5kCiAqIHJlcGxheXMgZnJvbSBMYXN0LUV2ZW50LUlELCBzbyBhIGRyb3BwZWQgV2ktRmkgbGluayByZWNvdmVycyBvbiBpdHMgb3duLgogKi8KZXhwb3J0IGZ1bmN0aW9uIG9wZW5TdHJlYW0oc2Vzc2lvbklkLCBsYXN0U2VxLCBvbkV2ZW50LCBvblN0YXRlKSB7CiAgY29uc3QgcXVlcnkgPSBsYXN0U2VxID8gYD9sYXN0RXZlbnRJZD0ke2VuY29kZVVSSUNvbXBvbmVudChsYXN0U2VxKX1gIDogJyc7CiAgY29uc3Qgc291cmNlID0gbmV3IEV2ZW50U291cmNlKGAvYXBpL3Nlc3Npb25zLyR7c2Vzc2lvbklkfS9ldmVudHMke3F1ZXJ5fWApOwogIHNvdXJjZS5hZGRFdmVudExpc3RlbmVyKCdtZXNzYWdlJywgKGV2KSA9PiB7CiAgICB0cnkgewogICAgICBvbkV2ZW50KEpTT04ucGFyc2UoZXYuZGF0YSkpOwogICAgfSBjYXRjaCB7CiAgICAgIC8qIGlnbm9yZSBtYWxmb3JtZWQgZnJhbWUgKi8KICAgIH0KICB9KTsKICBzb3VyY2UuYWRkRXZlbnRMaXN0ZW5lcignb3BlbicsICgpID0+IG9uU3RhdGU/Lignb3BlbicpKTsKICBzb3VyY2UuYWRkRXZlbnRMaXN0ZW5lcignZXJyb3InLCAoKSA9PiBvblN0YXRlPy4oJ3JlY29ubmVjdGluZycpKTsKICByZXR1cm4gc291cmNlOwp9Cg==","app.css":"LyogQ2xhdWRlIENvZGUgQnJpZGdlIOKAlCBjbGllbnQgY29uc29sZQogICBJbnN0cnVtZW50LXBhbmVsIGRpcmVjdGlvbjogcGV0cm9sIGVuYW1lbCwgcGFwZXIgaW5rLCBwaWxvdC1sYW1wIGFtYmVyLiAqLwoKOnJvb3QgewogIC0tcGFuZWwtZGVlcDogIzBjMTcxYjsKICAtLXBhbmVsOiAjMTIyMTI3OwogIC0tcGFuZWwtcmFpc2VkOiAjMTgyZjM3OwogIC0tcnVsZTogIzI2NDU0ZTsKICAtLXJ1bGUtc29mdDogIzFjMzgzZjsKCiAgLS1pbms6ICNlOWU1ZDk7CiAgLS1pbmstZGltOiAjOGVhM2E5OwogIC0taW5rLWZhaW50OiAjNWY3NjdkOwoKICAtLWxpdmU6ICM0ZmJmYTA7CiAgLS1hbWJlcjogI2U4YTMzZDsKICAtLXJ1c3Q6ICNkNDYxNGE7CiAgLS12aW9sZXRpc2g6ICM4ZDlmZDg7CgogIC0tbW9ubzogIkpldEJyYWlucyBNb25vIiwgIklCTSBQbGV4IE1vbm8iLCAiRGVqYVZ1IFNhbnMgTW9ubyIsIHVpLW1vbm9zcGFjZSwgTWVubG8sIENvbnNvbGFzLCBtb25vc3BhY2U7CiAgLS1zYW5zOiAiSW50ZXIiLCAiU2Vnb2UgVUkiLCBzeXN0ZW0tdWksIC1hcHBsZS1zeXN0ZW0sICJIZWx2ZXRpY2EgTmV1ZSIsIEFyaWFsLCBzYW5zLXNlcmlmOwoKICAtLWxlZ2VuZDogNjAwIDEwcHgvMS4yIHZhcigtLW1vbm8pOwogIC0tbGVnZW5kLXRyYWNrOiAwLjE0ZW07CgogIC0tcmFpbDogMjcycHg7CiAgLS1yYWRpdXM6IDNweDsKICAtLXNoYWRvdzogMCAxcHggMCByZ2JhKDI1NSwgMjU1LCAyNTUsIDAuMDQpIGluc2V0LCAwIDhweCAyNHB4IHJnYmEoMCwgMCwgMCwgMC4zNSk7Cn0KCiogeyBib3gtc2l6aW5nOiBib3JkZXItYm94OyB9CgovKiBDbGFzcy1sZXZlbCBkaXNwbGF5IHJ1bGVzIHdvdWxkIG90aGVyd2lzZSBiZWF0IHRoZSBoaWRkZW4gYXR0cmlidXRlLiAqLwpbaGlkZGVuXSB7IGRpc3BsYXk6IG5vbmUgIWltcG9ydGFudDsgfQoKaHRtbCwgYm9keSB7CiAgaGVpZ2h0OiAxMDAlOwogIG1hcmdpbjogMDsKfQoKYm9keSB7CiAgYmFja2dyb3VuZDogdmFyKC0tcGFuZWwtZGVlcCk7CiAgY29sb3I6IHZhcigtLWluayk7CiAgZm9udDogMTVweC8xLjU1IHZhcigtLXNhbnMpOwogIC13ZWJraXQtdGV4dC1zaXplLWFkanVzdDogMTAwJTsKICBvdmVyZmxvdzogaGlkZGVuOwp9CgpidXR0b24sIGlucHV0LCB0ZXh0YXJlYSwgc2VsZWN0IHsgZm9udDogaW5oZXJpdDsgY29sb3I6IGluaGVyaXQ7IH0KCmJ1dHRvbiB7CiAgYmFja2dyb3VuZDogbm9uZTsKICBib3JkZXI6IG5vbmU7CiAgY3Vyc29yOiBwb2ludGVyOwp9Cgo6Zm9jdXMtdmlzaWJsZSB7CiAgb3V0bGluZTogMnB4IHNvbGlkIHZhcigtLWxpdmUpOwogIG91dGxpbmUtb2Zmc2V0OiAycHg7Cn0KCjo6LXdlYmtpdC1zY3JvbGxiYXIgeyB3aWR0aDogMTBweDsgaGVpZ2h0OiAxMHB4OyB9Cjo6LXdlYmtpdC1zY3JvbGxiYXItdGh1bWIgeyBiYWNrZ3JvdW5kOiB2YXIoLS1ydWxlLXNvZnQpOyBib3JkZXItcmFkaXVzOiAxMHB4OyB9Cjo6LXdlYmtpdC1zY3JvbGxiYXItdHJhY2sgeyBiYWNrZ3JvdW5kOiB0cmFuc3BhcmVudDsgfQoKLmxlZ2VuZCB7CiAgZm9udDogdmFyKC0tbGVnZW5kKTsKICBsZXR0ZXItc3BhY2luZzogdmFyKC0tbGVnZW5kLXRyYWNrKTsKICB0ZXh0LXRyYW5zZm9ybTogdXBwZXJjYXNlOwogIGNvbG9yOiB2YXIoLS1pbmstZmFpbnQpOwp9CgovKiAtLS0tLS0tLS0tLS0tLS0tIHNoZWxsIC0tLS0tLS0tLS0tLS0tLS0gKi8KCi5zaGVsbCB7CiAgZGlzcGxheTogZ3JpZDsKICBncmlkLXRlbXBsYXRlLXJvd3M6IGF1dG8gMWZyOwogIC8qIFdpdGhvdXQgdGhpcyB0aGUgc2luZ2xlIGltcGxpY2l0IGNvbHVtbiBpcyBhdXRvLXNpemVkIHRvIG1heC1jb250ZW50LCBhbmQKICAgICBvbmUgbG9uZyBmaWxlIHBhdGggd2lkZW5zIHRoZSBlbnRpcmUgYXBwIHBhc3QgdGhlIHZpZXdwb3J0LiAqLwogIGdyaWQtdGVtcGxhdGUtY29sdW1uczogbWlubWF4KDAsIDFmcik7CiAgaGVpZ2h0OiAxMDB2aDsKICBoZWlnaHQ6IDEwMGR2aDsKfQoKLnRvcGJhciB7CiAgZGlzcGxheTogZmxleDsKICBhbGlnbi1pdGVtczogY2VudGVyOwogIGdhcDogMTZweDsKICBwYWRkaW5nOiAwIDE0cHg7CiAgaGVpZ2h0OiA1MnB4OwogIGJhY2tncm91bmQ6IHZhcigtLXBhbmVsKTsKICBib3JkZXItYm90dG9tOiAxcHggc29saWQgdmFyKC0tcnVsZSk7CiAgYm94LXNoYWRvdzogMCAxcHggMCByZ2JhKDI1NSwgMjU1LCAyNTUsIDAuMDMpIGluc2V0Owp9CgouYnJhbmQgewogIGRpc3BsYXk6IGZsZXg7CiAgYWxpZ24taXRlbXM6IGJhc2VsaW5lOwogIGdhcDogOHB4OwogIGZsZXgtc2hyaW5rOiAwOwp9CgouYnJhbmRfX21hcmsgewogIGZvbnQ6IDcwMCAxM3B4LzEgdmFyKC0tbW9ubyk7CiAgbGV0dGVyLXNwYWNpbmc6IDAuMThlbTsKICBjb2xvcjogdmFyKC0taW5rKTsKfQoKLmJyYW5kX19zdWIgewogIGZvbnQ6IHZhcigtLWxlZ2VuZCk7CiAgbGV0dGVyLXNwYWNpbmc6IHZhcigtLWxlZ2VuZC10cmFjayk7CiAgdGV4dC10cmFuc2Zvcm06IHVwcGVyY2FzZTsKICBjb2xvcjogdmFyKC0taW5rLWZhaW50KTsKfQoKLnRvcGJhcl9fd2hlcmUgewogIGZsZXg6IDE7CiAgbWluLXdpZHRoOiAwOwogIGRpc3BsYXk6IGZsZXg7CiAgYWxpZ24taXRlbXM6IGNlbnRlcjsKICBnYXA6IDhweDsKICBmb250OiAxMnB4LzEgdmFyKC0tbW9ubyk7CiAgY29sb3I6IHZhcigtLWluay1kaW0pOwogIHdoaXRlLXNwYWNlOiBub3dyYXA7CiAgb3ZlcmZsb3c6IGhpZGRlbjsKICB0ZXh0LW92ZXJmbG93OiBlbGxpcHNpczsKfQoKLnRvcGJhcl9fYWN0aW9ucyB7IGRpc3BsYXk6IGZsZXg7IGdhcDogNnB4OyBhbGlnbi1pdGVtczogY2VudGVyOyB9CgouaWNvbmJ0biB7CiAgd2lkdGg6IDM0cHg7CiAgaGVpZ2h0OiAzNHB4OwogIGRpc3BsYXk6IGdyaWQ7CiAgcGxhY2UtaXRlbXM6IGNlbnRlcjsKICBib3JkZXI6IDFweCBzb2xpZCB2YXIoLS1ydWxlLXNvZnQpOwogIGJvcmRlci1yYWRpdXM6IHZhcigtLXJhZGl1cyk7CiAgY29sb3I6IHZhcigtLWluay1kaW0pOwogIGJhY2tncm91bmQ6IHZhcigtLXBhbmVsLXJhaXNlZCk7Cn0KCi5pY29uYnRuOmhvdmVyIHsgY29sb3I6IHZhcigtLWluayk7IGJvcmRlci1jb2xvcjogdmFyKC0tcnVsZSk7IH0KLmljb25idG5bYXJpYS1wcmVzc2VkPSJ0cnVlIl0geyBjb2xvcjogdmFyKC0tbGl2ZSk7IGJvcmRlci1jb2xvcjogdmFyKC0tbGl2ZSk7IH0KCi8qIC0tLS0tLS0tLS0tLS0tLS0gbWV0ZXJzICh0aGUgc2lnbmF0dXJlIGVsZW1lbnQpIC0tLS0tLS0tLS0tLS0tLS0gKi8KCi5tZXRlcnN0cmlwIHsKICBkaXNwbGF5OiBmbGV4OwogIGdhcDogMThweDsKICBhbGlnbi1pdGVtczogY2VudGVyOwogIGZsZXgtc2hyaW5rOiAwOwogIG92ZXJmbG93LXg6IGF1dG87CiAgc2Nyb2xsYmFyLXdpZHRoOiBub25lOwp9CgoubWV0ZXJzdHJpcDo6LXdlYmtpdC1zY3JvbGxiYXIgeyBkaXNwbGF5OiBub25lOyB9CgoubWV0ZXIgeyBtaW4td2lkdGg6IDEzMnB4OyB9CgoubWV0ZXJfX2hlYWQgewogIGRpc3BsYXk6IGZsZXg7CiAganVzdGlmeS1jb250ZW50OiBzcGFjZS1iZXR3ZWVuOwogIGFsaWduLWl0ZW1zOiBiYXNlbGluZTsKICBnYXA6IDhweDsKICBtYXJnaW4tYm90dG9tOiA0cHg7Cn0KCi5tZXRlcl9fbGFiZWwgewogIGZvbnQ6IHZhcigtLWxlZ2VuZCk7CiAgbGV0dGVyLXNwYWNpbmc6IHZhcigtLWxlZ2VuZC10cmFjayk7CiAgdGV4dC10cmFuc2Zvcm06IHVwcGVyY2FzZTsKICBjb2xvcjogdmFyKC0taW5rLWZhaW50KTsKICB3aGl0ZS1zcGFjZTogbm93cmFwOwp9CgoubWV0ZXJfX3ZhbHVlIHsKICBmb250OiA2MDAgMTJweC8xIHZhcigtLW1vbm8pOwogIGZvbnQtdmFyaWFudC1udW1lcmljOiB0YWJ1bGFyLW51bXM7CiAgY29sb3I6IHZhcigtLWluay1kaW0pOwp9CgoubWV0ZXJfX3RyYWNrIHsKICBwb3NpdGlvbjogcmVsYXRpdmU7CiAgaGVpZ2h0OiA4cHg7CiAgYm9yZGVyLXJhZGl1czogMXB4OwogIGJhY2tncm91bmQ6CiAgICByZXBlYXRpbmctbGluZWFyLWdyYWRpZW50KDkwZGVnLCByZ2JhKDAsIDAsIDAsIDAuNTUpIDAgMXB4LCB0cmFuc3BhcmVudCAxcHggNXB4KSwKICAgIHZhcigtLXJ1bGUtc29mdCk7CiAgb3ZlcmZsb3c6IGhpZGRlbjsKfQoKLm1ldGVyX19maWxsIHsKICBwb3NpdGlvbjogYWJzb2x1dGU7CiAgaW5zZXQ6IDAgYXV0byAwIDA7CiAgYmFja2dyb3VuZDogdmFyKC0tbGl2ZSk7CiAgdHJhbnNpdGlvbjogd2lkdGggNDIwbXMgY3ViaWMtYmV6aWVyKDAuMiwgMC43LCAwLjIsIDEpLCBiYWNrZ3JvdW5kLWNvbG9yIDMwMG1zOwp9CgoubWV0ZXJfX25vdGNoIHsKICBwb3NpdGlvbjogYWJzb2x1dGU7CiAgdG9wOiAtMnB4OwogIGJvdHRvbTogLTJweDsKICB3aWR0aDogMnB4OwogIHRyYW5zZm9ybTogdHJhbnNsYXRlWCgtMXB4KTsKICBiYWNrZ3JvdW5kOiB2YXIoLS1pbmstZmFpbnQpOwp9CgoubWV0ZXJfX25vdGNoLS1ibG9jayB7IGJhY2tncm91bmQ6IHZhcigtLXJ1c3QpOyB9Ci5tZXRlcl9fbm90Y2gtLXdhcm4geyBiYWNrZ3JvdW5kOiB2YXIoLS1hbWJlcik7IG9wYWNpdHk6IDAuNzU7IH0KCi5tZXRlcltkYXRhLWxldmVsPSJ3YXJuIl0gLm1ldGVyX19maWxsIHsgYmFja2dyb3VuZDogdmFyKC0tYW1iZXIpOyB9Ci5tZXRlcltkYXRhLWxldmVsPSJibG9jayJdIC5tZXRlcl9fZmlsbCB7IGJhY2tncm91bmQ6IHZhcigtLXJ1c3QpOyB9Ci5tZXRlcltkYXRhLWxldmVsPSJ3YXJuIl0gLm1ldGVyX192YWx1ZSB7IGNvbG9yOiB2YXIoLS1hbWJlcik7IH0KLm1ldGVyW2RhdGEtbGV2ZWw9ImJsb2NrIl0gLm1ldGVyX192YWx1ZSB7IGNvbG9yOiB2YXIoLS1ydXN0KTsgfQoubWV0ZXJbZGF0YS1sZXZlbD0ib2ZmIl0gLm1ldGVyX19maWxsIHsgYmFja2dyb3VuZDogdmFyKC0taW5rLWZhaW50KTsgfQoKLm1ldGVyX19mb290IHsKICBtYXJnaW4tdG9wOiA0cHg7CiAgZm9udDogMTBweC8xIHZhcigtLW1vbm8pOwogIGNvbG9yOiB2YXIoLS1pbmstZmFpbnQpOwogIGZvbnQtdmFyaWFudC1udW1lcmljOiB0YWJ1bGFyLW51bXM7Cn0KCi8qIC0tLS0tLS0tLS0tLS0tLS0gYm9keSBsYXlvdXQgLS0tLS0tLS0tLS0tLS0tLSAqLwoKLmJvZHkgewogIGRpc3BsYXk6IGdyaWQ7CiAgLyogbWlubWF4KDAsIDFmcikgbGV0cyB0aGUgY29sdW1uIHNocmluayBiZWxvdyBpdHMgY29udGVudDsgYSBwbGFpbiAxZnIKICAgICB0cmFjayBpcyBmbG9vcmVkIGF0IG1heC1jb250ZW50LCB3aGljaCBsb25nIGZpbGUgcGF0aHMgdGhlbiBibG93IHBhc3QuICovCiAgZ3JpZC10ZW1wbGF0ZS1jb2x1bW5zOiB2YXIoLS1yYWlsKSBtaW5tYXgoMCwgMWZyKTsKICBncmlkLXRlbXBsYXRlLWFyZWFzOiAicmFpbCBtYWluIjsKICBtaW4taGVpZ2h0OiAwOwogIGhlaWdodDogMTAwJTsKfQoKLmJvZHlbZGF0YS1maWxlcz0ib3BlbiJdIHsKICBncmlkLXRlbXBsYXRlLWNvbHVtbnM6IHZhcigtLXJhaWwpIG1pbm1heCgwLCAxZnIpIDMyMHB4OwogIGdyaWQtdGVtcGxhdGUtYXJlYXM6ICJyYWlsIG1haW4gZmlsZXMiOwp9CgovKiBOYW1lZCBhcmVhcyBrZWVwIHRoZSBsYXlvdXQgc3RhYmxlIHdoZW4gYSByZWdpb24gaXMgaGlkZGVuOiBhIGhpZGRlbgogICBlbGVtZW50IHN0b3BzIGJlaW5nIGEgZ3JpZCBpdGVtLCBhbmQgcG9zaXRpb25hbCBwbGFjZW1lbnQgd291bGQgc2hpZnQKICAgZXZlcnl0aGluZyBhZnRlciBpdCBpbnRvIHRoZSB3cm9uZyB0cmFjay4gKi8KLnJhaWwgeyBncmlkLWFyZWE6IHJhaWw7IH0KLm1haW4geyBncmlkLWFyZWE6IG1haW47IH0KLmZpbGVzIHsgZ3JpZC1hcmVhOiBmaWxlczsgfQoKLnJhaWwgewogIGJhY2tncm91bmQ6IHZhcigtLXBhbmVsKTsKICBib3JkZXItcmlnaHQ6IDFweCBzb2xpZCB2YXIoLS1ydWxlKTsKICBkaXNwbGF5OiBmbGV4OwogIGZsZXgtZGlyZWN0aW9uOiBjb2x1bW47CiAgbWluLWhlaWdodDogMDsKfQoKLnJhaWxfX2hlYWQgewogIGRpc3BsYXk6IGZsZXg7CiAgYWxpZ24taXRlbXM6IGNlbnRlcjsKICBqdXN0aWZ5LWNvbnRlbnQ6IHNwYWNlLWJldHdlZW47CiAgcGFkZGluZzogMTJweCAxNHB4IDhweDsKfQoKLnJhaWxfX2xpc3QgewogIG92ZXJmbG93LXk6IGF1dG87CiAgcGFkZGluZzogMCA4cHggMTJweDsKICBmbGV4OiAxOwogIG1pbi1oZWlnaHQ6IDA7Cn0KCi5zZXNzaW9uaXRlbSB7CiAgZGlzcGxheTogYmxvY2s7CiAgd2lkdGg6IDEwMCU7CiAgdGV4dC1hbGlnbjogbGVmdDsKICBwYWRkaW5nOiA5cHggMTBweDsKICBib3JkZXItcmFkaXVzOiB2YXIoLS1yYWRpdXMpOwogIGJvcmRlcjogMXB4IHNvbGlkIHRyYW5zcGFyZW50OwogIG1hcmdpbi1ib3R0b206IDJweDsKfQoKLnNlc3Npb25pdGVtOmhvdmVyIHsgYmFja2dyb3VuZDogdmFyKC0tcGFuZWwtcmFpc2VkKTsgfQoKLnNlc3Npb25pdGVtW2FyaWEtY3VycmVudD0idHJ1ZSJdIHsKICBiYWNrZ3JvdW5kOiB2YXIoLS1wYW5lbC1yYWlzZWQpOwogIGJvcmRlci1jb2xvcjogdmFyKC0tcnVsZSk7Cn0KCi5zZXNzaW9uaXRlbV9fdGl0bGUgewogIGRpc3BsYXk6IGJsb2NrOwogIGZvbnQtc2l6ZTogMTMuNXB4OwogIGNvbG9yOiB2YXIoLS1pbmspOwogIG92ZXJmbG93OiBoaWRkZW47CiAgdGV4dC1vdmVyZmxvdzogZWxsaXBzaXM7CiAgd2hpdGUtc3BhY2U6IG5vd3JhcDsKfQoKLnNlc3Npb25pdGVtX19tZXRhIHsKICBkaXNwbGF5OiBmbGV4OwogIGdhcDogOHB4OwogIGFsaWduLWl0ZW1zOiBjZW50ZXI7CiAgbWFyZ2luLXRvcDogM3B4OwogIGZvbnQ6IDEwcHgvMS4zIHZhcigtLW1vbm8pOwogIGxldHRlci1zcGFjaW5nOiAwLjA2ZW07CiAgY29sb3I6IHZhcigtLWluay1mYWludCk7Cn0KCi5sYW1wIHsKICB3aWR0aDogNnB4OwogIGhlaWdodDogNnB4OwogIGJvcmRlci1yYWRpdXM6IDUwJTsKICBiYWNrZ3JvdW5kOiB2YXIoLS1pbmstZmFpbnQpOwogIGZsZXgtc2hyaW5rOiAwOwp9CgoubGFtcC0tcnVubmluZyB7IGJhY2tncm91bmQ6IHZhcigtLWxpdmUpOyBhbmltYXRpb246IHB1bHNlIDEuNnMgZWFzZS1pbi1vdXQgaW5maW5pdGU7IH0KLmxhbXAtLWlkbGUgeyBiYWNrZ3JvdW5kOiB2YXIoLS1saXZlKTsgb3BhY2l0eTogMC40OyB9CgpAa2V5ZnJhbWVzIHB1bHNlIHsKICAwJSwgMTAwJSB7IG9wYWNpdHk6IDE7IGJveC1zaGFkb3c6IDAgMCAwIDAgcmdiYSg3OSwgMTkxLCAxNjAsIDAuNSk7IH0KICA1MCUgeyBvcGFjaXR5OiAwLjU1OyBib3gtc2hhZG93OiAwIDAgMCA0cHggcmdiYSg3OSwgMTkxLCAxNjAsIDApOyB9Cn0KCi5idG4gewogIGRpc3BsYXk6IGlubGluZS1mbGV4OwogIGFsaWduLWl0ZW1zOiBjZW50ZXI7CiAgZ2FwOiA2cHg7CiAgcGFkZGluZzogN3B4IDEycHg7CiAgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tcnVsZSk7CiAgYm9yZGVyLXJhZGl1czogdmFyKC0tcmFkaXVzKTsKICBiYWNrZ3JvdW5kOiB2YXIoLS1wYW5lbC1yYWlzZWQpOwogIGNvbG9yOiB2YXIoLS1pbmstZGltKTsKICBmb250OiB2YXIoLS1sZWdlbmQpOwogIGxldHRlci1zcGFjaW5nOiB2YXIoLS1sZWdlbmQtdHJhY2spOwogIHRleHQtdHJhbnNmb3JtOiB1cHBlcmNhc2U7Cn0KCi5idG46aG92ZXIgeyBjb2xvcjogdmFyKC0taW5rKTsgYm9yZGVyLWNvbG9yOiB2YXIoLS1saXZlKTsgfQouYnRuOmRpc2FibGVkIHsgb3BhY2l0eTogMC40OyBjdXJzb3I6IG5vdC1hbGxvd2VkOyB9Ci5idG4tLXByaW1hcnkgeyBiYWNrZ3JvdW5kOiB2YXIoLS1saXZlKTsgY29sb3I6ICMwNjIwMWE7IGJvcmRlci1jb2xvcjogdmFyKC0tbGl2ZSk7IGZvbnQtd2VpZ2h0OiA3MDA7IH0KLmJ0bi0tcHJpbWFyeTpob3ZlciB7IGNvbG9yOiAjMDYyMDFhOyBmaWx0ZXI6IGJyaWdodG5lc3MoMS4wOCk7IH0KLmJ0bi0tZGFuZ2VyOmhvdmVyIHsgYm9yZGVyLWNvbG9yOiB2YXIoLS1ydXN0KTsgY29sb3I6IHZhcigtLXJ1c3QpOyB9CgovKiAtLS0tLS0tLS0tLS0tLS0tIGNvbnZlcnNhdGlvbiAtLS0tLS0tLS0tLS0tLS0tICovCgoubWFpbiB7CiAgZGlzcGxheTogZ3JpZDsKICBncmlkLXRlbXBsYXRlLXJvd3M6IGF1dG8gbWlubWF4KDAsIDFmcikgYXV0bzsKICBncmlkLXRlbXBsYXRlLWFyZWFzOiAiYmFubmVyIiAidGhyZWFkIiAiY29tcG9zZXIiOwogIG1pbi1oZWlnaHQ6IDA7CiAgbWluLXdpZHRoOiAwOwogIGJhY2tncm91bmQ6IHZhcigtLXBhbmVsLWRlZXApOwp9CgouYmFubmVyIHsgZ3JpZC1hcmVhOiBiYW5uZXI7IH0KLnRocmVhZCB7IGdyaWQtYXJlYTogdGhyZWFkOyB9Ci5jb21wb3NlciB7IGdyaWQtYXJlYTogY29tcG9zZXI7IH0KCi5iYW5uZXIgewogIGRpc3BsYXk6IGZsZXg7CiAgZ2FwOiAxMHB4OwogIGFsaWduLWl0ZW1zOiBmbGV4LXN0YXJ0OwogIHBhZGRpbmc6IDEwcHggMTZweDsKICBib3JkZXItYm90dG9tOiAxcHggc29saWQgdmFyKC0tcnVsZS1zb2Z0KTsKICBmb250LXNpemU6IDEzcHg7CiAgYmFja2dyb3VuZDogdmFyKC0tcGFuZWwpOwp9CgouYmFubmVyLS13YXJuIHsgY29sb3I6IHZhcigtLWFtYmVyKTsgYm9yZGVyLWxlZnQ6IDNweCBzb2xpZCB2YXIoLS1hbWJlcik7IH0KLmJhbm5lci0tYmxvY2sgeyBjb2xvcjogdmFyKC0tcnVzdCk7IGJvcmRlci1sZWZ0OiAzcHggc29saWQgdmFyKC0tcnVzdCk7IH0KCi5pbmxpbmVtc2cgewogIHBhZGRpbmc6IDlweCAxM3B4OwogIG1hcmdpbi1ib3R0b206IDE0cHg7CiAgYm9yZGVyLXJhZGl1czogdmFyKC0tcmFkaXVzKTsKICBiYWNrZ3JvdW5kOiB2YXIoLS1wYW5lbCk7CiAgYm9yZGVyLWxlZnQ6IDNweCBzb2xpZCB2YXIoLS1hbWJlcik7CiAgY29sb3I6IHZhcigtLWFtYmVyKTsKICBmb250LXNpemU6IDEzcHg7Cn0KCi5pbmxpbmVtc2ctLWVycm9yIHsgYm9yZGVyLWxlZnQtY29sb3I6IHZhcigtLXJ1c3QpOyBjb2xvcjogdmFyKC0tcnVzdCk7IH0KCi50aHJlYWQgewogIHBvc2l0aW9uOiByZWxhdGl2ZTsKICBtaW4td2lkdGg6IDA7CiAgb3ZlcmZsb3cteDogaGlkZGVuOwogIG92ZXJmbG93LXk6IGF1dG87CiAgcGFkZGluZzogMjJweCAxNnB4IDI4cHg7CiAgbWluLWhlaWdodDogMDsKICBzY3JvbGwtYmVoYXZpb3I6IHNtb290aDsKfQoKLnRocmVhZF9faW5uZXIgeyBtYXgtd2lkdGg6IDg2MHB4OyBtaW4td2lkdGg6IDA7IG1hcmdpbjogMCBhdXRvOyB9CgpAbWVkaWEgKHByZWZlcnMtcmVkdWNlZC1tb3Rpb246IHJlZHVjZSkgewogIC50aHJlYWQgeyBzY3JvbGwtYmVoYXZpb3I6IGF1dG87IH0KICAubGFtcC0tcnVubmluZyB7IGFuaW1hdGlvbjogbm9uZTsgfQogIC5tZXRlcl9fZmlsbCB7IHRyYW5zaXRpb246IG5vbmU7IH0KfQoKLnR1cm4geyBtYXJnaW4tYm90dG9tOiAyMHB4OyB9CgoubXNnLS11c2VyIHsKICBiYWNrZ3JvdW5kOiB2YXIoLS1wYW5lbCk7CiAgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tcnVsZS1zb2Z0KTsKICBib3JkZXItbGVmdDogM3B4IHNvbGlkIHZhcigtLXZpb2xldGlzaCk7CiAgYm9yZGVyLXJhZGl1czogdmFyKC0tcmFkaXVzKTsKICBwYWRkaW5nOiAxMXB4IDE0cHg7CiAgbWFyZ2luLWJvdHRvbTogMThweDsKICB3aGl0ZS1zcGFjZTogcHJlLXdyYXA7CiAgb3ZlcmZsb3ctd3JhcDogYW55d2hlcmU7Cn0KCi5tc2ctLWFzc2lzdGFudCB7IG1hcmdpbi1ib3R0b206IDE0cHg7IG92ZXJmbG93LXdyYXA6IGFueXdoZXJlOyB9CgoubXNnLS1hc3Npc3RhbnQgcCB7IG1hcmdpbjogMCAwIDAuODVlbTsgfQoubXNnLS1hc3Npc3RhbnQgcDpsYXN0LWNoaWxkIHsgbWFyZ2luLWJvdHRvbTogMDsgfQoubXNnLS1hc3Npc3RhbnQgaDEsIC5tc2ctLWFzc2lzdGFudCBoMiwgLm1zZy0tYXNzaXN0YW50IGgzIHsKICBmb250LXNpemU6IDE1cHg7CiAgbWFyZ2luOiAxLjJlbSAwIDAuNWVtOwogIGxldHRlci1zcGFjaW5nOiAwLjAxZW07Cn0KLm1zZy0tYXNzaXN0YW50IHVsLCAubXNnLS1hc3Npc3RhbnQgb2wgeyBtYXJnaW46IDAgMCAwLjg1ZW07IHBhZGRpbmctbGVmdDogMS4zZW07IH0KLm1zZy0tYXNzaXN0YW50IGxpIHsgbWFyZ2luLWJvdHRvbTogMC4yNWVtOyB9Ci5tc2ctLWFzc2lzdGFudCBhIHsgY29sb3I6IHZhcigtLWxpdmUpOyB9Cgpjb2RlIHsKICBmb250OiAxMi41cHgvMS41IHZhcigtLW1vbm8pOwogIGJhY2tncm91bmQ6IHZhcigtLXBhbmVsKTsKICBib3JkZXI6IDFweCBzb2xpZCB2YXIoLS1ydWxlLXNvZnQpOwogIGJvcmRlci1yYWRpdXM6IDJweDsKICBwYWRkaW5nOiAwLjFlbSAwLjM1ZW07Cn0KCnByZSB7CiAgbWFyZ2luOiAwIDAgMC45ZW07CiAgcGFkZGluZzogMTFweCAxM3B4OwogIGJhY2tncm91bmQ6IHZhcigtLXBhbmVsKTsKICBib3JkZXI6IDFweCBzb2xpZCB2YXIoLS1ydWxlLXNvZnQpOwogIGJvcmRlci1yYWRpdXM6IHZhcigtLXJhZGl1cyk7CiAgb3ZlcmZsb3cteDogYXV0bzsKfQoKcHJlIGNvZGUgeyBiYWNrZ3JvdW5kOiBub25lOyBib3JkZXI6IG5vbmU7IHBhZGRpbmc6IDA7IH0KCi50aGlua2luZyB7CiAgYm9yZGVyLWxlZnQ6IDJweCBzb2xpZCB2YXIoLS1ydWxlKTsKICBwYWRkaW5nOiAycHggMCAycHggMTJweDsKICBtYXJnaW4tYm90dG9tOiAxMnB4OwogIGNvbG9yOiB2YXIoLS1pbmstZmFpbnQpOwogIGZvbnQtc2l6ZTogMTMuNXB4OwogIGZvbnQtc3R5bGU6IGl0YWxpYzsKICB3aGl0ZS1zcGFjZTogcHJlLXdyYXA7Cn0KCi5hdHRhY2gtcm93IHsgZGlzcGxheTogZmxleDsgZ2FwOiA4cHg7IGZsZXgtd3JhcDogd3JhcDsgbWFyZ2luLWJvdHRvbTogOHB4OyB9Ci5hdHRhY2gtcm93IGltZyB7IG1heC1oZWlnaHQ6IDEyMHB4OyBib3JkZXItcmFkaXVzOiB2YXIoLS1yYWRpdXMpOyBib3JkZXI6IDFweCBzb2xpZCB2YXIoLS1ydWxlKTsgfQoKLyogLS0tLS0tLS0tLS0tLS0tLSB0b29sIGNhcmRzIC0tLS0tLS0tLS0tLS0tLS0gKi8KCi50b29sIHsKICBtYXgtd2lkdGg6IDEwMCU7CiAgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tcnVsZS1zb2Z0KTsKICBib3JkZXItcmFkaXVzOiB2YXIoLS1yYWRpdXMpOwogIGJhY2tncm91bmQ6IHZhcigtLXBhbmVsKTsKICBtYXJnaW4tYm90dG9tOiAxMHB4OwogIG92ZXJmbG93OiBoaWRkZW47Cn0KCi50b29sX19oZWFkIHsKICBkaXNwbGF5OiBmbGV4OwogIGFsaWduLWl0ZW1zOiBjZW50ZXI7CiAgZ2FwOiAxMHB4OwogIHdpZHRoOiAxMDAlOwogIHBhZGRpbmc6IDhweCAxMXB4OwogIHRleHQtYWxpZ246IGxlZnQ7Cn0KCi50b29sX19uYW1lIHsKICBmb250OiA2MDAgMTFweC8xIHZhcigtLW1vbm8pOwogIGxldHRlci1zcGFjaW5nOiAwLjFlbTsKICB0ZXh0LXRyYW5zZm9ybTogdXBwZXJjYXNlOwogIGNvbG9yOiB2YXIoLS1saXZlKTsKICBmbGV4LXNocmluazogMDsKfQoKLnRvb2xfX3N1bW1hcnkgewogIGZvbnQ6IDEycHgvMS40IHZhcigtLW1vbm8pOwogIGNvbG9yOiB2YXIoLS1pbmstZGltKTsKICBvdmVyZmxvdzogaGlkZGVuOwogIHRleHQtb3ZlcmZsb3c6IGVsbGlwc2lzOwogIHdoaXRlLXNwYWNlOiBub3dyYXA7CiAgZmxleDogMTsKICBtaW4td2lkdGg6IDA7Cn0KCi50b29sX19zdGF0dXMgeyBmbGV4LXNocmluazogMDsgfQoudG9vbF9fc3RhdHVzLS1lcnJvciB7IGNvbG9yOiB2YXIoLS1ydXN0KTsgfQoKLnRvb2xfX2JvZHkgewogIGJvcmRlci10b3A6IDFweCBzb2xpZCB2YXIoLS1ydWxlLXNvZnQpOwogIHBhZGRpbmc6IDEwcHggMTFweDsKICBmb250OiAxMnB4LzEuNSB2YXIoLS1tb25vKTsKICB3aGl0ZS1zcGFjZTogcHJlLXdyYXA7CiAgLyogcHJlLXdyYXAgYWxvbmUgd2lsbCBub3QgYnJlYWsgYW4gdW5icm9rZW4gMTIwLWNoYXJhY3RlciBwYXRoLiAqLwogIG92ZXJmbG93LXdyYXA6IGFueXdoZXJlOwogIG92ZXJmbG93LXg6IGF1dG87CiAgY29sb3I6IHZhcigtLWluay1kaW0pOwogIG1heC1oZWlnaHQ6IDQyMHB4OwogIG92ZXJmbG93LXk6IGF1dG87Cn0KCi50b29sX19ib2R5W2hpZGRlbl0geyBkaXNwbGF5OiBub25lOyB9CgoudG9vbFtkYXRhLXN0YXRlPSJlcnJvciJdIC50b29sX19uYW1lIHsgY29sb3I6IHZhcigtLXJ1c3QpOyB9CgouZGlmZiB7IGZvbnQ6IDEycHgvMS41NSB2YXIoLS1tb25vKTsgfQouZGlmZl9fbGluZSB7IGRpc3BsYXk6IGJsb2NrOyBwYWRkaW5nOiAwIDZweDsgd2hpdGUtc3BhY2U6IHByZS13cmFwOyBvdmVyZmxvdy13cmFwOiBhbnl3aGVyZTsgfQouZGlmZl9fbGluZS0tYWRkIHsgYmFja2dyb3VuZDogcmdiYSg3OSwgMTkxLCAxNjAsIDAuMTIpOyBjb2xvcjogI2E3ZTVkMDsgfQouZGlmZl9fbGluZS0tZGVsIHsgYmFja2dyb3VuZDogcmdiYSgyMTIsIDk3LCA3NCwgMC4xMik7IGNvbG9yOiAjZWZhYjljOyB9Ci5kaWZmX19saW5lLS1jdHggeyBjb2xvcjogdmFyKC0taW5rLWZhaW50KTsgfQoKLyogLS0tLS0tLS0tLS0tLS0tLSBjb21wb3NlciAtLS0tLS0tLS0tLS0tLS0tICovCgouY29tcG9zZXIgewogIGJvcmRlci10b3A6IDFweCBzb2xpZCB2YXIoLS1ydWxlKTsKICBiYWNrZ3JvdW5kOiB2YXIoLS1wYW5lbCk7CiAgcGFkZGluZzogMTBweCAxNnB4IDEycHg7Cn0KCi5jb21wb3Nlcl9faW5uZXIgeyBtYXgtd2lkdGg6IDg2MHB4OyBtaW4td2lkdGg6IDA7IG1hcmdpbjogMCBhdXRvOyB9CgouY29tcG9zZXJfX2JveCB7CiAgZGlzcGxheTogZmxleDsKICBhbGlnbi1pdGVtczogZmxleC1lbmQ7CiAgZ2FwOiA4cHg7CiAgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tcnVsZSk7CiAgYm9yZGVyLXJhZGl1czogdmFyKC0tcmFkaXVzKTsKICBiYWNrZ3JvdW5kOiB2YXIoLS1wYW5lbC1yYWlzZWQpOwogIHBhZGRpbmc6IDhweDsKfQoKLmNvbXBvc2VyX19ib3g6Zm9jdXMtd2l0aGluIHsgYm9yZGVyLWNvbG9yOiB2YXIoLS1saXZlKTsgfQouY29tcG9zZXJfX2JveC5pcy1kcmFnZ2luZyB7IGJvcmRlci1jb2xvcjogdmFyKC0tbGl2ZSk7IGJvcmRlci1zdHlsZTogZGFzaGVkOyB9CgouY29tcG9zZXIgdGV4dGFyZWEgewogIGZsZXg6IDE7CiAgbWluLWhlaWdodDogMjRweDsKICBtYXgtaGVpZ2h0OiAxODBweDsKICByZXNpemU6IG5vbmU7CiAgYm9yZGVyOiBub25lOwogIGJhY2tncm91bmQ6IG5vbmU7CiAgb3V0bGluZTogbm9uZTsKICBwYWRkaW5nOiA0cHggMnB4OwogIGxpbmUtaGVpZ2h0OiAxLjU7Cn0KCi5jb21wb3Nlcl9fY29udHJvbHMgewogIGRpc3BsYXk6IGZsZXg7CiAgYWxpZ24taXRlbXM6IGNlbnRlcjsKICBnYXA6IDEwcHg7CiAgbWFyZ2luLXRvcDogOHB4OwogIGZsZXgtd3JhcDogd3JhcDsKfQoKLnNlbGVjdCB7CiAgYmFja2dyb3VuZDogdmFyKC0tcGFuZWwtcmFpc2VkKTsKICBib3JkZXI6IDFweCBzb2xpZCB2YXIoLS1ydWxlLXNvZnQpOwogIGJvcmRlci1yYWRpdXM6IHZhcigtLXJhZGl1cyk7CiAgcGFkZGluZzogNXB4IDhweDsKICBmb250OiAxMXB4LzEgdmFyKC0tbW9ubyk7CiAgbGV0dGVyLXNwYWNpbmc6IDAuMDZlbTsKICBjb2xvcjogdmFyKC0taW5rLWRpbSk7Cn0KCi5zZWxlY3Q6ZGlzYWJsZWQgeyBvcGFjaXR5OiAwLjQ1OyB9CgouY29tcG9zZXJfX2hpbnQgewogIGZsZXg6IDE7CiAgdGV4dC1hbGlnbjogcmlnaHQ7CiAgZm9udDogMTBweC8xIHZhcigtLW1vbm8pOwogIGxldHRlci1zcGFjaW5nOiAwLjA4ZW07CiAgY29sb3I6IHZhcigtLWluay1mYWludCk7CiAgdGV4dC10cmFuc2Zvcm06IHVwcGVyY2FzZTsKfQoKLyogLS0tLS0tLS0tLS0tLS0tLSBmaWxlcyBwYW5lbCAtLS0tLS0tLS0tLS0tLS0tICovCgouZmlsZXMgewogIGJhY2tncm91bmQ6IHZhcigtLXBhbmVsKTsKICBib3JkZXItbGVmdDogMXB4IHNvbGlkIHZhcigtLXJ1bGUpOwogIGRpc3BsYXk6IGZsZXg7CiAgZmxleC1kaXJlY3Rpb246IGNvbHVtbjsKICBtaW4taGVpZ2h0OiAwOwp9CgouZmlsZXNbaGlkZGVuXSB7IGRpc3BsYXk6IG5vbmU7IH0KCi5maWxlc19faGVhZCB7CiAgcGFkZGluZzogMTJweCAxNHB4IDhweDsKICBib3JkZXItYm90dG9tOiAxcHggc29saWQgdmFyKC0tcnVsZS1zb2Z0KTsKfQoKLmZpbGVzX19wYXRoIHsKICBmb250OiAxMXB4LzEuNCB2YXIoLS1tb25vKTsKICBjb2xvcjogdmFyKC0taW5rLWRpbSk7CiAgbWFyZ2luLXRvcDogNHB4OwogIG92ZXJmbG93LXdyYXA6IGFueXdoZXJlOwp9CgouZmlsZXNfX2xpc3QgeyBvdmVyZmxvdy15OiBhdXRvOyBmbGV4OiAxOyBwYWRkaW5nOiA2cHg7IG1pbi1oZWlnaHQ6IDA7IH0KCi5maWxlaXRlbSB7CiAgZGlzcGxheTogZmxleDsKICB3aWR0aDogMTAwJTsKICBnYXA6IDhweDsKICBhbGlnbi1pdGVtczogY2VudGVyOwogIHBhZGRpbmc6IDVweCA4cHg7CiAgYm9yZGVyLXJhZGl1czogdmFyKC0tcmFkaXVzKTsKICBmb250OiAxMi41cHgvMS40IHZhcigtLW1vbm8pOwogIGNvbG9yOiB2YXIoLS1pbmstZGltKTsKICB0ZXh0LWFsaWduOiBsZWZ0Owp9CgouZmlsZWl0ZW06aG92ZXIgeyBiYWNrZ3JvdW5kOiB2YXIoLS1wYW5lbC1yYWlzZWQpOyBjb2xvcjogdmFyKC0taW5rKTsgfQouZmlsZWl0ZW1fX3NpemUgeyBtYXJnaW4tbGVmdDogYXV0bzsgZm9udC1zaXplOiAxMHB4OyBjb2xvcjogdmFyKC0taW5rLWZhaW50KTsgfQouZmlsZWl0ZW0tLWRpciB7IGNvbG9yOiB2YXIoLS12aW9sZXRpc2gpOyB9CgovKiAtLS0tLS0tLS0tLS0tLS0tIGRpYWxvZ3MgLS0tLS0tLS0tLS0tLS0tLSAqLwoKLm92ZXJsYXkgewogIHBvc2l0aW9uOiBmaXhlZDsKICBpbnNldDogMDsKICBiYWNrZ3JvdW5kOiByZ2JhKDYsIDE0LCAxNywgMC44KTsKICBkaXNwbGF5OiBncmlkOwogIHBsYWNlLWl0ZW1zOiBjZW50ZXI7CiAgcGFkZGluZzogMjBweDsKICB6LWluZGV4OiA0MDsKfQoKLm92ZXJsYXlbaGlkZGVuXSB7IGRpc3BsYXk6IG5vbmU7IH0KCi5jYXJkIHsKICB3aWR0aDogbWluKDU2MHB4LCAxMDAlKTsKICBtYXgtaGVpZ2h0OiA4NnZoOwogIG92ZXJmbG93OiBhdXRvOwogIGJhY2tncm91bmQ6IHZhcigtLXBhbmVsKTsKICBib3JkZXI6IDFweCBzb2xpZCB2YXIoLS1ydWxlKTsKICBib3JkZXItcmFkaXVzOiB2YXIoLS1yYWRpdXMpOwogIGJveC1zaGFkb3c6IHZhcigtLXNoYWRvdyk7CiAgcGFkZGluZzogMjBweDsKfQoKLmNhcmQtLXdpZGUgeyB3aWR0aDogbWluKDkwMHB4LCAxMDAlKTsgfQoKLmNhcmQgaDIgewogIG1hcmdpbjogMCAwIDRweDsKICBmb250OiA3MDAgMTVweC8xLjMgdmFyKC0tc2Fucyk7CiAgbGV0dGVyLXNwYWNpbmc6IDAuMDFlbTsKfQoKLmNhcmRfX3N1YiB7IG1hcmdpbjogMCAwIDE4cHg7IGNvbG9yOiB2YXIoLS1pbmstZGltKTsgZm9udC1zaXplOiAxM3B4OyB9CgouZmllbGQgeyBtYXJnaW4tYm90dG9tOiAxNHB4OyB9Ci5maWVsZCBsYWJlbCB7IGRpc3BsYXk6IGJsb2NrOyBtYXJnaW4tYm90dG9tOiA1cHg7IH0KCi5maWVsZCBpbnB1dCwgLmZpZWxkIHNlbGVjdCwgLmZpZWxkIHRleHRhcmVhIHsKICB3aWR0aDogMTAwJTsKICBwYWRkaW5nOiA5cHggMTBweDsKICBiYWNrZ3JvdW5kOiB2YXIoLS1wYW5lbC1kZWVwKTsKICBib3JkZXI6IDFweCBzb2xpZCB2YXIoLS1ydWxlKTsKICBib3JkZXItcmFkaXVzOiB2YXIoLS1yYWRpdXMpOwogIG91dGxpbmU6IG5vbmU7Cn0KCi5maWVsZCBpbnB1dDpmb2N1cywgLmZpZWxkIHNlbGVjdDpmb2N1cyB7IGJvcmRlci1jb2xvcjogdmFyKC0tbGl2ZSk7IH0KCi5jYXJkX19hY3Rpb25zIHsgZGlzcGxheTogZmxleDsgZ2FwOiA4cHg7IGp1c3RpZnktY29udGVudDogZmxleC1lbmQ7IG1hcmdpbi10b3A6IDE4cHg7IH0KCi5lcnJvciB7CiAgY29sb3I6IHZhcigtLXJ1c3QpOwogIGZvbnQtc2l6ZTogMTNweDsKICBtYXJnaW4tdG9wOiAxMHB4OwogIG1pbi1oZWlnaHQ6IDFlbTsKfQoKLmZpbGVwZWVrIHsKICBmb250OiAxMnB4LzEuNiB2YXIoLS1tb25vKTsKICB3aGl0ZS1zcGFjZTogcHJlOwogIG92ZXJmbG93OiBhdXRvOwogIG1heC1oZWlnaHQ6IDYwdmg7CiAgYmFja2dyb3VuZDogdmFyKC0tcGFuZWwtZGVlcCk7CiAgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tcnVsZS1zb2Z0KTsKICBib3JkZXItcmFkaXVzOiB2YXIoLS1yYWRpdXMpOwogIHBhZGRpbmc6IDEycHg7Cn0KCi5lbXB0eSB7CiAgcG9zaXRpb246IGFic29sdXRlOwogIGluc2V0OiAwOwogIGRpc3BsYXk6IGdyaWQ7CiAgcGxhY2UtaXRlbXM6IGNlbnRlcjsKICB0ZXh0LWFsaWduOiBjZW50ZXI7CiAgcGFkZGluZzogMzBweDsKICBjb2xvcjogdmFyKC0taW5rLWZhaW50KTsKfQoKLmVtcHR5X19pbm5lciB7IG1heC13aWR0aDogMzQwcHg7IH0KLmVtcHR5IGgzIHsgZm9udDogNzAwIDE1cHgvMS40IHZhcigtLXNhbnMpOyBjb2xvcjogdmFyKC0taW5rLWRpbSk7IG1hcmdpbjogMCAwIDZweDsgfQouZW1wdHkgcCB7IG1hcmdpbjogMCAwIDE2cHg7IGZvbnQtc2l6ZTogMTMuNXB4OyB9CgovKiAtLS0tLS0tLS0tLS0tLS0tIHJlc3BvbnNpdmUgLS0tLS0tLS0tLS0tLS0tLSAqLwoKLnJhaWxfX3NjcmltIHsgZGlzcGxheTogbm9uZTsgfQoKQG1lZGlhIChtYXgtd2lkdGg6IDkwMHB4KSB7CiAgLmJvZHksCiAgLmJvZHlbZGF0YS1maWxlcz0ib3BlbiJdIHsKICAgIGdyaWQtdGVtcGxhdGUtY29sdW1uczogbWlubWF4KDAsIDFmcik7CiAgICBncmlkLXRlbXBsYXRlLWFyZWFzOiAibWFpbiI7CiAgfQoKICAucmFpbCB7CiAgICBwb3NpdGlvbjogZml4ZWQ7CiAgICB0b3A6IDUycHg7CiAgICBib3R0b206IDA7CiAgICBsZWZ0OiAwOwogICAgd2lkdGg6IDg0dnc7CiAgICBtYXgtd2lkdGg6IHZhcigtLXJhaWwpOwogICAgei1pbmRleDogMzA7CiAgICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTEwMiUpOwogICAgdHJhbnNpdGlvbjogdHJhbnNmb3JtIDIyMG1zIGN1YmljLWJlemllcigwLjIsIDAuNywgMC4yLCAxKTsKICB9CgogIC5ib2R5W2RhdGEtcmFpbD0ib3BlbiJdIC5yYWlsIHsgdHJhbnNmb3JtOiBub25lOyB9CgogIC5ib2R5W2RhdGEtcmFpbD0ib3BlbiJdIC5yYWlsX19zY3JpbSB7CiAgICBkaXNwbGF5OiBibG9jazsKICAgIHBvc2l0aW9uOiBmaXhlZDsKICAgIGluc2V0OiA1MnB4IDAgMDsKICAgIGJhY2tncm91bmQ6IHJnYmEoNiwgMTQsIDE3LCAwLjYpOwogICAgei1pbmRleDogMjA7CiAgfQoKICAuZmlsZXMgewogICAgcG9zaXRpb246IGZpeGVkOwogICAgdG9wOiA1MnB4OwogICAgYm90dG9tOiAwOwogICAgcmlnaHQ6IDA7CiAgICB3aWR0aDogODh2dzsKICAgIHotaW5kZXg6IDMwOwogIH0KCiAgLm1ldGVyc3RyaXAgeyBkaXNwbGF5OiBub25lOyB9CiAgLnRocmVhZCB7IHBhZGRpbmc6IDE2cHggMTJweCAyMnB4OyB9CiAgLmNvbXBvc2VyIHsgcGFkZGluZzogOHB4IDEycHggMTBweDsgfQp9CgpAbWVkaWEgKHByZWZlcnMtcmVkdWNlZC1tb3Rpb246IHJlZHVjZSkgewogIC5yYWlsIHsgdHJhbnNpdGlvbjogbm9uZTsgfQp9Cgoub25seS1uYXJyb3cgeyBkaXNwbGF5OiBub25lOyB9CgpAbWVkaWEgKG1heC13aWR0aDogOTAwcHgpIHsKICAub25seS1uYXJyb3cgeyBkaXNwbGF5OiBncmlkOyB9Cn0KCi51cGRhdGVidG4gewogIGZvbnQ6IGluaGVyaXQ7IGZvbnQtc2l6ZTogMTJweDsgcGFkZGluZzogNHB4IDEwcHg7IGJvcmRlci1yYWRpdXM6IDk5OXB4OyBjdXJzb3I6IHBvaW50ZXI7CiAgY29sb3I6IHZhcigtLWxpdmUpOyBiYWNrZ3JvdW5kOiB0cmFuc3BhcmVudDsgYm9yZGVyOiAxcHggc29saWQgdmFyKC0tbGl2ZSk7IHdoaXRlLXNwYWNlOiBub3dyYXA7Cn0KLnVwZGF0ZWJ0bjpkaXNhYmxlZCB7IG9wYWNpdHk6IC42OyBjdXJzb3I6IHByb2dyZXNzOyB9Cg==","app.js":"aW1wb3J0IHsgYXBpLCBvcGVuU3RyZWFtIH0gZnJvbSAnLi9hcGkuanMnOwppbXBvcnQgeyBlc2NhcGVIdG1sLCBmb3JtYXRCeXRlcywgbWV0ZXJIdG1sLCByZW5kZXJEaWZmLCByZW5kZXJNYXJrZG93biwgdG9vbExhYmVsLCB0b29sU3VtbWFyeSB9IGZyb20gJy4vcmVuZGVyLmpzJzsKCmNvbnN0ICQgPSAoaWQpID0+IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKGlkKTsKCmNvbnN0IGVsID0gewogIHNoZWxsOiAkKCdzaGVsbCcpLAogIGJvZHlHcmlkOiAkKCdib2R5R3JpZCcpLAogIHdoZXJlOiAkKCd3aGVyZScpLAogIG1ldGVyc3RyaXA6ICQoJ21ldGVyc3RyaXAnKSwKICBzZXNzaW9uTGlzdDogJCgnc2Vzc2lvbkxpc3QnKSwKICB0aHJlYWQ6ICQoJ3RocmVhZCcpLAogIHRocmVhZElubmVyOiAkKCd0aHJlYWRJbm5lcicpLAogIGVtcHR5U3RhdGU6ICQoJ2VtcHR5U3RhdGUnKSwKICBub3RpY2U6ICQoJ25vdGljZScpLAogIGlucHV0OiAkKCdpbnB1dCcpLAogIHNlbmQ6ICQoJ3NlbmQnKSwKICBzdG9wOiAkKCdzdG9wJyksCiAgaGludDogJCgnaGludCcpLAogIG1vZGVsU2VsZWN0OiAkKCdtb2RlbFNlbGVjdCcpLAogIGF0dGFjaFJvdzogJCgnYXR0YWNoUm93JyksCiAgYXR0YWNoQnRuOiAkKCdhdHRhY2hCdG4nKSwKICBmaWxlSW5wdXQ6ICQoJ2ZpbGVJbnB1dCcpLAogIGNvbXBvc2VyQm94OiAkKCdjb21wb3NlckJveCcpLAogIGZpbGVzUGFuZWw6ICQoJ2ZpbGVzUGFuZWwnKSwKICBmaWxlc0xpc3Q6ICQoJ2ZpbGVzTGlzdCcpLAogIGZpbGVzUGF0aDogJCgnZmlsZXNQYXRoJyksCiAgZmlsZXNUb2dnbGU6ICQoJ2ZpbGVzVG9nZ2xlJyksCiAgcmVmcmVzaFVzYWdlOiAkKCdyZWZyZXNoVXNhZ2UnKSwKICByYWlsVG9nZ2xlOiAkKCdyYWlsVG9nZ2xlJyksCiAgcmFpbFNjcmltOiAkKCdyYWlsU2NyaW0nKSwKICBzZXR1cE92ZXJsYXk6ICQoJ3NldHVwT3ZlcmxheScpLAogIHNldHVwRm9ybTogJCgnc2V0dXBGb3JtJyksCiAgYnJpZGdlSW5wdXQ6ICQoJ2JyaWRnZUlucHV0JyksCiAgdG9rZW5JbnB1dDogJCgndG9rZW5JbnB1dCcpLAogIHNldHVwRXJyb3I6ICQoJ3NldHVwRXJyb3InKSwKICBzZXR1cFNraXA6ICQoJ3NldHVwU2tpcCcpLAogIHJvb3RJbnB1dDogJCgncm9vdElucHV0JyksCiAgYWRkUm9vdDogJCgnYWRkUm9vdCcpLAogIG5ld092ZXJsYXk6ICQoJ25ld092ZXJsYXknKSwKICBuZXdGb3JtOiAkKCduZXdGb3JtJyksCiAgcHJvamVjdFNlbGVjdDogJCgncHJvamVjdFNlbGVjdCcpLAogIG5ld01vZGVsOiAkKCduZXdNb2RlbCcpLAogIG5ld0Vycm9yOiAkKCduZXdFcnJvcicpLAogIG5ld0NhbmNlbDogJCgnbmV3Q2FuY2VsJyksCiAgcGVla092ZXJsYXk6ICQoJ3BlZWtPdmVybGF5JyksCiAgcGVla05hbWU6ICQoJ3BlZWtOYW1lJyksCiAgcGVla01ldGE6ICQoJ3BlZWtNZXRhJyksCiAgcGVla0JvZHk6ICQoJ3BlZWtCb2R5JyksCiAgcGVla0Nsb3NlOiAkKCdwZWVrQ2xvc2UnKSwKfTsKCmNvbnN0IHN0YXRlID0gewogIGJvb3Q6IG51bGwsCiAgc2Vzc2lvbnM6IFtdLAogIGN1cnJlbnQ6IG51bGwsCiAgc3RyZWFtOiBudWxsLAogIHJ1bm5pbmc6IGZhbHNlLAogIGF0dGFjaG1lbnRzOiBbXSwKICB0b29sQ2FyZHM6IG5ldyBNYXAoKSwKICBsaXZlOiBudWxsLAogIGZpbGVzQ3dkOiAnLicsCn07CgovKiAtLS0tLS0tLS0tLS0tLS0tIHNlbGYtdXBkYXRlIC0tLS0tLS0tLS0tLS0tLS0gKi8KCmNvbnN0IHVwZGF0ZUJ0biA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCd1cGRhdGVCdG4nKTsKCmFzeW5jIGZ1bmN0aW9uIGNoZWNrRm9yVXBkYXRlKCkgewogIHRyeSB7CiAgICBjb25zdCBpbmZvID0gYXdhaXQgYXBpLmNoZWNrVXBkYXRlKCk7CiAgICBpZiAoIWluZm8uYXZhaWxhYmxlKSB7IHVwZGF0ZUJ0bi5oaWRkZW4gPSB0cnVlOyByZXR1cm47IH0KICAgIHVwZGF0ZUJ0bi50ZXh0Q29udGVudCA9IGBVcGRhdGUgdG8gJHtpbmZvLmxhdGVzdH1gOwogICAgdXBkYXRlQnRuLnRpdGxlID0gYFRoaXMgYWdlbnQgaXMgJHtpbmZvLmN1cnJlbnR9OyB0aGUgYnJpZGdlIGhhcyAke2luZm8ubGF0ZXN0fS5gOwogICAgdXBkYXRlQnRuLmhpZGRlbiA9IGZhbHNlOwogIH0gY2F0Y2ggeyAvKiBicmlkZ2UgdW5yZWFjaGFibGU6IHRyeSBhZ2FpbiBsYXRlciAqLyB9Cn0KCnVwZGF0ZUJ0bi5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIGFzeW5jICgpID0+IHsKICBpZiAoIWNvbmZpcm0oJ1VwZGF0ZSB0aGUgYWdlbnQgZnJvbSB0aGUgYnJpZGdlIGFuZCByZXN0YXJ0IGl0PycpKSByZXR1cm47CiAgdXBkYXRlQnRuLmRpc2FibGVkID0gdHJ1ZTsKICB1cGRhdGVCdG4udGV4dENvbnRlbnQgPSAnVXBkYXRpbmfigKYnOwogIHRyeSB7CiAgICBjb25zdCByZXN1bHQgPSBhd2FpdCBhcGkuYXBwbHlVcGRhdGUoKTsKICAgIGlmICghcmVzdWx0LnJlc3RhcnRpbmcpIHsKICAgICAgdXBkYXRlQnRuLnRleHRDb250ZW50ID0gcmVzdWx0LnVwZGF0ZWQgPyAnVXBkYXRlZCDigJQgcmVzdGFydCB0aGUgYWdlbnQnIDogJ1VwIHRvIGRhdGUnOwogICAgICByZXR1cm47CiAgICB9CiAgICAvLyBXYWl0IGZvciB0aGUgbmV3IHByb2Nlc3MgdG8gY29tZSB1cCwgdGhlbiBsb2FkIGl0cyBVSS4KICAgIGZvciAobGV0IGkgPSAwOyBpIDwgNjA7IGkgKz0gMSkgewogICAgICBhd2FpdCBuZXcgUHJvbWlzZSgocikgPT4gc2V0VGltZW91dChyLCAxMDAwKSk7CiAgICAgIHRyeSB7CiAgICAgICAgY29uc3QgYm9vdCA9IGF3YWl0IGFwaS5ib290c3RyYXAoKTsKICAgICAgICBpZiAoYm9vdC5hZ2VudD8udmVyc2lvbiA9PT0gcmVzdWx0LnZlcnNpb24pIHsgbG9jYXRpb24ucmVsb2FkKCk7IHJldHVybjsgfQogICAgICB9IGNhdGNoIHsgLyogc3RpbGwgcmVzdGFydGluZyAqLyB9CiAgICB9CiAgICB1cGRhdGVCdG4udGV4dENvbnRlbnQgPSAnUmVzdGFydCB0aGUgYWdlbnQgbWFudWFsbHknOwogIH0gY2F0Y2ggKGVycikgewogICAgdXBkYXRlQnRuLmRpc2FibGVkID0gZmFsc2U7CiAgICB1cGRhdGVCdG4udGV4dENvbnRlbnQgPSAnVXBkYXRlIGZhaWxlZCDigJQgcmV0cnknOwogICAgdXBkYXRlQnRuLnRpdGxlID0gZXJyLm1lc3NhZ2U7CiAgfQp9KTsKCnNldEludGVydmFsKGNoZWNrRm9yVXBkYXRlLCAzMCAqIDYwICogMTAwMCk7CgovKiAtLS0tLS0tLS0tLS0tLS0tIGJvb3QgLS0tLS0tLS0tLS0tLS0tLSAqLwoKYXN5bmMgZnVuY3Rpb24gYm9vdCgpIHsKICBzdGF0ZS5ib290ID0gYXdhaXQgYXBpLmJvb3RzdHJhcCgpOwogIGVsLnNoZWxsLmhpZGRlbiA9IGZhbHNlOwogIGNoZWNrRm9yVXBkYXRlKCk7CiAgLy8gVGhlIGFnZW50IHJ1bnMgbG9jYWxseSwgc28gbm90aGluZyBoZXJlIGlzIGdhdGVkIG9uIGEgcGFzc3dvcmQuIFdoYXQgaXQKICAvLyBkb2VzIG5lZWQgaXMgc29tZXdoZXJlIHRvIHNlbmQgbW9kZWwgY2FsbHMuCiAgaWYgKCFzdGF0ZS5ib290LnNldHRpbmdzLmNvbm5lY3RlZCkgewogICAgZWwuYnJpZGdlSW5wdXQudmFsdWUgPSBzdGF0ZS5ib290LnNldHRpbmdzLmJyaWRnZSA/PyAnJzsKICAgIGVsLnNldHVwRXJyb3IudGV4dENvbnRlbnQgPSBzdGF0ZS5ib290LnNldHRpbmdzLmVycm9yID8/ICcnOwogICAgZWwuc2V0dXBPdmVybGF5LmhpZGRlbiA9IGZhbHNlOwogICAgZWwudG9rZW5JbnB1dC5mb2N1cygpOwogIH0gZWxzZSB7CiAgICBlbC5zZXR1cE92ZXJsYXkuaGlkZGVuID0gdHJ1ZTsKICB9CiAgc3RhdGUuc2Vzc2lvbnMgPSBzdGF0ZS5ib290LnNlc3Npb25zOwogIHBhaW50TWV0ZXJzKHN0YXRlLmJvb3QudXNhZ2UpOwogIHBhaW50U2Vzc2lvbnMoKTsKICBwYWludE1vZGVsT3B0aW9ucygpOwogIGNvbnN0IGxhc3QgPSBsb2NhbFN0b3JhZ2UuZ2V0SXRlbSgnY2NiOmxhc3RTZXNzaW9uJyk7CiAgaWYgKGxhc3QgJiYgc3RhdGUuc2Vzc2lvbnMuc29tZSgocykgPT4gcy5pZCA9PT0gbGFzdCkpIGF3YWl0IG9wZW5TZXNzaW9uKGxhc3QpOwogIGVsc2UgcGFpbnRFbXB0eSgpOwp9CgplbC5zZXR1cEZvcm0uYWRkRXZlbnRMaXN0ZW5lcignc3VibWl0JywgYXN5bmMgKGV2ZW50KSA9PiB7CiAgZXZlbnQucHJldmVudERlZmF1bHQoKTsKICBlbC5zZXR1cEVycm9yLnRleHRDb250ZW50ID0gJ0NoZWNraW5n4oCmJzsKICB0cnkgewogICAgY29uc3QgcmVzdWx0ID0gYXdhaXQgYXBpLnNhdmVTZXR0aW5ncyh7CiAgICAgIGJyaWRnZTogZWwuYnJpZGdlSW5wdXQudmFsdWUudHJpbSgpLAogICAgICB0b2tlbjogZWwudG9rZW5JbnB1dC52YWx1ZS50cmltKCksCiAgICB9KTsKICAgIGlmICghcmVzdWx0Lm9rKSB7CiAgICAgIGVsLnNldHVwRXJyb3IudGV4dENvbnRlbnQgPSByZXN1bHQuZXJyb3IgPz8gJ1RoZSBicmlkZ2UgcmVmdXNlZCB0aGF0IHRva2VuLic7CiAgICAgIHJldHVybjsKICAgIH0KICAgIGVsLnNldHVwT3ZlcmxheS5oaWRkZW4gPSB0cnVlOwogICAgYXdhaXQgYm9vdCgpOwogIH0gY2F0Y2ggKGVycikgewogICAgZWwuc2V0dXBFcnJvci50ZXh0Q29udGVudCA9IGVyci5tZXNzYWdlOwogIH0KfSk7CgplbC5zZXR1cFNraXAuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB7IGVsLnNldHVwT3ZlcmxheS5oaWRkZW4gPSB0cnVlOyB9KTsKCi8qIC0tLS0tLS0tLS0tLS0tLS0gbWV0ZXJzIC0tLS0tLS0tLS0tLS0tLS0gKi8KCmZ1bmN0aW9uIHBhaW50TWV0ZXJzKHVzYWdlKSB7CiAgaWYgKCF1c2FnZSkgcmV0dXJuOwogIGlmICghdXNhZ2Uub2sgJiYgIXVzYWdlLmxpbWl0cy5sZW5ndGgpIHsKICAgIGVsLm1ldGVyc3RyaXAuaW5uZXJIVE1MID0gYDxkaXYgY2xhc3M9Im1ldGVyIj48ZGl2IGNsYXNzPSJtZXRlcl9faGVhZCI+PHNwYW4gY2xhc3M9Im1ldGVyX19sYWJlbCI+VXNhZ2U8L3NwYW4+PC9kaXY+CiAgICAgIDxkaXYgY2xhc3M9Im1ldGVyX19mb290Ij4ke2VzY2FwZUh0bWwodXNhZ2UuZXJyb3IgPz8gJ3VuYXZhaWxhYmxlJyl9PC9kaXY+PC9kaXY+YDsKICAgIHJldHVybjsKICB9CiAgZWwubWV0ZXJzdHJpcC5pbm5lckhUTUwgPSB1c2FnZS5saW1pdHMubWFwKG1ldGVySHRtbCkuam9pbignJyk7CiAgaWYgKHVzYWdlLnBhdXNlZCkgc2hvd05vdGljZSgnYmxvY2snLCAnVGhlIGFkbWluaXN0cmF0b3IgcGF1c2VkIHRoaXMgYnJpZGdlLiBOZXcgdHVybnMgYXJlIHJlZnVzZWQuJyk7Cn0KCmFzeW5jIGZ1bmN0aW9uIHJlZnJlc2hVc2FnZSgpIHsKICB0cnkgewogICAgY29uc3QgZGF0YSA9IGF3YWl0IGFwaS51c2FnZSgpOwogICAgcGFpbnRNZXRlcnMoZGF0YS51c2FnZSk7CiAgICBzdGF0ZS5ib290Lm1vZGVscyA9IGRhdGEubW9kZWxzOwogICAgcGFpbnRNb2RlbE9wdGlvbnMoKTsKICB9IGNhdGNoIHsgLyogdHJhbnNpZW50OyB0aGUgbmV4dCBwb2xsIHJldHJpZXMgKi8gfQp9CgplbC5yZWZyZXNoVXNhZ2UuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCByZWZyZXNoVXNhZ2UpOwpzZXRJbnRlcnZhbChyZWZyZXNoVXNhZ2UsIDEyMF8wMDApOwoKLyogLS0tLS0tLS0tLS0tLS0tLSBzZXNzaW9ucyAtLS0tLS0tLS0tLS0tLS0tICovCgpmdW5jdGlvbiBwYWludFNlc3Npb25zKCkgewogIGlmICghc3RhdGUuc2Vzc2lvbnMubGVuZ3RoKSB7CiAgICBlbC5zZXNzaW9uTGlzdC5pbm5lckhUTUwgPSAnPHAgY2xhc3M9ImxlZ2VuZCIgc3R5bGU9InBhZGRpbmc6OHB4IDEwcHgiPk5vIHNlc3Npb25zIHlldDwvcD4nOwogICAgcmV0dXJuOwogIH0KICBlbC5zZXNzaW9uTGlzdC5pbm5lckhUTUwgPSBzdGF0ZS5zZXNzaW9ucy5tYXAoKHNlc3Npb24pID0+IHsKICAgIGNvbnN0IGxhbXBDbGFzcyA9IHNlc3Npb24ucnVubmluZyA/ICdsYW1wLS1ydW5uaW5nJyA6IHNlc3Npb24uYWxpdmUgPyAnbGFtcC0taWRsZScgOiAnJzsKICAgIGNvbnN0IG1vZGVsTGFiZWwgPSAoc3RhdGUuYm9vdC5tb2RlbHMuZmluZCgobSkgPT4gbS5pZCA9PT0gc2Vzc2lvbi5tb2RlbCk/LmxhYmVsKSA/PyBzZXNzaW9uLm1vZGVsOwogICAgcmV0dXJuIGAKICAgICAgPGJ1dHRvbiBjbGFzcz0ic2Vzc2lvbml0ZW0iIGRhdGEtaWQ9IiR7c2Vzc2lvbi5pZH0iIGFyaWEtY3VycmVudD0iJHtzZXNzaW9uLmlkID09PSBzdGF0ZS5jdXJyZW50Py5pZH0iPgogICAgICAgIDxzcGFuIGNsYXNzPSJzZXNzaW9uaXRlbV9fdGl0bGUiPiR7ZXNjYXBlSHRtbChzZXNzaW9uLnRpdGxlIHx8ICdVbnRpdGxlZCBzZXNzaW9uJyl9PC9zcGFuPgogICAgICAgIDxzcGFuIGNsYXNzPSJzZXNzaW9uaXRlbV9fbWV0YSI+CiAgICAgICAgICA8aSBjbGFzcz0ibGFtcCAke2xhbXBDbGFzc30iPjwvaT4KICAgICAgICAgIDxzcGFuPiR7ZXNjYXBlSHRtbChzZXNzaW9uLnByb2plY3ROYW1lID8/ICcnKX08L3NwYW4+CiAgICAgICAgICA8c3Bhbj7Ctzwvc3Bhbj4KICAgICAgICAgIDxzcGFuPiR7ZXNjYXBlSHRtbChtb2RlbExhYmVsKX08L3NwYW4+CiAgICAgICAgPC9zcGFuPgogICAgICA8L2J1dHRvbj5gOwogIH0pLmpvaW4oJycpOwp9CgplbC5zZXNzaW9uTGlzdC5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIChldmVudCkgPT4gewogIGNvbnN0IGJ1dHRvbiA9IGV2ZW50LnRhcmdldC5jbG9zZXN0KCcuc2Vzc2lvbml0ZW0nKTsKICBpZiAoYnV0dG9uKSB7CiAgICB2b2lkIG9wZW5TZXNzaW9uKGJ1dHRvbi5kYXRhc2V0LmlkKTsKICAgIGNsb3NlUmFpbCgpOwogIH0KfSk7CgpmdW5jdGlvbiBwYWludEVtcHR5KCkgewogIGVsLmVtcHR5U3RhdGUuaGlkZGVuID0gc3RhdGUuc2Vzc2lvbnMubGVuZ3RoID4gMCAmJiBCb29sZWFuKHN0YXRlLmN1cnJlbnQpOwogIGlmICghc3RhdGUuY3VycmVudCkgewogICAgZWwuZW1wdHlTdGF0ZS5oaWRkZW4gPSBmYWxzZTsKICAgIGVsLnRocmVhZElubmVyLmlubmVySFRNTCA9ICcnOwogICAgZWwud2hlcmUudGV4dENvbnRlbnQgPSAnTm8gc2Vzc2lvbiBvcGVuJzsKICB9Cn0KCmFzeW5jIGZ1bmN0aW9uIG9wZW5TZXNzaW9uKGlkKSB7CiAgaWYgKHN0YXRlLnN0cmVhbSkgewogICAgc3RhdGUuc3RyZWFtLmNsb3NlKCk7CiAgICBzdGF0ZS5zdHJlYW0gPSBudWxsOwogIH0KICBzdGF0ZS50b29sQ2FyZHMuY2xlYXIoKTsKICBzdGF0ZS5saXZlID0gbnVsbDsKICBlbC50aHJlYWRJbm5lci5pbm5lckhUTUwgPSAnJzsKICBoaWRlTm90aWNlKCk7CgogIGNvbnN0IHNlc3Npb24gPSBzdGF0ZS5zZXNzaW9ucy5maW5kKChzKSA9PiBzLmlkID09PSBpZCk7CiAgaWYgKCFzZXNzaW9uKSByZXR1cm47CiAgc3RhdGUuY3VycmVudCA9IHNlc3Npb247CiAgbG9jYWxTdG9yYWdlLnNldEl0ZW0oJ2NjYjpsYXN0U2Vzc2lvbicsIGlkKTsKICBlbC5lbXB0eVN0YXRlLmhpZGRlbiA9IHRydWU7CiAgZWwud2hlcmUudGV4dENvbnRlbnQgPSBgJHtzZXNzaW9uLnByb2plY3ROYW1lfSDigJQgJHtzZXNzaW9uLnByb2plY3RQYXRofWA7CiAgcGFpbnRTZXNzaW9ucygpOwogIHBhaW50TW9kZWxPcHRpb25zKCk7CiAgc2V0Q29tcG9zZXJFbmFibGVkKHRydWUpOwoKICBjb25zdCBoaXN0b3J5ID0gYXdhaXQgYXBpLmhpc3RvcnkoaWQpOwogIGZvciAoY29uc3QgZXZlbnQgb2YgaGlzdG9yeS5ldmVudHMpIGFwcGx5RXZlbnQoZXZlbnQsIHsgcmVwbGF5OiB0cnVlIH0pOwogIHNjcm9sbFRvRW5kKHRydWUpOwoKICAvLyBSZXN1bWUgYWZ0ZXIgd2hhdCB0aGUgaGlzdG9yeSBhbHJlYWR5IGNvdmVyZWQsIHNvIG5vdGhpbmcgcmVuZGVycyB0d2ljZS4KICBzdGF0ZS5zdHJlYW0gPSBvcGVuU3RyZWFtKGlkLCBoaXN0b3J5Lmxhc3RTZXEsIChldmVudCkgPT4gYXBwbHlFdmVudChldmVudCwge30pLCAoc3RhdHVzKSA9PiB7CiAgICBlbC5oaW50LnRleHRDb250ZW50ID0gc3RhdHVzID09PSAncmVjb25uZWN0aW5nJwogICAgICA/ICdSZWNvbm5lY3RpbmfigKYnCiAgICAgIDogJ0VudGVyIHNlbmRzIMK3IFNoaWZ0K0VudGVyIG5ld2xpbmUnOwogIH0pOwogIHZvaWQgbG9hZEZpbGVzKCcuJyk7Cn0KCi8qIC0tLS0tLS0tLS0tLS0tLS0gbmV3IHNlc3Npb24gLS0tLS0tLS0tLS0tLS0tLSAqLwoKZnVuY3Rpb24gb3Blbk5ld0RpYWxvZygpIHsKICBlbC5uZXdFcnJvci50ZXh0Q29udGVudCA9ICcnOwogIGVsLnByb2plY3RTZWxlY3QuaW5uZXJIVE1MID0gc3RhdGUuYm9vdC5wcm9qZWN0cy5sZW5ndGgKICAgID8gc3RhdGUuYm9vdC5wcm9qZWN0cy5tYXAoKHApID0+IGA8b3B0aW9uIHZhbHVlPSIke2VzY2FwZUh0bWwocC5wYXRoKX0iPiR7ZXNjYXBlSHRtbChwLm5hbWUpfSDigJQgJHtlc2NhcGVIdG1sKHAucGF0aCl9PC9vcHRpb24+YCkuam9pbignJykKICAgIDogJzxvcHRpb24gdmFsdWU9IiI+QWRkIGEgZm9sZGVyIGJlbG93IHRvIHN0YXJ0PC9vcHRpb24+JzsKICBlbC5uZXdNb2RlbC5pbm5lckhUTUwgPSBzdGF0ZS5ib290Lm1vZGVscwogICAgLm1hcCgobSkgPT4gYDxvcHRpb24gdmFsdWU9IiR7bS5pZH0iICR7bS5hbGxvd2VkID8gJycgOiAnZGlzYWJsZWQnfT4ke2VzY2FwZUh0bWwobS5sYWJlbCl9JHttLmFsbG93ZWQgPyAnJyA6ICcg4oCUIG92ZXIgY2FwJ308L29wdGlvbj5gKQogICAgLmpvaW4oJycpOwogIGVsLm5ld01vZGVsLnZhbHVlID0gc3RhdGUuYm9vdC5jb25maWcubW9kZWxzLmRlZmF1bHQgPz8gJyc7CiAgZWwubmV3T3ZlcmxheS5oaWRkZW4gPSBmYWxzZTsKfQoKZWwuYWRkUm9vdC5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIGFzeW5jICgpID0+IHsKICBjb25zdCBwYXRoID0gZWwucm9vdElucHV0LnZhbHVlLnRyaW0oKTsKICBpZiAoIXBhdGgpIHJldHVybjsKICBlbC5uZXdFcnJvci50ZXh0Q29udGVudCA9ICcnOwogIHRyeSB7CiAgICBjb25zdCB7IHJvb3RzIH0gPSBhd2FpdCBhcGkuYWRkUm9vdChwYXRoKTsKICAgIHN0YXRlLmJvb3QucHJvamVjdHMgPSByb290czsKICAgIGVsLnJvb3RJbnB1dC52YWx1ZSA9ICcnOwogICAgb3Blbk5ld0RpYWxvZygpOwogIH0gY2F0Y2ggKGVycikgewogICAgZWwubmV3RXJyb3IudGV4dENvbnRlbnQgPSBlcnIubWVzc2FnZTsKICB9Cn0pOwoKJCgnbmV3U2Vzc2lvbicpLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgb3Blbk5ld0RpYWxvZyk7CiQoJ25ld1Nlc3Npb25FbXB0eScpLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgb3Blbk5ld0RpYWxvZyk7CmVsLm5ld0NhbmNlbC5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHsgZWwubmV3T3ZlcmxheS5oaWRkZW4gPSB0cnVlOyB9KTsKCmVsLm5ld0Zvcm0uYWRkRXZlbnRMaXN0ZW5lcignc3VibWl0JywgYXN5bmMgKGV2ZW50KSA9PiB7CiAgZXZlbnQucHJldmVudERlZmF1bHQoKTsKICBlbC5uZXdFcnJvci50ZXh0Q29udGVudCA9ICcnOwogIHRyeSB7CiAgICBjb25zdCB7IHNlc3Npb24gfSA9IGF3YWl0IGFwaS5jcmVhdGVTZXNzaW9uKHsKICAgICAgcHJvamVjdFBhdGg6IGVsLnByb2plY3RTZWxlY3QudmFsdWUsCiAgICAgIG1vZGVsOiBlbC5uZXdNb2RlbC52YWx1ZSwKICAgIH0pOwogICAgc3RhdGUuc2Vzc2lvbnMudW5zaGlmdCh7IC4uLnNlc3Npb24sIGFsaXZlOiBmYWxzZSwgcnVubmluZzogZmFsc2UgfSk7CiAgICBlbC5uZXdPdmVybGF5LmhpZGRlbiA9IHRydWU7CiAgICBwYWludFNlc3Npb25zKCk7CiAgICBhd2FpdCBvcGVuU2Vzc2lvbihzZXNzaW9uLmlkKTsKICAgIGVsLmlucHV0LmZvY3VzKCk7CiAgfSBjYXRjaCAoZXJyKSB7CiAgICBlbC5uZXdFcnJvci50ZXh0Q29udGVudCA9IGVyci5tZXNzYWdlOwogIH0KfSk7CgovKiAtLS0tLS0tLS0tLS0tLS0tIGV2ZW50IGFwcGxpY2F0aW9uIC0tLS0tLS0tLS0tLS0tLS0gKi8KCmZ1bmN0aW9uIGFwcGx5RXZlbnQoZXZlbnQsIHsgcmVwbGF5ID0gZmFsc2UgfSA9IHt9KSB7CiAgY29uc3QgeyB0eXBlLCBkYXRhIH0gPSBldmVudDsKICBpZiAodHlwZSA9PT0gJ3N0YXR1cycpIHsKICAgIHNldFJ1bm5pbmcoZGF0YS5zdGF0dXMgPT09ICdydW5uaW5nJyk7CiAgICBjb25zdCBzZXNzaW9uID0gc3RhdGUuc2Vzc2lvbnMuZmluZCgocykgPT4gcy5pZCA9PT0gZGF0YS5pZCk7CiAgICBpZiAoc2Vzc2lvbikgewogICAgICBPYmplY3QuYXNzaWduKHNlc3Npb24sIHsgcnVubmluZzogZGF0YS5zdGF0dXMgPT09ICdydW5uaW5nJywgYWxpdmU6IGRhdGEuYWxpdmUsIG1vZGVsOiBkYXRhLm1vZGVsIH0pOwogICAgICBwYWludFNlc3Npb25zKCk7CiAgICB9CiAgICByZXR1cm47CiAgfQogIGlmICh0eXBlID09PSAndXNlcl9tZXNzYWdlJykgeyBhcHBlbmRVc2VyTWVzc2FnZShkYXRhLmNvbnRlbnQpOyByZXR1cm47IH0KICBpZiAodHlwZSA9PT0gJ2Vycm9yJykgeyBhcHBlbmRTeXN0ZW1MaW5lKCdlcnJvcicsIGRhdGEubWVzc2FnZSk7IHJldHVybjsgfQogIGlmICh0eXBlID09PSAnbm90aWNlJykgeyBhcHBlbmRTeXN0ZW1MaW5lKCdub3RpY2UnLCBkYXRhLm1lc3NhZ2UpOyByZXR1cm47IH0KICAvLyBBIHJldHJpZWQgbW9kZWwgY2FsbCBzdGFydHMgb3Zlciwgc28gZHJvcCB0aGUgaGFsZi1zdHJlYW1lZCB0ZXh0LgogIGlmICh0eXBlID09PSAncmV0cnknKSB7IGNsZWFyTGl2ZSgpOyBhcHBlbmRTeXN0ZW1MaW5lKCdub3RpY2UnLCBkYXRhLm1lc3NhZ2UpOyByZXR1cm47IH0KICBpZiAodHlwZSA9PT0gJ2Jsb2NrZWQnKSB7IHNob3dOb3RpY2UoJ2Jsb2NrJywgZGF0YS5yZWFzb25zLmpvaW4oJyAnKSk7IHNldFJ1bm5pbmcoZmFsc2UpOyByZXR1cm47IH0KICBpZiAodHlwZSA9PT0gJ3dhcm5pbmcnKSB7IHNob3dOb3RpY2UoJ3dhcm4nLCB3YXJuaW5nVGV4dChkYXRhKSk7IHJldHVybjsgfQogIGlmICh0eXBlID09PSAnc3RyZWFtX2V2ZW50JykgeyBhcHBseVN0cmVhbURlbHRhKGRhdGEpOyByZXR1cm47IH0KICBpZiAodHlwZSAhPT0gJ2NsYXVkZScpIHJldHVybjsKCiAgY29uc3QgbXNnID0gZGF0YTsKICBpZiAobXNnLnR5cGUgPT09ICdhc3Npc3RhbnQnKSB7IGFwcGVuZEFzc2lzdGFudE1lc3NhZ2UobXNnLm1lc3NhZ2UpOyByZXR1cm47IH0KICBpZiAobXNnLnR5cGUgPT09ICd1c2VyJykgeyBhcHBseVVzZXJUdXJuKG1zZy5tZXNzYWdlLCByZXBsYXkpOyByZXR1cm47IH0KICBpZiAobXNnLnR5cGUgPT09ICdyZXN1bHQnICYmIG1zZy5pc19lcnJvcikgewogICAgYXBwZW5kU3lzdGVtTGluZSgnZXJyb3InLCBtc2cucmVzdWx0ID8/ICdUaGUgdHVybiBlbmRlZCB3aXRoIGFuIGVycm9yLicpOwogIH0KfQoKZnVuY3Rpb24gd2FybmluZ1RleHQodmVyZGljdCkgewogIGNvbnN0IHdvcnN0ID0gdmVyZGljdC53YXJuaW5ncy5maW5kKCh3KSA9PiB3LnBlcmNlbnQgIT0gbnVsbCk7CiAgaWYgKCF3b3JzdCkgcmV0dXJuIHZlcmRpY3Qud2FybmluZ3NbMF0/LmxhYmVsID8/ICdBcHByb2FjaGluZyBhIHVzYWdlIGxpbWl0Lic7CiAgcmV0dXJuIGAke3dvcnN0LmxhYmVsfSBpcyBhdCAke3dvcnN0LnBlcmNlbnR9JSDigJQgdHVybnMgc3RvcCBhdCAke3dvcnN0LmJsb2NrfSUuYDsKfQoKLyoqIExpdmUgdHVybnMgZWNobyB0aGUgdXNlciBtZXNzYWdlIHRoZW1zZWx2ZXM7IHJlcGxheXMgY2FycnkgaXQgaW4gdGhlIHRyYW5zY3JpcHQuICovCmZ1bmN0aW9uIGFwcGx5VXNlclR1cm4obWVzc2FnZSwgcmVwbGF5KSB7CiAgY29uc3QgYmxvY2tzID0gQXJyYXkuaXNBcnJheShtZXNzYWdlPy5jb250ZW50KSA/IG1lc3NhZ2UuY29udGVudCA6IFtdOwogIGNvbnN0IHJlc3VsdHMgPSBibG9ja3MuZmlsdGVyKChiKSA9PiBiLnR5cGUgPT09ICd0b29sX3Jlc3VsdCcpOwogIGZvciAoY29uc3QgcmVzdWx0IG9mIHJlc3VsdHMpIGFwcGx5VG9vbFJlc3VsdChyZXN1bHQpOwogIGNvbnN0IHZpc2libGUgPSBibG9ja3MuZmlsdGVyKChiKSA9PiBiLnR5cGUgPT09ICd0ZXh0JyB8fCBiLnR5cGUgPT09ICdpbWFnZScpOwogIGlmIChyZXBsYXkgJiYgdmlzaWJsZS5sZW5ndGgpIGFwcGVuZFVzZXJNZXNzYWdlKHZpc2libGUpOwp9CgovKiAtLS0tLS0tLS0tLS0tLS0tIHJlbmRlcmluZyBpbnRvIHRoZSB0aHJlYWQgLS0tLS0tLS0tLS0tLS0tLSAqLwoKZnVuY3Rpb24gbmVhckJvdHRvbSgpIHsKICByZXR1cm4gZWwudGhyZWFkLnNjcm9sbEhlaWdodCAtIGVsLnRocmVhZC5zY3JvbGxUb3AgLSBlbC50aHJlYWQuY2xpZW50SGVpZ2h0IDwgMTQwOwp9CgpmdW5jdGlvbiBzY3JvbGxUb0VuZChmb3JjZSA9IGZhbHNlKSB7CiAgaWYgKGZvcmNlIHx8IG5lYXJCb3R0b20oKSkgZWwudGhyZWFkLnNjcm9sbFRvcCA9IGVsLnRocmVhZC5zY3JvbGxIZWlnaHQ7Cn0KCmZ1bmN0aW9uIGFwcGVuZChub2RlKSB7CiAgY29uc3Qgc3RpY2sgPSBuZWFyQm90dG9tKCk7CiAgZWwudGhyZWFkSW5uZXIuYXBwZW5kQ2hpbGQobm9kZSk7CiAgaWYgKHN0aWNrKSBzY3JvbGxUb0VuZCh0cnVlKTsKICByZXR1cm4gbm9kZTsKfQoKZnVuY3Rpb24gYXBwZW5kVXNlck1lc3NhZ2UoY29udGVudCkgewogIGNvbnN0IGJsb2NrcyA9IEFycmF5LmlzQXJyYXkoY29udGVudCkgPyBjb250ZW50IDogW3sgdHlwZTogJ3RleHQnLCB0ZXh0OiBTdHJpbmcoY29udGVudCkgfV07CiAgY29uc3QgaW1hZ2VzID0gYmxvY2tzLmZpbHRlcigoYikgPT4gYi50eXBlID09PSAnaW1hZ2UnKTsKICBjb25zdCB0ZXh0ID0gYmxvY2tzLmZpbHRlcigoYikgPT4gYi50eXBlID09PSAndGV4dCcpLm1hcCgoYikgPT4gYi50ZXh0KS5qb2luKCdcbicpOwogIGNvbnN0IG5vZGUgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCdkaXYnKTsKICBub2RlLmNsYXNzTmFtZSA9ICdtc2ctLXVzZXInOwogIGxldCBodG1sID0gJyc7CiAgaWYgKGltYWdlcy5sZW5ndGgpIHsKICAgIC8vIFJlcGxheWVkIHRyYW5zY3JpcHRzIGFyZSBmaWxlcyBvbiBkaXNrLCBzbyB0aGVpciBtZWRpYSB0eXBlIGFuZCBwYXlsb2FkCiAgICAvLyBhcmUgZXNjYXBlZCBsaWtlIGFueSBvdGhlciB1bnRydXN0ZWQgYXR0cmlidXRlIHZhbHVlLgogICAgaHRtbCArPSBgPGRpdiBjbGFzcz0iYXR0YWNoLXJvdyI+JHtpbWFnZXMKICAgICAgLm1hcCgoaW1nKSA9PiBgPGltZyBhbHQ9IkF0dGFjaGVkIGltYWdlIiBzcmM9ImRhdGE6JHtlc2NhcGVIdG1sKGltZy5zb3VyY2U/Lm1lZGlhX3R5cGUpfTtiYXNlNjQsJHtlc2NhcGVIdG1sKGltZy5zb3VyY2U/LmRhdGEpfSI+YCkKICAgICAgLmpvaW4oJycpfTwvZGl2PmA7CiAgfQogIGh0bWwgKz0gZXNjYXBlSHRtbCh0ZXh0KTsKICBub2RlLmlubmVySFRNTCA9IGh0bWw7CiAgYXBwZW5kKG5vZGUpOwp9CgpmdW5jdGlvbiBhcHBlbmRTeXN0ZW1MaW5lKGtpbmQsIG1lc3NhZ2UpIHsKICBjb25zdCBub2RlID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2Jyk7CiAgbm9kZS5jbGFzc05hbWUgPSBraW5kID09PSAnZXJyb3InID8gJ2lubGluZW1zZyBpbmxpbmVtc2ctLWVycm9yJyA6ICdpbmxpbmVtc2cnOwogIG5vZGUudGV4dENvbnRlbnQgPSBtZXNzYWdlOwogIGFwcGVuZChub2RlKTsKfQoKZnVuY3Rpb24gY2xlYXJMaXZlKCkgewogIGlmIChzdGF0ZS5saXZlPy5ub2RlPy5pc0Nvbm5lY3RlZCkgc3RhdGUubGl2ZS5ub2RlLnJlbW92ZSgpOwogIHN0YXRlLmxpdmUgPSBudWxsOwp9CgpmdW5jdGlvbiBhcHBseVN0cmVhbURlbHRhKHBheWxvYWQpIHsKICBjb25zdCBpbm5lciA9IHBheWxvYWQ/LmV2ZW50OwogIGlmICghaW5uZXIpIHJldHVybjsKICBpZiAoaW5uZXIudHlwZSA9PT0gJ2NvbnRlbnRfYmxvY2tfZGVsdGEnKSB7CiAgICBjb25zdCBkZWx0YSA9IGlubmVyLmRlbHRhID8/IHt9OwogICAgY29uc3QgdGV4dCA9IGRlbHRhLnRleHQgPz8gZGVsdGEudGhpbmtpbmc7CiAgICBpZiAoIXRleHQpIHJldHVybjsKICAgIGNvbnN0IGlzVGhpbmtpbmcgPSBkZWx0YS50eXBlID09PSAndGhpbmtpbmdfZGVsdGEnOwogICAgaWYgKCFzdGF0ZS5saXZlIHx8IHN0YXRlLmxpdmUudGhpbmtpbmcgIT09IGlzVGhpbmtpbmcpIHsKICAgICAgY2xlYXJMaXZlKCk7CiAgICAgIGNvbnN0IG5vZGUgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCdkaXYnKTsKICAgICAgbm9kZS5jbGFzc05hbWUgPSBpc1RoaW5raW5nID8gJ3RoaW5raW5nJyA6ICdtc2ctLWFzc2lzdGFudCc7CiAgICAgIHN0YXRlLmxpdmUgPSB7IG5vZGU6IGFwcGVuZChub2RlKSwgYnVmZmVyOiAnJywgdGhpbmtpbmc6IGlzVGhpbmtpbmcgfTsKICAgIH0KICAgIHN0YXRlLmxpdmUuYnVmZmVyICs9IHRleHQ7CiAgICBzdGF0ZS5saXZlLm5vZGUuaW5uZXJIVE1MID0gc3RhdGUubGl2ZS50aGlua2luZwogICAgICA/IGVzY2FwZUh0bWwoc3RhdGUubGl2ZS5idWZmZXIpCiAgICAgIDogcmVuZGVyTWFya2Rvd24oc3RhdGUubGl2ZS5idWZmZXIpOwogICAgc2Nyb2xsVG9FbmQoKTsKICB9Cn0KCmZ1bmN0aW9uIGFwcGVuZEFzc2lzdGFudE1lc3NhZ2UobWVzc2FnZSkgewogIGNsZWFyTGl2ZSgpOwogIGNvbnN0IGJsb2NrcyA9IEFycmF5LmlzQXJyYXkobWVzc2FnZT8uY29udGVudCkgPyBtZXNzYWdlLmNvbnRlbnQgOiBbXTsKICBmb3IgKGNvbnN0IGJsb2NrIG9mIGJsb2NrcykgewogICAgaWYgKGJsb2NrLnR5cGUgPT09ICd0ZXh0JyAmJiBibG9jay50ZXh0LnRyaW0oKSkgewogICAgICBjb25zdCBub2RlID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2Jyk7CiAgICAgIG5vZGUuY2xhc3NOYW1lID0gJ21zZy0tYXNzaXN0YW50JzsKICAgICAgbm9kZS5pbm5lckhUTUwgPSByZW5kZXJNYXJrZG93bihibG9jay50ZXh0KTsKICAgICAgYXBwZW5kKG5vZGUpOwogICAgfSBlbHNlIGlmIChibG9jay50eXBlID09PSAndGhpbmtpbmcnICYmIGJsb2NrLnRoaW5raW5nKSB7CiAgICAgIGNvbnN0IG5vZGUgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCdkaXYnKTsKICAgICAgbm9kZS5jbGFzc05hbWUgPSAndGhpbmtpbmcnOwogICAgICBub2RlLnRleHRDb250ZW50ID0gYmxvY2sudGhpbmtpbmc7CiAgICAgIGFwcGVuZChub2RlKTsKICAgIH0gZWxzZSBpZiAoYmxvY2sudHlwZSA9PT0gJ3Rvb2xfdXNlJykgewogICAgICBhcHBlbmQoYnVpbGRUb29sQ2FyZChibG9jaykpOwogICAgfQogIH0KfQoKZnVuY3Rpb24gYnVpbGRUb29sQ2FyZChibG9jaykgewogIGNvbnN0IGNhcmQgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCdkaXYnKTsKICBjYXJkLmNsYXNzTmFtZSA9ICd0b29sJzsKICBjYXJkLmRhdGFzZXQuc3RhdGUgPSAncGVuZGluZyc7CiAgY2FyZC5pbm5lckhUTUwgPSBgCiAgICA8YnV0dG9uIGNsYXNzPSJ0b29sX19oZWFkIiB0eXBlPSJidXR0b24iIGFyaWEtZXhwYW5kZWQ9ImZhbHNlIj4KICAgICAgPHNwYW4gY2xhc3M9InRvb2xfX25hbWUiPiR7ZXNjYXBlSHRtbCh0b29sTGFiZWwoYmxvY2submFtZSkpfTwvc3Bhbj4KICAgICAgPHNwYW4gY2xhc3M9InRvb2xfX3N1bW1hcnkiPiR7ZXNjYXBlSHRtbCh0b29sU3VtbWFyeShibG9jay5uYW1lLCBibG9jay5pbnB1dCkpfTwvc3Bhbj4KICAgICAgPHNwYW4gY2xhc3M9InRvb2xfX3N0YXR1cyBsZWdlbmQiPnJ1bm5pbmc8L3NwYW4+CiAgICA8L2J1dHRvbj4KICAgIDxkaXYgY2xhc3M9InRvb2xfX2JvZHkiIGhpZGRlbj48L2Rpdj5gOwoKICBjb25zdCBib2R5ID0gY2FyZC5xdWVyeVNlbGVjdG9yKCcudG9vbF9fYm9keScpOwogIGJvZHkuaW5uZXJIVE1MID0gZGV0YWlsSHRtbChibG9jay5uYW1lLCBibG9jay5pbnB1dCk7CgogIGNvbnN0IGhlYWQgPSBjYXJkLnF1ZXJ5U2VsZWN0b3IoJy50b29sX19oZWFkJyk7CiAgaGVhZC5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHsKICAgIGNvbnN0IG9wZW4gPSBib2R5LmhpZGRlbjsKICAgIGJvZHkuaGlkZGVuID0gIW9wZW47CiAgICBoZWFkLnNldEF0dHJpYnV0ZSgnYXJpYS1leHBhbmRlZCcsIFN0cmluZyhvcGVuKSk7CiAgfSk7CgogIHN0YXRlLnRvb2xDYXJkcy5zZXQoYmxvY2suaWQsIGNhcmQpOwogIHJldHVybiBjYXJkOwp9CgovKiogVG9vbC1zcGVjaWZpYyBkZXRhaWw6IGRpZmZzIGZvciBlZGl0cywgdGhlIGNvbW1hbmQgZm9yIEJhc2gsIEpTT04gb3RoZXJ3aXNlLiAqLwpmdW5jdGlvbiBkZXRhaWxIdG1sKG5hbWUsIGlucHV0ID0ge30pIHsKICBpZiAobmFtZSA9PT0gJ2VkaXRfZmlsZScpIHsKICAgIHJldHVybiByZW5kZXJEaWZmKGlucHV0Lm9sZF9zdHJpbmcgPz8gJycsIGlucHV0Lm5ld19zdHJpbmcgPz8gJycpOwogIH0KICBpZiAobmFtZSA9PT0gJ3dyaXRlX2ZpbGUnKSB7CiAgICByZXR1cm4gYDxkaXYgY2xhc3M9ImRpZmYiPiR7ZXNjYXBlSHRtbChpbnB1dC5jb250ZW50ID8/ICcnKX08L2Rpdj5gOwogIH0KICBpZiAobmFtZSA9PT0gJ3J1bl9zaGVsbCcpIHsKICAgIHJldHVybiBlc2NhcGVIdG1sKGlucHV0LmNvbW1hbmQgPz8gJycpOwogIH0KICByZXR1cm4gZXNjYXBlSHRtbChKU09OLnN0cmluZ2lmeShpbnB1dCwgbnVsbCwgMikpOwp9CgpmdW5jdGlvbiBhcHBseVRvb2xSZXN1bHQocmVzdWx0KSB7CiAgY29uc3QgY2FyZCA9IHN0YXRlLnRvb2xDYXJkcy5nZXQocmVzdWx0LnRvb2xfdXNlX2lkKTsKICBpZiAoIWNhcmQpIHJldHVybjsKICBjb25zdCBpc0Vycm9yID0gQm9vbGVhbihyZXN1bHQuaXNfZXJyb3IpOwogIGNhcmQuZGF0YXNldC5zdGF0ZSA9IGlzRXJyb3IgPyAnZXJyb3InIDogJ2RvbmUnOwogIGNvbnN0IHN0YXR1cyA9IGNhcmQucXVlcnlTZWxlY3RvcignLnRvb2xfX3N0YXR1cycpOwogIHN0YXR1cy50ZXh0Q29udGVudCA9IGlzRXJyb3IgPyAnZmFpbGVkJyA6ICdkb25lJzsKICBzdGF0dXMuY2xhc3NMaXN0LnRvZ2dsZSgndG9vbF9fc3RhdHVzLS1lcnJvcicsIGlzRXJyb3IpOwoKICBjb25zdCB0ZXh0ID0gQXJyYXkuaXNBcnJheShyZXN1bHQuY29udGVudCkKICAgID8gcmVzdWx0LmNvbnRlbnQubWFwKChjKSA9PiAoYy50eXBlID09PSAndGV4dCcgPyBjLnRleHQgOiBgWyR7Yy50eXBlfV1gKSkuam9pbignXG4nKQogICAgOiBTdHJpbmcocmVzdWx0LmNvbnRlbnQgPz8gJycpOwoKICBjb25zdCBib2R5ID0gY2FyZC5xdWVyeVNlbGVjdG9yKCcudG9vbF9fYm9keScpOwogIGNvbnN0IGV4aXN0aW5nID0gYm9keS5pbm5lckhUTUw7CiAgYm9keS5pbm5lckhUTUwgPSBgJHtleGlzdGluZ308aHIgc3R5bGU9ImJvcmRlcjpub25lO2JvcmRlci10b3A6MXB4IHNvbGlkIHZhcigtLXJ1bGUtc29mdCk7bWFyZ2luOjEwcHggMCI+JHtlc2NhcGVIdG1sKAogICAgdGV4dC5sZW5ndGggPiA4MDAwID8gYCR7dGV4dC5zbGljZSgwLCA4MDAwKX1cbuKApiAke3RleHQubGVuZ3RoIC0gODAwMH0gbW9yZSBjaGFyYWN0ZXJzYCA6IHRleHQsCiAgKX1gOwogIGlmIChpc0Vycm9yKSBib2R5LmhpZGRlbiA9IGZhbHNlOwp9CgovKiAtLS0tLS0tLS0tLS0tLS0tIG5vdGljZXMgLS0tLS0tLS0tLS0tLS0tLSAqLwoKZnVuY3Rpb24gc2hvd05vdGljZShraW5kLCBtZXNzYWdlKSB7CiAgZWwubm90aWNlLmhpZGRlbiA9IGZhbHNlOwogIGVsLm5vdGljZS5jbGFzc05hbWUgPSBgYmFubmVyIGJhbm5lci0tJHtraW5kfWA7CiAgZWwubm90aWNlLnRleHRDb250ZW50ID0gbWVzc2FnZTsKfQoKZnVuY3Rpb24gaGlkZU5vdGljZSgpIHsKICBlbC5ub3RpY2UuaGlkZGVuID0gdHJ1ZTsKfQoKLyogLS0tLS0tLS0tLS0tLS0tLSBjb21wb3NlciAtLS0tLS0tLS0tLS0tLS0tICovCgpmdW5jdGlvbiBzZXRDb21wb3NlckVuYWJsZWQoZW5hYmxlZCkgewogIGVsLmlucHV0LmRpc2FibGVkID0gIWVuYWJsZWQ7CiAgZWwuc2VuZC5kaXNhYmxlZCA9ICFlbmFibGVkOwogIGVsLm1vZGVsU2VsZWN0LmRpc2FibGVkID0gIWVuYWJsZWQ7Cn0KCmZ1bmN0aW9uIHNldFJ1bm5pbmcocnVubmluZykgewogIHN0YXRlLnJ1bm5pbmcgPSBydW5uaW5nOwogIGVsLnN0b3AuaGlkZGVuID0gIXJ1bm5pbmc7CiAgZWwuc2VuZC5kaXNhYmxlZCA9IHJ1bm5pbmcgfHwgIXN0YXRlLmN1cnJlbnQ7CiAgZWwuaW5wdXQuZGlzYWJsZWQgPSAhc3RhdGUuY3VycmVudDsKICBlbC5tb2RlbFNlbGVjdC5kaXNhYmxlZCA9IHJ1bm5pbmcgfHwgIXN0YXRlLmN1cnJlbnQ7Cn0KCmZ1bmN0aW9uIHBhaW50TW9kZWxPcHRpb25zKCkgewogIGlmICghc3RhdGUuYm9vdCkgcmV0dXJuOwogIGNvbnN0IGN1cnJlbnQgPSBzdGF0ZS5jdXJyZW50OwogIGVsLm1vZGVsU2VsZWN0LmlubmVySFRNTCA9IHN0YXRlLmJvb3QubW9kZWxzCiAgICAubWFwKChtKSA9PiBgPG9wdGlvbiB2YWx1ZT0iJHttLmlkfSIgJHttLmFsbG93ZWQgPyAnJyA6ICdkaXNhYmxlZCd9PiR7ZXNjYXBlSHRtbChtLmxhYmVsKX0ke20uYWxsb3dlZCA/ICcnIDogJyDigJQgb3ZlciBjYXAnfTwvb3B0aW9uPmApCiAgICAuam9pbignJyk7CiAgaWYgKGN1cnJlbnQpIGVsLm1vZGVsU2VsZWN0LnZhbHVlID0gY3VycmVudC5tb2RlbDsKfQoKZWwubW9kZWxTZWxlY3QuYWRkRXZlbnRMaXN0ZW5lcignY2hhbmdlJywgYXN5bmMgKCkgPT4gewogIGlmICghc3RhdGUuY3VycmVudCkgcmV0dXJuOwogIHRyeSB7CiAgICBjb25zdCB7IHNlc3Npb24gfSA9IGF3YWl0IGFwaS5zZXRNb2RlbChzdGF0ZS5jdXJyZW50LmlkLCB7IG1vZGVsOiBlbC5tb2RlbFNlbGVjdC52YWx1ZSB9KTsKICAgIE9iamVjdC5hc3NpZ24oc3RhdGUuY3VycmVudCwgc2Vzc2lvbik7CiAgICBwYWludFNlc3Npb25zKCk7CiAgfSBjYXRjaCAoZXJyKSB7CiAgICBzaG93Tm90aWNlKCdibG9jaycsIGVyci5tZXNzYWdlKTsKICAgIGVsLm1vZGVsU2VsZWN0LnZhbHVlID0gc3RhdGUuY3VycmVudC5tb2RlbDsKICB9Cn0pOwoKZWwuaW5wdXQuYWRkRXZlbnRMaXN0ZW5lcignaW5wdXQnLCAoKSA9PiB7CiAgZWwuaW5wdXQuc3R5bGUuaGVpZ2h0ID0gJ2F1dG8nOwogIGVsLmlucHV0LnN0eWxlLmhlaWdodCA9IGAke01hdGgubWluKDE4MCwgZWwuaW5wdXQuc2Nyb2xsSGVpZ2h0KX1weGA7Cn0pOwoKZWwuaW5wdXQuYWRkRXZlbnRMaXN0ZW5lcigna2V5ZG93bicsIChldmVudCkgPT4gewogIGlmIChldmVudC5rZXkgPT09ICdFbnRlcicgJiYgIWV2ZW50LnNoaWZ0S2V5KSB7CiAgICBldmVudC5wcmV2ZW50RGVmYXVsdCgpOwogICAgdm9pZCBzdWJtaXQoKTsKICB9Cn0pOwoKZWwuc2VuZC5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHZvaWQgc3VibWl0KCkpOwoKZWwuc3RvcC5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIGFzeW5jICgpID0+IHsKICBpZiAoIXN0YXRlLmN1cnJlbnQpIHJldHVybjsKICBhd2FpdCBhcGkuaW50ZXJydXB0KHN0YXRlLmN1cnJlbnQuaWQpOwp9KTsKCmFzeW5jIGZ1bmN0aW9uIHN1Ym1pdCgpIHsKICBpZiAoIXN0YXRlLmN1cnJlbnQgfHwgc3RhdGUucnVubmluZykgcmV0dXJuOwogIGNvbnN0IHRleHQgPSBlbC5pbnB1dC52YWx1ZS50cmltKCk7CiAgaWYgKCF0ZXh0ICYmICFzdGF0ZS5hdHRhY2htZW50cy5sZW5ndGgpIHJldHVybjsKICBoaWRlTm90aWNlKCk7CgogIGNvbnN0IHBheWxvYWQgPSB7CiAgICB0ZXh0LAogICAgaW1hZ2VzOiBzdGF0ZS5hdHRhY2htZW50cy5tYXAoKHsgbWVkaWFUeXBlLCBkYXRhQmFzZTY0IH0pID0+ICh7IG1lZGlhVHlwZSwgZGF0YUJhc2U2NCB9KSksCiAgfTsKICBlbC5pbnB1dC52YWx1ZSA9ICcnOwogIGVsLmlucHV0LnN0eWxlLmhlaWdodCA9ICdhdXRvJzsKICBjb25zdCBzZW50ID0gc3RhdGUuYXR0YWNobWVudHM7CiAgc3RhdGUuYXR0YWNobWVudHMgPSBbXTsKICBwYWludEF0dGFjaG1lbnRzKCk7CiAgc2V0UnVubmluZyh0cnVlKTsKCiAgdHJ5IHsKICAgIGNvbnN0IHJlcyA9IGF3YWl0IGFwaS5zZW5kTWVzc2FnZShzdGF0ZS5jdXJyZW50LmlkLCBwYXlsb2FkKTsKICAgIGlmIChyZXMucG9saWN5Py5sZXZlbCA9PT0gJ3dhcm4nKSBzaG93Tm90aWNlKCd3YXJuJywgd2FybmluZ1RleHQocmVzLnBvbGljeSkpOwogIH0gY2F0Y2ggKGVycikgewogICAgc2V0UnVubmluZyhmYWxzZSk7CiAgICBzdGF0ZS5hdHRhY2htZW50cyA9IHNlbnQ7CiAgICBwYWludEF0dGFjaG1lbnRzKCk7CiAgICBlbC5pbnB1dC52YWx1ZSA9IHRleHQ7CiAgICBzaG93Tm90aWNlKCdibG9jaycsIGVyci5tZXNzYWdlKTsKICB9Cn0KCi8qIC0tLS0tLS0tLS0tLS0tLS0gYXR0YWNobWVudHMgLS0tLS0tLS0tLS0tLS0tLSAqLwoKZnVuY3Rpb24gcGFpbnRBdHRhY2htZW50cygpIHsKICBlbC5hdHRhY2hSb3cuaW5uZXJIVE1MID0gc3RhdGUuYXR0YWNobWVudHMKICAgIC5tYXAoKGEsIGkpID0+IGA8aW1nIHNyYz0iZGF0YToke2VzY2FwZUh0bWwoYS5tZWRpYVR5cGUpfTtiYXNlNjQsJHtlc2NhcGVIdG1sKGEuZGF0YUJhc2U2NCl9IiBhbHQ9IiR7ZXNjYXBlSHRtbChhLm5hbWUpfSIgZGF0YS1pbmRleD0iJHtpfSIgdGl0bGU9IkNsaWNrIHRvIHJlbW92ZSI+YCkKICAgIC5qb2luKCcnKTsKfQoKZWwuYXR0YWNoUm93LmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKGV2ZW50KSA9PiB7CiAgY29uc3QgaW5kZXggPSBldmVudC50YXJnZXQuZGF0YXNldD8uaW5kZXg7CiAgaWYgKGluZGV4ID09IG51bGwpIHJldHVybjsKICBzdGF0ZS5hdHRhY2htZW50cy5zcGxpY2UoTnVtYmVyKGluZGV4KSwgMSk7CiAgcGFpbnRBdHRhY2htZW50cygpOwp9KTsKCmVsLmF0dGFjaEJ0bi5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IGVsLmZpbGVJbnB1dC5jbGljaygpKTsKZWwuZmlsZUlucHV0LmFkZEV2ZW50TGlzdGVuZXIoJ2NoYW5nZScsICgpID0+IHsKICB2b2lkIGFkZEZpbGVzKFsuLi5lbC5maWxlSW5wdXQuZmlsZXNdKTsKICBlbC5maWxlSW5wdXQudmFsdWUgPSAnJzsKfSk7Cgphc3luYyBmdW5jdGlvbiBhZGRGaWxlcyhmaWxlcykgewogIGNvbnN0IG1heCA9IHN0YXRlLmJvb3QuY29uZmlnLmxpbWl0cy5tYXhVcGxvYWRCeXRlczsKICBmb3IgKGNvbnN0IGZpbGUgb2YgZmlsZXMpIHsKICAgIGlmICghZmlsZS50eXBlLnN0YXJ0c1dpdGgoJ2ltYWdlLycpKSBjb250aW51ZTsKICAgIGlmIChmaWxlLnNpemUgPiBtYXgpIHsKICAgICAgc2hvd05vdGljZSgnd2FybicsIGAke2ZpbGUubmFtZX0gaXMgbGFyZ2VyIHRoYW4gdGhlICR7Zm9ybWF0Qnl0ZXMobWF4KX0gYXR0YWNobWVudCBsaW1pdC5gKTsKICAgICAgY29udGludWU7CiAgICB9CiAgICBjb25zdCBkYXRhQmFzZTY0ID0gYXdhaXQgbmV3IFByb21pc2UoKHJlc29sdmUpID0+IHsKICAgICAgY29uc3QgcmVhZGVyID0gbmV3IEZpbGVSZWFkZXIoKTsKICAgICAgcmVhZGVyLm9ubG9hZCA9ICgpID0+IHJlc29sdmUoU3RyaW5nKHJlYWRlci5yZXN1bHQpLnNwbGl0KCcsJylbMV0pOwogICAgICByZWFkZXIucmVhZEFzRGF0YVVSTChmaWxlKTsKICAgIH0pOwogICAgc3RhdGUuYXR0YWNobWVudHMucHVzaCh7IG5hbWU6IGZpbGUubmFtZSwgbWVkaWFUeXBlOiBmaWxlLnR5cGUsIGRhdGFCYXNlNjQgfSk7CiAgfQogIHBhaW50QXR0YWNobWVudHMoKTsKfQoKZWwuaW5wdXQuYWRkRXZlbnRMaXN0ZW5lcigncGFzdGUnLCAoZXZlbnQpID0+IHsKICBjb25zdCBmaWxlcyA9IFsuLi4oZXZlbnQuY2xpcGJvYXJkRGF0YT8uZmlsZXMgPz8gW10pXTsKICBpZiAoZmlsZXMubGVuZ3RoKSB7CiAgICBldmVudC5wcmV2ZW50RGVmYXVsdCgpOwogICAgdm9pZCBhZGRGaWxlcyhmaWxlcyk7CiAgfQp9KTsKClsnZHJhZ292ZXInLCAnZHJhZ2VudGVyJ10uZm9yRWFjaCgobmFtZSkgPT4gZWwuY29tcG9zZXJCb3guYWRkRXZlbnRMaXN0ZW5lcihuYW1lLCAoZXZlbnQpID0+IHsKICBldmVudC5wcmV2ZW50RGVmYXVsdCgpOwogIGVsLmNvbXBvc2VyQm94LmNsYXNzTGlzdC5hZGQoJ2lzLWRyYWdnaW5nJyk7Cn0pKTsKClsnZHJhZ2xlYXZlJywgJ2Ryb3AnXS5mb3JFYWNoKChuYW1lKSA9PiBlbC5jb21wb3NlckJveC5hZGRFdmVudExpc3RlbmVyKG5hbWUsIChldmVudCkgPT4gewogIGV2ZW50LnByZXZlbnREZWZhdWx0KCk7CiAgZWwuY29tcG9zZXJCb3guY2xhc3NMaXN0LnJlbW92ZSgnaXMtZHJhZ2dpbmcnKTsKICBpZiAobmFtZSA9PT0gJ2Ryb3AnKSB2b2lkIGFkZEZpbGVzKFsuLi4oZXZlbnQuZGF0YVRyYW5zZmVyPy5maWxlcyA/PyBbXSldKTsKfSkpOwoKLyogLS0tLS0tLS0tLS0tLS0tLSBmaWxlcyBwYW5lbCAtLS0tLS0tLS0tLS0tLS0tICovCgplbC5maWxlc1RvZ2dsZS5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHsKICBjb25zdCBvcGVuID0gZWwuZmlsZXNQYW5lbC5oaWRkZW47CiAgZWwuZmlsZXNQYW5lbC5oaWRkZW4gPSAhb3BlbjsKICBlbC5ib2R5R3JpZC5kYXRhc2V0LmZpbGVzID0gb3BlbiA/ICdvcGVuJyA6ICdjbG9zZWQnOwogIGVsLmZpbGVzVG9nZ2xlLnNldEF0dHJpYnV0ZSgnYXJpYS1wcmVzc2VkJywgU3RyaW5nKG9wZW4pKTsKICBpZiAob3Blbikgdm9pZCBsb2FkRmlsZXMoc3RhdGUuZmlsZXNDd2QpOwp9KTsKCmFzeW5jIGZ1bmN0aW9uIGxvYWRGaWxlcyhwYXRoKSB7CiAgaWYgKCFzdGF0ZS5jdXJyZW50KSByZXR1cm47CiAgdHJ5IHsKICAgIGNvbnN0IGRhdGEgPSBhd2FpdCBhcGkubGlzdERpcihzdGF0ZS5jdXJyZW50LnByb2plY3RQYXRoLCBwYXRoKTsKICAgIHN0YXRlLmZpbGVzQ3dkID0gZGF0YS5jd2Q7CiAgICBlbC5maWxlc1BhdGgudGV4dENvbnRlbnQgPSBkYXRhLmN3ZCA9PT0gJy4nID8gc3RhdGUuY3VycmVudC5wcm9qZWN0UGF0aCA6IGRhdGEuY3dkOwogICAgY29uc3QgdXAgPSBkYXRhLmN3ZCA9PT0gJy4nCiAgICAgID8gJycKICAgICAgOiAnPGJ1dHRvbiBjbGFzcz0iZmlsZWl0ZW0gZmlsZWl0ZW0tLWRpciIgZGF0YS10eXBlPSJ1cCI+Li4gdXAgb25lIGxldmVsPC9idXR0b24+JzsKICAgIGVsLmZpbGVzTGlzdC5pbm5lckhUTUwgPSB1cCArIGRhdGEuZW50cmllcy5tYXAoKGVudHJ5KSA9PiBgCiAgICAgIDxidXR0b24gY2xhc3M9ImZpbGVpdGVtICR7ZW50cnkudHlwZSA9PT0gJ2RpcicgPyAnZmlsZWl0ZW0tLWRpcicgOiAnJ30iCiAgICAgICAgICAgICAgZGF0YS10eXBlPSIke2VudHJ5LnR5cGV9IiBkYXRhLXBhdGg9IiR7ZXNjYXBlSHRtbChlbnRyeS5wYXRoKX0iPgogICAgICAgIDxzcGFuPiR7ZW50cnkudHlwZSA9PT0gJ2RpcicgPyAn4pa4JyA6ICfCtyd9PC9zcGFuPgogICAgICAgIDxzcGFuPiR7ZXNjYXBlSHRtbChlbnRyeS5uYW1lKX08L3NwYW4+CiAgICAgICAgPHNwYW4gY2xhc3M9ImZpbGVpdGVtX19zaXplIj4ke2Zvcm1hdEJ5dGVzKGVudHJ5LnNpemUpfTwvc3Bhbj4KICAgICAgPC9idXR0b24+YCkuam9pbignJyk7CiAgfSBjYXRjaCAoZXJyKSB7CiAgICBlbC5maWxlc0xpc3QuaW5uZXJIVE1MID0gYDxwIGNsYXNzPSJsZWdlbmQiIHN0eWxlPSJwYWRkaW5nOjhweCI+JHtlc2NhcGVIdG1sKGVyci5tZXNzYWdlKX08L3A+YDsKICB9Cn0KCmVsLmZpbGVzTGlzdC5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIGFzeW5jIChldmVudCkgPT4gewogIGNvbnN0IGJ1dHRvbiA9IGV2ZW50LnRhcmdldC5jbG9zZXN0KCcuZmlsZWl0ZW0nKTsKICBpZiAoIWJ1dHRvbikgcmV0dXJuOwogIGlmIChidXR0b24uZGF0YXNldC50eXBlID09PSAndXAnKSB7CiAgICBjb25zdCBwYXJlbnQgPSBzdGF0ZS5maWxlc0N3ZC5pbmNsdWRlcygnLycpID8gc3RhdGUuZmlsZXNDd2QucmVwbGFjZSgvXC9bXi9dKyQvLCAnJykgOiAnLic7CiAgICByZXR1cm4gbG9hZEZpbGVzKHBhcmVudCk7CiAgfQogIGlmIChidXR0b24uZGF0YXNldC50eXBlID09PSAnZGlyJykgcmV0dXJuIGxvYWRGaWxlcyhidXR0b24uZGF0YXNldC5wYXRoKTsKCiAgY29uc3QgZmlsZSA9IGF3YWl0IGFwaS5yZWFkRmlsZShzdGF0ZS5jdXJyZW50LnByb2plY3RQYXRoLCBidXR0b24uZGF0YXNldC5wYXRoKTsKICBlbC5wZWVrTmFtZS50ZXh0Q29udGVudCA9IGJ1dHRvbi5kYXRhc2V0LnBhdGg7CiAgZWwucGVla01ldGEudGV4dENvbnRlbnQgPSBmaWxlLmJpbmFyeQogICAgPyAnQmluYXJ5IGZpbGUg4oCUIG5vdCBzaG93bicKICAgIDogYCR7Zm9ybWF0Qnl0ZXMoZmlsZS5zaXplKX0ke2ZpbGUudHJ1bmNhdGVkID8gJyDCtyB0cnVuY2F0ZWQnIDogJyd9YDsKICBlbC5wZWVrQm9keS50ZXh0Q29udGVudCA9IGZpbGUuYmluYXJ5ID8gJycgOiBmaWxlLmNvbnRlbnQ7CiAgZWwucGVla092ZXJsYXkuaGlkZGVuID0gZmFsc2U7CiAgcmV0dXJuIHVuZGVmaW5lZDsKfSk7CgplbC5wZWVrQ2xvc2UuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB7IGVsLnBlZWtPdmVybGF5LmhpZGRlbiA9IHRydWU7IH0pOwplbC5wZWVrT3ZlcmxheS5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIChldmVudCkgPT4gewogIGlmIChldmVudC50YXJnZXQgPT09IGVsLnBlZWtPdmVybGF5KSBlbC5wZWVrT3ZlcmxheS5oaWRkZW4gPSB0cnVlOwp9KTsKCi8qIC0tLS0tLS0tLS0tLS0tLS0gcmFpbCAobW9iaWxlKSAtLS0tLS0tLS0tLS0tLS0tICovCgpmdW5jdGlvbiBjbG9zZVJhaWwoKSB7CiAgZWwuYm9keUdyaWQuZGF0YXNldC5yYWlsID0gJ2Nsb3NlZCc7Cn0KCmVsLnJhaWxUb2dnbGUuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB7CiAgZWwuYm9keUdyaWQuZGF0YXNldC5yYWlsID0gZWwuYm9keUdyaWQuZGF0YXNldC5yYWlsID09PSAnb3BlbicgPyAnY2xvc2VkJyA6ICdvcGVuJzsKfSk7CgplbC5yYWlsU2NyaW0uYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCBjbG9zZVJhaWwpOwoKZG9jdW1lbnQuYWRkRXZlbnRMaXN0ZW5lcigna2V5ZG93bicsIChldmVudCkgPT4gewogIGlmIChldmVudC5rZXkgPT09ICdFc2NhcGUnKSB7CiAgICBlbC5wZWVrT3ZlcmxheS5oaWRkZW4gPSB0cnVlOwogICAgZWwubmV3T3ZlcmxheS5oaWRkZW4gPSB0cnVlOwogICAgY2xvc2VSYWlsKCk7CiAgfQp9KTsKCmJvb3QoKS5jYXRjaCgoZXJyKSA9PiB7CiAgZG9jdW1lbnQuYm9keS5pbm5lckhUTUwgPSBgPGRpdiBjbGFzcz0iZW1wdHkiPjxkaXYgY2xhc3M9ImVtcHR5X19pbm5lciI+PGgzPkJyaWRnZSB1bmF2YWlsYWJsZTwvaDM+PHA+JHtlc2NhcGVIdG1sKGVyci5tZXNzYWdlKX08L3A+PC9kaXY+PC9kaXY+YDsKfSk7Cg==","index.html":"PCFkb2N0eXBlIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4KPGhlYWQ+CjxtZXRhIGNoYXJzZXQ9InV0Zi04Ij4KPG1ldGEgbmFtZT0idmlld3BvcnQiIGNvbnRlbnQ9IndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xLCB2aWV3cG9ydC1maXQ9Y292ZXIiPgo8bWV0YSBuYW1lPSJjb2xvci1zY2hlbWUiIGNvbnRlbnQ9ImRhcmsiPgo8dGl0bGU+Q2xhdWRlIENvZGUgQnJpZGdlIOKAlCBhZ2VudDwvdGl0bGU+CjxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iYXBwLmNzcyI+CjxsaW5rIHJlbD0iaWNvbiIgaHJlZj0iZGF0YTppbWFnZS9zdmcreG1sLDxzdmcgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJyB2aWV3Qm94PScwIDAgMTYgMTYnPjxyZWN0IHdpZHRoPScxNicgaGVpZ2h0PScxNicgZmlsbD0nJTIzMTIyMTI3Jy8+PHJlY3QgeD0nMicgeT0nNycgd2lkdGg9JzEyJyBoZWlnaHQ9JzInIGZpbGw9JyUyMzRmYmZhMCcvPjwvc3ZnPiI+CjwvaGVhZD4KPGJvZHk+Cgo8ZGl2IGNsYXNzPSJzaGVsbCIgaWQ9InNoZWxsIiBoaWRkZW4+CiAgPGhlYWRlciBjbGFzcz0idG9wYmFyIj4KICAgIDxidXR0b24gY2xhc3M9Imljb25idG4gb25seS1uYXJyb3ciIGlkPSJyYWlsVG9nZ2xlIiBhcmlhLWxhYmVsPSJTaG93IHNlc3Npb25zIj7imLA8L2J1dHRvbj4KICAgIDxkaXYgY2xhc3M9ImJyYW5kIj4KICAgICAgPHNwYW4gY2xhc3M9ImJyYW5kX19tYXJrIj5DQ0I8L3NwYW4+CiAgICAgIDxzcGFuIGNsYXNzPSJicmFuZF9fc3ViIj5icmlkZ2U8L3NwYW4+CiAgICA8L2Rpdj4KICAgIDxkaXYgY2xhc3M9InRvcGJhcl9fd2hlcmUiIGlkPSJ3aGVyZSI+Tm8gc2Vzc2lvbiBvcGVuPC9kaXY+CiAgICA8ZGl2IGNsYXNzPSJtZXRlcnN0cmlwIiBpZD0ibWV0ZXJzdHJpcCI+PC9kaXY+CiAgICA8ZGl2IGNsYXNzPSJ0b3BiYXJfX2FjdGlvbnMiPgogICAgICA8YnV0dG9uIGNsYXNzPSJ1cGRhdGVidG4iIGlkPSJ1cGRhdGVCdG4iIHR5cGU9ImJ1dHRvbiIgaGlkZGVuPjwvYnV0dG9uPgogICAgICA8YnV0dG9uIGNsYXNzPSJpY29uYnRuIiBpZD0iZmlsZXNUb2dnbGUiIGFyaWEtcHJlc3NlZD0iZmFsc2UiIGFyaWEtbGFiZWw9IlRvZ2dsZSBwcm9qZWN0IGZpbGVzIiB0aXRsZT0iUHJvamVjdCBmaWxlcyI+4perPC9idXR0b24+CiAgICAgIDxidXR0b24gY2xhc3M9Imljb25idG4iIGlkPSJyZWZyZXNoVXNhZ2UiIGFyaWEtbGFiZWw9IlJlZnJlc2ggdXNhZ2UiIHRpdGxlPSJSZWZyZXNoIHVzYWdlIj7ihrs8L2J1dHRvbj4KICAgIDwvZGl2PgogIDwvaGVhZGVyPgoKICA8ZGl2IGNsYXNzPSJib2R5IiBpZD0iYm9keUdyaWQiIGRhdGEtcmFpbD0iY2xvc2VkIiBkYXRhLWZpbGVzPSJjbG9zZWQiPgogICAgPGRpdiBjbGFzcz0icmFpbF9fc2NyaW0iIGlkPSJyYWlsU2NyaW0iPjwvZGl2PgoKICAgIDxhc2lkZSBjbGFzcz0icmFpbCI+CiAgICAgIDxkaXYgY2xhc3M9InJhaWxfX2hlYWQiPgogICAgICAgIDxzcGFuIGNsYXNzPSJsZWdlbmQiPlNlc3Npb25zPC9zcGFuPgogICAgICAgIDxidXR0b24gY2xhc3M9ImJ0biIgaWQ9Im5ld1Nlc3Npb24iPk5ldzwvYnV0dG9uPgogICAgICA8L2Rpdj4KICAgICAgPGRpdiBjbGFzcz0icmFpbF9fbGlzdCIgaWQ9InNlc3Npb25MaXN0Ij48L2Rpdj4KICAgIDwvYXNpZGU+CgogICAgPG1haW4gY2xhc3M9Im1haW4iPgogICAgICA8ZGl2IGNsYXNzPSJiYW5uZXIiIGlkPSJub3RpY2UiIGhpZGRlbj48L2Rpdj4KCiAgICAgIDxkaXYgY2xhc3M9InRocmVhZCIgaWQ9InRocmVhZCI+CiAgICAgICAgPGRpdiBjbGFzcz0iZW1wdHkiIGlkPSJlbXB0eVN0YXRlIj4KICAgICAgICAgIDxkaXYgY2xhc3M9ImVtcHR5X19pbm5lciI+CiAgICAgICAgICAgIDxoMz5Ob3RoaW5nIG9wZW4geWV0PC9oMz4KICAgICAgICAgICAgPHA+UG9pbnQgdGhlIGFnZW50IGF0IGEgZm9sZGVyIG9uIHRoaXMgbWFjaGluZSBhbmQgc3RhcnQgd29ya2luZy48L3A+CiAgICAgICAgICAgIDxidXR0b24gY2xhc3M9ImJ0biBidG4tLXByaW1hcnkiIGlkPSJuZXdTZXNzaW9uRW1wdHkiPlN0YXJ0IGEgc2Vzc2lvbjwvYnV0dG9uPgogICAgICAgICAgPC9kaXY+CiAgICAgICAgPC9kaXY+CiAgICAgICAgPGRpdiBjbGFzcz0idGhyZWFkX19pbm5lciIgaWQ9InRocmVhZElubmVyIj48L2Rpdj4KICAgICAgPC9kaXY+CgogICAgICA8ZGl2IGNsYXNzPSJjb21wb3NlciI+CiAgICAgICAgPGRpdiBjbGFzcz0iY29tcG9zZXJfX2lubmVyIj4KICAgICAgICAgIDxkaXYgY2xhc3M9ImF0dGFjaC1yb3ciIGlkPSJhdHRhY2hSb3ciPjwvZGl2PgogICAgICAgICAgPGRpdiBjbGFzcz0iY29tcG9zZXJfX2JveCIgaWQ9ImNvbXBvc2VyQm94Ij4KICAgICAgICAgICAgPHRleHRhcmVhIGlkPSJpbnB1dCIgcm93cz0iMSIgcGxhY2Vob2xkZXI9IkRlc2NyaWJlIHdoYXQgeW91IHdhbnQgYnVpbHQsIGNoYW5nZWQsIG9yIGV4cGxhaW5lZOKApiIgZGlzYWJsZWQ+PC90ZXh0YXJlYT4KICAgICAgICAgICAgPGJ1dHRvbiBjbGFzcz0iaWNvbmJ0biIgaWQ9ImF0dGFjaEJ0biIgYXJpYS1sYWJlbD0iQXR0YWNoIGltYWdlIiB0aXRsZT0iQXR0YWNoIGltYWdlIj7iipU8L2J1dHRvbj4KICAgICAgICAgICAgPGJ1dHRvbiBjbGFzcz0iYnRuIGJ0bi0tcHJpbWFyeSIgaWQ9InNlbmQiIGRpc2FibGVkPlNlbmQ8L2J1dHRvbj4KICAgICAgICAgIDwvZGl2PgogICAgICAgICAgPGRpdiBjbGFzcz0iY29tcG9zZXJfX2NvbnRyb2xzIj4KICAgICAgICAgICAgPHNlbGVjdCBjbGFzcz0ic2VsZWN0IiBpZD0ibW9kZWxTZWxlY3QiIGRpc2FibGVkIGFyaWEtbGFiZWw9Ik1vZGVsIj48L3NlbGVjdD4KICAgICAgICAgICAgPGJ1dHRvbiBjbGFzcz0iYnRuIGJ0bi0tZGFuZ2VyIiBpZD0ic3RvcCIgaGlkZGVuPlN0b3A8L2J1dHRvbj4KICAgICAgICAgICAgPHNwYW4gY2xhc3M9ImNvbXBvc2VyX19oaW50IiBpZD0iaGludCI+RW50ZXIgc2VuZHMgwrcgU2hpZnQrRW50ZXIgbmV3bGluZTwvc3Bhbj4KICAgICAgICAgIDwvZGl2PgogICAgICAgIDwvZGl2PgogICAgICA8L2Rpdj4KICAgIDwvbWFpbj4KCiAgICA8YXNpZGUgY2xhc3M9ImZpbGVzIiBpZD0iZmlsZXNQYW5lbCIgaGlkZGVuPgogICAgICA8ZGl2IGNsYXNzPSJmaWxlc19faGVhZCI+CiAgICAgICAgPHNwYW4gY2xhc3M9ImxlZ2VuZCI+UHJvamVjdCBmaWxlczwvc3Bhbj4KICAgICAgICA8ZGl2IGNsYXNzPSJmaWxlc19fcGF0aCIgaWQ9ImZpbGVzUGF0aCI+4oCUPC9kaXY+CiAgICAgIDwvZGl2PgogICAgICA8ZGl2IGNsYXNzPSJmaWxlc19fbGlzdCIgaWQ9ImZpbGVzTGlzdCI+PC9kaXY+CiAgICA8L2FzaWRlPgogIDwvZGl2Pgo8L2Rpdj4KCjxpbnB1dCB0eXBlPSJmaWxlIiBpZD0iZmlsZUlucHV0IiBhY2NlcHQ9ImltYWdlL3BuZyxpbWFnZS9qcGVnLGltYWdlL2dpZixpbWFnZS93ZWJwIiBtdWx0aXBsZSBoaWRkZW4+Cgo8IS0tIENvbm5lY3QgdG8gdGhlIGJyaWRnZSAtLT4KPGRpdiBjbGFzcz0ib3ZlcmxheSIgaWQ9InNldHVwT3ZlcmxheSIgaGlkZGVuPgogIDxmb3JtIGNsYXNzPSJjYXJkIiBpZD0ic2V0dXBGb3JtIj4KICAgIDxoMj5Db25uZWN0IHRvIHlvdXIgYnJpZGdlPC9oMj4KICAgIDxwIGNsYXNzPSJjYXJkX19zdWIiPgogICAgICBUaGUgYWdlbnQgd29ya3Mgb24gdGhpcyBtYWNoaW5lLiBJdCBuZWVkcyBzb21ld2hlcmUgdG8gc2VuZCBtb2RlbCBjYWxscyDigJQgdGhlIG1hY2hpbmUgaG9sZGluZyB5b3VyCiAgICAgIENsYXVkZSBzdWJzY3JpcHRpb24uIEJvdGggdmFsdWVzIGNvbWUgZnJvbSB0aGF0IG1hY2hpbmUncyBhZG1pbiBjb25zb2xlLgogICAgPC9wPgogICAgPGRpdiBjbGFzcz0iZmllbGQiPgogICAgICA8bGFiZWwgY2xhc3M9ImxlZ2VuZCIgZm9yPSJicmlkZ2VJbnB1dCI+QnJpZGdlIGFkZHJlc3M8L2xhYmVsPgogICAgICA8aW5wdXQgaWQ9ImJyaWRnZUlucHV0IiB0eXBlPSJ0ZXh0IiBzcGVsbGNoZWNrPSJmYWxzZSIgcGxhY2Vob2xkZXI9Imh0dHA6Ly8xOTIuMTY4LjEuMjA6ODc4NyIgcmVxdWlyZWQ+CiAgICA8L2Rpdj4KICAgIDxkaXYgY2xhc3M9ImZpZWxkIj4KICAgICAgPGxhYmVsIGNsYXNzPSJsZWdlbmQiIGZvcj0idG9rZW5JbnB1dCI+VG9rZW48L2xhYmVsPgogICAgICA8aW5wdXQgaWQ9InRva2VuSW5wdXQiIHR5cGU9InBhc3N3b3JkIiBhdXRvY29tcGxldGU9ImN1cnJlbnQtcGFzc3dvcmQiIHNwZWxsY2hlY2s9ImZhbHNlIiByZXF1aXJlZD4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz0iY2FyZF9fYWN0aW9ucyI+CiAgICAgIDxidXR0b24gY2xhc3M9ImJ0biIgdHlwZT0iYnV0dG9uIiBpZD0ic2V0dXBTa2lwIj5MYXRlcjwvYnV0dG9uPgogICAgICA8YnV0dG9uIGNsYXNzPSJidG4gYnRuLS1wcmltYXJ5IiB0eXBlPSJzdWJtaXQiPkNvbm5lY3Q8L2J1dHRvbj4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz0iZXJyb3IiIGlkPSJzZXR1cEVycm9yIj48L2Rpdj4KICA8L2Zvcm0+CjwvZGl2PgoKPCEtLSBOZXcgc2Vzc2lvbiAtLT4KPGRpdiBjbGFzcz0ib3ZlcmxheSIgaWQ9Im5ld092ZXJsYXkiIGhpZGRlbj4KICA8Zm9ybSBjbGFzcz0iY2FyZCIgaWQ9Im5ld0Zvcm0iPgogICAgPGgyPlN0YXJ0IGEgc2Vzc2lvbjwvaDI+CiAgICA8cCBjbGFzcz0iY2FyZF9fc3ViIj4KICAgICAgVGhlIGFnZW50IGVkaXRzIGZpbGVzIGFuZCBydW5zIGNvbW1hbmRzIGluc2lkZSB0aGUgZm9sZGVyIHlvdSBwaWNrLCBvbiB0aGlzIG1hY2hpbmUuIEl0IG5ldmVyCiAgICAgIHJlYWNoZXMgb3V0c2lkZSB0aGF0IGZvbGRlci4KICAgIDwvcD4KICAgIDxkaXYgY2xhc3M9ImZpZWxkIj4KICAgICAgPGxhYmVsIGNsYXNzPSJsZWdlbmQiIGZvcj0icHJvamVjdFNlbGVjdCI+Rm9sZGVyPC9sYWJlbD4KICAgICAgPHNlbGVjdCBpZD0icHJvamVjdFNlbGVjdCIgcmVxdWlyZWQ+PC9zZWxlY3Q+CiAgICA8L2Rpdj4KICAgIDxkaXYgY2xhc3M9ImZpZWxkIj4KICAgICAgPGxhYmVsIGNsYXNzPSJsZWdlbmQiIGZvcj0icm9vdElucHV0Ij5BZGQgYW5vdGhlciBmb2xkZXI8L2xhYmVsPgogICAgICA8ZGl2IHN0eWxlPSJkaXNwbGF5OmZsZXg7IGdhcDo4cHgiPgogICAgICAgIDxpbnB1dCBpZD0icm9vdElucHV0IiB0eXBlPSJ0ZXh0IiBzcGVsbGNoZWNrPSJmYWxzZSIgcGxhY2Vob2xkZXI9Ii9ob21lL3lvdS9wcm9qZWN0Ij4KICAgICAgICA8YnV0dG9uIGNsYXNzPSJidG4iIHR5cGU9ImJ1dHRvbiIgaWQ9ImFkZFJvb3QiPkFkZDwvYnV0dG9uPgogICAgICA8L2Rpdj4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz0iZmllbGQiPgogICAgICA8bGFiZWwgY2xhc3M9ImxlZ2VuZCIgZm9yPSJuZXdNb2RlbCI+TW9kZWw8L2xhYmVsPgogICAgICA8c2VsZWN0IGlkPSJuZXdNb2RlbCI+PC9zZWxlY3Q+CiAgICA8L2Rpdj4KICAgIDxkaXYgY2xhc3M9ImNhcmRfX2FjdGlvbnMiPgogICAgICA8YnV0dG9uIGNsYXNzPSJidG4iIHR5cGU9ImJ1dHRvbiIgaWQ9Im5ld0NhbmNlbCI+Q2FuY2VsPC9idXR0b24+CiAgICAgIDxidXR0b24gY2xhc3M9ImJ0biBidG4tLXByaW1hcnkiIHR5cGU9InN1Ym1pdCI+U3RhcnQ8L2J1dHRvbj4KICAgIDwvZGl2PgogICAgPGRpdiBjbGFzcz0iZXJyb3IiIGlkPSJuZXdFcnJvciI+PC9kaXY+CiAgPC9mb3JtPgo8L2Rpdj4KCjwhLS0gRmlsZSBwZWVrIC0tPgo8ZGl2IGNsYXNzPSJvdmVybGF5IiBpZD0icGVla092ZXJsYXkiIGhpZGRlbj4KICA8ZGl2IGNsYXNzPSJjYXJkIGNhcmQtLXdpZGUiPgogICAgPGgyIGlkPSJwZWVrTmFtZSI+RmlsZTwvaDI+CiAgICA8cCBjbGFzcz0iY2FyZF9fc3ViIiBpZD0icGVla01ldGEiPjwvcD4KICAgIDxkaXYgY2xhc3M9ImZpbGVwZWVrIiBpZD0icGVla0JvZHkiPjwvZGl2PgogICAgPGRpdiBjbGFzcz0iY2FyZF9fYWN0aW9ucyI+PGJ1dHRvbiBjbGFzcz0iYnRuIiBpZD0icGVla0Nsb3NlIj5DbG9zZTwvYnV0dG9uPjwvZGl2PgogIDwvZGl2Pgo8L2Rpdj4KCjxzY3JpcHQgdHlwZT0ibW9kdWxlIiBzcmM9ImFwcC5qcyI+PC9zY3JpcHQ+CjwvYm9keT4KPC9odG1sPgo=","render.js":"LyogUHJlc2VudGF0aW9uIGhlbHBlcnM6IHNhZmUgbWFya2Rvd24sIHRvb2wgc3VtbWFyaWVzLCBkaWZmcywgbWV0ZXJzLiAqLwoKZXhwb3J0IGZ1bmN0aW9uIGVzY2FwZUh0bWwodmFsdWUpIHsKICByZXR1cm4gU3RyaW5nKHZhbHVlID8/ICcnKQogICAgLnJlcGxhY2UoLyYvZywgJyZhbXA7JykKICAgIC5yZXBsYWNlKC88L2csICcmbHQ7JykKICAgIC5yZXBsYWNlKC8+L2csICcmZ3Q7JykKICAgIC5yZXBsYWNlKC8iL2csICcmcXVvdDsnKTsKfQoKZnVuY3Rpb24gaW5saW5lTWFya2Rvd24odGV4dCkgewogIC8vIElucHV0IGlzIGFscmVhZHkgSFRNTC1lc2NhcGVkLCBzbyB0aGVzZSBvbmx5IGV2ZXIgcHJvZHVjZSBvdXIgb3duIHRhZ3MuCiAgcmV0dXJuIHRleHQKICAgIC5yZXBsYWNlKC9gKFteYF0rKWAvZywgJzxjb2RlPiQxPC9jb2RlPicpCiAgICAucmVwbGFjZSgvXCpcKihbXipdKylcKlwqL2csICc8c3Ryb25nPiQxPC9zdHJvbmc+JykKICAgIC5yZXBsYWNlKC8oXnxbXHMoXSlcKihbXipcbl0rKVwqL2csICckMTxlbT4kMjwvZW0+JykKICAgIC5yZXBsYWNlKC9cWyhbXlxdXSspXF1cKChodHRwcz86W14pXHNdKylcKS9nLCAnPGEgaHJlZj0iJDIiIHRhcmdldD0iX2JsYW5rIiByZWw9Im5vb3BlbmVyIG5vcmVmZXJyZXIiPiQxPC9hPicpOwp9CgpmdW5jdGlvbiByZW5kZXJCbG9jayhibG9jaykgewogIGNvbnN0IGxpbmVzID0gYmxvY2suc3BsaXQoJ1xuJyk7CiAgbGV0IGh0bWwgPSAnJzsKICBsZXQgbGlzdCA9IG51bGw7CgogIGNvbnN0IGNsb3NlTGlzdCA9ICgpID0+IHsKICAgIGlmIChsaXN0KSB7CiAgICAgIGh0bWwgKz0gYDwvJHtsaXN0fT5gOwogICAgICBsaXN0ID0gbnVsbDsKICAgIH0KICB9OwoKICBmb3IgKGNvbnN0IHJhdyBvZiBsaW5lcykgewogICAgY29uc3QgbGluZSA9IHJhdy50cmltRW5kKCk7CiAgICBjb25zdCBoZWFkaW5nID0gL14oI3sxLDZ9KVxzKyguKikkLy5leGVjKGxpbmUpOwogICAgY29uc3QgYnVsbGV0ID0gL15ccypbLSpdXHMrKC4qKSQvLmV4ZWMobGluZSk7CiAgICBjb25zdCBudW1iZXJlZCA9IC9eXHMqXGQrXC5ccysoLiopJC8uZXhlYyhsaW5lKTsKCiAgICBpZiAoaGVhZGluZykgewogICAgICBjbG9zZUxpc3QoKTsKICAgICAgY29uc3QgbGV2ZWwgPSBNYXRoLm1pbigzLCBoZWFkaW5nWzFdLmxlbmd0aCk7CiAgICAgIGh0bWwgKz0gYDxoJHtsZXZlbH0+JHtpbmxpbmVNYXJrZG93bihoZWFkaW5nWzJdKX08L2gke2xldmVsfT5gOwogICAgfSBlbHNlIGlmIChidWxsZXQpIHsKICAgICAgaWYgKGxpc3QgIT09ICd1bCcpIHsgY2xvc2VMaXN0KCk7IGxpc3QgPSAndWwnOyBodG1sICs9ICc8dWw+JzsgfQogICAgICBodG1sICs9IGA8bGk+JHtpbmxpbmVNYXJrZG93bihidWxsZXRbMV0pfTwvbGk+YDsKICAgIH0gZWxzZSBpZiAobnVtYmVyZWQpIHsKICAgICAgaWYgKGxpc3QgIT09ICdvbCcpIHsgY2xvc2VMaXN0KCk7IGxpc3QgPSAnb2wnOyBodG1sICs9ICc8b2w+JzsgfQogICAgICBodG1sICs9IGA8bGk+JHtpbmxpbmVNYXJrZG93bihudW1iZXJlZFsxXSl9PC9saT5gOwogICAgfSBlbHNlIGlmICghbGluZS50cmltKCkpIHsKICAgICAgY2xvc2VMaXN0KCk7CiAgICB9IGVsc2UgewogICAgICBjbG9zZUxpc3QoKTsKICAgICAgaHRtbCArPSBgPHA+JHtpbmxpbmVNYXJrZG93bihsaW5lKX08L3A+YDsKICAgIH0KICB9CiAgY2xvc2VMaXN0KCk7CiAgcmV0dXJuIGh0bWw7Cn0KCi8qKiBNaW5pbWFsIG1hcmtkb3duOiBmZW5jZXMsIGhlYWRpbmdzLCBsaXN0cywgYm9sZC9pdGFsaWMvY29kZS9saW5rcy4gKi8KZXhwb3J0IGZ1bmN0aW9uIHJlbmRlck1hcmtkb3duKHRleHQpIHsKICBjb25zdCBlc2NhcGVkID0gZXNjYXBlSHRtbCh0ZXh0KTsKICBjb25zdCBwYXJ0cyA9IGVzY2FwZWQuc3BsaXQoL2BgYC8pOwogIGxldCBodG1sID0gJyc7CiAgcGFydHMuZm9yRWFjaCgocGFydCwgaW5kZXgpID0+IHsKICAgIGlmIChpbmRleCAlIDIgPT09IDEpIHsKICAgICAgY29uc3QgbmV3bGluZSA9IHBhcnQuaW5kZXhPZignXG4nKTsKICAgICAgY29uc3QgYm9keSA9IG5ld2xpbmUgPT09IC0xID8gcGFydCA6IHBhcnQuc2xpY2UobmV3bGluZSArIDEpOwogICAgICBodG1sICs9IGA8cHJlPjxjb2RlPiR7Ym9keS5yZXBsYWNlKC9cbiQvLCAnJyl9PC9jb2RlPjwvcHJlPmA7CiAgICB9IGVsc2UgewogICAgICBodG1sICs9IHJlbmRlckJsb2NrKHBhcnQpOwogICAgfQogIH0pOwogIHJldHVybiBodG1sOwp9CgovKiAtLS0tLS0tLS0tLS0tLS0tIHRvb2wgY2FyZHMgLS0tLS0tLS0tLS0tLS0tLSAqLwoKY29uc3QgVFJVTkNBVEUgPSAxNjA7CgpmdW5jdGlvbiBzaG9ydGVuKHZhbHVlLCBtYXggPSBUUlVOQ0FURSkgewogIGNvbnN0IHRleHQgPSBTdHJpbmcodmFsdWUgPz8gJycpLnJlcGxhY2UoL1xzKy9nLCAnICcpLnRyaW0oKTsKICByZXR1cm4gdGV4dC5sZW5ndGggPiBtYXggPyBgJHt0ZXh0LnNsaWNlKDAsIG1heCl94oCmYCA6IHRleHQ7Cn0KCi8qKiBBIG9uZS1saW5lIGRlc2NyaXB0aW9uIG9mIHdoYXQgYSB0b29sIGNhbGwgaXMgYWJvdXQgdG8gZG8uICovCmV4cG9ydCBmdW5jdGlvbiB0b29sU3VtbWFyeShuYW1lLCBpbnB1dCA9IHt9KSB7CiAgc3dpdGNoIChuYW1lKSB7CiAgICBjYXNlICdydW5fc2hlbGwnOiByZXR1cm4gc2hvcnRlbihpbnB1dC5jb21tYW5kKTsKICAgIGNhc2UgJ3JlYWRfZmlsZSc6CiAgICBjYXNlICd3cml0ZV9maWxlJzoKICAgIGNhc2UgJ2VkaXRfZmlsZSc6CiAgICBjYXNlICdsaXN0X2Rpcic6IHJldHVybiBzaG9ydGVuKGlucHV0LnBhdGggPz8gJy4nKTsKICAgIGNhc2UgJ2dsb2InOiByZXR1cm4gc2hvcnRlbihpbnB1dC5wYXR0ZXJuKTsKICAgIGNhc2UgJ2dyZXAnOiByZXR1cm4gc2hvcnRlbihgJHtpbnB1dC5wYXR0ZXJuID8/ICcnfSAke2lucHV0Lmdsb2IgPz8gJyd9YC50cmltKCkpOwogICAgZGVmYXVsdDogcmV0dXJuIHNob3J0ZW4oaW5wdXQuZGVzY3JpcHRpb24gPz8gaW5wdXQucGF0aCA/PyBKU09OLnN0cmluZ2lmeShpbnB1dCkpOwogIH0KfQoKLyoqIEh1bWFuLXJlYWRhYmxlIGxhYmVsIGZvciBhIHRvb2wsIHVzZWQgYXMgdGhlIGNhcmQncyB0aXRsZS4gKi8KY29uc3QgVE9PTF9MQUJFTFMgPSB7CiAgcmVhZF9maWxlOiAncmVhZCcsCiAgd3JpdGVfZmlsZTogJ3dyaXRlJywKICBlZGl0X2ZpbGU6ICdlZGl0JywKICBsaXN0X2RpcjogJ2xpc3QnLAogIGdsb2I6ICdnbG9iJywKICBncmVwOiAnZ3JlcCcsCiAgcnVuX3NoZWxsOiAnc2hlbGwnLAp9OwoKZXhwb3J0IGZ1bmN0aW9uIHRvb2xMYWJlbChuYW1lKSB7CiAgcmV0dXJuIFRPT0xfTEFCRUxTW25hbWVdID8/IG5hbWU7Cn0KCi8qIC0tLS0tLS0tLS0tLS0tLS0gZGlmZnMgLS0tLS0tLS0tLS0tLS0tLSAqLwoKLyoqIExvbmdlc3QtY29tbW9uLXN1YnNlcXVlbmNlIGxpbmUgZGlmZiwgZ29vZCBlbm91Z2ggZm9yIGVkaXQgcHJldmlld3MuICovCmV4cG9ydCBmdW5jdGlvbiBsaW5lRGlmZihiZWZvcmUsIGFmdGVyKSB7CiAgY29uc3QgYSA9IFN0cmluZyhiZWZvcmUgPz8gJycpLnNwbGl0KCdcbicpOwogIGNvbnN0IGIgPSBTdHJpbmcoYWZ0ZXIgPz8gJycpLnNwbGl0KCdcbicpOwogIGNvbnN0IHRhYmxlID0gQXJyYXkuZnJvbSh7IGxlbmd0aDogYS5sZW5ndGggKyAxIH0sICgpID0+IG5ldyBVaW50MzJBcnJheShiLmxlbmd0aCArIDEpKTsKCiAgZm9yIChsZXQgaSA9IGEubGVuZ3RoIC0gMTsgaSA+PSAwOyBpIC09IDEpIHsKICAgIGZvciAobGV0IGogPSBiLmxlbmd0aCAtIDE7IGogPj0gMDsgaiAtPSAxKSB7CiAgICAgIHRhYmxlW2ldW2pdID0gYVtpXSA9PT0gYltqXSA/IHRhYmxlW2kgKyAxXVtqICsgMV0gKyAxIDogTWF0aC5tYXgodGFibGVbaSArIDFdW2pdLCB0YWJsZVtpXVtqICsgMV0pOwogICAgfQogIH0KCiAgY29uc3Qgcm93cyA9IFtdOwogIGxldCBpID0gMDsKICBsZXQgaiA9IDA7CiAgd2hpbGUgKGkgPCBhLmxlbmd0aCAmJiBqIDwgYi5sZW5ndGgpIHsKICAgIGlmIChhW2ldID09PSBiW2pdKSB7IHJvd3MucHVzaCh7IGtpbmQ6ICdjdHgnLCB0ZXh0OiBhW2ldIH0pOyBpICs9IDE7IGogKz0gMTsgfQogICAgZWxzZSBpZiAodGFibGVbaSArIDFdW2pdID49IHRhYmxlW2ldW2ogKyAxXSkgeyByb3dzLnB1c2goeyBraW5kOiAnZGVsJywgdGV4dDogYVtpXSB9KTsgaSArPSAxOyB9CiAgICBlbHNlIHsgcm93cy5wdXNoKHsga2luZDogJ2FkZCcsIHRleHQ6IGJbal0gfSk7IGogKz0gMTsgfQogIH0KICB3aGlsZSAoaSA8IGEubGVuZ3RoKSB7IHJvd3MucHVzaCh7IGtpbmQ6ICdkZWwnLCB0ZXh0OiBhW2ldIH0pOyBpICs9IDE7IH0KICB3aGlsZSAoaiA8IGIubGVuZ3RoKSB7IHJvd3MucHVzaCh7IGtpbmQ6ICdhZGQnLCB0ZXh0OiBiW2pdIH0pOyBqICs9IDE7IH0KICByZXR1cm4gcm93czsKfQoKY29uc3QgTUFSSyA9IHsgYWRkOiAnKycsIGRlbDogJ+KIkicsIGN0eDogJyAnIH07CgpleHBvcnQgZnVuY3Rpb24gcmVuZGVyRGlmZihiZWZvcmUsIGFmdGVyKSB7CiAgY29uc3Qgcm93cyA9IGxpbmVEaWZmKGJlZm9yZSwgYWZ0ZXIpOwogIGNvbnN0IGJvZHkgPSByb3dzCiAgICAubWFwKChyb3cpID0+IGA8c3BhbiBjbGFzcz0iZGlmZl9fbGluZSBkaWZmX19saW5lLS0ke3Jvdy5raW5kfSI+JHtNQVJLW3Jvdy5raW5kXX0gJHtlc2NhcGVIdG1sKHJvdy50ZXh0KX08L3NwYW4+YCkKICAgIC5qb2luKCcnKTsKICByZXR1cm4gYDxkaXYgY2xhc3M9ImRpZmYiPiR7Ym9keX08L2Rpdj5gOwp9CgovKiAtLS0tLS0tLS0tLS0tLS0tIGZvcm1hdHRpbmcgLS0tLS0tLS0tLS0tLS0tLSAqLwoKZXhwb3J0IGZ1bmN0aW9uIGZvcm1hdEJ5dGVzKGJ5dGVzKSB7CiAgaWYgKGJ5dGVzID09IG51bGwpIHJldHVybiAnJzsKICBpZiAoYnl0ZXMgPCAxMDI0KSByZXR1cm4gYCR7Ynl0ZXN9IEJgOwogIGlmIChieXRlcyA8IDEwMjQgKiAxMDI0KSByZXR1cm4gYCR7TWF0aC5yb3VuZChieXRlcyAvIDEwMjQpfSBLQmA7CiAgcmV0dXJuIGAkeyhieXRlcyAvICgxMDI0ICogMTAyNCkpLnRvRml4ZWQoMSl9IE1CYDsKfQoKZXhwb3J0IGZ1bmN0aW9uIGZvcm1hdFJlc2V0KGlzbykgewogIGlmICghaXNvKSByZXR1cm4gJyc7CiAgY29uc3QgbXMgPSBEYXRlLnBhcnNlKGlzbykgLSBEYXRlLm5vdygpOwogIGlmIChOdW1iZXIuaXNOYU4obXMpKSByZXR1cm4gJyc7CiAgaWYgKG1zIDw9IDApIHJldHVybiAncmVzZXR0aW5nJzsKICBjb25zdCBtaW51dGVzID0gTWF0aC5yb3VuZChtcyAvIDYwMDAwKTsKICBpZiAobWludXRlcyA8IDYwKSByZXR1cm4gYHJlc2V0cyBpbiAke21pbnV0ZXN9bWA7CiAgY29uc3QgaG91cnMgPSBNYXRoLmZsb29yKG1pbnV0ZXMgLyA2MCk7CiAgaWYgKGhvdXJzIDwgMjQpIHJldHVybiBgcmVzZXRzIGluICR7aG91cnN9aCAke21pbnV0ZXMgJSA2MH1tYDsKICByZXR1cm4gYHJlc2V0cyBpbiAke01hdGguZmxvb3IoaG91cnMgLyAyNCl9ZCAke2hvdXJzICUgMjR9aGA7Cn0KCmV4cG9ydCBmdW5jdGlvbiBmb3JtYXRVc2QodmFsdWUpIHsKICBpZiAoIXZhbHVlKSByZXR1cm4gJyQwLjAwJzsKICByZXR1cm4gdmFsdWUgPCAwLjAxID8gYCQke3ZhbHVlLnRvRml4ZWQoNCl9YCA6IGAkJHt2YWx1ZS50b0ZpeGVkKDIpfWA7Cn0KCi8qIC0tLS0tLS0tLS0tLS0tLS0gbWV0ZXJzIC0tLS0tLS0tLS0tLS0tLS0gKi8KCi8qKgogKiBUaGUgc2lnbmF0dXJlIGNvbnRyb2w6IGEgc2VnbWVudGVkIGdhdWdlIHdob3NlIHRyYWNrIGNhcnJpZXMgcGh5c2ljYWwKICogbm90Y2hlcyBhdCB0aGUgd2FybiBhbmQgYmxvY2sgdGhyZXNob2xkcy4KICovCmV4cG9ydCBmdW5jdGlvbiBtZXRlckh0bWwobGltaXQpIHsKICBjb25zdCBwZXJjZW50ID0gTWF0aC5tYXgoMCwgTWF0aC5taW4oMTAwLCBsaW1pdC5wZXJjZW50ID8/IDApKTsKICBjb25zdCBjYXAgPSBsaW1pdC5jYXAgPz8gbnVsbDsKICBjb25zdCBub3RjaGVzID0gY2FwICYmIGNhcC5lbmFibGVkICE9PSBmYWxzZQogICAgPyBgPGkgY2xhc3M9Im1ldGVyX19ub3RjaCBtZXRlcl9fbm90Y2gtLXdhcm4iIHN0eWxlPSJsZWZ0OiR7Y2FwLndhcm59JSI+PC9pPgogICAgICAgPGkgY2xhc3M9Im1ldGVyX19ub3RjaCBtZXRlcl9fbm90Y2gtLWJsb2NrIiBzdHlsZT0ibGVmdDoke2NhcC5ibG9ja30lIj48L2k+YAogICAgOiAnJzsKICByZXR1cm4gYAogICAgPGRpdiBjbGFzcz0ibWV0ZXIiIGRhdGEtbGV2ZWw9IiR7bGltaXQubGV2ZWwgPz8gJ29rJ30iIHRpdGxlPSIke2VzY2FwZUh0bWwobGltaXQubGFiZWwpfSDigJQgJHtwZXJjZW50fSUiPgogICAgICA8ZGl2IGNsYXNzPSJtZXRlcl9faGVhZCI+CiAgICAgICAgPHNwYW4gY2xhc3M9Im1ldGVyX19sYWJlbCI+JHtlc2NhcGVIdG1sKGxpbWl0LmxhYmVsKX08L3NwYW4+CiAgICAgICAgPHNwYW4gY2xhc3M9Im1ldGVyX192YWx1ZSI+JHtwZXJjZW50fSU8L3NwYW4+CiAgICAgIDwvZGl2PgogICAgICA8ZGl2IGNsYXNzPSJtZXRlcl9fdHJhY2siPgogICAgICAgIDxkaXYgY2xhc3M9Im1ldGVyX19maWxsIiBzdHlsZT0id2lkdGg6JHtwZXJjZW50fSUiPjwvZGl2PgogICAgICAgICR7bm90Y2hlc30KICAgICAgPC9kaXY+CiAgICAgIDxkaXYgY2xhc3M9Im1ldGVyX19mb290Ij4ke2VzY2FwZUh0bWwoZm9ybWF0UmVzZXQobGltaXQucmVzZXRzQXQpKX08L2Rpdj4KICAgIDwvZGl2PmA7Cn0K"}
DEFAULT_BRIDGE = "https://claude-bridge.sardine-hue.ts.net"

MAX_TOOL_RESULT = 30000
MAX_READ_BYTES = 400_000
DEFAULT_MAX_TOKENS = 8192
SHELL_TIMEOUT = 120

# The bridge may restart (an update, a reboot); Funnel answers 502 meanwhile.
# Model calls wait it out instead of failing the turn.
RETRY_STATUSES = (0, 502, 503, 504, 529)
RETRY_DELAYS = (2, 3, 5, 8, 10, 10, 10, 10, 10)

# --------------------------------------------------------------------------
# configuration and state
# --------------------------------------------------------------------------


def config_dir():
    if IS_WINDOWS:
        base = os.environ.get("APPDATA") or os.path.expanduser("~")
    else:
        base = os.environ.get("XDG_CONFIG_HOME") or os.path.join(os.path.expanduser("~"), ".config")
    path = os.path.join(base, "ccb-agent")
    os.makedirs(path, exist_ok=True)
    return path


class Store:
    """Config and sessions, persisted as JSON next to each other."""

    def __init__(self):
        self.dir = config_dir()
        self.config_path = os.path.join(self.dir, "config.json")
        self.sessions_path = os.path.join(self.dir, "sessions.json")
        self.lock = threading.Lock()
        self.config = self._load(self.config_path, {"bridge": "", "token": "", "roots": [], "maxTokens": DEFAULT_MAX_TOKENS})
        self.sessions = self._load(self.sessions_path, {})

    @staticmethod
    def _load(path, fallback):
        try:
            with open(path, "r", encoding="utf-8") as handle:
                return json.load(handle)
        except Exception:
            return fallback

    def _save(self, path, value):
        tmp = "%s.%d.tmp" % (path, os.getpid())
        # The config holds the bridge token: keep it readable by this user only.
        fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
        with os.fdopen(fd, "w", encoding="utf-8") as handle:
            json.dump(value, handle, indent=2)
        os.replace(tmp, path)

    def save_config(self):
        with self.lock:
            self._save(self.config_path, self.config)

    def save_sessions(self):
        with self.lock:
            self._save(self.sessions_path, self.sessions)


STORE = Store()


# --------------------------------------------------------------------------
# workspace-scoped filesystem access
# --------------------------------------------------------------------------


class ToolError(Exception):
    pass


def resolve_in_root(root, relative):
    """Resolve `relative` under `root`, refusing anything that escapes it."""
    root_real = os.path.realpath(root)
    target = os.path.realpath(os.path.join(root_real, relative or "."))
    if target != root_real and not target.startswith(root_real + os.sep):
        raise ToolError("Path is outside the workspace: %s" % relative)
    return target


SKIP_DIRS = {".git", "node_modules", ".venv", "__pycache__", "dist", "build", ".next", ".cache"}
BINARY_EXT = {
    ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".pdf", ".zip", ".gz", ".tar",
    ".so", ".dll", ".exe", ".bin", ".wasm", ".mp3", ".mp4", ".woff", ".woff2", ".ttf",
}


def read_text(path, limit=MAX_READ_BYTES):
    if os.path.splitext(path)[1].lower() in BINARY_EXT:
        raise ToolError("That is a binary file.")
    size = os.path.getsize(path)
    with open(path, "r", encoding="utf-8", errors="replace") as handle:
        data = handle.read(limit)
    if size > limit:
        data += "\n… truncated, %d bytes total" % size
    return data


# --------------------------------------------------------------------------
# tools exposed to the model
# --------------------------------------------------------------------------

TOOL_SPECS = [
    {
        "name": "read_file",
        "description": "Read a UTF-8 text file from the workspace. Returns the file content.",
        "input_schema": {
            "type": "object",
            "properties": {"path": {"type": "string", "description": "Path relative to the workspace root."}},
            "required": ["path"],
        },
    },
    {
        "name": "write_file",
        "description": "Create a file or replace its entire contents. Creates parent directories as needed.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string"},
                "content": {"type": "string"},
            },
            "required": ["path", "content"],
        },
    },
    {
        "name": "edit_file",
        "description": (
            "Replace an exact string in a file. old_string must appear exactly once unless "
            "replace_all is true. Prefer this over write_file for changes to existing files."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string"},
                "old_string": {"type": "string"},
                "new_string": {"type": "string"},
                "replace_all": {"type": "boolean"},
            },
            "required": ["path", "old_string", "new_string"],
        },
    },
    {
        "name": "list_dir",
        "description": "List the entries of a directory in the workspace.",
        "input_schema": {
            "type": "object",
            "properties": {"path": {"type": "string", "description": "Defaults to the workspace root."}},
        },
    },
    {
        "name": "glob",
        "description": "Find files matching a glob pattern, e.g. '**/*.py'. Returns paths relative to the workspace.",
        "input_schema": {
            "type": "object",
            "properties": {"pattern": {"type": "string"}},
            "required": ["pattern"],
        },
    },
    {
        "name": "grep",
        "description": "Search file contents for a regular expression. Returns matching lines with their paths.",
        "input_schema": {
            "type": "object",
            "properties": {
                "pattern": {"type": "string"},
                "glob": {"type": "string", "description": "Optional file filter, e.g. '*.js'."},
            },
            "required": ["pattern"],
        },
    },
    {
        "name": "run_shell",
        "description": (
            "Run a shell command in the workspace and return its combined output. "
            "Uses cmd.exe on Windows and /bin/sh elsewhere."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "command": {"type": "string"},
                "timeout": {"type": "integer", "description": "Seconds; defaults to 120."},
            },
            "required": ["command"],
        },
    },
]


def walk_workspace(root):
    for dirpath, dirnames, filenames in os.walk(root):
        dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.startswith(".")]
        for name in filenames:
            yield os.path.join(dirpath, name)


def run_tool(root, name, args):
    """Execute one tool call. Returns text; raises ToolError for tool failures."""
    if name == "read_file":
        return read_text(resolve_in_root(root, args.get("path", "")))

    if name == "write_file":
        target = resolve_in_root(root, args.get("path", ""))
        os.makedirs(os.path.dirname(target), exist_ok=True)
        with open(target, "w", encoding="utf-8") as handle:
            handle.write(args.get("content", ""))
        return "Wrote %d bytes to %s" % (len(args.get("content", "")), args.get("path"))

    if name == "edit_file":
        target = resolve_in_root(root, args.get("path", ""))
        old, new = args.get("old_string", ""), args.get("new_string", "")
        if not old:
            raise ToolError("old_string must not be empty; use write_file to create a file.")
        with open(target, "r", encoding="utf-8") as handle:
            content = handle.read()
        count = content.count(old)
        if count == 0:
            raise ToolError("old_string was not found in %s" % args.get("path"))
        if count > 1 and not args.get("replace_all"):
            raise ToolError(
                "old_string appears %d times in %s; pass replace_all or include more context."
                % (count, args.get("path"))
            )
        content = content.replace(old, new) if args.get("replace_all") else content.replace(old, new, 1)
        with open(target, "w", encoding="utf-8") as handle:
            handle.write(content)
        return "Edited %s (%d replacement%s)" % (args.get("path"), count if args.get("replace_all") else 1,
                                                 "s" if args.get("replace_all") and count > 1 else "")

    if name == "list_dir":
        target = resolve_in_root(root, args.get("path", "."))
        rows = []
        for entry in sorted(os.listdir(target)):
            if entry.startswith("."):
                continue
            full = os.path.join(target, entry)
            rows.append("%s%s" % (entry, "/" if os.path.isdir(full) else ""))
        return "\n".join(rows) or "(empty directory)"

    if name == "glob":
        pattern = args.get("pattern", "")
        root_real = os.path.realpath(root)
        hits = []
        for path in walk_workspace(root_real):
            rel = os.path.relpath(path, root_real).replace(os.sep, "/")
            if fnmatch.fnmatch(rel, pattern) or fnmatch.fnmatch(os.path.basename(rel), pattern):
                hits.append(rel)
            if len(hits) >= 500:
                break
        return "\n".join(sorted(hits)) or "(no matches)"

    if name == "grep":
        try:
            regex = re.compile(args.get("pattern", ""))
        except re.error as err:
            raise ToolError("Invalid regular expression: %s" % err)
        file_glob = args.get("glob")
        root_real = os.path.realpath(root)
        out = []
        for path in walk_workspace(root_real):
            rel = os.path.relpath(path, root_real).replace(os.sep, "/")
            if file_glob and not (fnmatch.fnmatch(rel, file_glob) or fnmatch.fnmatch(os.path.basename(rel), file_glob)):
                continue
            if os.path.splitext(path)[1].lower() in BINARY_EXT:
                continue
            try:
                with open(path, "r", encoding="utf-8", errors="ignore") as handle:
                    for number, line in enumerate(handle, 1):
                        if regex.search(line):
                            out.append("%s:%d: %s" % (rel, number, line.rstrip()[:300]))
                            if len(out) >= 300:
                                return "\n".join(out)
            except OSError:
                continue
        return "\n".join(out) or "(no matches)"

    if name == "run_shell":
        command = args.get("command", "")
        timeout = int(args.get("timeout") or SHELL_TIMEOUT)
        try:
            completed = subprocess.run(
                command,
                shell=True,
                cwd=os.path.realpath(root),
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                timeout=timeout,
            )
        except subprocess.TimeoutExpired:
            raise ToolError("Command timed out after %ds." % timeout)
        output = completed.stdout.decode("utf-8", errors="replace")
        suffix = "" if completed.returncode == 0 else "\n(exit code %d)" % completed.returncode
        return (output or "(no output)") + suffix

    raise ToolError("Unknown tool: %s" % name)


# --------------------------------------------------------------------------
# bridge client
# --------------------------------------------------------------------------


class RetryableError(Exception):
    pass


class BridgeError(Exception):
    def __init__(self, message, status=0):
        super().__init__(message)
        self.status = status


def bridge_request(path, payload=None, method="GET", stream=False, timeout=600, session_id=None):
    cfg = STORE.config
    base = (cfg.get("bridge") or "").rstrip("/")
    if not base:
        raise BridgeError("No bridge configured.")
    data = json.dumps(payload).encode("utf-8") if payload is not None else None
    request = urllib.request.Request(base + path, data=data, method=method)
    request.add_header("authorization", "Bearer %s" % cfg.get("token", ""))
    request.add_header("x-ccb-client", "agent/%s %s" % (VERSION, socket.gethostname()[:40]))
    if session_id:
        request.add_header("x-ccb-session", session_id)
    if data is not None:
        request.add_header("content-type", "application/json")
    try:
        response = urllib.request.urlopen(request, timeout=timeout)
    except urllib.error.HTTPError as err:
        body = err.read().decode("utf-8", errors="replace")
        try:
            message = json.loads(body).get("error") or body
        except Exception:
            message = body
        raise BridgeError(message, err.code)
    except urllib.error.URLError as err:
        raise BridgeError("Cannot reach the bridge: %s" % err.reason)
    if stream:
        return response
    return json.loads(response.read().decode("utf-8"))


def stream_messages(payload, session_id=None):
    """Yield parsed SSE events from the bridge's /v1/messages endpoint."""
    response = bridge_request("/v1/messages", payload, method="POST", stream=True, session_id=session_id)
    try:
        for raw in response:
            line = raw.decode("utf-8", errors="replace").strip()
            if not line.startswith("data:"):
                continue
            chunk = line[5:].strip()
            if not chunk or chunk == "[DONE]":
                continue
            try:
                yield json.loads(chunk)
            except ValueError:
                continue
    finally:
        response.close()


# --------------------------------------------------------------------------
# agent session
# --------------------------------------------------------------------------

SYSTEM_PROMPT = """You are a coding agent working directly on the user's machine.

Use the provided tools to inspect and change real files in the workspace, and to
run commands. Prefer edit_file over write_file when changing an existing file.
Read a file before editing it. Keep replies short: describe what you changed,
not what you are about to do. The workspace root is {root} on {system}."""


class Session:
    """One conversation, with its own history, workspace and subscribers."""

    def __init__(self, meta):
        self.meta = meta
        self.history = meta.get("history", [])
        self.subscribers = []
        self.sub_lock = threading.Lock()
        self.events = []
        self.seq = 0
        self.running = False
        self.cancel = threading.Event()

    # -- events ---------------------------------------------------------

    def emit(self, kind, data, ephemeral=False):
        self.seq += 1
        event = {"seq": self.seq, "type": kind, "data": data, "at": time.time()}
        if not ephemeral:
            self.events.append(event)
            del self.events[:-1200]
        payload = "id: %d\nevent: message\ndata: %s\n\n" % (event["seq"], json.dumps(event))
        with self.sub_lock:
            for q in list(self.subscribers):
                q.put(payload)

    def status(self):
        return {
            "id": self.meta["id"],
            "status": "running" if self.running else "idle",
            "model": self.meta.get("model"),
            "alive": True,
            "turnCount": self.meta.get("turnCount", 0),
        }

    def subscribe(self, last_seq):
        q = queue.Queue()
        with self.sub_lock:
            self.subscribers.append(q)
        for event in self.events:
            if event["seq"] > last_seq:
                q.put("id: %d\nevent: message\ndata: %s\n\n" % (event["seq"], json.dumps(event)))
        q.put("id: %d\nevent: message\ndata: %s\n\n"
              % (self.seq, json.dumps({"seq": self.seq, "type": "status", "data": self.status()})))
        return q

    def unsubscribe(self, q):
        with self.sub_lock:
            if q in self.subscribers:
                self.subscribers.remove(q)

    # -- turns ----------------------------------------------------------

    def start_turn(self, content):
        if self.running:
            raise ToolError("This session is already working on a turn.")
        self.running = True
        self.cancel.clear()
        self.history.append({"role": "user", "content": content})
        self.emit("user_message", {"content": content})
        self.emit("status", self.status())
        if not self.meta.get("title"):
            for block in content:
                if block.get("type") == "text":
                    self.meta["title"] = block["text"].strip().replace("\n", " ")[:60]
                    break
        threading.Thread(target=self._run, daemon=True).start()

    def interrupt(self):
        if not self.running:
            return False
        self.cancel.set()
        self.emit("notice", {"message": "Stopping…"})
        return True

    def _run(self):
        started = time.time()
        steps = 0
        try:
            while True:
                steps += 1
                if self.cancel.is_set():
                    self.emit("notice", {"message": "Stopped."})
                    break
                blocks, stop_reason = self._call_model()
                if blocks is None:
                    break
                self.history.append({"role": "assistant", "content": blocks})
                self.emit("claude", {"type": "assistant", "message": {"role": "assistant", "content": blocks}})

                tool_uses = [b for b in blocks if b.get("type") == "tool_use"]
                if stop_reason != "tool_use" or not tool_uses:
                    break
                results = [self._run_tool(block) for block in tool_uses]
                self.history.append({"role": "user", "content": results})
                self.emit("claude", {"type": "user", "message": {"role": "user", "content": results}})
                if steps > 60:
                    self.emit("error", {"message": "Stopped after 60 steps to avoid looping."})
                    break
        except BridgeError as err:
            self.emit("blocked" if err.status in (401, 403, 429) else "error",
                      {"message": str(err), "reasons": [str(err)]})
        except Exception:
            self.emit("error", {"message": traceback.format_exc(limit=2)})
        finally:
            self.running = False
            self.meta["turnCount"] = self.meta.get("turnCount", 0) + 1
            self.meta["history"] = self.history
            STORE.sessions[self.meta["id"]] = self.meta
            STORE.save_sessions()
            self.emit("claude", {"type": "result", "duration_ms": int((time.time() - started) * 1000)})
            self.emit("status", self.status())

    def _call_model(self):
        """A model call that survives a bridge restart: retry while it is unreachable."""
        for delay in RETRY_DELAYS + (None,):
            try:
                return self._call_model_once()
            except RetryableError as err:
                if delay is None or self.cancel.is_set():
                    raise BridgeError("The bridge is unreachable: %s" % err)
                self.emit("retry", {"message": "Bridge unavailable (%s) — retrying in %ds…" % (err, delay)})
                if self.cancel.wait(delay):
                    return None, None

    def _call_model_once(self):
        """One streaming model call. Returns (content blocks, stop_reason)."""
        payload = {
            "model": self.meta["model"],
            "max_tokens": STORE.config.get("maxTokens", DEFAULT_MAX_TOKENS),
            "stream": True,
            "system": SYSTEM_PROMPT.format(root=self.meta["root"], system=platform.platform()),
            "tools": TOOL_SPECS,
            "messages": self.history,
        }
        blocks = {}
        stop_reason = None
        finished = False
        try:
            for event in stream_messages(payload, self.meta["id"]):
                if self._apply_event(blocks, event):
                    stop_reason = event.get("delta", {}).get("stop_reason", stop_reason)
                finished = finished or event.get("type") == "message_stop"
                if self.cancel.is_set():
                    return self._finish_blocks(blocks), stop_reason
        except BridgeError as err:
            if err.status in RETRY_STATUSES:
                raise RetryableError(str(err) or "status %d" % err.status)
            raise
        except (OSError, http.client.HTTPException) as err:
            # The stream broke mid-reply (bridge restarted): redo the whole call.
            raise RetryableError("connection lost: %s" % err)
        if not finished:
            # Ended without message_stop: the bridge went away mid-reply.
            raise RetryableError("reply was cut off")
        return self._finish_blocks(blocks), stop_reason

    def _apply_event(self, blocks, event):
        """Fold one SSE event into blocks. True when it is a message_delta."""
        kind = event.get("type")
        if kind == "content_block_start":
            blocks[event["index"]] = dict(event["content_block"])
            if blocks[event["index"]].get("type") == "tool_use":
                blocks[event["index"]]["_json"] = ""
        elif kind == "content_block_delta":
            delta = event.get("delta", {})
            block = blocks.setdefault(event["index"], {"type": "text", "text": ""})
            kind_of_delta = delta.get("type")
            if kind_of_delta == "text_delta":
                block["text"] = block.get("text", "") + delta["text"]
                self.emit("stream_event", {"event": event}, ephemeral=True)
            elif kind_of_delta == "input_json_delta":
                block["_json"] = block.get("_json", "") + delta.get("partial_json", "")
            elif kind_of_delta == "thinking_delta":
                # Thinking blocks must be echoed back intact, signature and
                # all, or the next request is rejected as malformed.
                block["thinking"] = block.get("thinking", "") + delta.get("thinking", "")
                self.emit("stream_event", {"event": event}, ephemeral=True)
            elif kind_of_delta == "signature_delta":
                block["signature"] = block.get("signature", "") + delta.get("signature", "")
        elif kind == "message_delta":
            return True
        elif kind == "error":
            error = event.get("error", {})
            if error.get("type") in ("overloaded_error", "api_error"):
                raise RetryableError(error.get("message", "upstream error"))
            raise BridgeError(error.get("message", "stream error"))
        return False

    @staticmethod
    def _finish_blocks(blocks):
        ordered = []
        for index in sorted(blocks):
            block = blocks[index]
            if block.get("type") == "tool_use":
                raw = block.pop("_json", "") or "{}"
                try:
                    block["input"] = json.loads(raw)
                except ValueError:
                    block["input"] = {}
            if block.get("type") == "text" and not block.get("text", "").strip():
                continue
            if block.get("type") == "thinking" and not block.get("thinking", "").strip():
                continue
            ordered.append(block)
        return ordered or None

    def _run_tool(self, block):
        name, args = block.get("name"), block.get("input") or {}
        try:
            output = run_tool(self.meta["root"], name, args)
            is_error = False
        except ToolError as err:
            output, is_error = str(err), True
        except Exception as err:
            output, is_error = "%s: %s" % (type(err).__name__, err), True
        if len(output) > MAX_TOOL_RESULT:
            output = output[:MAX_TOOL_RESULT] + "\n… truncated"
        return {
            "type": "tool_result",
            "tool_use_id": block.get("id"),
            "content": [{"type": "text", "text": output}],
            "is_error": is_error,
        }


SESSIONS = {}
SESSIONS_LOCK = threading.Lock()


def get_session(session_id):
    with SESSIONS_LOCK:
        if session_id in SESSIONS:
            return SESSIONS[session_id]
        meta = STORE.sessions.get(session_id)
        if not meta:
            return None
        session = Session(meta)
        SESSIONS[session_id] = session
        return session


# --------------------------------------------------------------------------
# HTTP surface for the local UI
# --------------------------------------------------------------------------


def ui_asset(name):
    if EMBEDDED_UI:
        blob = EMBEDDED_UI.get(name)
        return base64.b64decode(blob) if blob else None
    path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ui", name)
    try:
        with open(path, "rb") as handle:
            return handle.read()
    except OSError:
        return None


class Handler(BaseHTTPRequestHandler):
    server_version = "ccb-agent/" + VERSION
    protocol_version = "HTTP/1.1"

    def log_message(self, fmt, *args):
        if os.environ.get("CCB_AGENT_DEBUG"):
            sys.stderr.write("%s - %s\n" % (self.address_string(), fmt % args))

    # -- helpers --------------------------------------------------------

    def _send(self, status, body=b"", content_type="application/json", extra=None):
        self.send_response(status)
        self.send_header("content-type", content_type)
        self.send_header("content-length", str(len(body)))
        self.send_header("cache-control", "no-store")
        for key, value in (extra or {}).items():
            self.send_header(key, value)
        self.end_headers()
        if self.command != "HEAD":
            self.wfile.write(body)

    def _json(self, status, value):
        self._send(status, json.dumps(value).encode("utf-8"))

    def _body(self):
        length = int(self.headers.get("content-length") or 0)
        if not length:
            return {}
        try:
            return json.loads(self.rfile.read(length).decode("utf-8"))
        except ValueError:
            raise ToolError("Invalid JSON body.")

    def _guard(self):
        """Only this machine may drive the agent; it runs commands as you."""
        host = (self.headers.get("host") or "").split(":")[0].strip("[]")
        if host not in ("localhost", "127.0.0.1", "::1"):
            self._send(421, b"Open the agent at http://localhost", "text/plain")
            return False
        origin = self.headers.get("origin")
        if origin:
            allowed = {"http://localhost:%d" % PORT, "http://127.0.0.1:%d" % PORT}
            if origin not in allowed:
                self._json(403, {"error": "Cross-origin requests are refused."})
                return False
        return True

    # -- routing --------------------------------------------------------

    def do_GET(self):
        if not self._guard():
            return
        path = self.path.split("?")[0]
        query = {}
        if "?" in self.path:
            for part in self.path.split("?", 1)[1].split("&"):
                if "=" in part:
                    key, value = part.split("=", 1)
                    query[key] = urllib.parse.unquote_plus(value)
        try:
            if path == "/api/bootstrap":
                return self._json(200, self.bootstrap())
            if path == "/api/usage":
                return self._json(200, self.usage())
            if path == "/api/sessions":
                return self._json(200, {"sessions": list_sessions()})
            if path.startswith("/api/sessions/") and path.endswith("/history"):
                session = get_session(path.split("/")[3])
                if not session:
                    return self._json(404, {"error": "Session not found."})
                return self._json(200, {"source": "live", "events": session.events,
                                        "lastSeq": session.seq})
            if path.startswith("/api/sessions/") and path.endswith("/events"):
                return self.sse(path.split("/")[3], query.get("lastEventId"))
            if path == "/api/update":
                return self._json(200, check_update())
            if path == "/api/files":
                return self._json(200, browse(query.get("project"), query.get("path", ".")))
            if path == "/api/file":
                return self._json(200, peek(query.get("project"), query.get("path")))
            return self.static(path)
        except ToolError as err:
            return self._json(400, {"error": str(err)})
        except BridgeError as err:
            return self._json(err.status or 502, {"error": str(err)})
        except Exception as err:
            return self._json(500, {"error": "%s: %s" % (type(err).__name__, err)})

    def do_POST(self):
        if not self._guard():
            return
        path = self.path.split("?")[0]
        try:
            if path == "/api/update":
                # Replacing our own code: demand a JSON content type so a
                # cross-site form can never reach this, even without Origin.
                if not (self.headers.get("content-type") or "").startswith("application/json"):
                    return self._json(415, {"error": "JSON required."})
                return self._json(200, apply_update())
            body = self._body()
            if path == "/api/config":
                return self._json(200, save_settings(body))
            if path == "/api/roots":
                return self._json(200, add_root(body.get("path", "")))
            if path == "/api/sessions":
                return self._json(201, {"session": create_session(body)})
            if path.startswith("/api/sessions/") and path.endswith("/message"):
                return self._json(202, send_message(path.split("/")[3], body))
            if path.startswith("/api/sessions/") and path.endswith("/interrupt"):
                session = get_session(path.split("/")[3])
                return self._json(200, {"ok": bool(session and session.interrupt())})
            if path.startswith("/api/sessions/") and path.endswith("/model"):
                return self._json(200, {"session": set_model(path.split("/")[3], body)})
            return self._json(404, {"error": "Not found"})
        except ToolError as err:
            return self._json(400, {"error": str(err)})
        except BridgeError as err:
            return self._json(err.status or 502, {"error": str(err)})
        except Exception as err:
            return self._json(500, {"error": "%s: %s" % (type(err).__name__, err)})

    def do_DELETE(self):
        if not self._guard():
            return
        path = self.path.split("?")[0]
        if path.startswith("/api/sessions/"):
            session_id = path.split("/")[3]
            with SESSIONS_LOCK:
                SESSIONS.pop(session_id, None)
            STORE.sessions.pop(session_id, None)
            STORE.save_sessions()
            return self._json(200, {"ok": True})
        if path == "/api/roots":
            target = urllib.parse.unquote_plus(self.path.split("path=", 1)[1]) if "path=" in self.path else ""
            STORE.config["roots"] = [r for r in STORE.config["roots"] if r.get("path") != target]
            STORE.save_config()
            return self._json(200, {"roots": STORE.config["roots"]})
        return self._json(404, {"error": "Not found"})

    # -- endpoints ------------------------------------------------------

    def bootstrap(self):
        policy = safe_policy()
        return {
            "config": {
                "models": {"allowed": policy.get("models", []), "default": default_model(policy)},
                "projects": STORE.config.get("roots", []),
                "limits": {"maxUploadBytes": 10 * 1024 * 1024},
                "paused": policy.get("paused", False),
            },
            "models": policy.get("models", []),
            "projects": STORE.config.get("roots", []),
            "sessions": list_sessions(),
            "usage": policy.get("usage", {"ok": False, "error": policy.get("error"), "limits": []}),
            "settings": {"bridge": STORE.config.get("bridge", ""), "connected": policy.get("ok", False),
                         "error": policy.get("error")},
            "agent": {"version": VERSION, "platform": platform.platform(), "cwd": os.getcwd()},
        }

    def usage(self):
        policy = safe_policy()
        return {"usage": policy.get("usage", {"ok": False, "error": policy.get("error"), "limits": []}),
                "models": policy.get("models", [])}

    def sse(self, session_id, last_event_id):
        session = get_session(session_id)
        if not session:
            return self._json(404, {"error": "Session not found."})
        try:
            last_seq = int(last_event_id or self.headers.get("last-event-id") or 0)
        except ValueError:
            last_seq = 0
        self.send_response(200)
        self.send_header("content-type", "text/event-stream; charset=utf-8")
        self.send_header("cache-control", "no-cache, no-transform")
        self.send_header("connection", "keep-alive")
        self.end_headers()
        q = session.subscribe(last_seq)
        try:
            self.wfile.write(b"retry: 2000\n\n")
            self.wfile.flush()
            while True:
                try:
                    payload = q.get(timeout=20)
                except queue.Empty:
                    payload = ": ping\n\n"
                self.wfile.write(payload.encode("utf-8"))
                self.wfile.flush()
        except (BrokenPipeError, ConnectionResetError, OSError):
            pass
        finally:
            session.unsubscribe(q)

    def static(self, path):
        name = "index.html" if path in ("/", "") else path.lstrip("/")
        blob = ui_asset(name)
        if blob is None and not name.startswith("api/"):
            blob = ui_asset("index.html")
            name = "index.html"
        if blob is None:
            return self._send(404, b"Not found", "text/plain")
        kind = mimetypes.guess_type(name)[0] or "application/octet-stream"
        if name.endswith(".js"):
            kind = "text/javascript; charset=utf-8"
        return self._send(200, blob, kind)


# --------------------------------------------------------------------------
# endpoint helpers
# --------------------------------------------------------------------------


# --------------------------------------------------------------------------
# self-update from the bridge
# --------------------------------------------------------------------------


def _version_tuple(text):
    try:
        return tuple(int(part) for part in text.split("."))
    except (AttributeError, ValueError):
        return (0,)


def fetch_latest_agent():
    response = bridge_request("/agent.py", stream=True, timeout=60)
    try:
        text = response.read().decode("utf-8")
    finally:
        response.close()
    match = re.search(r'^VERSION = "([^"]+)"', text, re.M)
    if not match:
        raise BridgeError("The bridge served something that is not the agent.")
    return text, match.group(1)


def check_update():
    try:
        _, latest = fetch_latest_agent()
    except BridgeError as err:
        return {"current": VERSION, "latest": None, "available": False, "error": str(err)}
    return {"current": VERSION, "latest": latest,
            "available": _version_tuple(latest) > _version_tuple(VERSION),
            "autoRestart": not IS_WINDOWS}


def apply_update():
    """Replace this file with the bridge's current agent, then restart into it."""
    if any(session.running for session in list(SESSIONS.values())):
        raise ToolError("Wait for the running turn to finish, then update.")
    text, latest = fetch_latest_agent()
    if _version_tuple(latest) <= _version_tuple(VERSION):
        return {"updated": False, "version": VERSION}
    compile(text, AGENT_PATH, "exec")  # refuse anything that is not valid Python
    shutil.copy2(AGENT_PATH, AGENT_PATH + ".bak")
    tmp = AGENT_PATH + ".new"
    with open(tmp, "w", encoding="utf-8", newline="") as handle:
        handle.write(text)
    os.replace(tmp, AGENT_PATH)
    if IS_WINDOWS:
        # execv on Windows detaches from the console; ask for a manual restart.
        return {"updated": True, "version": latest, "restarting": False}
    threading.Thread(target=_restart, daemon=True).start()
    return {"updated": True, "version": latest, "restarting": True}


def _restart():
    time.sleep(0.5)  # let the HTTP response go out first
    RESTART.set()
    SERVER.shutdown()  # main() then re-execs itself once serve_forever returns


def announce():
    """Tell the bridge this agent exists, so it shows up in the admin console."""
    try:
        bridge_request("/api/hello", {}, method="POST", timeout=15)
        return True
    except BridgeError:
        return False


def safe_policy():
    """Ask the bridge what is allowed; never let an outage break the UI."""
    try:
        data = bridge_request("/api/policy", timeout=20)
        data["ok"] = True
        return data
    except BridgeError as err:
        return {"ok": False, "error": str(err), "models": [], "usage": {"ok": False, "error": str(err), "limits": []}}


def default_model(policy):
    models = policy.get("models", [])
    preferred = policy.get("default")
    ids = [m["id"] for m in models]
    return preferred if preferred in ids else (ids[0] if ids else None)


def list_sessions():
    rows = []
    for meta in STORE.sessions.values():
        live = SESSIONS.get(meta["id"])
        rows.append({
            "id": meta["id"],
            "title": meta.get("title"),
            "projectPath": meta.get("root"),
            "projectName": meta.get("rootName") or os.path.basename(meta.get("root", "")),
            "model": meta.get("model"),
            "turnCount": meta.get("turnCount", 0),
            "createdAt": meta.get("createdAt"),
            "lastActivity": meta.get("lastActivity"),
            "alive": bool(live),
            "running": bool(live and live.running),
        })
    rows.sort(key=lambda r: r.get("lastActivity") or "", reverse=True)
    return rows


def save_settings(body):
    if "bridge" in body:
        STORE.config["bridge"] = (body["bridge"] or "").strip().rstrip("/")
    if body.get("token"):
        STORE.config["token"] = body["token"].strip()
    STORE.save_config()
    announce()
    policy = safe_policy()
    return {"ok": policy.get("ok", False), "error": policy.get("error"), "models": policy.get("models", [])}


def add_root(path):
    expanded = os.path.realpath(os.path.expanduser(path.strip()))
    if not os.path.isdir(expanded):
        raise ToolError("Not a directory: %s" % expanded)
    roots = STORE.config.setdefault("roots", [])
    if not any(r.get("path") == expanded for r in roots):
        roots.append({"name": os.path.basename(expanded) or expanded, "path": expanded})
        STORE.save_config()
    return {"roots": roots}


def known_root(path):
    for root in STORE.config.get("roots", []):
        if root.get("path") == path:
            return root
    raise ToolError("That folder is not on the agent's list.")


def create_session(body):
    root = known_root(body.get("projectPath"))
    policy = safe_policy()
    model = body.get("model") or default_model(policy)
    if not model:
        raise ToolError("The bridge has not allowed any models.")
    meta = {
        "id": str(uuid.uuid4()),
        "title": None,
        "root": root["path"],
        "rootName": root["name"],
        "model": model,
        "createdAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "lastActivity": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "turnCount": 0,
        "history": [],
    }
    STORE.sessions[meta["id"]] = meta
    STORE.save_sessions()
    with SESSIONS_LOCK:
        SESSIONS[meta["id"]] = Session(meta)
    return {k: v for k, v in meta.items() if k != "history"}


def set_model(session_id, body):
    session = get_session(session_id)
    if not session:
        raise ToolError("Session not found.")
    if session.running:
        raise ToolError("Cannot change the model while a turn is running.")
    model = body.get("model")
    if model:
        allowed = [m["id"] for m in safe_policy().get("models", [])]
        if allowed and model not in allowed:
            raise ToolError('Model "%s" is not allowed by the bridge.' % model)
        session.meta["model"] = model
        STORE.save_sessions()
    return {k: v for k, v in session.meta.items() if k != "history"}


def send_message(session_id, body):
    session = get_session(session_id)
    if not session:
        raise ToolError("Session not found.")
    content = []
    for image in body.get("images") or []:
        if not image.get("dataBase64") or not image.get("mediaType"):
            raise ToolError("Malformed image attachment.")
        content.append({"type": "image", "source": {"type": "base64",
                                                    "media_type": image["mediaType"],
                                                    "data": image["dataBase64"]}})
    text = (body.get("text") or "").strip()
    if text:
        content.append({"type": "text", "text": text})
    if not content:
        raise ToolError("Message is empty.")
    session.meta["lastActivity"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
    session.start_turn(content)
    return {"ok": True}


def browse(project, relative):
    root = known_root(project)["path"]
    target = resolve_in_root(root, relative or ".")
    entries = []
    for name in sorted(os.listdir(target)):
        if name.startswith(".") and name != ".claude":
            continue
        full = os.path.join(target, name)
        is_dir = os.path.isdir(full)
        if is_dir and name in SKIP_DIRS:
            continue
        entries.append({
            "name": name,
            "type": "dir" if is_dir else "file",
            "size": None if is_dir else os.path.getsize(full),
            "path": os.path.relpath(full, root).replace(os.sep, "/"),
        })
    entries.sort(key=lambda e: (e["type"] != "dir", e["name"].lower()))
    return {"cwd": os.path.relpath(target, root).replace(os.sep, "/"), "entries": entries}


def peek(project, relative):
    if not relative:
        raise ToolError("path is required.")
    root = known_root(project)["path"]
    target = resolve_in_root(root, relative)
    size = os.path.getsize(target)
    if os.path.splitext(target)[1].lower() in BINARY_EXT:
        return {"path": relative, "size": size, "binary": True, "content": None}
    return {"path": relative, "size": size, "binary": False,
            "truncated": size > MAX_READ_BYTES, "content": read_text(target)}


# --------------------------------------------------------------------------
# entry point
# --------------------------------------------------------------------------

PORT = 8600


def main():
    global PORT, SERVER
    parser = argparse.ArgumentParser(description="Claude Code Bridge client agent")
    parser.add_argument("--bridge", default=STORE.config.get("bridge") or DEFAULT_BRIDGE,
                        help="Bridge base URL, e.g. http://192.168.1.20:8787")
    parser.add_argument("--token", default=STORE.config.get("token", ""), help="Bridge access token")
    parser.add_argument("--port", type=int, default=int(os.environ.get("CCB_AGENT_PORT", PORT)))
    parser.add_argument("--workspace", default=None, help="Folder to work in (defaults to the current directory)")
    parser.add_argument("--version", action="store_true")
    args = parser.parse_args()

    if args.version:
        print("ccb-agent %s" % VERSION)
        return

    PORT = args.port
    if args.bridge:
        STORE.config["bridge"] = args.bridge.rstrip("/")
    if args.token:
        STORE.config["token"] = args.token
    workspace = os.path.realpath(os.path.expanduser(args.workspace or os.getcwd()))
    roots = STORE.config.setdefault("roots", [])
    if os.path.isdir(workspace) and not any(r.get("path") == workspace for r in roots):
        roots.append({"name": os.path.basename(workspace) or workspace, "path": workspace})
    STORE.save_config()

    if STORE.config.get("bridge") and STORE.config.get("token"):
        threading.Thread(target=announce, daemon=True).start()

    server = SERVER = ThreadingHTTPServer(("127.0.0.1", PORT), Handler)
    server.daemon_threads = True

    print("Claude Code Bridge agent %s" % VERSION)
    print("  bridge    %s" % (STORE.config.get("bridge") or "(not set — configure it in the UI)"))
    print("  workspace %s" % workspace)
    print("  open      http://localhost:%d" % PORT)
    if not shutil.which("git"):
        print("  note: git was not found on PATH; the agent can still edit files")
    sys.stdout.flush()
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        print("\nstopped")
        return
    if RESTART.is_set():
        # Re-exec in the main thread: from a worker thread, the main thread
        # would return here and end the process before the exec happened.
        server.server_close()
        print("updated; restarting\n", flush=True)
        sys.stdout.flush()
        os.execv(sys.executable, [sys.executable, AGENT_PATH] + sys.argv[1:])


if __name__ == "__main__":
    main()
