#!/bin/sh # cheapllm.shop setup for macOS and Linux (and WSL): installs and configures the tools from # https://cheapllm.shop/integrations that you pick in a list. Windows: irm https://cheapllm.shop/setup.ps1 | iex # # curl -fsSL https://cheapllm.shop/setup.sh | sh # curl -fsSL https://cheapllm.shop/setup.sh | sh -s -- --dry-run # # Options: --key=sk-cheap-... --dry-run --only=claude-code,codex --all # --yes (no list: set up what is found) --install / --no-install (missing tools) # Every file it changes is copied to .cheapllm-bak first. Needs node or python3. set -eu ORIGIN='https://cheapllm.shop' DRY=0; ONLY=''; ALL=0; YES=0; INSTALL=ask KEY="${CHEAPLLM_API_KEY:-}" for a in "$@"; do case "$a" in --dry-run) DRY=1 ;; --key=*) KEY="${a#--key=}" ;; --only=*) ONLY="${a#--only=}" ;; --all) ALL=1 ;; --yes|-y) YES=1 ;; --install) INSTALL=yes ;; --no-install) INSTALL=no ;; -h|--help) echo "Options: --key=... --dry-run --only=a,b --all --yes --install --no-install. See $ORIGIN/integrations"; exit 0 ;; *) echo "unknown option: $a" >&2; exit 2 ;; esac done say() { printf '%s\n' "$*"; } say "" say " cheapllm.shop setup" say " ------------------" if [ -z "$KEY" ] && [ -r /dev/tty ] && [ -w /dev/tty ]; then printf ' Your API key (sk-cheap-..., get one free at %s/signup): ' "$ORIGIN" > /dev/tty stty -echo < /dev/tty 2>/dev/null || true IFS= read -r KEY < /dev/tty || true stty echo < /dev/tty 2>/dev/null || true printf '\n' > /dev/tty fi KEY=$(printf '%s' "$KEY" | tr -d ' \r\n\t') if ! printf '%s' "$KEY" | grep -Eq '^sk-cheap-[A-Za-z0-9_-]+$'; then say " No valid key (it starts with sk-cheap-). Get one free at $ORIGIN/signup"; exit 1 fi # Check the key with a request that costs nothing. if command -v curl >/dev/null 2>&1; then code=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$ORIGIN/v1/messages/count_tokens" \ -H "x-api-key: $KEY" -H 'content-type: application/json' -H 'anthropic-version: 2023-06-01' \ --data '{"model":"x","messages":[{"role":"user","content":"hi"}]}' || echo 000) case "$code" in 401|403) say " That key was refused. Check it on $ORIGIN/dashboard"; exit 1 ;; 2*) say " Key accepted." ;; *) say " Could not check the key (HTTP $code), continuing." ;; esac fi case "$(uname -s)" in Darwin) CL_OS=mac ;; *) CL_OS=linux ;; esac CL_CATALOG_B64='W3siaWQiOiJjbGF1ZGUtY29kZSIsIm5hbWUiOiJDbGF1ZGUgQ29kZSIsImdyb3VwIjoiVGVybWluYWwgYWdlbnRzIiwiZGV0ZWN0Ijp7ImJpbnMiOlsiY2xhdWRlIl0sInBhdGhzIjp7InVuaXgiOlsifi8uY2xhdWRlIl0sIndpbmRvd3MiOlsiJVVTRVJQUk9GSUxFJS8uY2xhdWRlIl19fSwiaW5zdGFsbCI6eyJ1bml4IjoiY3VybCAtZnNTTCBodHRwczovL2NsYXVkZS5haS9pbnN0YWxsLnNoIHwgYmFzaCIsIndpbmRvd3MiOiJpcm0gaHR0cHM6Ly9jbGF1ZGUuYWkvaW5zdGFsbC5wczEgfCBpZXgifSwiYWN0aW9ucyI6W3sidHlwZSI6Impzb24iLCJwYXRoIjp7InVuaXgiOiJ+Ly5jbGF1ZGUvc2V0dGluZ3MuanNvbiIsIndpbmRvd3MiOiIlVVNFUlBST0ZJTEUlLy5jbGF1ZGUvc2V0dGluZ3MuanNvbiJ9LCJwcml2YXRlIjp0cnVlLCJtZXJnZSI6eyJlbnYiOnsiQU5USFJPUElDX0JBU0VfVVJMIjoiaHR0cHM6Ly9jaGVhcGxsbS5zaG9wIiwiQU5USFJPUElDX0FVVEhfVE9LRU4iOiJ7S0VZfSIsIkFOVEhST1BJQ19NT0RFTCI6ImRlZXBzZWVrLXY0LjEtZmxhc2gtMTAxNyIsIkFOVEhST1BJQ19ERUZBVUxUX09QVVNfTU9ERUwiOiJkZWVwc2Vlay12NC4xLWZsYXNoLTEwMTciLCJBTlRIUk9QSUNfREVGQVVMVF9TT05ORVRfTU9ERUwiOiJkZWVwc2Vlay12NC4xLWZsYXNoLTEwMTciLCJBTlRIUk9QSUNfREVGQVVMVF9IQUlLVV9NT0RFTCI6ImRlZXBzZWVrLXY0LjEtZmxhc2gtMTAxNyIsIkNMQVVERV9DT0RFX0RJU0FCTEVfRVhQRVJJTUVOVEFMX0JFVEFTIjoiMSJ9fSwicmVtb3ZlIjpbImVudi5BTlRIUk9QSUNfQVBJX0tFWSJdfV0sImFmdGVyIjoiUnVuIGNsYXVkZSwgdGhlbiAvc3RhdHVzIHRvIGNoZWNrIHRoZSBiYXNlIFVSTC4ifSx7ImlkIjoiY29kZXgiLCJuYW1lIjoiQ29kZXggQ0xJIiwiZ3JvdXAiOiJUZXJtaW5hbCBhZ2VudHMiLCJkZXRlY3QiOnsiYmlucyI6WyJjb2RleCJdLCJwYXRocyI6eyJ1bml4IjpbIn4vLmNvZGV4Il0sIndpbmRvd3MiOlsiJVVTRVJQUk9GSUxFJS8uY29kZXgiXX19LCJpbnN0YWxsIjp7InVuaXgiOiJucG0gaW5zdGFsbCAtZyBAb3BlbmFpL2NvZGV4Iiwid2luZG93cyI6Im5wbSBpbnN0YWxsIC1nIEBvcGVuYWkvY29kZXgiLCJuZWVkcyI6WyJucG0iXX0sImFjdGlvbnMiOlt7InR5cGUiOiJlbnYiLCJ2YXJzIjp7IkNIRUFQTExNX0FQSV9LRVkiOiJ7S0VZfSJ9fSx7InR5cGUiOiJ0b21sIiwicGF0aCI6eyJ1bml4Ijoifi8uY29kZXgvY29uZmlnLnRvbWwiLCJ3aW5kb3dzIjoiJVVTRVJQUk9GSUxFJS8uY29kZXgvY29uZmlnLnRvbWwifSwidG9wIjp7Im1vZGVsIjoiZGVlcHNlZWstdjQuMS1mbGFzaC0xMDE3IiwibW9kZWxfcHJvdmlkZXIiOiJjaGVhcGxsbSJ9LCJ0YWJsZXMiOnsibW9kZWxfcHJvdmlkZXJzLmNoZWFwbGxtIjp7Im5hbWUiOiJDaGVhcExMTSIsImJhc2VfdXJsIjoiaHR0cHM6Ly9jaGVhcGxsbS5zaG9wL3YxIiwiZW52X2tleSI6IkNIRUFQTExNX0FQSV9LRVkiLCJ3aXJlX2FwaSI6InJlc3BvbnNlcyJ9fX1dLCJhZnRlciI6Ik9wZW4gYSBuZXcgdGVybWluYWwgYW5kIHJ1biBjb2RleC4ifSx7ImlkIjoib3BlbmNvZGUiLCJuYW1lIjoiT3BlbkNvZGUiLCJncm91cCI6IlRlcm1pbmFsIGFnZW50cyIsImRldGVjdCI6eyJiaW5zIjpbIm9wZW5jb2RlIl0sInBhdGhzIjp7InVuaXgiOlsifi8uY29uZmlnL29wZW5jb2RlIl0sIndpbmRvd3MiOlsiJVVTRVJQUk9GSUxFJS8uY29uZmlnL29wZW5jb2RlIl19fSwiaW5zdGFsbCI6eyJ1bml4IjoiY3VybCAtZnNTTCBodHRwczovL29wZW5jb2RlLmFpL2luc3RhbGwgfCBiYXNoIiwid2luZG93cyI6Im5wbSBpbnN0YWxsIC1nIG9wZW5jb2RlLWFpIiwibmVlZHMiOnsid2luZG93cyI6WyJucG0iXX19LCJhY3Rpb25zIjpbeyJ0eXBlIjoiZW52IiwidmFycyI6eyJDSEVBUExMTV9BUElfS0VZIjoie0tFWX0ifX0seyJ0eXBlIjoianNvbiIsInBhdGgiOnsidW5peCI6In4vLmNvbmZpZy9vcGVuY29kZS9vcGVuY29kZS5qc29uIiwid2luZG93cyI6IiVVU0VSUFJPRklMRSUvLmNvbmZpZy9vcGVuY29kZS9vcGVuY29kZS5qc29uIn0sIm1lcmdlIjp7IiRzY2hlbWEiOiJodHRwczovL29wZW5jb2RlLmFpL2NvbmZpZy5qc29uIiwicHJvdmlkZXIiOnsiY2hlYXBsbG0iOnsibnBtIjoiQGFpLXNkay9vcGVuYWktY29tcGF0aWJsZSIsIm5hbWUiOiJDaGVhcExMTSIsIm9wdGlvbnMiOnsiYmFzZVVSTCI6Imh0dHBzOi8vY2hlYXBsbG0uc2hvcC92MSIsImFwaUtleSI6IntlbnY6Q0hFQVBMTE1fQVBJX0tFWX0ifSwibW9kZWxzIjp7ImRlZXBzZWVrLXY0LjEtZmxhc2gtMTAxNyI6eyJuYW1lIjoiRGVlcFNlZWsgVjQuMSBGbGFzaCIsImxpbWl0Ijp7ImNvbnRleHQiOjEyODAwMCwib3V0cHV0IjoxNjAwMH19fX19LCJtb2RlbCI6ImNoZWFwbGxtL2RlZXBzZWVrLXY0LjEtZmxhc2gtMTAxNyJ9fV0sImFmdGVyIjoiT3BlbiBhIG5ldyB0ZXJtaW5hbCBhbmQgcnVuIG9wZW5jb2RlLiJ9LHsiaWQiOiJkc2giLCJuYW1lIjoiRGVlcFNlZWsgSGFybmVzcyAoZHNoKSIsImdyb3VwIjoiVGVybWluYWwgYWdlbnRzIiwiZGV0ZWN0Ijp7ImJpbnMiOlsiZHNoIl0sInBhdGhzIjp7InVuaXgiOlsifi8uZHNoIl0sIndpbmRvd3MiOlsiJVVTRVJQUk9GSUxFJS8uZHNoIl19fSwiaW5zdGFsbCI6eyJ1bml4IjoibnBtIGluc3RhbGwgLWcgQGRlZXBzZWVrLWFpL2RzaCIsIndpbmRvd3MiOiJucG0gaW5zdGFsbCAtZyBAZGVlcHNlZWstYWkvZHNoIiwibmVlZHMiOlsibnBtIl19LCJhY3Rpb25zIjpbeyJ0eXBlIjoiZW52IiwidmFycyI6eyJDSEVBUExMTV9BUElfS0VZIjoie0tFWX0ifX0seyJ0eXBlIjoiZmlsZSIsInBhdGgiOnsidW5peCI6In4vLmRzaC9wcm9maWxlcy9jaGVhcGxsbS9jb3JkaXMucGF0Y2gueW1sIiwid2luZG93cyI6IiVVU0VSUFJPRklMRSUvLmRzaC9wcm9maWxlcy9jaGVhcGxsbS9jb3JkaXMucGF0Y2gueW1sIn0sImNvbnRlbnQiOiItIGlkOiBhZ2VudC1kZWZhdWx0LW1vZGVsXG4gIG5hbWU6IFwiQGRlZXBzZWVrLWFpL2RzaC1hZ2VudC1kZWZhdWx0LW1vZGVsXCJcbiAgY29uZmlnOiB7IHByb3ZpZGVyOiBjaGVhcGxsbSwgbW9kZWw6IGRlZXBzZWVrLXY0LjEtZmxhc2gtMTAxNyB9XG4tIGlkOiBsbG0tcGktYWlcbiAgbmFtZTogXCJAZGVlcHNlZWstYWkvZHNoLWxsbS1waS1haVwiXG4gIGNvbmZpZzpcbiAgICBwcm92aWRlcnM6XG4gICAgICBjaGVhcGxsbTpcbiAgICAgICAgZGlzcGxheU5hbWU6IENoZWFwTExNXG4gICAgICAgIGFwaUtleUVudjogQ0hFQVBMTE1fQVBJX0tFWVxuICAgICAgICBhcGk6IG9wZW5haS1jb21wbGV0aW9uc1xuICAgICAgICBiYXNlVVJMOiBodHRwczovL2NoZWFwbGxtLnNob3AvdjFcbiAgICAgICAgbW9kZWxzOlxuICAgICAgICAgIC0gaWQ6IGRlZXBzZWVrLXY0LjEtZmxhc2gtMTAxN1xuICAgICAgICAgICAgbmFtZTogRGVlcFNlZWsgVjQuMSBGbGFzaFxuIn1dLCJhZnRlciI6Ik9wZW4gYSBuZXcgdGVybWluYWwgYW5kIHJ1biBkc2ggLS1wcm9maWxlIGNoZWFwbGxtLiJ9LHsiaWQiOiJjcnVzaCIsIm5hbWUiOiJDcnVzaCIsImdyb3VwIjoiVGVybWluYWwgYWdlbnRzIiwiZGV0ZWN0Ijp7ImJpbnMiOlsiY3J1c2giXSwicGF0aHMiOnsidW5peCI6WyJ+Ly5jb25maWcvY3J1c2giXSwid2luZG93cyI6WyIlTE9DQUxBUFBEQVRBJS9jcnVzaCJdfX0sImluc3RhbGwiOnsidW5peCI6Im5wbSBpbnN0YWxsIC1nIEBjaGFybWxhbmQvY3J1c2giLCJ3aW5kb3dzIjoibnBtIGluc3RhbGwgLWcgQGNoYXJtbGFuZC9jcnVzaCIsIm5lZWRzIjpbIm5wbSJdfSwiYWN0aW9ucyI6W3sidHlwZSI6ImVudiIsInZhcnMiOnsiQ0hFQVBMTE1fQVBJX0tFWSI6IntLRVl9In19LHsidHlwZSI6Impzb24iLCJwYXRoIjp7InVuaXgiOiJ+Ly5jb25maWcvY3J1c2gvY3J1c2guanNvbiIsIndpbmRvd3MiOiIlTE9DQUxBUFBEQVRBJS9jcnVzaC9jcnVzaC5qc29uIn0sIm1lcmdlIjp7InByb3ZpZGVycyI6eyJjaGVhcGxsbSI6eyJuYW1lIjoiQ2hlYXBMTE0iLCJ0eXBlIjoib3BlbmFpLWNvbXBhdCIsImJhc2VfdXJsIjoiaHR0cHM6Ly9jaGVhcGxsbS5zaG9wL3YxIiwiYXBpX2tleSI6IiRDSEVBUExMTV9BUElfS0VZIiwibW9kZWxzIjpbeyJpZCI6ImRlZXBzZWVrLXY0LjEtZmxhc2gtMTAxNyIsIm5hbWUiOiJEZWVwU2VlayBWNC4xIEZsYXNoIiwiY29udGV4dF93aW5kb3ciOjEyODAwMCwiZGVmYXVsdF9tYXhfdG9rZW5zIjo4MTkyfV19fX19XSwiYWZ0ZXIiOiJPcGVuIGEgbmV3IHRlcm1pbmFsLCBydW4gY3J1c2ggYW5kIHBpY2sgdGhlIG1vZGVsIHdpdGggQ3RybCtMLiJ9LHsiaWQiOiJnb29zZSIsIm5hbWUiOiJHb29zZSIsImdyb3VwIjoiVGVybWluYWwgYWdlbnRzIiwiZGV0ZWN0Ijp7ImJpbnMiOlsiZ29vc2UiXSwicGF0aHMiOnsidW5peCI6WyJ+Ly5jb25maWcvZ29vc2UiXSwid2luZG93cyI6WyIlQVBQREFUQSUvQmxvY2svZ29vc2UiXX19LCJpbnN0YWxsIjp7InVuaXgiOiJjdXJsIC1mc1NMIGh0dHBzOi8vZ2l0aHViLmNvbS9ibG9jay9nb29zZS9yZWxlYXNlcy9kb3dubG9hZC9zdGFibGUvZG93bmxvYWRfY2xpLnNoIHwgQ09ORklHVVJFPWZhbHNlIGJhc2gifSwiYWN0aW9ucyI6W3sidHlwZSI6ImVudiIsInZhcnMiOnsiQ0hFQVBMTE1fQVBJX0tFWSI6IntLRVl9In19LHsidHlwZSI6ImZpbGUiLCJwYXRoIjp7InVuaXgiOiJ+Ly5jb25maWcvZ29vc2UvY3VzdG9tX3Byb3ZpZGVycy9jaGVhcGxsbS5qc29uIiwid2luZG93cyI6IiVBUFBEQVRBJS9CbG9jay9nb29zZS9jb25maWcvY3VzdG9tX3Byb3ZpZGVycy9jaGVhcGxsbS5qc29uIn0sImpzb24iOnsibmFtZSI6ImNoZWFwbGxtIiwiZW5naW5lIjoib3BlbmFpIiwiZGlzcGxheV9uYW1lIjoiQ2hlYXBMTE0iLCJhcGlfa2V5X2VudiI6IkNIRUFQTExNX0FQSV9LRVkiLCJiYXNlX3VybCI6Imh0dHBzOi8vY2hlYXBsbG0uc2hvcC92MS9jaGF0L2NvbXBsZXRpb25zIiwic3VwcG9ydHNfc3RyZWFtaW5nIjp0cnVlLCJtb2RlbHMiOlt7Im5hbWUiOiJkZWVwc2Vlay12NC4xLWZsYXNoLTEwMTciLCJjb250ZXh0X2xpbWl0IjoxMjgwMDB9XX19XSwiYWZ0ZXIiOiJSdW4gZ29vc2UgY29uZmlndXJlIGFuZCBwaWNrIHRoZSBDaGVhcExMTSBwcm92aWRlci4ifSx7ImlkIjoicXdlbi1jb2RlIiwibmFtZSI6IlF3ZW4gQ29kZSIsImdyb3VwIjoiVGVybWluYWwgYWdlbnRzIiwiZGV0ZWN0Ijp7ImJpbnMiOlsicXdlbiJdLCJwYXRocyI6eyJ1bml4IjpbIn4vLnF3ZW4iXSwid2luZG93cyI6WyIlVVNFUlBST0ZJTEUlLy5xd2VuIl19fSwiaW5zdGFsbCI6eyJ1bml4IjoibnBtIGluc3RhbGwgLWcgQHF3ZW4tY29kZS9xd2VuLWNvZGUiLCJ3aW5kb3dzIjoibnBtIGluc3RhbGwgLWcgQHF3ZW4tY29kZS9xd2VuLWNvZGUiLCJuZWVkcyI6WyJucG0iXX0sImFjdGlvbnMiOlt7InR5cGUiOiJibG9jayIsImNvbW1lbnQiOiIjIiwicGF0aCI6eyJ1bml4Ijoifi8ucXdlbi8uZW52Iiwid2luZG93cyI6IiVVU0VSUFJPRklMRSUvLnF3ZW4vLmVudiJ9LCJwcml2YXRlIjp0cnVlLCJkcm9wIjpbIl5cXHMqKGV4cG9ydFxccyspP09QRU5BSV8oQkFTRV9VUkx8QVBJX0tFWXxNT0RFTClcXHMqPSJdLCJ0ZXh0IjoiT1BFTkFJX0JBU0VfVVJMPWh0dHBzOi8vY2hlYXBsbG0uc2hvcC92MVxuT1BFTkFJX0FQSV9LRVk9e0tFWX1cbk9QRU5BSV9NT0RFTD1kZWVwc2Vlay12NC4xLWZsYXNoLTEwMTcifV0sImFmdGVyIjoiUnVuIHF3ZW4uIElmIGl0IHN0aWxsIGFza3MgaG93IHRvIHNpZ24gaW4sIHBpY2sgXCJPcGVuQUktY29tcGF0aWJsZVwiLiJ9LHsiaWQiOiJhaWRlciIsIm5hbWUiOiJBaWRlciIsImdyb3VwIjoiVGVybWluYWwgYWdlbnRzIiwiZGV0ZWN0Ijp7ImJpbnMiOlsiYWlkZXIiXX0sImluc3RhbGwiOnsidW5peCI6ImN1cmwgLUxzU2YgaHR0cHM6Ly9haWRlci5jaGF0L2luc3RhbGwuc2ggfCBzaCIsIndpbmRvd3MiOiJwb3dlcnNoZWxsIC1FeGVjdXRpb25Qb2xpY3kgQnlQYXNzIC1jIFwiaXJtIGh0dHBzOi8vYWlkZXIuY2hhdC9pbnN0YWxsLnBzMSB8IGlleFwiIn0sImFjdGlvbnMiOlt7InR5cGUiOiJibG9jayIsImNvbW1lbnQiOiIjIiwicGF0aCI6eyJ1bml4Ijoifi8uYWlkZXIuY29uZi55bWwiLCJ3aW5kb3dzIjoiJVVTRVJQUk9GSUxFJS8uYWlkZXIuY29uZi55bWwifSwicHJpdmF0ZSI6dHJ1ZSwiZHJvcCI6WyJeKG1vZGVsfG9wZW5haS1hcGktYmFzZXxvcGVuYWktYXBpLWtleSlcXHMqOiJdLCJ0ZXh0IjoibW9kZWw6IG9wZW5haS9kZWVwc2Vlay12NC4xLWZsYXNoLTEwMTdcbm9wZW5haS1hcGktYmFzZTogaHR0cHM6Ly9jaGVhcGxsbS5zaG9wL3YxXG5vcGVuYWktYXBpLWtleToge0tFWX0ifV0sImFmdGVyIjoiUnVuIGFpZGVyIGluIGEgcHJvamVjdCBmb2xkZXIuIn0seyJpZCI6Im9wZW5oYW5kcyIsIm5hbWUiOiJPcGVuSGFuZHMiLCJncm91cCI6IlRlcm1pbmFsIGFnZW50cyIsImRldGVjdCI6eyJiaW5zIjpbIm9wZW5oYW5kcyJdLCJwYXRocyI6eyJ1bml4IjpbIn4vLm9wZW5oYW5kcyJdLCJ3aW5kb3dzIjpbIiVVU0VSUFJPRklMRSUvLm9wZW5oYW5kcyJdfX0sImFjdGlvbnMiOlt7InR5cGUiOiJtYW51YWwiLCJzdGVwcyI6WyJTZXR0aW5ncyA+IExMTSA+IEFkdmFuY2VkIChpbiB0aGUgQ0xJOiAvc2V0dGluZ3MpLiIsIkN1c3RvbSBtb2RlbDogb3BlbmFpL2RlZXBzZWVrLXY0LjEtZmxhc2gtMTAxNyIsIkJhc2UgVVJMOiBodHRwczovL2NoZWFwbGxtLnNob3AvdjEiLCJBUEkga2V5OiB7S0VZfSJdfV19LHsiaWQiOiJ3YXJwIiwibmFtZSI6IldhcnAiLCJncm91cCI6IlRlcm1pbmFsIGFnZW50cyIsImRldGVjdCI6eyJiaW5zIjpbIndhcnAtdGVybWluYWwiXSwicGF0aHMiOnsibWFjIjpbIi9BcHBsaWNhdGlvbnMvV2FycC5hcHAiXSwid2luZG93cyI6WyIlTE9DQUxBUFBEQVRBJS9Qcm9ncmFtcy9XYXJwIl19fSwiYWN0aW9ucyI6W3sidHlwZSI6Im1hbnVhbCIsInN0ZXBzIjpbIlNldHRpbmdzID4gc2VhcmNoIFwiaW5mZXJlbmNlIGVuZHBvaW50XCIgPiBhZGQgYW4gZW5kcG9pbnQuIiwiVVJMOiBodHRwczovL2NoZWFwbGxtLnNob3AvdjEiLCJBUEkga2V5OiB7S0VZfSIsIk1vZGVsIElEOiBkZWVwc2Vlay12NC4xLWZsYXNoLTEwMTcsIHRoZW4gcGljayBpdCBpbiB0aGUgYWdlbnQgbW9kZWwgbWVudSAobm90IFwiQXV0b1wiKS4iXX1dfSx7ImlkIjoiZ2VtaW5pLWNsaSIsIm5hbWUiOiJHZW1pbmkgQ0xJIiwiZ3JvdXAiOiJUZXJtaW5hbCBhZ2VudHMiLCJ1bnN1cHBvcnRlZCI6dHJ1ZSwiZGV0ZWN0Ijp7ImJpbnMiOlsiZ2VtaW5pIl19LCJhY3Rpb25zIjpbeyJ0eXBlIjoibWFudWFsIiwic3RlcHMiOlsiR2VtaW5pIENMSSBvbmx5IHNwZWFrcyBHb29nbGUncyBmb3JtYXQgYW5kIGNhbid0IGJlIHBvaW50ZWQgYXQgdXMuIFVzZSBDbGF1ZGUgQ29kZSwgQ29kZXgsIE9wZW5Db2RlIG9yIFF3ZW4gQ29kZSBpbnN0ZWFkIChRd2VuIENvZGUgaXMgYSBHZW1pbmkgQ0xJIGZvcmspLiJdfV19LHsiaWQiOiJjb3BpbG90IiwibmFtZSI6IlZTIENvZGU6IEdpdEh1YiBDb3BpbG90IiwiZ3JvdXAiOiJFZGl0b3JzIiwiZGV0ZWN0Ijp7ImJpbnMiOlsiY29kZSJdLCJwYXRocyI6eyJtYWMiOlsifi9MaWJyYXJ5L0FwcGxpY2F0aW9uIFN1cHBvcnQvQ29kZSJdLCJsaW51eCI6WyJ+Ly5jb25maWcvQ29kZSJdLCJ3aW5kb3dzIjpbIiVBUFBEQVRBJS9Db2RlIl19fSwiYWN0aW9ucyI6W3sidHlwZSI6Impzb24tYXJyYXkiLCJwYXRoIjp7Im1hYyI6In4vTGlicmFyeS9BcHBsaWNhdGlvbiBTdXBwb3J0L0NvZGUvVXNlci9jaGF0TGFuZ3VhZ2VNb2RlbHMuanNvbiIsImxpbnV4Ijoifi8uY29uZmlnL0NvZGUvVXNlci9jaGF0TGFuZ3VhZ2VNb2RlbHMuanNvbiIsIndpbmRvd3MiOiIlQVBQREFUQSUvQ29kZS9Vc2VyL2NoYXRMYW5ndWFnZU1vZGVscy5qc29uIn0sIm1hdGNoIjp7Im5hbWUiOiJDaGVhcExMTSJ9LCJwcml2YXRlIjp0cnVlLCJpdGVtIjp7Im5hbWUiOiJDaGVhcExMTSIsInZlbmRvciI6ImN1c3RvbWVuZHBvaW50IiwiYXBpS2V5Ijoie0tFWX0iLCJhcGlUeXBlIjoiY2hhdC1jb21wbGV0aW9ucyIsIm1vZGVscyI6W3siaWQiOiJkZWVwc2Vlay12NC4xLWZsYXNoLTEwMTciLCJuYW1lIjoiRGVlcFNlZWsgVjQuMSBGbGFzaCIsInVybCI6Imh0dHBzOi8vY2hlYXBsbG0uc2hvcC92MS9jaGF0L2NvbXBsZXRpb25zIiwidG9vbENhbGxpbmciOnRydWUsInZpc2lvbiI6dHJ1ZSwibWF4SW5wdXRUb2tlbnMiOjEyODAwMCwibWF4T3V0cHV0VG9rZW5zIjoxNjAwMH1dfX0seyJ0eXBlIjoibWFudWFsIiwic3RlcHMiOlsiUmVsb2FkIFZTIENvZGUgKERldmVsb3BlcjogUmVsb2FkIFdpbmRvdyksIHRoZW4gcGljayBcIkRlZXBTZWVrIFY0LjEgRmxhc2hcIiBpbiB0aGUgY2hhdCBtb2RlbCBwaWNrZXIuIiwiQ29waWxvdCBCdXNpbmVzcy9FbnRlcnByaXNlOiBhbiBhZG1pbiBtdXN0IGFsbG93IFwiQnJpbmcgWW91ciBPd24gTGFuZ3VhZ2UgTW9kZWwgS2V5XCIuIl19XX0seyJpZCI6ImNsaW5lIiwibmFtZSI6IkNsaW5lIiwiZ3JvdXAiOiJFZGl0b3JzIiwiZGV0ZWN0Ijp7ImV4dCI6InNhb3Vkcml6d2FuLmNsYXVkZS1kZXYifSwiaW5zdGFsbCI6eyJ1bml4IjoiY29kZSAtLWluc3RhbGwtZXh0ZW5zaW9uIHNhb3Vkcml6d2FuLmNsYXVkZS1kZXYiLCJ3aW5kb3dzIjoiY29kZSAtLWluc3RhbGwtZXh0ZW5zaW9uIHNhb3Vkcml6d2FuLmNsYXVkZS1kZXYiLCJuZWVkcyI6WyJjb2RlIl19LCJhY3Rpb25zIjpbeyJ0eXBlIjoibWFudWFsIiwic3RlcHMiOlsiQ2xpbmUgPiBzZXR0aW5ncyAoZ2VhcikgPiBBUEkgUHJvdmlkZXI6IE9wZW5BSSBDb21wYXRpYmxlLiIsIkJhc2UgVVJMOiBodHRwczovL2NoZWFwbGxtLnNob3AvdjEiLCJBUEkga2V5OiB7S0VZfSIsIk1vZGVsIElEOiBkZWVwc2Vlay12NC4xLWZsYXNoLTEwMTc7IHR1cm4gaW1hZ2Ugc3VwcG9ydCBvbiwgY29udGV4dCB3aW5kb3cgMTI4MDAwLiJdfV19LHsiaWQiOiJyb28iLCJuYW1lIjoiUm9vIENvZGUiLCJncm91cCI6IkVkaXRvcnMiLCJkZXRlY3QiOnsiZXh0Ijoicm9vdmV0ZXJpbmFyeWluYy5yb28tY2xpbmUifSwiaW5zdGFsbCI6eyJ1bml4IjoiY29kZSAtLWluc3RhbGwtZXh0ZW5zaW9uIFJvb1ZldGVyaW5hcnlJbmMucm9vLWNsaW5lIiwid2luZG93cyI6ImNvZGUgLS1pbnN0YWxsLWV4dGVuc2lvbiBSb29WZXRlcmluYXJ5SW5jLnJvby1jbGluZSIsIm5lZWRzIjpbImNvZGUiXX0sImFjdGlvbnMiOlt7InR5cGUiOiJtYW51YWwiLCJzdGVwcyI6WyJSb28gQ29kZSA+IFNldHRpbmdzID4gQVBJIFByb3ZpZGVyOiBPcGVuQUkgQ29tcGF0aWJsZS4iLCJCYXNlIFVSTDogaHR0cHM6Ly9jaGVhcGxsbS5zaG9wL3YxIiwiQVBJIGtleToge0tFWX0iLCJNb2RlbDogZGVlcHNlZWstdjQuMS1mbGFzaC0xMDE3OyB0dXJuIGltYWdlcyBvbi4iXX1dfSx7ImlkIjoia2lsbyIsIm5hbWUiOiJLaWxvIENvZGUiLCJncm91cCI6IkVkaXRvcnMiLCJkZXRlY3QiOnsiZXh0Ijoia2lsb2NvZGUua2lsby1jb2RlIn0sImluc3RhbGwiOnsidW5peCI6ImNvZGUgLS1pbnN0YWxsLWV4dGVuc2lvbiBraWxvY29kZS5LaWxvLUNvZGUiLCJ3aW5kb3dzIjoiY29kZSAtLWluc3RhbGwtZXh0ZW5zaW9uIGtpbG9jb2RlLktpbG8tQ29kZSIsIm5lZWRzIjpbImNvZGUiXX0sImFjdGlvbnMiOlt7InR5cGUiOiJtYW51YWwiLCJzdGVwcyI6WyJLaWxvIENvZGUgPiBTZXR0aW5ncyA+IFByb3ZpZGVycyA+IEN1c3RvbSBwcm92aWRlciwgUHJvdmlkZXIgQVBJIFwiT3BlbkFJIENvbXBhdGlibGVcIi4iLCJQcm92aWRlciBJRDogY2hlYXBsbG0iLCJCYXNlIFVSTDogaHR0cHM6Ly9jaGVhcGxsbS5zaG9wL3YxIiwiQVBJIGtleToge0tFWX0iLCJNb2RlbHM6IGFkZCBkZWVwc2Vlay12NC4xLWZsYXNoLTEwMTcuIl19XX0seyJpZCI6ImNvbnRpbnVlIiwibmFtZSI6IkNvbnRpbnVlIiwiZ3JvdXAiOiJFZGl0b3JzIiwiZGV0ZWN0Ijp7ImV4dCI6ImNvbnRpbnVlLmNvbnRpbnVlIiwicGF0aHMiOnsidW5peCI6WyJ+Ly5jb250aW51ZSJdLCJ3aW5kb3dzIjpbIiVVU0VSUFJPRklMRSUvLmNvbnRpbnVlIl19fSwiaW5zdGFsbCI6eyJ1bml4IjoiY29kZSAtLWluc3RhbGwtZXh0ZW5zaW9uIENvbnRpbnVlLmNvbnRpbnVlIiwid2luZG93cyI6ImNvZGUgLS1pbnN0YWxsLWV4dGVuc2lvbiBDb250aW51ZS5jb250aW51ZSIsIm5lZWRzIjpbImNvZGUiXX0sImFjdGlvbnMiOlt7InR5cGUiOiJmaWxlIiwiaWZNaXNzaW5nIjp0cnVlLCJwcml2YXRlIjp0cnVlLCJwYXRoIjp7InVuaXgiOiJ+Ly5jb250aW51ZS9jb25maWcueWFtbCIsIndpbmRvd3MiOiIlVVNFUlBST0ZJTEUlLy5jb250aW51ZS9jb25maWcueWFtbCJ9LCJjb250ZW50IjoibmFtZTogQ2hlYXBMTE1cbnZlcnNpb246IDAuMC4xXG5zY2hlbWE6IHYxXG5tb2RlbHM6XG4gIC0gbmFtZTogRGVlcFNlZWsgVjQuMSBGbGFzaFxuICAgIHByb3ZpZGVyOiBvcGVuYWlcbiAgICBtb2RlbDogZGVlcHNlZWstdjQuMS1mbGFzaC0xMDE3XG4gICAgYXBpQmFzZTogaHR0cHM6Ly9jaGVhcGxsbS5zaG9wL3YxXG4gICAgYXBpS2V5OiB7S0VZfVxuICAgIHJvbGVzOiBbY2hhdCwgZWRpdCwgYXBwbHldXG4iLCJlbHNlU3RlcHMiOlsiWW91ciB+Ly5jb250aW51ZS9jb25maWcueWFtbCBhbHJlYWR5IGV4aXN0cywgc28gaXQgd2FzIGxlZnQgYWxvbmUuIEFkZCB0aGlzIHVuZGVyIG1vZGVsczoiLCIgIC0gbmFtZTogRGVlcFNlZWsgVjQuMSBGbGFzaFxuICAgIHByb3ZpZGVyOiBvcGVuYWlcbiAgICBtb2RlbDogZGVlcHNlZWstdjQuMS1mbGFzaC0xMDE3XG4gICAgYXBpQmFzZTogaHR0cHM6Ly9jaGVhcGxsbS5zaG9wL3YxXG4gICAgYXBpS2V5OiB7S0VZfVxuICAgIHJvbGVzOiBbY2hhdCwgZWRpdCwgYXBwbHldIl19XX0seyJpZCI6ImN1cnNvciIsIm5hbWUiOiJDdXJzb3IiLCJncm91cCI6IkVkaXRvcnMiLCJkZXRlY3QiOnsiYmlucyI6WyJjdXJzb3IiXSwicGF0aHMiOnsibWFjIjpbIi9BcHBsaWNhdGlvbnMvQ3Vyc29yLmFwcCJdLCJ3aW5kb3dzIjpbIiVMT0NBTEFQUERBVEElL1Byb2dyYW1zL2N1cnNvciJdfX0sImFjdGlvbnMiOlt7InR5cGUiOiJtYW51YWwiLCJzdGVwcyI6WyJDdXJzb3IgU2V0dGluZ3MgPiBNb2RlbHMgPiBPcGVuQUkgQVBJIEtleToge0tFWX0iLCJUdXJuIG9uIFwiT3ZlcnJpZGUgT3BlbkFJIEJhc2UgVVJMXCI6IGh0dHBzOi8vY2hlYXBsbG0uc2hvcC92MSIsIkFkZCBhIGN1c3RvbSBtb2RlbCBuYW1lZCBkZWVwc2Vlay12NC4xLWZsYXNoLTEwMTcgYW5kIGVuYWJsZSBpdC4iLCJGb3IgYWdlbnQgd29yaywgQ2xpbmUsIFJvbyBDb2RlIG9yIEtpbG8gQ29kZSBpbnNpZGUgQ3Vyc29yIHdvcmsgYmV0dGVyLiJdfV19LHsiaWQiOiJ6ZWQiLCJuYW1lIjoiWmVkIiwiZ3JvdXAiOiJFZGl0b3JzIiwiZGV0ZWN0Ijp7ImJpbnMiOlsiemVkIiwiemVkaXRvciJdLCJwYXRocyI6eyJtYWMiOlsiL0FwcGxpY2F0aW9ucy9aZWQuYXBwIiwifi8uY29uZmlnL3plZCJdLCJsaW51eCI6WyJ+Ly5jb25maWcvemVkIl0sIndpbmRvd3MiOlsiJUFQUERBVEElL1plZCJdfX0sImFjdGlvbnMiOlt7InR5cGUiOiJlbnYiLCJ2YXJzIjp7IkNIRUFQTExNX0FQSV9LRVkiOiJ7S0VZfSJ9fSx7InR5cGUiOiJqc29uIiwicGF0aCI6eyJ1bml4Ijoifi8uY29uZmlnL3plZC9zZXR0aW5ncy5qc29uIiwid2luZG93cyI6IiVBUFBEQVRBJS9aZWQvc2V0dGluZ3MuanNvbiJ9LCJtZXJnZSI6eyJsYW5ndWFnZV9tb2RlbHMiOnsib3BlbmFpX2NvbXBhdGlibGUiOnsiY2hlYXBsbG0iOnsiYXBpX3VybCI6Imh0dHBzOi8vY2hlYXBsbG0uc2hvcC92MSIsImF2YWlsYWJsZV9tb2RlbHMiOlt7Im5hbWUiOiJkZWVwc2Vlay12NC4xLWZsYXNoLTEwMTciLCJkaXNwbGF5X25hbWUiOiJEZWVwU2VlayBWNC4xIEZsYXNoIiwibWF4X3Rva2VucyI6MTI4MDAwLCJjYXBhYmlsaXRpZXMiOnsidG9vbHMiOnRydWUsImltYWdlcyI6dHJ1ZSwicGFyYWxsZWxfdG9vbF9jYWxscyI6ZmFsc2UsInByb21wdF9jYWNoZV9rZXkiOmZhbHNlfX1dfX19fX1dLCJhZnRlciI6IlJlc3RhcnQgWmVkIGFuZCBwaWNrIERlZXBTZWVrIFY0LjEgRmxhc2ggaW4gdGhlIEFnZW50IHBhbmVsLiJ9LHsiaWQiOiJqZXRicmFpbnMiLCJuYW1lIjoiSmV0QnJhaW5zIEFJIEFzc2lzdGFudCIsImdyb3VwIjoiRWRpdG9ycyIsImRldGVjdCI6eyJwYXRocyI6eyJtYWMiOlsifi9MaWJyYXJ5L0FwcGxpY2F0aW9uIFN1cHBvcnQvSmV0QnJhaW5zIl0sImxpbnV4IjpbIn4vLmNvbmZpZy9KZXRCcmFpbnMiXSwid2luZG93cyI6WyIlQVBQREFUQSUvSmV0QnJhaW5zIl19fSwiYWN0aW9ucyI6W3sidHlwZSI6Im1hbnVhbCIsInN0ZXBzIjpbIlNldHRpbmdzID4gVG9vbHMgPiBBSSBBc3Npc3RhbnQgPiBQcm92aWRlcnMgJiBBUEkga2V5cyA+IE9wZW5BSS1jb21wYXRpYmxlLiIsIkJhc2UgVVJMOiBodHRwczovL2NoZWFwbGxtLnNob3AvdjEiLCJBUEkga2V5OiB7S0VZfSIsIlBpY2sgZGVlcHNlZWstdjQuMS1mbGFzaC0xMDE3LiJdfV19LHsiaWQiOiJ3aW5kc3VyZiIsIm5hbWUiOiJXaW5kc3VyZiIsImdyb3VwIjoiRWRpdG9ycyIsImRldGVjdCI6eyJiaW5zIjpbIndpbmRzdXJmIl0sInBhdGhzIjp7Im1hYyI6WyIvQXBwbGljYXRpb25zL1dpbmRzdXJmLmFwcCJdLCJ3aW5kb3dzIjpbIiVMT0NBTEFQUERBVEElL1Byb2dyYW1zL1dpbmRzdXJmIl19fSwiaW5zdGFsbCI6eyJ1bml4Ijoid2luZHN1cmYgLS1pbnN0YWxsLWV4dGVuc2lvbiBzYW91ZHJpendhbi5jbGF1ZGUtZGV2Iiwid2luZG93cyI6IndpbmRzdXJmIC0taW5zdGFsbC1leHRlbnNpb24gc2FvdWRyaXp3YW4uY2xhdWRlLWRldiIsIm5lZWRzIjpbIndpbmRzdXJmIl19LCJhY3Rpb25zIjpbeyJ0eXBlIjoibWFudWFsIiwic3RlcHMiOlsiV2luZHN1cmYgaGFzIG5vIGN1c3RvbSBiYXNlIFVSTDsgdGhlIGluc3RhbGxlciBhZGRzIENsaW5lIHRvIGl0LiBJbiBDbGluZTogQVBJIFByb3ZpZGVyIE9wZW5BSSBDb21wYXRpYmxlLCBCYXNlIFVSTCBodHRwczovL2NoZWFwbGxtLnNob3AvdjEsIGtleSB7S0VZfSwgbW9kZWwgZGVlcHNlZWstdjQuMS1mbGFzaC0xMDE3LiJdfV19LHsiaWQiOiJhdmFudGUiLCJuYW1lIjoiTmVvdmltOiBhdmFudGUubnZpbSIsImdyb3VwIjoiRWRpdG9ycyIsImRldGVjdCI6eyJwYXRocyI6eyJ1bml4IjpbIn4vLmxvY2FsL3NoYXJlL252aW0vbGF6eS9hdmFudGUubnZpbSJdLCJ3aW5kb3dzIjpbIiVMT0NBTEFQUERBVEElL252aW0tZGF0YS9sYXp5L2F2YW50ZS5udmltIl19fSwiYWN0aW9ucyI6W3sidHlwZSI6ImVudiIsInZhcnMiOnsiQ0hFQVBMTE1fQVBJX0tFWSI6IntLRVl9In19LHsidHlwZSI6Im1hbnVhbCIsInN0ZXBzIjpbIkluIHlvdXIgYXZhbnRlIHNldHVwOiIsInJlcXVpcmUoXCJhdmFudGVcIikuc2V0dXAoe1xuICBwcm92aWRlciA9IFwiY2hlYXBsbG1cIixcbiAgcHJvdmlkZXJzID0geyBjaGVhcGxsbSA9IHsgX19pbmhlcml0ZWRfZnJvbSA9IFwib3BlbmFpXCIsIGVuZHBvaW50ID0gXCJodHRwczovL2NoZWFwbGxtLnNob3AvdjFcIiwgYXBpX2tleV9uYW1lID0gXCJDSEVBUExMTV9BUElfS0VZXCIsIG1vZGVsID0gXCJkZWVwc2Vlay12NC4xLWZsYXNoLTEwMTdcIiB9IH0sXG59KSJdfV19LHsiaWQiOiJncHRlbCIsIm5hbWUiOiJFbWFjczogZ3B0ZWwiLCJncm91cCI6IkVkaXRvcnMiLCJwcmVzZWxlY3QiOiJuZXZlciIsImRldGVjdCI6eyJiaW5zIjpbImVtYWNzIl19LCJhY3Rpb25zIjpbeyJ0eXBlIjoibWFudWFsIiwic3RlcHMiOlsiSW4geW91ciBpbml0IGZpbGU6IiwiKHNldHEgZ3B0ZWwtbW9kZWwgJ2RlZXBzZWVrLXY0LjEtZmxhc2gtMTAxN1xuICAgICAgZ3B0ZWwtYmFja2VuZCAoZ3B0ZWwtbWFrZS1vcGVuYWkgXCJDaGVhcExMTVwiIDpob3N0IFwiY2hlYXBsbG0uc2hvcFwiIDplbmRwb2ludCBcIi92MS9jaGF0L2NvbXBsZXRpb25zXCIgOnN0cmVhbSB0IDprZXkgXCJ7S0VZfVwiIDptb2RlbHMgJyhkZWVwc2Vlay12NC4xLWZsYXNoLTEwMTcpKSkiXX1dfSx7ImlkIjoib3BlbndlYnVpIiwibmFtZSI6Ik9wZW4gV2ViVUkiLCJncm91cCI6IkNoYXQgYXBwcyIsImRldGVjdCI6eyJiaW5zIjpbIm9wZW4td2VidWkiXX0sImFjdGlvbnMiOlt7InR5cGUiOiJtYW51YWwiLCJzdGVwcyI6WyJBZG1pbiBTZXR0aW5ncyA+IENvbm5lY3Rpb25zID4gT3BlbkFJIEFQSSA+ICsiLCJVUkw6IGh0dHBzOi8vY2hlYXBsbG0uc2hvcC92MSIsIktleToge0tFWX0iLCJTZWxmLWhvc3RlZDogT1BFTkFJX0FQSV9CQVNFX1VSTD1odHRwczovL2NoZWFwbGxtLnNob3AvdjEgYW5kIE9QRU5BSV9BUElfS0VZIGFsc28gd29yay4iXX1dfSx7ImlkIjoibGlicmVjaGF0IiwibmFtZSI6IkxpYnJlQ2hhdCIsImdyb3VwIjoiQ2hhdCBhcHBzIiwicHJlc2VsZWN0IjoibmV2ZXIiLCJhY3Rpb25zIjpbeyJ0eXBlIjoibWFudWFsIiwic3RlcHMiOlsiSW4gbGlicmVjaGF0LnlhbWwsIHRoZW4gc2V0IENIRUFQTExNX0FQSV9LRVkgaW4gTGlicmVDaGF0J3MgLmVudjoiLCJlbmRwb2ludHM6XG4gIGN1c3RvbTpcbiAgICAtIG5hbWU6IFwiQ2hlYXBMTE1cIlxuICAgICAgYXBpS2V5OiBcIiR7Q0hFQVBMTE1fQVBJX0tFWX1cIlxuICAgICAgYmFzZVVSTDogXCJodHRwczovL2NoZWFwbGxtLnNob3AvdjFcIlxuICAgICAgbW9kZWxzOlxuICAgICAgICBkZWZhdWx0OiBbXCJkZWVwc2Vlay12NC4xLWZsYXNoLTEwMTdcIl1cbiAgICAgICAgZmV0Y2g6IHRydWUiXX1dfSx7ImlkIjoibG9iZWNoYXQiLCJuYW1lIjoiTG9iZUNoYXQiLCJncm91cCI6IkNoYXQgYXBwcyIsInByZXNlbGVjdCI6Im5ldmVyIiwiYWN0aW9ucyI6W3sidHlwZSI6Im1hbnVhbCIsInN0ZXBzIjpbIlNldHRpbmdzID4gQUkgU2VydmljZSBQcm92aWRlciA+ICsgY3VzdG9tIHByb3ZpZGVyLCByZXF1ZXN0IGZvcm1hdCBPcGVuQUkuIiwiUHJveHkgVVJMOiBodHRwczovL2NoZWFwbGxtLnNob3AvdjEiLCJBUEkga2V5OiB7S0VZfSIsIkZldGNoIHRoZSBtb2RlbCBsaXN0LiJdfV19LHsiaWQiOiJqYW4iLCJuYW1lIjoiSmFuIiwiZ3JvdXAiOiJDaGF0IGFwcHMiLCJkZXRlY3QiOnsicGF0aHMiOnsibWFjIjpbIi9BcHBsaWNhdGlvbnMvSmFuLmFwcCJdLCJ3aW5kb3dzIjpbIiVMT0NBTEFQUERBVEElL1Byb2dyYW1zL2phbiJdfX0sImFjdGlvbnMiOlt7InR5cGUiOiJtYW51YWwiLCJzdGVwcyI6WyJTZXR0aW5ncyA+IE1vZGVsIFByb3ZpZGVycyA+IEFkZCBQcm92aWRlciAoT3BlbkFJLWNvbXBhdGlibGUpLiIsIkJhc2UgVVJMOiBodHRwczovL2NoZWFwbGxtLnNob3AvdjEiLCJBUEkga2V5OiB7S0VZfSJdfV19LHsiaWQiOiJjaGF0Ym94IiwibmFtZSI6IkNoYXRib3giLCJncm91cCI6IkNoYXQgYXBwcyIsImRldGVjdCI6eyJwYXRocyI6eyJtYWMiOlsiL0FwcGxpY2F0aW9ucy9DaGF0Ym94LmFwcCJdLCJ3aW5kb3dzIjpbIiVMT0NBTEFQUERBVEElL1Byb2dyYW1zL2NoYXRib3giXX19LCJhY3Rpb25zIjpbeyJ0eXBlIjoibWFudWFsIiwic3RlcHMiOlsiU2V0dGluZ3MgPiBNb2RlbCBQcm92aWRlciA+IEFkZCA+IEN1c3RvbSBQcm92aWRlciwgQVBJIG1vZGUgXCJPcGVuQUkgQVBJIENvbXBhdGlibGVcIi4iLCJBUEkgSG9zdDogaHR0cHM6Ly9jaGVhcGxsbS5zaG9wL3YxIChrZWVwIHRoZSBwYXRoIC9jaGF0L2NvbXBsZXRpb25zKSIsIkFQSSBrZXk6IHtLRVl9IiwiTW9kZWw6IGRlZXBzZWVrLXY0LjEtZmxhc2gtMTAxNyJdfV19LHsiaWQiOiJhbnl0aGluZ2xsbSIsIm5hbWUiOiJBbnl0aGluZ0xMTSIsImdyb3VwIjoiQ2hhdCBhcHBzIiwiZGV0ZWN0Ijp7InBhdGhzIjp7Im1hYyI6WyIvQXBwbGljYXRpb25zL0FueXRoaW5nTExNLmFwcCJdLCJ3aW5kb3dzIjpbIiVMT0NBTEFQUERBVEElL1Byb2dyYW1zL2FueXRoaW5nbGxtLWRlc2t0b3AiXX19LCJhY3Rpb25zIjpbeyJ0eXBlIjoibWFudWFsIiwic3RlcHMiOlsiU2V0dGluZ3MgPiBMTE0gUHJlZmVyZW5jZSA+IEdlbmVyaWMgT3BlbkFJLiIsIkJhc2UgVVJMOiBodHRwczovL2NoZWFwbGxtLnNob3AvdjEiLCJBUEkga2V5OiB7S0VZfSIsIk1vZGVsOiBkZWVwc2Vlay12NC4xLWZsYXNoLTEwMTcsIGNvbnRleHQgd2luZG93IDEyODAwMCJdfV19LHsiaWQiOiJtc3R5IiwibmFtZSI6Ik1zdHkiLCJncm91cCI6IkNoYXQgYXBwcyIsImRldGVjdCI6eyJwYXRocyI6eyJtYWMiOlsiL0FwcGxpY2F0aW9ucy9Nc3R5LmFwcCJdLCJ3aW5kb3dzIjpbIiVMT0NBTEFQUERBVEElL1Byb2dyYW1zL01zdHkiXX19LCJhY3Rpb25zIjpbeyJ0eXBlIjoibWFudWFsIiwic3RlcHMiOlsiTW9kZWwgUHJvdmlkZXJzID4gQWRkIFJlbW90ZSBNb2RlbCBQcm92aWRlciA+IE9wZW5BSS1jb21wYXRpYmxlLiIsIkVuZHBvaW50OiBodHRwczovL2NoZWFwbGxtLnNob3AvdjEiLCJBUEkga2V5OiB7S0VZfSIsIk1vZGVsOiBkZWVwc2Vlay12NC4xLWZsYXNoLTEwMTciXX1dfSx7ImlkIjoic2lsbHl0YXZlcm4iLCJuYW1lIjoiU2lsbHlUYXZlcm4iLCJncm91cCI6IkNoYXQgYXBwcyIsInByZXNlbGVjdCI6Im5ldmVyIiwiYWN0aW9ucyI6W3sidHlwZSI6Im1hbnVhbCIsInN0ZXBzIjpbIkFQSTogQ2hhdCBDb21wbGV0aW9uID4gU291cmNlOiBDdXN0b20gKE9wZW5BSS1jb21wYXRpYmxlKS4iLCJFbmRwb2ludDogaHR0cHM6Ly9jaGVhcGxsbS5zaG9wL3YxIChubyB0cmFpbGluZyBzbGFzaCkiLCJBUEkga2V5OiB7S0VZfSIsIk1vZGVsOiBkZWVwc2Vlay12NC4xLWZsYXNoLTEwMTciXX1dfSx7ImlkIjoiY3VybCIsIm5hbWUiOiJjdXJsIiwiZ3JvdXAiOiJDb2RlIGFuZCBmcmFtZXdvcmtzIiwicHJlc2VsZWN0IjoibmV2ZXIiLCJhY3Rpb25zIjpbeyJ0eXBlIjoiZW52IiwidmFycyI6eyJDSEVBUExMTV9BUElfS0VZIjoie0tFWX0ifX0seyJ0eXBlIjoibWFudWFsIiwic3RlcHMiOlsiVHJ5IGl0IChtYWNPUy9MaW51eCk6IiwiY3VybCBodHRwczovL2NoZWFwbGxtLnNob3AvdjEvY2hhdC9jb21wbGV0aW9ucyAtSCBcIkF1dGhvcml6YXRpb246IEJlYXJlciAkQ0hFQVBMTE1fQVBJX0tFWVwiIC1IIFwiQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uXCIgLWQgJ3tcIm1vZGVsXCI6IFwiZGVlcHNlZWstdjQuMS1mbGFzaC0xMDE3XCIsIFwibWVzc2FnZXNcIjogW3tcInJvbGVcIjogXCJ1c2VyXCIsIFwiY29udGVudFwiOiBcIkhlbGxvIVwifV19JyJdfV19LHsiaWQiOiJvcGVuYWktcHkiLCJuYW1lIjoiT3BlbkFJIFNESyAoUHl0aG9uKSIsImdyb3VwIjoiQ29kZSBhbmQgZnJhbWV3b3JrcyIsInByZXNlbGVjdCI6Im5ldmVyIiwiZ2xvYmFsIjp0cnVlLCJhY3Rpb25zIjpbeyJ0eXBlIjoiZW52IiwidmFycyI6eyJPUEVOQUlfQkFTRV9VUkwiOiJodHRwczovL2NoZWFwbGxtLnNob3AvdjEiLCJPUEVOQUlfQVBJX0JBU0UiOiJodHRwczovL2NoZWFwbGxtLnNob3AvdjEiLCJPUEVOQUlfQVBJX0tFWSI6IntLRVl9In19XSwiYWZ0ZXIiOiJPcGVuQUkoKSBub3cgdGFsa3MgdG8gdXMgYnkgZGVmYXVsdDsgdXNlIG1vZGVsPVwiZGVlcHNlZWstdjQuMS1mbGFzaC0xMDE3XCIuIn0seyJpZCI6Im9wZW5haS1qcyIsIm5hbWUiOiJPcGVuQUkgU0RLIChOb2RlKSIsImdyb3VwIjoiQ29kZSBhbmQgZnJhbWV3b3JrcyIsInByZXNlbGVjdCI6Im5ldmVyIiwiZ2xvYmFsIjp0cnVlLCJhY3Rpb25zIjpbeyJ0eXBlIjoiZW52IiwidmFycyI6eyJPUEVOQUlfQkFTRV9VUkwiOiJodHRwczovL2NoZWFwbGxtLnNob3AvdjEiLCJPUEVOQUlfQVBJX0tFWSI6IntLRVl9In19XSwiYWZ0ZXIiOiJuZXcgT3BlbkFJKCkgbm93IHRhbGtzIHRvIHVzIGJ5IGRlZmF1bHQ7IHVzZSBtb2RlbDogXCJkZWVwc2Vlay12NC4xLWZsYXNoLTEwMTdcIi4ifSx7ImlkIjoiYW50aHJvcGljLXNkayIsIm5hbWUiOiJBbnRocm9waWMgU0RLIiwiZ3JvdXAiOiJDb2RlIGFuZCBmcmFtZXdvcmtzIiwicHJlc2VsZWN0IjoibmV2ZXIiLCJnbG9iYWwiOnRydWUsImFjdGlvbnMiOlt7InR5cGUiOiJlbnYiLCJ2YXJzIjp7IkFOVEhST1BJQ19CQVNFX1VSTCI6Imh0dHBzOi8vY2hlYXBsbG0uc2hvcCIsIkFOVEhST1BJQ19BUElfS0VZIjoie0tFWX0ifX1dLCJhZnRlciI6IkFudGhyb3BpYygpIG5vdyB0YWxrcyB0byB1cyBieSBkZWZhdWx0OyBhbnkgbW9kZWwgbmFtZSBpcyBhY2NlcHRlZC4ifSx7ImlkIjoibGFuZ2NoYWluIiwibmFtZSI6IkxhbmdDaGFpbiIsImdyb3VwIjoiQ29kZSBhbmQgZnJhbWV3b3JrcyIsInByZXNlbGVjdCI6Im5ldmVyIiwiZ2xvYmFsIjp0cnVlLCJhY3Rpb25zIjpbeyJ0eXBlIjoiZW52IiwidmFycyI6eyJPUEVOQUlfQkFTRV9VUkwiOiJodHRwczovL2NoZWFwbGxtLnNob3AvdjEiLCJPUEVOQUlfQVBJX0tFWSI6IntLRVl9In19XSwiYWZ0ZXIiOiJDaGF0T3BlbkFJKG1vZGVsPVwiZGVlcHNlZWstdjQuMS1mbGFzaC0xMDE3XCIpIG5vdyB0YWxrcyB0byB1cy4ifSx7ImlkIjoibGxhbWFpbmRleCIsIm5hbWUiOiJMbGFtYUluZGV4IiwiZ3JvdXAiOiJDb2RlIGFuZCBmcmFtZXdvcmtzIiwicHJlc2VsZWN0IjoibmV2ZXIiLCJhY3Rpb25zIjpbeyJ0eXBlIjoiZW52IiwidmFycyI6eyJDSEVBUExMTV9BUElfS0VZIjoie0tFWX0ifX0seyJ0eXBlIjoibWFudWFsIiwic3RlcHMiOlsicGlwIGluc3RhbGwgbGxhbWEtaW5kZXgtbGxtcy1vcGVuYWktbGlrZSwgdGhlbjoiLCJPcGVuQUlMaWtlKG1vZGVsPVwiZGVlcHNlZWstdjQuMS1mbGFzaC0xMDE3XCIsIGFwaV9iYXNlPVwiaHR0cHM6Ly9jaGVhcGxsbS5zaG9wL3YxXCIsIGFwaV9rZXk9b3MuZW52aXJvbltcIkNIRUFQTExNX0FQSV9LRVlcIl0sIGlzX2NoYXRfbW9kZWw9VHJ1ZSwgaXNfZnVuY3Rpb25fY2FsbGluZ19tb2RlbD1UcnVlLCBjb250ZXh0X3dpbmRvdz0xMjgwMDApIl19XX0seyJpZCI6ImFpLXNkayIsIm5hbWUiOiJWZXJjZWwgQUkgU0RLIiwiZ3JvdXAiOiJDb2RlIGFuZCBmcmFtZXdvcmtzIiwicHJlc2VsZWN0IjoibmV2ZXIiLCJhY3Rpb25zIjpbeyJ0eXBlIjoiZW52IiwidmFycyI6eyJDSEVBUExMTV9BUElfS0VZIjoie0tFWX0ifX0seyJ0eXBlIjoibWFudWFsIiwic3RlcHMiOlsiY3JlYXRlT3BlbkFJQ29tcGF0aWJsZSh7IG5hbWU6IFwiY2hlYXBsbG1cIiwgYmFzZVVSTDogXCJodHRwczovL2NoZWFwbGxtLnNob3AvdjFcIiwgYXBpS2V5OiBwcm9jZXNzLmVudi5DSEVBUExMTV9BUElfS0VZIH0pIiwidGhlbiBtb2RlbDogY2hlYXBsbG0oXCJkZWVwc2Vlay12NC4xLWZsYXNoLTEwMTdcIikiXX1dfSx7ImlkIjoibGl0ZWxsbSIsIm5hbWUiOiJMaXRlTExNIiwiZ3JvdXAiOiJDb2RlIGFuZCBmcmFtZXdvcmtzIiwicHJlc2VsZWN0IjoibmV2ZXIiLCJhY3Rpb25zIjpbeyJ0eXBlIjoiZW52IiwidmFycyI6eyJDSEVBUExMTV9BUElfS0VZIjoie0tFWX0ifX0seyJ0eXBlIjoibWFudWFsIiwic3RlcHMiOlsibGl0ZWxsbS5jb21wbGV0aW9uKG1vZGVsPVwib3BlbmFpL2RlZXBzZWVrLXY0LjEtZmxhc2gtMTAxN1wiLCBhcGlfYmFzZT1cImh0dHBzOi8vY2hlYXBsbG0uc2hvcC92MVwiLCBhcGlfa2V5PW9zLmVudmlyb25bXCJDSEVBUExMTV9BUElfS0VZXCJdLCBtZXNzYWdlcz1bLi4uXSkiLCJQcm94eSBjb25maWc6IG1vZGVsOiBvcGVuYWkvZGVlcHNlZWstdjQuMS1mbGFzaC0xMDE3LCBhcGlfYmFzZTogaHR0cHM6Ly9jaGVhcGxsbS5zaG9wL3YxLCBhcGlfa2V5OiBvcy5lbnZpcm9uL0NIRUFQTExNX0FQSV9LRVkiXX1dfSx7ImlkIjoicHlkYW50aWMtYWkiLCJuYW1lIjoiUHlkYW50aWMgQUkiLCJncm91cCI6IkNvZGUgYW5kIGZyYW1ld29ya3MiLCJwcmVzZWxlY3QiOiJuZXZlciIsImFjdGlvbnMiOlt7InR5cGUiOiJlbnYiLCJ2YXJzIjp7IkNIRUFQTExNX0FQSV9LRVkiOiJ7S0VZfSJ9fSx7InR5cGUiOiJtYW51YWwiLCJzdGVwcyI6WyJPcGVuQUlDaGF0TW9kZWwoXCJkZWVwc2Vlay12NC4xLWZsYXNoLTEwMTdcIiwgcHJvdmlkZXI9T3BlbkFJQ29tcGF0aWJsZVByb3ZpZGVyKFwiY2hlYXBsbG1cIiwgYmFzZV91cmw9XCJodHRwczovL2NoZWFwbGxtLnNob3AvdjFcIiwgYXBpX2tleT1vcy5lbnZpcm9uW1wiQ0hFQVBMTE1fQVBJX0tFWVwiXSkpIl19XX0seyJpZCI6Im44biIsIm5hbWUiOiJuOG4iLCJncm91cCI6IkNvZGUgYW5kIGZyYW1ld29ya3MiLCJwcmVzZWxlY3QiOiJuZXZlciIsImFjdGlvbnMiOlt7InR5cGUiOiJtYW51YWwiLCJzdGVwcyI6WyJDcmVkZW50aWFscyA+IE5ldyA+IE9wZW5BSS4iLCJBUEkga2V5OiB7S0VZfSIsIkJhc2UgVVJMOiBodHRwczovL2NoZWFwbGxtLnNob3AvdjEiLCJJbiBhbiBcIk9wZW5BSSBDaGF0IE1vZGVsXCIgbm9kZSwgdHlwZSB0aGUgbW9kZWwgSUQgZGVlcHNlZWstdjQuMS1mbGFzaC0xMDE3LiJdfV19XQ==' export CL_KEY="$KEY" CL_DRY="$DRY" CL_ONLY="$ONLY" CL_ALL="$ALL" CL_YES="$YES" CL_INSTALL="$INSTALL" CL_OS CL_CATALOG_B64 if command -v node >/dev/null 2>&1; then node - <<'CHEAPLLM_NODE_EOF' // cheapllm.shop configurator (macOS/Linux, Node). Embedded in setup.sh; configure.py does the same in Python. // Input through the environment: CL_KEY, CL_CATALOG_B64, CL_OS (mac|linux), CL_DRY, CL_ONLY, CL_ALL, CL_YES, CL_INSTALL (yes|no|ask). 'use strict'; const fs = require('fs'), path = require('path'), os = require('os'), cp = require('child_process'); const E = process.env, HOME = os.homedir(), OS = E.CL_OS === 'mac' ? 'mac' : 'linux'; const KEY = E.CL_KEY, DRY = E.CL_DRY === '1'; const fillKey = (v) => (typeof v === 'string' ? v.split('{KEY}').join(KEY) : Array.isArray(v) ? v.map(fillKey) : v && typeof v === 'object' ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, fillKey(x)])) : v); const CATALOG = fillKey(JSON.parse(Buffer.from(E.CL_CATALOG_B64, 'base64').toString('utf8'))); // ------------------------------------------------------------------ terminal let ttyIn = null, ttyOut = null; try { ttyIn = fs.openSync('/dev/tty', 'r'); ttyOut = fs.openSync('/dev/tty', 'w'); } catch { ttyIn = ttyOut = null; } const say = (s = '') => process.stdout.write(s + '\n'); function ask(q) { if (ttyIn === null) return ''; fs.writeSync(ttyOut, q); const buf = Buffer.alloc(4096); let n = 0; try { n = fs.readSync(ttyIn, buf, 0, buf.length, null); } catch { return ''; } return buf.toString('utf8', 0, n).replace(/\r?\n$/, ''); } // ------------------------------------------------------------------ helpers const pick = (p) => (p ? p[OS] ?? p.unix : undefined); const expand = (p) => p.replace(/^~(?=\/|$)/, HOME); const short = (p) => p.replace(HOME, '~'); const exists = (p) => fs.existsSync(p); const onPath = (bin) => (E.PATH || '').split(':').some((d) => { try { return fs.statSync(path.join(d, bin)).isFile(); } catch { return false; } }); const read = (p) => { try { return fs.readFileSync(p, 'utf8'); } catch { return null; } }; function write(p, text, priv) { if (DRY) return; fs.mkdirSync(path.dirname(p), { recursive: true }); if (exists(p)) fs.copyFileSync(p, p + '.cheapllm-bak'); fs.writeFileSync(p, text); if (priv) fs.chmodSync(p, 0o600); } const isObj = (v) => v && typeof v === 'object' && !Array.isArray(v); function merge(t, s) { for (const [k, v] of Object.entries(s)) { if (isObj(v) && isObj(t[k])) merge(t[k], v); else t[k] = v; } return t; } function readJson(p, fallback) { const t = read(p); if (t === null || !t.trim()) return fallback; try { return JSON.parse(t); } catch { throw new Error('NOT_JSON'); } } function extFound(prefix) { for (const d of pick({ unix: ['~/.vscode/extensions'] }) || []) { try { if (fs.readdirSync(expand(d)).some((f) => f.toLowerCase().startsWith(prefix.toLowerCase() + '-'))) return true; } catch {} } return false; } function detect(t) { const d = t.detect || {}; if ((d.bins || []).some(onPath)) return true; if ((pick(d.paths) || []).some((p) => exists(expand(p)))) return true; if (d.ext && extFound(d.ext)) return true; return false; } const installFor = (t) => { if (!t.install) return null; const cmd = t.install[OS] ?? t.install.unix; if (!cmd) return null; const n = t.install.needs; return { cmd, needs: Array.isArray(n) ? n : (n && (n[OS] ?? n.unix)) || [] }; }; // ------------------------------------------------------------------ choose const items = CATALOG.map((t) => ({ t, found: detect(t), inst: installFor(t) })); const auto = (it) => it.t.actions.some((a) => a.type !== 'manual'); function status(it) { if (it.t.unsupported) return 'not supported, see notes'; if (it.t.global) return 'sets variables for every program'; const kind = auto(it) ? '' : ', steps to follow'; if (!it.t.detect) return auto(it) ? 'key in your environment + snippet' : 'steps to follow'; if (it.found) return 'found' + kind; return (it.inst ? 'not found, can install' : 'not found') + kind; } let chosen; const only = (E.CL_ONLY || '').split(',').map((s) => s.trim()).filter(Boolean); if (only.length) chosen = new Set(only.filter((id) => items.some((it) => it.t.id === id))); else if (E.CL_ALL === '1') chosen = new Set(items.map((it) => it.t.id)); else chosen = new Set(items.filter((it) => it.found && it.t.preselect !== 'never' && !it.t.unsupported).map((it) => it.t.id)); if (!only.length && E.CL_ALL !== '1' && E.CL_YES !== '1' && ttyIn !== null) { for (;;) { say(''); say(' What should be set up? Ticked = found on this machine.'); let group = ''; items.forEach((it, i) => { if (it.t.group !== group) { group = it.t.group; say(`\n ${group}`); } const mark = chosen.has(it.t.id) ? '[x]' : '[ ]'; say(` ${String(i + 1).padStart(3)} ${mark} ${it.t.name.padEnd(26)} ${status(it)}`); }); const a = ask('\n Numbers to tick or untick (e.g. 2 5 12), a = all, n = none, Enter = go: ').trim().toLowerCase(); if (!a) break; if (a === 'a') { items.forEach((it) => chosen.add(it.t.id)); continue; } if (a === 'n') { chosen.clear(); continue; } for (const tok of a.split(/[\s,]+/)) { const m = /^(\d+)(?:-(\d+))?$/.exec(tok); if (!m) continue; const lo = +m[1], hi = m[2] ? +m[2] : lo; for (let i = lo; i <= hi; i++) { const it = items[i - 1]; if (it) chosen.has(it.t.id) ? chosen.delete(it.t.id) : chosen.add(it.t.id); } } } } const selected = items.filter((it) => chosen.has(it.t.id)); if (!selected.length) { say('\n Nothing selected. Run it again any time.'); process.exit(0); } // ------------------------------------------------------------------ install const done = [], manual = [], after = [], envVars = {}; const missing = selected.filter((it) => !it.found && it.inst); if (missing.length) { let go = E.CL_INSTALL === 'yes'; if (E.CL_INSTALL !== 'yes' && E.CL_INSTALL !== 'no' && ttyIn !== null && !DRY) { const a = ask(`\n Install ${missing.map((it) => it.t.name).join(', ')}? [Y/n] `).trim().toLowerCase(); go = a === '' || a === 'y' || a === 'yes'; } for (const it of missing) { const lack = it.inst.needs.filter((b) => !onPath(b)); if (DRY) { done.push(`${it.t.name}: would install with: ${it.inst.cmd}`); continue; } if (!go) { manual.push([it.t.name, [`Not installed. To install: ${it.inst.cmd}`]]); continue; } if (lack.length) { manual.push([it.t.name, [`Needs ${lack.join(', ')} first (Node.js for npm, VS Code's "code" command for extensions), then: ${it.inst.cmd}`]]); continue; } say(`\n Installing ${it.t.name}: ${it.inst.cmd}`); const r = cp.spawnSync('sh', ['-c', it.inst.cmd], { stdio: [ttyIn !== null ? ttyIn : 'ignore', 'inherit', 'inherit'] }); if (r.status === 0) { it.found = true; done.push(`${it.t.name}: installed`); } else manual.push([it.t.name, [`Install failed (exit ${r.status}). Try by hand: ${it.inst.cmd}`]]); } } // ------------------------------------------------------------------ configure function apply(it, a) { const p = a.path ? expand(pick(a.path)) : null; switch (a.type) { case 'env': Object.assign(envVars, a.vars); return null; case 'manual': manual.push([it.t.name, a.steps]); return null; case 'json': { let j; try { j = readJson(p, {}); } catch { manual.push([it.t.name, [`${short(p)} has comments, so it was left alone. Merge this into it:`, JSON.stringify(a.merge, null, 2)]]); return null; } if (!isObj(j)) j = {}; merge(j, a.merge); for (const r of a.remove || []) { const ks = r.split('.'); let o = j; for (const k of ks.slice(0, -1)) o = o && o[k]; if (isObj(o)) delete o[ks[ks.length - 1]]; } write(p, JSON.stringify(j, null, 2) + '\n', a.private); return short(p); } case 'json-array': { let arr; try { arr = readJson(p, []); } catch { manual.push([it.t.name, [`${short(p)} has comments, so it was left alone. Add this entry:`, JSON.stringify(a.item, null, 2)]]); return null; } if (!Array.isArray(arr)) arr = []; const i = arr.findIndex((x) => isObj(x) && Object.entries(a.match).every(([k, v]) => x[k] === v)); if (i >= 0) arr[i] = a.item; else arr.push(a.item); write(p, JSON.stringify(arr, null, 2) + '\n', a.private); return short(p); } case 'toml': { const val = (v) => (typeof v === 'number' || typeof v === 'boolean' ? String(v) : JSON.stringify(String(v))); const lines = (read(p) || '').split('\n'), out = []; const tableNames = Object.keys(a.tables || {}); let top = true, skip = false; for (const l of lines) { const h = /^\s*\[([^\]]+)\]\s*$/.exec(l); if (h) { top = false; skip = tableNames.includes(h[1].trim()); } if (skip) continue; if (top && Object.keys(a.top || {}).some((k) => new RegExp(`^\\s*${k}\\s*=`).test(l))) continue; out.push(l); } while (out.length && !out[out.length - 1].trim()) out.pop(); const head = Object.entries(a.top || {}).map(([k, v]) => `${k} = ${val(v)}`).join('\n') + '\n'; const tables = tableNames.map((n) => `\n[${n}]\n` + Object.entries(a.tables[n]).map(([k, v]) => `${k} = ${val(v)}`).join('\n') + '\n').join(''); write(p, head + (out.length ? out.join('\n') + '\n' : '') + tables); return short(p); } case 'block': { const c = a.comment || '#', begin = `${c} >>> cheapllm.shop >>>`, end = `${c} <<< cheapllm.shop <<<`; const drops = (a.drop || []).map((r) => new RegExp(r)); const out = []; let inBlock = false; for (const l of (read(p) || '').split('\n')) { if (l === begin) { inBlock = true; continue; } if (l === end) { inBlock = false; continue; } if (inBlock || drops.some((r) => r.test(l))) continue; out.push(l); } while (out.length && !out[out.length - 1].trim()) out.pop(); write(p, (out.length ? out.join('\n') + '\n\n' : '') + `${begin}\n${a.text}\n${end}\n`, a.private); return short(p); } case 'file': { if (a.ifMissing && exists(p)) { manual.push([it.t.name, a.elseSteps || [`${short(p)} exists and was left alone.`]]); return null; } write(p, a.json ? JSON.stringify(a.json, null, 2) + '\n' : a.content, a.private); return short(p); } } return null; } for (const it of selected) { const files = []; for (const a of it.t.actions) { try { const f = apply(it, a); if (f) files.push(f); } catch (e) { manual.push([it.t.name, [`Not changed: ${e.message}`]]); } } if (files.length) done.push(`${it.t.name}: ${files.join(', ')}`); if (it.t.after) after.push(`${it.t.name}: ${it.t.after}`); } // environment variables: one file, loaded by bash, zsh and fish if (Object.keys(envVars).length) { const envFile = path.join(HOME, '.config', 'cheapllm', 'env'); const vars = {}; for (const m of (read(envFile) || '').matchAll(/^export ([A-Z0-9_]+)='([^']*)'$/gm)) vars[m[1]] = m[2]; Object.assign(vars, envVars); write(envFile, `# written by cheapllm.shop setup\n` + Object.entries(vars).map(([k, v]) => `export ${k}='${v}'`).join('\n') + '\n', true); const line = '[ -f "$HOME/.config/cheapllm/env" ] && . "$HOME/.config/cheapllm/env" # cheapllm.shop'; const shell = path.basename(E.SHELL || ''); const rcs = ['.bashrc', '.zshrc'].map((f) => path.join(HOME, f)).filter((f) => exists(f) || f.endsWith('.' + shell + 'rc')); if (OS === 'mac' && exists(path.join(HOME, '.bash_profile'))) rcs.push(path.join(HOME, '.bash_profile')); for (const rc of rcs) { const t = read(rc) || ''; if (!t.includes('.config/cheapllm/env')) write(rc, t + (t && !t.endsWith('\n') ? '\n' : '') + line + '\n'); } if (exists(path.join(HOME, '.config', 'fish')) || shell === 'fish') { write(path.join(HOME, '.config', 'fish', 'conf.d', 'cheapllm.fish'), Object.entries(vars).map(([k, v]) => `set -gx ${k} '${v}'`).join('\n') + '\n', true); } done.push(`Environment: ${Object.keys(envVars).join(', ')} in ~/.config/cheapllm/env` + (rcs.length ? ` (loaded from ${rcs.map(short).join(', ')})` : '')); } // ------------------------------------------------------------------ report say(''); for (const d of done) say((DRY ? ' would ' : ' done ') + d); if (manual.length) { say('\n By hand (these apps keep their settings where a script can\'t write):'); for (const [name, steps] of manual) { say(`\n ${name}`); for (const s of steps) say(' ' + s.split('\n').join('\n ')); } } if (after.length) { say('\n Next'); for (const a of after) say(' ' + a); } say(''); say(DRY ? ' Dry run: nothing was written or installed.' : ' Old versions of changed files are saved as *.cheapllm-bak.'); CHEAPLLM_NODE_EOF elif command -v python3 >/dev/null 2>&1; then python3 - <<'CHEAPLLM_PY_EOF' # cheapllm.shop configurator (macOS/Linux, Python 3). Same behaviour as configure.js, for machines without Node. # Input through the environment: CL_KEY, CL_CATALOG_B64, CL_OS (mac|linux), CL_DRY, CL_ONLY, CL_ALL, CL_YES, CL_INSTALL. import base64, json, os, re, shutil, subprocess, sys E = os.environ HOME = os.path.expanduser('~') OS = 'mac' if E.get('CL_OS') == 'mac' else 'linux' KEY = E['CL_KEY'] DRY = E.get('CL_DRY') == '1' def fill_key(v): if isinstance(v, str): return v.replace('{KEY}', KEY) if isinstance(v, list): return [fill_key(x) for x in v] if isinstance(v, dict): return {k: fill_key(x) for k, x in v.items()} return v CATALOG = fill_key(json.loads(base64.b64decode(E['CL_CATALOG_B64']).decode('utf-8'))) # ------------------------------------------------------------------ terminal try: TTY_IN = open('/dev/tty', 'r') TTY_OUT = open('/dev/tty', 'w') except OSError: TTY_IN = TTY_OUT = None def say(s=''): print(s, flush=True) def ask(q): if TTY_IN is None: return '' TTY_OUT.write(q); TTY_OUT.flush() return TTY_IN.readline().rstrip('\r\n') # ------------------------------------------------------------------ helpers def pick(p): if not p: return None return p.get(OS, p.get('unix')) def expand(p): return HOME + p[1:] if p == '~' or p.startswith('~/') else p def short(p): return p.replace(HOME, '~') def read(p): try: with open(p, encoding='utf-8') as f: return f.read() except OSError: return None def write(p, text, priv=False): if DRY: return os.makedirs(os.path.dirname(p), exist_ok=True) if os.path.exists(p): shutil.copyfile(p, p + '.cheapllm-bak') with open(p, 'w', encoding='utf-8') as f: f.write(text) if priv: os.chmod(p, 0o600) def merge(t, s): for k, v in s.items(): if isinstance(v, dict) and isinstance(t.get(k), dict): merge(t[k], v) else: t[k] = v return t class NotJson(Exception): pass def read_json(p, fallback): t = read(p) if t is None or not t.strip(): return fallback try: return json.loads(t) except ValueError: raise NotJson() def ext_found(prefix): d = expand('~/.vscode/extensions') try: return any(f.lower().startswith(prefix.lower() + '-') for f in os.listdir(d)) except OSError: return False def detect(t): d = t.get('detect') or {} if any(shutil.which(b) for b in d.get('bins', [])): return True if any(os.path.exists(expand(p)) for p in (pick(d.get('paths')) or [])): return True return bool(d.get('ext') and ext_found(d['ext'])) def install_for(t): i = t.get('install') if not i: return None cmd = i.get(OS, i.get('unix')) if not cmd: return None n = i.get('needs') or [] needs = n if isinstance(n, list) else (n.get(OS, n.get('unix')) or []) return {'cmd': cmd, 'needs': needs} # ------------------------------------------------------------------ choose items = [{'t': t, 'found': detect(t), 'inst': install_for(t)} for t in CATALOG] def auto(it): return any(a['type'] != 'manual' for a in it['t']['actions']) def status(it): t = it['t'] if t.get('unsupported'): return 'not supported, see notes' if t.get('global'): return 'sets variables for every program' kind = '' if auto(it) else ', steps to follow' if not t.get('detect'): return 'key in your environment + snippet' if auto(it) else 'steps to follow' if it['found']: return 'found' + kind return ('not found, can install' if it['inst'] else 'not found') + kind only = [s.strip() for s in (E.get('CL_ONLY') or '').split(',') if s.strip()] ids = [it['t']['id'] for it in items] if only: chosen = set(i for i in only if i in ids) elif E.get('CL_ALL') == '1': chosen = set(ids) else: chosen = set(it['t']['id'] for it in items if it['found'] and it['t'].get('preselect') != 'never' and not it['t'].get('unsupported')) if not only and E.get('CL_ALL') != '1' and E.get('CL_YES') != '1' and TTY_IN is not None: while True: say(''); say(' What should be set up? Ticked = found on this machine.') group = '' for i, it in enumerate(items): if it['t']['group'] != group: group = it['t']['group']; say('\n ' + group) mark = '[x]' if it['t']['id'] in chosen else '[ ]' say(' %3d %s %-26s %s' % (i + 1, mark, it['t']['name'], status(it))) a = ask('\n Numbers to tick or untick (e.g. 2 5 12), a = all, n = none, Enter = go: ').strip().lower() if not a: break if a == 'a': chosen = set(ids); continue if a == 'n': chosen = set(); continue for tok in re.split(r'[\s,]+', a): m = re.match(r'^(\d+)(?:-(\d+))?$', tok) if not m: continue lo = int(m.group(1)); hi = int(m.group(2) or lo) for i in range(lo, hi + 1): if 1 <= i <= len(items): k = items[i - 1]['t']['id'] chosen.discard(k) if k in chosen else chosen.add(k) selected = [it for it in items if it['t']['id'] in chosen] if not selected: say('\n Nothing selected. Run it again any time.'); sys.exit(0) # ------------------------------------------------------------------ install done, manual, after, env_vars = [], [], [], {} missing = [it for it in selected if not it['found'] and it['inst']] if missing: go = E.get('CL_INSTALL') == 'yes' if E.get('CL_INSTALL') not in ('yes', 'no') and TTY_IN is not None and not DRY: a = ask('\n Install %s? [Y/n] ' % ', '.join(it['t']['name'] for it in missing)).strip().lower() go = a in ('', 'y', 'yes') for it in missing: name, inst = it['t']['name'], it['inst'] lack = [b for b in inst['needs'] if not shutil.which(b)] if DRY: done.append('%s: would install with: %s' % (name, inst['cmd'])); continue if not go: manual.append((name, ['Not installed. To install: ' + inst['cmd']])); continue if lack: manual.append((name, ['Needs %s first (Node.js for npm, VS Code\'s "code" command for extensions), then: %s' % (', '.join(lack), inst['cmd'])])); continue say('\n Installing %s: %s' % (name, inst['cmd'])) r = subprocess.call(['sh', '-c', inst['cmd']], stdin=TTY_IN if TTY_IN is not None else subprocess.DEVNULL) if r == 0: it['found'] = True; done.append(name + ': installed') else: manual.append((name, ['Install failed (exit %s). Try by hand: %s' % (r, inst['cmd'])])) # ------------------------------------------------------------------ configure def apply(it, a): name = it['t']['name'] p = expand(pick(a['path'])) if a.get('path') else None typ = a['type'] if typ == 'env': env_vars.update(a['vars']); return None if typ == 'manual': manual.append((name, a['steps'])); return None if typ == 'json': try: j = read_json(p, {}) except NotJson: manual.append((name, ['%s has comments, so it was left alone. Merge this into it:' % short(p), json.dumps(a['merge'], indent=2)])); return None if not isinstance(j, dict): j = {} merge(j, a['merge']) for r in a.get('remove', []): ks = r.split('.'); o = j for k in ks[:-1]: o = o.get(k) if isinstance(o, dict) else None if isinstance(o, dict): o.pop(ks[-1], None) write(p, json.dumps(j, indent=2) + '\n', a.get('private')) return short(p) if typ == 'json-array': try: arr = read_json(p, []) except NotJson: manual.append((name, ['%s has comments, so it was left alone. Add this entry:' % short(p), json.dumps(a['item'], indent=2)])); return None if not isinstance(arr, list): arr = [] idx = next((i for i, x in enumerate(arr) if isinstance(x, dict) and all(x.get(k) == v for k, v in a['match'].items())), -1) if idx >= 0: arr[idx] = a['item'] else: arr.append(a['item']) write(p, json.dumps(arr, indent=2) + '\n', a.get('private')) return short(p) if typ == 'toml': def val(v): return str(v).lower() if isinstance(v, bool) else str(v) if isinstance(v, (int, float)) else json.dumps(str(v)) tops = a.get('top') or {}; tables = a.get('tables') or {} out = []; top = True; skip = False for l in (read(p) or '').split('\n'): h = re.match(r'^\s*\[([^\]]+)\]\s*$', l) if h: top = False; skip = h.group(1).strip() in tables if skip: continue if top and any(re.match(r'^\s*%s\s*=' % re.escape(k), l) for k in tops): continue out.append(l) while out and not out[-1].strip(): out.pop() head = '\n'.join('%s = %s' % (k, val(v)) for k, v in tops.items()) + '\n' tail = ''.join('\n[%s]\n' % n + '\n'.join('%s = %s' % (k, val(v)) for k, v in kv.items()) + '\n' for n, kv in tables.items()) write(p, head + ('\n'.join(out) + '\n' if out else '') + tail) return short(p) if typ == 'block': c = a.get('comment', '#'); begin = c + ' >>> cheapllm.shop >>>'; end = c + ' <<< cheapllm.shop <<<' drops = [re.compile(r) for r in a.get('drop', [])] out = []; inside = False for l in (read(p) or '').split('\n'): if l == begin: inside = True; continue if l == end: inside = False; continue if inside or any(r.search(l) for r in drops): continue out.append(l) while out and not out[-1].strip(): out.pop() write(p, ('\n'.join(out) + '\n\n' if out else '') + '%s\n%s\n%s\n' % (begin, a['text'], end), a.get('private')) return short(p) if typ == 'file': if a.get('ifMissing') and os.path.exists(p): manual.append((name, a.get('elseSteps') or ['%s exists and was left alone.' % short(p)])); return None write(p, json.dumps(a['json'], indent=2) + '\n' if a.get('json') else a['content'], a.get('private')) return short(p) return None for it in selected: files = [] for a in it['t']['actions']: try: f = apply(it, a) if f: files.append(f) except Exception as e: manual.append((it['t']['name'], ['Not changed: %s' % e])) if files: done.append('%s: %s' % (it['t']['name'], ', '.join(files))) if it['t'].get('after'): after.append('%s: %s' % (it['t']['name'], it['t']['after'])) if env_vars: env_file = os.path.join(HOME, '.config', 'cheapllm', 'env') vars_ = dict(re.findall(r"^export ([A-Z0-9_]+)='([^']*)'$", read(env_file) or '', re.M)) vars_.update(env_vars) write(env_file, '# written by cheapllm.shop setup\n' + '\n'.join("export %s='%s'" % kv for kv in vars_.items()) + '\n', True) line = '[ -f "$HOME/.config/cheapllm/env" ] && . "$HOME/.config/cheapllm/env" # cheapllm.shop' sh = os.path.basename(E.get('SHELL', '')) rcs = [os.path.join(HOME, f) for f in ('.bashrc', '.zshrc') if os.path.exists(os.path.join(HOME, f)) or f == '.' + sh + 'rc'] if OS == 'mac' and os.path.exists(os.path.join(HOME, '.bash_profile')): rcs.append(os.path.join(HOME, '.bash_profile')) for rc in rcs: t = read(rc) or '' if '.config/cheapllm/env' not in t: write(rc, t + ('\n' if t and not t.endswith('\n') else '') + line + '\n') if os.path.exists(os.path.join(HOME, '.config', 'fish')) or sh == 'fish': write(os.path.join(HOME, '.config', 'fish', 'conf.d', 'cheapllm.fish'), '\n'.join("set -gx %s '%s'" % kv for kv in vars_.items()) + '\n', True) done.append('Environment: %s in ~/.config/cheapllm/env%s' % (', '.join(env_vars), ' (loaded from %s)' % ', '.join(map(short, rcs)) if rcs else '')) say('') for d in done: say((' would ' if DRY else ' done ') + d) if manual: say("\n By hand (these apps keep their settings where a script can't write):") for name, steps in manual: say('\n ' + name) for s in steps: say(' ' + s.replace('\n', '\n ')) if after: say('\n Next') for a in after: say(' ' + a) say('') say(' Dry run: nothing was written or installed.' if DRY else ' Old versions of changed files are saved as *.cheapllm-bak.') CHEAPLLM_PY_EOF else say " This needs node or python3. Configure your tools by hand: $ORIGIN/integrations" exit 1 fi say " Open a new terminal (or run: . ~/.config/cheapllm/env), then start your tool." say " Everything else: $ORIGIN/integrations" say ""