#!/bin/sh # tk — the TurnKey terminal front door. # # curl -fsSL https://download.turnkey.tech | sh install # tk the tools you pay for # # One POSIX shell script. It needs curl, and sha-256 from any of openssl, # sha256sum or shasum. Nothing else: no python, no jq, no node, no browser. # # Sign-in is TurnKey's SSO: a six-digit code, emailed to you, typed here. # There are no passwords. This script holds no secret of its own — the OAuth # client is public and PKCE-protected (RFC 7636), and the token it stores is # scoped and expires within the hour. # # Why this and not the device flow, or SSH: DECISION.md in turnkey/tk. set -eu umask 077 export LC_ALL=C TK_VERSION=0.1.1 TK_ISSUER=${TK_ISSUER:-https://signin.turnkey.tech} TK_CLIENT_ID=${TK_CLIENT_ID:-49r294d7o9rbbh7dyblyk} TK_REDIRECT=${TK_REDIRECT:-http://127.0.0.1:1717/callback} TK_DOWNLOAD=${TK_DOWNLOAD:-https://download.turnkey.tech/tk} TK_SCOPE='openid profile email offline_access urn:logto:scope:organizations urn:logto:scope:organization_roles' STATE_DIR=${XDG_STATE_HOME:-$HOME/.local/state}/turnkey SESSION=$STATE_DIR/session.json # ── the house look ──────────────────────────────────────────────────────── # Two Block, and the palette the TurnKey TUIs already use. # Three rungs, because Jimmy's own machine is the bottom one: his console is # TERM=linux with eight colours, where a 256-colour index is approximated and # a dark one can land on black. DIM is 38;5;36 (7.48:1 on black) rather than # the darker greens the house used to reach for — it carries the key hints, # the tool descriptions and the PATH line, and instructions have to be read. tk_colours=0 if [ -t 1 ] && [ -z "${TK_NO_COLOR:-}${NO_COLOR:-}" ]; then # tput exits 0 and prints -1 on a terminal that has no colour (TERM=dumb), # so a non-digit answer means plain text, not "assume eight". A missing # tput is different: on a real tty eight is the safe assumption. tk_colours=$(tput colors 2>/dev/null) || tk_colours=8 case $tk_colours in ''|*[!0-9]*) tk_colours=0 ;; esac fi if [ "$tk_colours" -ge 256 ]; then GRN=$(printf '\033[38;5;84m'); HI=$(printf '\033[38;5;158m') MID=$(printf '\033[38;5;41m'); DIM=$(printf '\033[38;5;36m') CYN=$(printf '\033[38;5;80m'); MAG=$(printf '\033[38;5;206m') B=$(printf '\033[1m'); Z=$(printf '\033[0m') elif [ "$tk_colours" -ge 8 ]; then GRN=$(printf '\033[1;32m'); HI=$(printf '\033[1;37m') MID=$(printf '\033[32m'); DIM=$(printf '\033[37m') CYN=$(printf '\033[36m'); MAG=$(printf '\033[35m') B=$(printf '\033[1m'); Z=$(printf '\033[0m') else GRN=; HI=; MID=; DIM=; CYN=; MAG=; B=; Z= fi ESC=$(printf '\033') CR=$(printf '\r') wordmark() { printf '%s\n' \ "${GRN}▀█▀ █ █ █▀▄ █▄ █ █▄▀ ██▀ ▀▄▀${Z}" \ "${GRN} █ ▀▄█ █▀▄ █ ▀█ █ █ █▄▄ █ ${Z}" } die() { printf '%s\n' "${MAG}tk: $*${Z}" >&2; exit 1; } # ── tools ───────────────────────────────────────────────────────────────── # slug | name | one line | where it lives | terminal front end # # The last field is a command on your PATH. Empty means that tool has no # terminal front end yet, and tk says so rather than pretending. Entitlement # is never decided in this table: it comes from the `plan:` role on # your account in the SSO, the same fact every TurnKey tool checks. tools_table() { cat <<-'EOF' mercury|Mercury|an AI coworker that does the work, all day, on your terms|https://mercury.turnkey.tech| sisyphus|Sisyphus|25 new sales leads every weekday at 7am|https://sisyphus.turnkey.tech/app| speedrun|Speedrun|speed-run the ACT, SAT or CLT by test day|https://speedrun.turnkey.tech/app| atrium|Atrium|your websites, published from one folder|https://atrium.turnkey.tech| blueline|BlueLine|millwork estimates, straight off the drawings|https://blueline.turnkey.tech| EOF } tool_field() { # slug field tools_table | awk -F'|' -v s="$1" -v f="$2" '$1 == s { print $f; exit }' } # ── small helpers ───────────────────────────────────────────────────────── have() { command -v "$1" >/dev/null 2>&1; } need_curl() { have curl || die "curl is not installed. Install it, then run tk again. Debian/Ubuntu: sudo apt install curl Fedora/RHEL: sudo dnf install curl Alpine: sudo apk add curl" } b64url() { # stdin -> base64url, no padding if have openssl; then openssl base64 -A; else base64 | tr -d '\n'; fi \ | tr '+/' '-_' | tr -d '=' } unhex() { # hex on stdin -> raw bytes awk ' BEGIN { for (i = 0; i < 16; i++) { h[sprintf("%x", i)] = i; h[sprintf("%X", i)] = i } } { n = length($0) for (i = 1; i < n; i += 2) printf "%c", h[substr($0, i, 1)] * 16 + h[substr($0, i + 1, 1)] }' } sha256raw() { # stdin -> 32 raw bytes if have openssl; then openssl dgst -binary -sha256 elif have sha256sum; then sha256sum | cut -d' ' -f1 | unhex elif have shasum; then shasum -a 256 | cut -d' ' -f1 | unhex else die "tk needs sha-256. Install openssl (or coreutils) and run tk again." fi } rand_b64url() { dd if=/dev/urandom bs=1 count="${1:-48}" 2>/dev/null | b64url; } urlenc() { awk 'BEGIN { for (i = 0; i < 256; i++) o[sprintf("%c", i)] = i } { n = length($0) for (i = 1; i <= n; i++) { c = substr($0, i, 1) if (c ~ /[A-Za-z0-9._~-]/) printf "%s", c; else printf "%%%02X", o[c] } }' } now() { date +%s; } # JSON readers. Logto answers on one compact line, so these stay small. json_str() { # key (stdin) awk -v k="\"$1\":\"" '{ i = index($0, k); if (i == 0) next s = substr($0, i + length(k)); j = index(s, "\""); if (j == 0) next print substr(s, 1, j - 1); exit }' } json_num() { # key (stdin) awk -v k="\"$1\":" '{ i = index($0, k); if (i == 0) next s = substr($0, i + length(k)); n = "" for (j = 1; j <= length(s); j++) { c = substr(s, j, 1) if (c ~ /[0-9]/) n = n c; else break } if (n != "") { print n; exit } }' } json_list() { # key of an array of strings (stdin) awk -v k="\"$1\":[" '{ i = index($0, k); if (i == 0) next s = substr($0, i + length(k)); j = index(s, "]"); if (j == 0) next s = substr(s, 1, j - 1); n = split(s, a, ",") for (t = 1; t <= n; t++) { gsub(/^[ \t]*"/, "", a[t]); gsub(/"[ \t]*$/, "", a[t]) if (a[t] != "") print a[t] } exit }' } TMP= RAW_ON=0 SAVED_STTY= onexit() { if [ "$RAW_ON" = 1 ]; then raw_off; fi if [ -n "$TMP" ]; then rm -rf "$TMP"; fi } trap onexit EXIT HUP INT TERM mktmp() { if [ -n "$TMP" ]; then return 0; fi TMP=$(mktemp -d 2>/dev/null) || TMP=${TMPDIR:-/tmp}/tk.$$ mkdir -p "$TMP"; chmod 700 "$TMP" } # A token goes in a 0600 config file, never in argv: anything on this box can # read another process's command line out of /proc. curl_auth() { # token url... mktmp; tok=$1; shift printf 'header = "Authorization: Bearer %s"\n' "$tok" > "$TMP/auth.conf" chmod 600 "$TMP/auth.conf" curl -sS --max-time 30 -K "$TMP/auth.conf" "$@" || true rm -f "$TMP/auth.conf" } post_form() { # url body-string mktmp printf '%s' "$2" > "$TMP/form.req"; chmod 600 "$TMP/form.req" curl -sS --max-time 30 -X POST -H 'content-type: application/x-www-form-urlencoded' \ --data-binary "@$TMP/form.req" "$1" || true rm -f "$TMP/form.req" } # ── session ─────────────────────────────────────────────────────────────── session_get() { [ -f "$SESSION" ] || return 1; json_str "$1" < "$SESSION"; } session_num() { [ -f "$SESSION" ] || return 1; json_num "$1" < "$SESSION"; } session_save() { # access refresh expires_at email sub mkdir -p "$STATE_DIR"; chmod 700 "$STATE_DIR" t=$STATE_DIR/.session.$$ printf '{"v":1,"access_token":"%s","refresh_token":"%s","expires_at":%s,"email":"%s","sub":"%s"}\n' \ "$1" "$2" "$3" "$4" "$5" > "$t" chmod 600 "$t"; mv "$t" "$SESSION" } # ── sign in ─────────────────────────────────────────────────────────────── ex() { # jar method path body — one Experience API call if [ -n "$4" ]; then curl -sS --max-time 30 -c "$1" -b "$1" -X "$2" \ -H 'content-type: application/json' -H 'accept: application/json' \ -d "$4" "$TK_ISSUER$3" || true else curl -sS --max-time 30 -c "$1" -b "$1" -X "$2" -H 'accept: application/json' \ "$TK_ISSUER$3" || true fi } fail_exp() { # body message — one readable line out of a Logto error m=$(printf %s "$1" | json_str message) c=$(printf %s "$1" | json_str code) printf '%s\n' "${MAG}tk: $2.${Z}" >&2 if [ -n "$m" ]; then printf '%s\n' " $m" >&2; fi if [ -n "$c" ]; then printf '%s\n' " ${DIM}($c)${Z}" >&2; fi printf '%s\n' " If it keeps happening, email bot@turnkey.tech." >&2 exit 1 } signin() { need_curl; mktmp jar=$TMP/jar; : > "$jar"; chmod 600 "$jar" verifier=$(rand_b64url 48) challenge=$(printf %s "$verifier" | sha256raw | b64url) state=$(rand_b64url 12) scope=$(printf %s "$TK_SCOPE" | urlenc) redir=$(printf %s "$TK_REDIRECT" | urlenc) url="$TK_ISSUER/oidc/auth?client_id=$TK_CLIENT_ID&response_type=code&redirect_uri=$redir" url="$url&scope=$scope&state=$state&code_challenge=$challenge&code_challenge_method=S256" # prompt=consent is what actually buys the refresh token. offline_access in # the scope only asks; oidc-provider issues the token when consent granted # it, and without this the sign-in succeeds, stores an empty refresh_token, # and asks for a new email code an hour later. tk is a first-party app, so # Logto auto-consents server-side and no screen is ever drawn — if tk is # ever made third-party, this line starts demanding a browser. url="$url&prompt=consent" if ! loc=$(curl -sS --max-time 30 -c "$jar" -b "$jar" -o /dev/null -w '%{redirect_url}' "$url" 2>"$TMP/err"); then printf '%s\n' "${MAG}tk: could not reach ${TK_ISSUER}.${Z}" >&2 sed 's/^/ /' "$TMP/err" >&2 2>/dev/null || true printf '%s\n' " Check the network, then run tk again." >&2 exit 1 fi case "$loc" in */sign-in*|*/register*) : ;; '') die "sign-in did not start. Try again in a minute." ;; *) die "sign-in did not start as expected. Run 'tk update', then try again." ;; esac printf '\n'; wordmark; printf '\n' printf '%s' "${MID} your email ${GRN}▸${Z} " IFS= read -r email || exit 1 email=$(printf %s "$email" | tr -d '\r"\\' | sed 's/^[ ]*//;s/[ ]*$//') case "$email" in *@*.*) : ;; *) die "that does not look like an email address." ;; esac ex "$jar" PUT "/api/experience" '{"interactionEvent":"SignIn"}' >/dev/null out=$(ex "$jar" POST "/api/experience/verification/verification-code" \ '{"identifier":{"type":"email","value":"'"$email"'"},"interactionEvent":"SignIn"}') vid=$(printf %s "$out" | json_str verificationId) [ -n "$vid" ] || fail_exp "$out" "no code could be sent to $email" printf '%s\n' "${DIM} a six-digit code is on its way to ${CYN}${email}${DIM}. it lasts 10 minutes.${Z}" tries=0 while :; do tries=$((tries + 1)) printf '%s' "${MID} code ${GRN}▸${Z} " IFS= read -r typed || exit 1 typed=$(printf %s "$typed" | tr -cd '0-9') if [ -z "$typed" ]; then printf '%s\n' "${DIM} type the six digits from the email.${Z}" continue fi out=$(ex "$jar" POST "/api/experience/verification/verification-code/verify" \ '{"identifier":{"type":"email","value":"'"$email"'"},"verificationId":"'"$vid"'","code":"'"$typed"'"}') vok=$(printf %s "$out" | json_str verificationId) if [ -n "$vok" ]; then break; fi c=$(printf %s "$out" | json_str code) case "$c" in verification_code.code_mismatch) printf '%s\n' "${MAG} that code is not right.${Z}" ;; verification_code.expired) die "that code has expired. Run tk again for a new one." ;; verification_code.exceed_max_try) die "too many tries. Run tk again for a new code." ;; *) fail_exp "$out" "the code could not be checked" ;; esac if [ "$tries" -ge 5 ]; then die "too many tries. Run tk again for a new code."; fi done out=$(ex "$jar" POST "/api/experience/identification" '{"verificationId":"'"$vok"'"}') c=$(printf %s "$out" | json_str code) if [ "$c" = "user.user_not_exist" ]; then printf '%s\n' "${DIM} no account for ${CYN}${email}${DIM} yet — making one.${Z}" ex "$jar" PUT "/api/experience/interaction-event" '{"interactionEvent":"Register"}' >/dev/null out=$(ex "$jar" POST "/api/experience/identification" '{"verificationId":"'"$vok"'"}') c=$(printf %s "$out" | json_str code) fi case "$c" in ''|user.identity_not_exist) : ;; user.suspended) die "this account is paused. Email bot@turnkey.tech and we'll sort it." ;; *) fail_exp "$out" "sign-in was refused" ;; esac out=$(ex "$jar" POST "/api/experience/submit" '') target=$(printf %s "$out" | json_str redirectTo) [ -n "$target" ] || fail_exp "$out" "sign-in did not finish" authcode=; hop=0 while [ "$hop" -lt 8 ]; do hop=$((hop + 1)) nxt=$(curl -sS --max-time 30 -c "$jar" -b "$jar" -o /dev/null -w '%{redirect_url}' "$target" || true) case "$nxt" in "$TK_REDIRECT"*) authcode=$(printf %s "$nxt" | sed -n 's/.*[?&]code=\([^&]*\).*/\1/p') back=$(printf %s "$nxt" | sed -n 's/.*[?&]state=\([^&]*\).*/\1/p') [ "$back" = "$state" ] || die "sign-in came back wrong (state mismatch). Nothing was saved; try again." break ;; '') die "sign-in stopped early. Try again." ;; *) target=$nxt ;; esac done [ -n "$authcode" ] || die "sign-in did not come back with a code. Try again." body="grant_type=authorization_code&client_id=$TK_CLIENT_ID&code=$authcode" body="$body&redirect_uri=$redir&code_verifier=$verifier" tok=$(post_form "$TK_ISSUER/oidc/token" "$body") at=$(printf %s "$tok" | json_str access_token) [ -n "$at" ] || fail_exp "$tok" "the sign-in token could not be collected" rt=$(printf %s "$tok" | json_str refresh_token) ttl=$(printf %s "$tok" | json_num expires_in); [ -n "$ttl" ] || ttl=3600 me=$(curl_auth "$at" "$TK_ISSUER/oidc/me") who=$(printf %s "$me" | json_str email); [ -n "$who" ] || who=$email sub=$(printf %s "$me" | json_str sub) session_save "$at" "$rt" "$(( $(now) + ttl - 60 ))" "$who" "$sub" rm -f "$jar" ME=$me; AT=$at printf '%s\n' "${GRN} signed in as ${HI}${who}${Z}" } # ── the token, kept fresh ───────────────────────────────────────────────── AT= ME= load_token() { # sets AT; 0 if there is a usable one AT= [ -f "$SESSION" ] || return 1 a=$(session_get access_token) || return 1 [ -n "$a" ] || return 1 exp=$(session_num expires_at 2>/dev/null) || exp=0 if [ "$(now)" -lt "${exp:-0}" ]; then AT=$a; return 0; fi r=$(session_get refresh_token) || return 1 [ -n "$r" ] || return 1 tok=$(post_form "$TK_ISSUER/oidc/token" \ "grant_type=refresh_token&client_id=$TK_CLIENT_ID&refresh_token=$r") a=$(printf %s "$tok" | json_str access_token) [ -n "$a" ] || return 1 nr=$(printf %s "$tok" | json_str refresh_token); [ -n "$nr" ] || nr=$r ttl=$(printf %s "$tok" | json_num expires_in); [ -n "$ttl" ] || ttl=3600 session_save "$a" "$nr" "$(( $(now) + ttl - 60 ))" "$(session_get email)" "$(session_get sub)" AT=$a } ensure_session() { need_curl if load_token; then return 0; fi if [ ! -t 0 ]; then die "not signed in, and there is no terminal to sign in from. Run tk from a terminal."; fi signin load_token || die "signed in, but the session could not be saved under $STATE_DIR." } load_me() { if [ -n "$ME" ]; then return 0; fi ensure_session mktmp printf 'header = "Authorization: Bearer %s"\n' "$AT" > "$TMP/auth.conf" chmod 600 "$TMP/auth.conf" # curl itself prints 000 when it never got a response, so no fallback is # needed here — and adding one appends a second 000 to the first. st=$(curl -sS --max-time 30 -K "$TMP/auth.conf" -o "$TMP/me.json" -w '%{http_code}' \ "$TK_ISSUER/oidc/me" 2>"$TMP/err" || true) rm -f "$TMP/auth.conf" # 401/403 is the identity service saying this session is finished — that is # the only answer that earns deleting it. Everything else (no network, a # 502 from the edge) is us failing to ask, and signing someone out because # their wifi dropped means a new email code for nothing. case "$st" in 200) ME=$(cat "$TMP/me.json") ;; 401|403) rm -f "$SESSION"; die "that session has ended. Run tk again to sign in." ;; ''|000) printf '%s\n' "${MAG}tk: could not reach ${TK_ISSUER}.${Z}" >&2 sed 's/^/ /' "$TMP/err" >&2 2>/dev/null || true printf '%s\n' " Your sign-in is still saved. Check the network and run tk again." >&2 exit 1 ;; *) die "TurnKey's sign-in service answered $st. Your sign-in is still saved; try again in a minute." ;; esac case "$ME" in *'"sub"'*) : ;; *) die "the sign-in service sent something tk could not read. Run 'tk update', then try again." ;; esac } # Entitled tools, one slug a line: a `plan:` role on an account you # belong to. Nothing is decided here; the SSO is the source of truth. plan_slugs() { # ":" lines on stdin -> tool slugs sed -n 's/^[^:]*:plan:\([a-z0-9-]\{1,\}\)$/\1/p' | sort -u } entitled() { load_me printf %s "$ME" | json_list organization_roles | plan_slugs } # ── the menu ────────────────────────────────────────────────────────────── raw_on() { [ -t 0 ] || return 0 SAVED_STTY=$(stty -g 2>/dev/null) || return 0 # -icanon, not raw: Ctrl-C still reaches the trap, and \n still prints # as a newline, because ONLCR is left alone. stty -icanon -echo min 1 time 0 2>/dev/null || return 0 RAW_ON=1 printf '\033[?25l\033[?1000h\033[?1006h' } raw_off() { printf '\033[?1006l\033[?1000l\033[?25h' if [ -n "$SAVED_STTY" ]; then stty "$SAVED_STTY" 2>/dev/null || stty sane 2>/dev/null; fi RAW_ON=0 } getbyte() { dd bs=1 count=1 2>/dev/null; } MENU_TOP=8 draw_menu() { # list selected who=$(session_get email 2>/dev/null || printf '') printf '\n'; wordmark; printf '\n' printf '%s\n' " ${DIM}signed in as ${CYN}${who}${Z}" printf '%s\n' " ${DIM}up down or click · enter opens · q quits${Z}" printf '\n' printf '%s\n' "$1" | awk -v sel="$2" '{ print NR "\t" $0 }' | while IFS="$(printf '\t')" read -r i slug; do [ -n "$slug" ] || continue name=$(tool_field "$slug" 2); [ -n "$name" ] || name=$slug desc=$(tool_field "$slug" 3) if [ "$i" = "$2" ]; then printf '%s\n' " ${GRN}>${Z} ${B}${HI}${name}${Z} ${DIM}${desc}${Z}" else printf '%s\n' " ${MID}${name}${Z} ${DIM}${desc}${Z}" fi done printf '\n' } no_tools() { who=$(session_get email 2>/dev/null || printf '') printf '\n'; wordmark; printf '\n' printf '%s\n' " ${DIM}signed in as ${CYN}${who}${Z}" printf '\n' printf '%s\n' " ${HI}This account does not pay for a tool yet.${Z}" printf '%s\n' " ${DIM}When it does, the tool is on this list the next time you run tk.${Z}" printf '%s\n' " ${DIM}Start one at ${CYN}https://turnkey.tech${Z}" printf '\n' } menu() { list=$1 n=$(printf '%s\n' "$list" | grep -c . || true) if [ "${n:-0}" -eq 0 ]; then no_tools; return 0; fi if [ ! -t 0 ] || [ ! -t 1 ]; then draw_menu "$list" 0; return 0; fi sel=1 raw_on while :; do printf '\033[H\033[2J' draw_menu "$list" "$sel" k=$(getbyte) case "$k" in q|Q) raw_off; printf '\n'; return 0 ;; j|J) sel=$((sel + 1)) ;; k|K) sel=$((sel - 1)) ;; [1-9]) sel=$k ;; ''|"$CR") raw_off; open_tool "$(nth "$list" "$sel")"; return 0 ;; "$ESC") a=$(getbyte); b=$(getbyte) if [ "$a" = "[" ]; then case "$b" in A) sel=$((sel - 1)) ;; B) sel=$((sel + 1)) ;; '<') seq=; c= while :; do c=$(getbyte) case "$c" in M|m|'') break ;; esac seq="$seq$c" done if [ "$c" = "M" ]; then btn=$(printf %s "$seq" | awk -F';' '{print $1}') row=$(printf %s "$seq" | awk -F';' '{print $3}') idx=$(( ${row:-0} - MENU_TOP + 1 )) if [ "${btn:-9}" = "0" ] && [ "$idx" -ge 1 ] && [ "$idx" -le "$n" ]; then raw_off; open_tool "$(nth "$list" "$idx")"; return 0 fi fi ;; esac fi ;; esac if [ "$sel" -lt 1 ]; then sel=$n; fi if [ "$sel" -gt "$n" ]; then sel=1; fi done } nth() { printf '%s\n' "$1" | sed -n "${2}p"; } open_tool() { slug=$1 [ -n "$slug" ] || return 0 name=$(tool_field "$slug" 2); [ -n "$name" ] || name=$slug cmd=$(tool_field "$slug" 5) url=$(tool_field "$slug" 4) printf '\n' if [ -n "$cmd" ] && have "$cmd"; then ensure_session TK_ACCESS_TOKEN=$AT; TK_TOOL=$slug export TK_ACCESS_TOKEN TK_TOOL TK_ISSUER exec "$cmd" fi printf '%s\n' " ${HI}${name} has no terminal front end yet.${Z}" printf '%s\n' " ${DIM}Today it runs in a browser, at ${CYN}${url}${Z}" printf '%s\n' " ${DIM}Your account pays for it, so it is yours — the terminal one is being built.${Z}" printf '%s\n' " ${DIM}When it lands, tk opens it here with no change on your side.${Z}" printf '\n' } # ── commands ────────────────────────────────────────────────────────────── cmd_whoami() { load_me printf '%s\n' "${HI}$(printf %s "$ME" | json_str email)${Z} ${DIM}($(printf %s "$ME" | json_str sub))${Z}" orgs=$(printf %s "$ME" | json_list organizations) if [ -z "$orgs" ]; then printf '%s\n' "${DIM}no account${Z}"; else for o in $orgs; do roles=$(printf %s "$ME" | json_list organization_roles | sed -n "s/^$o://p" | tr '\n' ' ') printf '%s\n' " ${MID}${o}${Z} ${DIM}${roles}${Z}" done fi exp=$(session_num expires_at 2>/dev/null || printf 0) printf '%s\n' "${DIM}this token is good for $(( (${exp:-0} - $(now)) / 60 )) more minutes${Z}" } cmd_signout() { if [ -f "$SESSION" ]; then r=$(session_get refresh_token 2>/dev/null || printf '') if [ -n "$r" ]; then post_form "$TK_ISSUER/oidc/token/revocation" \ "client_id=$TK_CLIENT_ID&token=$r&token_type_hint=refresh_token" >/dev/null 2>&1 || true fi rm -f "$SESSION" fi printf '%s\n' "${DIM}signed out.${Z}" } cmd_install() { need_curl dest=${TK_INSTALL_DIR:-$HOME/.local/bin} mkdir -p "$dest" t=$dest/.tk.$$ curl -fsSL --max-time 60 "$TK_DOWNLOAD" -o "$t" || { rm -f "$t"; die "could not download $TK_DOWNLOAD"; } if ! head -n 1 "$t" | grep -q '^#!/bin/sh'; then rm -f "$t"; die "what came back from $TK_DOWNLOAD is not tk."; fi chmod 755 "$t"; mv "$t" "$dest/tk" printf '\n'; wordmark; printf '\n' printf '%s\n' " ${GRN}installed${Z} ${DIM}${dest}/tk${Z}" case ":$PATH:" in *":$dest:"*) printf '%s\n' " ${HI}now run:${Z} tk" ;; *) printf '%s\n' " ${HI}now run:${Z} ${dest}/tk" printf '%s\n' " ${DIM}(${dest} is not on your PATH — add it in ~/.profile to just type 'tk')${Z}" ;; esac printf '\n' } usage() { cat < open one tool by name tk whoami who you are, and what your account pays for tk signin sign in again tk signout forget this session and revoke it tk install install (or re-install) tk to ~/.local/bin tk update the same: fetch the current tk tk version print the version Sign-in is a six-digit code, emailed to you. There are no passwords. The session is kept in $SESSION (0600) and expires. EOF } main() { # Piped into sh (curl … | sh) there is no terminal to read from, so the # only sensible thing is to install and say how to run it. if [ ! -t 0 ] && [ $# -eq 0 ]; then cmd_install; return 0; fi case "${1:-}" in ''|menu|tools) ensure_session # Not menu "$(entitled)": a command substitution that dies takes its # message to stderr and hands back an empty string, and an empty list # is the screen that says this account pays for nothing. A network # blip must not be able to say that. if ! list=$(entitled); then exit 1; fi menu "$list" ;; signin|login) rm -f "$SESSION"; signin ;; signout|logout) cmd_signout ;; whoami|who) cmd_whoami ;; install|update) cmd_install ;; version|--version|-v) printf 'tk %s\n' "$TK_VERSION" ;; help|--help|-h) usage ;; -*) die "unknown option $1. Try 'tk help'." ;; *) [ -n "$(tool_field "$1" 2)" ] || die "there is no TurnKey tool called '$1'. Run 'tk' to see yours." ensure_session if entitled | grep -qx "$1"; then open_tool "$1" else printf '%s\n' "${HI}Your account does not pay for $(tool_field "$1" 2) yet.${Z}" printf '%s\n' "${DIM}Run 'tk' to see what it does have, or start it at https://turnkey.tech${Z}" fi ;; esac } main "$@"