#!/bin/bash set -euo pipefail # bootstrap a macOS machine by delegating to the hosted bootbox entrypoint. # # examples: # # $ ./boot.sh --op-token "$OP_TOKEN" # $ ./boot.sh --op-token "$OP_TOKEN" --ssh-key vmruk4ny353aly6tbom7z3v2hy/id_botbox1 # $ ./boot.sh --op-token "$OP_TOKEN" --me v0.3.1 # $ ./boot.sh --op-token "$OP_TOKEN" --tanaab v0.2.0 # $ DEBUG=1 ./boot.sh --op-token "$OP_TOKEN" --yes # # option precedence: cli options override environment variables, which override defaults. # # run `./boot.sh --help` for more advanced usage. MACOS_OLDEST_SUPPORTED="26.0" REQUIRED_CURL_VERSION="7.41.0" BOOTBOX_URL="https://bootbox.tanaab.sh/bootbox.sh" DEFAULT_SSH_KEY="vmruk4ny353aly6tbom7z3v2hy/id_pirog,vmruk4ny353aly6tbom7z3v2hy/id_botbox1" DEFAULT_ME_SOURCE="ssh" DEFAULT_TANAAB_SOURCE="ssh" ME_REPO_SSH_URL="git@github.com:pirog/me.git" ME_REPO_RELEASE_BASE_URL="https://github.com/pirog/me/releases/download" ME_REPO_RELEASE_ARCHIVE_PREFIX="piroplugin" TANAAB_REPO_SSH_URL="git@github.com:tanaabased/canon.git" TANAAB_REPO_RELEASE_BASE_URL="https://github.com/tanaabased/canon/releases/download" TANAAB_REPO_RELEASE_ARCHIVE_PREFIX="tanaab" TANAAB_PLUGIN_RELATIVE_TARGET="../../../../../canon" abort() { printf "%serror%s: %s\n" "${tty_red-}" "${tty_reset-}" "$@" >&2 exit 1 } abort_multi() { while read -r line; do printf "%serror%s: %s\n" "${tty_red-}" "${tty_reset-}" "${line}" >&2 done <<< "$@" exit 1 } value_enabled() { case "${1:-}" in '' | 0 | false | FALSE | False | no | NO | No | off | OFF | Off) return 1 ;; *) return 0 ;; esac } source_value_disabled() { case "${1:-}" in 0 | false | FALSE | False | no | NO | No | off | OFF | Off | null | NULL | Null) return 0 ;; *) return 1 ;; esac } mask_secret_for_display() { local value="$1" local length="${#value}" local prefix_length="4" local suffix_length="4" local suffix_start if [[ -z "${value}" ]]; then printf "none" return 0 fi if [[ "${length}" -le 4 ]]; then printf "****" return 0 fi if [[ "${length}" -le 8 ]]; then prefix_length="2" suffix_length="2" fi suffix_start=$((length - suffix_length)) printf "%s...%s" "${value:0:${prefix_length}}" "${value:${suffix_start}:${suffix_length}}" } trim_whitespace() { local value="$1" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" printf "%s" "${value}" } append_array_value() { local array_name="$1" local value local quoted value="$(trim_whitespace "$2")" if [[ -n "${value}" ]]; then printf -v quoted '%q' "${value}" eval "${array_name}+=(${quoted})" fi } append_csv_to_array() { local array_name="$1" local old_ifs="${IFS}" local entry local -a values=() if [[ -z "${2}" ]]; then return 0 fi IFS=',' read -r -a values <<< "${2}" IFS="${old_ifs}" if [[ "${#values[@]}" -eq 0 ]]; then return 0 fi for entry in "${values[@]}"; do append_array_value "${array_name}" "${entry}" done } array_join() { local delimiter="$1" local array_name="$2" local item local first="1" local value_count="0" local -a values=() eval "value_count=\${#${array_name}[@]}" if [[ "${value_count}" -eq 0 ]]; then return 0 fi eval "values=(\"\${${array_name}[@]}\")" for item in "${values[@]}"; do if [[ "${first}" == "1" ]]; then printf "%s" "${item}" first="0" else printf "%s%s" "${delimiter}" "${item}" fi done } chomp() { printf "%s" "${1/"$'\n'"/}" } shell_join() { local arg printf "%s" "${1:-}" if [[ $# -eq 0 ]]; then return 0 fi shift for arg in "$@"; do printf " " printf "%s" "${arg// /\ }" done } # shellcheck disable=SC2292 if [ -z "${BASH_VERSION:-}" ]; then abort "bash is required to interpret this script." fi if [[ -n "${CI-}" && -n "${INTERACTIVE-}" ]]; then abort "cannot run force-interactive mode in CI." fi # shellcheck disable=SC2016 if [[ -n "${INTERACTIVE-}" && -n "${NONINTERACTIVE-}" ]]; then abort 'both `$INTERACTIVE` and `$NONINTERACTIVE` are set. please unset at least one variable and try again.' fi if [[ -n "${POSIXLY_CORRECT+1}" ]]; then abort 'bash must not run in POSIX mode. please unset POSIXLY_CORRECT and try again.' fi if [[ -t 1 ]]; then tty_escape() { printf "\033[%sm" "$1"; } else tty_escape() { :; } fi tty_mkbold() { tty_escape "1;$1"; } tty_mkdim() { tty_escape "2;$1"; } tty_bold="$(tty_mkbold 39)" tty_dim="$(tty_mkdim 39)" # shellcheck disable=SC2034 # keep the shared palette available even when a given change doesn't use green directly tty_green="$(tty_escape 32)" tty_magenta="$(tty_escape 35)" tty_red="$(tty_mkbold 31)" tty_reset="$(tty_escape 0)" tty_underline="$(tty_escape "4;39")" tty_yellow="$(tty_escape 33)" tty_tp="$(tty_escape '38;2;0;200;138')" # #00c88a # shellcheck disable=SC2034 # reserved for future plan/action styling tty_ts="$(tty_escape '38;2;219;39;119')" # #db2777 SCRIPT_NAME="${0##*/}" # Keep a single top-level assignment so release automation can stamp the entrypoint in place. SCRIPT_VERSION="v1.0.0-beta.1" DEBUG="${PIROME_DEBUG:-${DEBUG:-${RUNNER_DEBUG:-}}}" FORCE="${PIROME_FORCE:-}" OP_TOKEN="${PIROME_OP_TOKEN:-${OP_SERVICE_ACCOUNT_TOKEN:-}}" SSH_KEYS_CSV="${PIROME_SSH_KEY:-${DEFAULT_SSH_KEY}}" ME_SOURCE="${PIROME_ME:-${DEFAULT_ME_SOURCE}}" TANAAB_SOURCE="${PIROME_TANAAB:-${DEFAULT_TANAAB_SOURCE}}" declare -a ORIGINAL_ARGS=("$@") declare -a SSH_KEYS=() declare -a SSH_KEYS_TO_INSTALL=() declare -a SSH_KEYS_TO_OVERWRITE=() declare -a SSH_KEYS_TO_SKIP=() declare -a ME_APPLY_DOTPKGS=() declare -a PLANNED_ACTIONS=() BOOT_TMPDIR="" BOOTBOX_SCRIPT_PATH="" CORE_NEEDS_REMEDIATION="0" CURL="" DETECTED_ARCH="" DETECTED_OS="" ARCH="" OS="" ME_SOURCE_KIND="" ME_SOURCE_LOCAL_PATH="" ME_SOURCE_VERSION_TAG="" ME_TARGET_PATH="" TANAAB_SOURCE_KIND="" TANAAB_SOURCE_LOCAL_PATH="" TANAAB_SOURCE_VERSION_TAG="" TANAAB_TARGET_PATH="" ME_APPLY_BREWFILE="" if [[ -n "${PIROME_SSH_KEYS:-}" ]]; then SSH_KEYS_CSV="${SSH_KEYS_CSV}${SSH_KEYS_CSV:+,}${PIROME_SSH_KEYS}" fi append_csv_to_array SSH_KEYS "${SSH_KEYS_CSV}" if [[ "${#ORIGINAL_ARGS[@]}" -gt 0 ]]; then for arg in "${ORIGINAL_ARGS[@]}"; do case "${arg}" in --ssh-key | --ssh-key=* | --ssh-keys | --ssh-keys=*) SSH_KEYS=() break ;; esac done fi debug_enabled() { value_enabled "${DEBUG:-}" } force_enabled() { value_enabled "${FORCE:-}" } debug() { if debug_enabled; then printf "${tty_dim}debug${tty_reset} %s\n" "$(shell_join "$@")" >&2 fi } log() { printf "%s\n" "$(shell_join "$@")" } warn() { printf "${tty_yellow}warn${tty_reset}: %s\n" "$(chomp "$@")" >&2 } show_version() { printf "%s\n" "${SCRIPT_VERSION}" exit 0 } is_semver_value() { [[ "${1:-}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]] } normalize_release_tag() { if [[ "${1}" == v* ]]; then printf "%s" "${1}" else printf "v%s" "${1}" fi } normalize_repo_source_value() { if is_semver_value "${1}"; then normalize_release_tag "${1}" else printf "%s" "${1}" fi } usage() { local debug_display="off" local force_display="off" local ssh_keys_display="none" local op_token_display="none" local me_display="none" local tanaab_display="none" if debug_enabled; then debug_display="on" fi if force_enabled; then force_display="on" fi ssh_keys_display="$(array_join "," SSH_KEYS)" ssh_keys_display="${ssh_keys_display:-none}" if [[ -n "${OP_TOKEN:-}" ]]; then op_token_display="$(mask_secret_for_display "${OP_TOKEN}")" fi me_display="$(normalize_repo_source_value "${ME_SOURCE}")" tanaab_display="$(normalize_repo_source_value "${TANAAB_SOURCE}")" cat </dev/null)" curl_name_and_version="${curl_version_output%% (*}" version_compare "$(major_minor "${curl_name_and_version##* }")" "$(major_minor "${REQUIRED_CURL_VERSION}")" } display_home_path() { local path="$1" if [[ "${path}" == "${HOME}" ]]; then printf "~" return 0 fi if [[ "${path}" == "${HOME}/"* ]]; then printf "%s/%s" "~" "${path#"${HOME}"/}" return 0 fi printf "%s" "${path}" } find_git_repo_root() { local path="$1" local parent while :; do if [[ -d "${path}/.git" ]]; then printf "%s" "${path}" return 0 fi if [[ -f "${path}/HEAD" && -d "${path}/objects" && -d "${path}/refs" ]]; then printf "%s" "${path}" return 0 fi parent="$(dirname "${path}")" if [[ "${parent}" == "${path}" ]]; then return 1 fi path="${parent}" done } resolve_local_repo_source_path() { local source_path="$1" local absolute_path local repo_root if ! absolute_path="$(cd "${source_path}" 2>/dev/null && pwd -P)"; then return 1 fi if ! repo_root="$(find_git_repo_root "${absolute_path}")"; then return 1 fi printf "%s" "${repo_root}" } resolve_repo_source_kind() { local source_value="$1" local allow_disable="${2:-0}" if [[ "${allow_disable}" == "1" ]] && source_value_disabled "${source_value}"; then printf "disabled" elif [[ "${source_value}" == "ssh" ]]; then printf "ssh" elif is_semver_value "${source_value}"; then printf "version" else printf "local" fi } me_target_display() { display_home_path "${ME_TARGET_PATH}" } tanaab_target_display() { display_home_path "${TANAAB_TARGET_PATH}" } me_apply_brewfile_display() { display_home_path "${ME_APPLY_BREWFILE}" } prepare_repo_source() { local source_value="$1" local target_path="$2" local repo_label="$3" local source_kind_var="$4" local source_local_path_var="$5" local source_version_tag_var="$6" local allow_disable="${7:-0}" local source_kind="" local source_local_path="" local source_version_tag="" source_kind="$(resolve_repo_source_kind "${source_value}" "${allow_disable}")" case "${source_kind}" in disabled) ;; ssh) ;; version) source_version_tag="$(normalize_release_tag "${source_value}")" ;; local) if ! source_local_path="$(resolve_local_repo_source_path "${source_value}")"; then abort "local ${repo_label} source ${tty_ts}${source_value}${tty_reset} must resolve to a local git repo." fi if [[ "${source_local_path}" == "${target_path}" ]]; then abort "local ${repo_label} source ${tty_ts}${source_local_path}${tty_reset} cannot be the same as target ${tty_ts}$(display_home_path "${target_path}")${tty_reset}." fi ;; *) abort "unsupported internal ${repo_label} source kind ${tty_bold}${source_kind}${tty_reset}." ;; esac printf -v "${source_kind_var}" "%s" "${source_kind}" printf -v "${source_local_path_var}" "%s" "${source_local_path}" printf -v "${source_version_tag_var}" "%s" "${source_version_tag}" } prepare_me_source() { ME_TARGET_PATH="${HOME}/tanaab/me" prepare_repo_source \ "${ME_SOURCE}" \ "${ME_TARGET_PATH}" \ "me" \ "ME_SOURCE_KIND" \ "ME_SOURCE_LOCAL_PATH" \ "ME_SOURCE_VERSION_TAG" } prepare_tanaab_source() { TANAAB_TARGET_PATH="${HOME}/tanaab/canon" prepare_repo_source \ "${TANAAB_SOURCE}" \ "${TANAAB_TARGET_PATH}" \ "tanaab" \ "TANAAB_SOURCE_KIND" \ "TANAAB_SOURCE_LOCAL_PATH" \ "TANAAB_SOURCE_VERSION_TAG" \ "1" } tanaab_enabled() { [[ "${TANAAB_SOURCE_KIND}" != "disabled" ]] } tanaab_plugin_checkout_path() { printf "%s/dotfiles/ai/.codex/plugins/tanaab" "${ME_TARGET_PATH}" } tanaab_plugin_checkout_display() { display_home_path "$(tanaab_plugin_checkout_path)" } prepare_tanaab_plugin_link() { local ai_dotpkg_path="${ME_TARGET_PATH}/dotfiles/ai" local plugin_checkout_path local plugin_parent_path plugin_checkout_path="$(tanaab_plugin_checkout_path)" if ! tanaab_enabled; then if [[ -L "${plugin_checkout_path}" || -e "${plugin_checkout_path}" ]]; then execute rm -rf "${plugin_checkout_path}" fi return 0 fi if [[ ! -d "${ai_dotpkg_path}" ]]; then abort "me checkout at ${tty_ts}$(me_target_display)${tty_reset} is missing required ${tty_ts}ai${tty_reset} dotpkg needed for the tanaab plugin link." fi plugin_parent_path="$(dirname "${plugin_checkout_path}")" execute mkdir -p "${plugin_parent_path}" if [[ -L "${plugin_checkout_path}" || -e "${plugin_checkout_path}" ]]; then execute rm -rf "${plugin_checkout_path}" fi execute ln -s "${TANAAB_PLUGIN_RELATIVE_TARGET}" "${plugin_checkout_path}" } discover_me_apply_payload() { local dotfiles_root="${ME_TARGET_PATH}/dotfiles" local dotpkg ME_APPLY_BREWFILE="${ME_TARGET_PATH}/Brewfile" ME_APPLY_DOTPKGS=() if [[ ! -f "${ME_APPLY_BREWFILE}" ]]; then abort "me checkout at ${tty_ts}$(me_target_display)${tty_reset} is missing required brewfile ${tty_ts}$(me_apply_brewfile_display)${tty_reset}." fi if [[ ! -d "${dotfiles_root}" ]]; then abort "me checkout at ${tty_ts}$(me_target_display)${tty_reset} is missing required dotfiles directory ${tty_ts}$(display_home_path "${dotfiles_root}")${tty_reset}." fi while IFS= read -r dotpkg; do append_array_value ME_APPLY_DOTPKGS "${dotpkg}" done < <(find "${dotfiles_root}" -mindepth 1 -maxdepth 1 -type d | LC_ALL=C sort) if [[ "${#ME_APPLY_DOTPKGS[@]}" -eq 0 ]]; then abort "me checkout at ${tty_ts}$(me_target_display)${tty_reset} must contain at least one top-level dotpkg under ${tty_ts}$(display_home_path "${dotfiles_root}")${tty_reset}." fi } build_git_ssh_command_from_ssh_keys() { local repo_name="${1:-repo}" local ssh_key local key_path local arg local command_string="" local -a ssh_command=(ssh) local -a existing_key_paths=() for ssh_key in "${SSH_KEYS[@]}"; do key_path="$(ssh_key_destination_path "${ssh_key}")" if [[ -f "${key_path}" ]]; then existing_key_paths+=("${key_path}") fi done if [[ "${#existing_key_paths[@]}" -eq 0 ]]; then abort "cannot clone ${tty_ts}${repo_name}${tty_reset} via ssh because no installed ssh key paths were found." fi for key_path in "${existing_key_paths[@]}"; do ssh_command+=(-i "${key_path}") done ssh_command+=(-o IdentitiesOnly=yes) for arg in "${ssh_command[@]}"; do printf -v command_string '%s%q ' "${command_string}" "${arg}" done printf "%s" "${command_string% }" } repo_release_archive_url() { local release_base_url="$1" local archive_prefix="$2" local tag="$3" printf "%s/%s/%s-%s.tar.gz" "${release_base_url}" "${tag}" "${archive_prefix}" "${tag}" } repo_prepare_target() { local target="$1" if [[ -e "${target}" ]]; then if ! force_enabled; then return 1 fi execute rm -rf "${target}" fi execute mkdir -p "$(dirname "${target}")" return 0 } fetch_repo_source_to_target() { local repo_name="$1" local source_value="$2" local target="$3" local ssh_url="$4" local release_base_url="$5" local archive_prefix="$6" local source_kind="$7" local git_ssh_command local archive_tag local archive_url local archive_path if ! repo_prepare_target "${target}"; then warn "${tty_tp}skipping${tty_reset} ${tty_ts}${repo_name}${tty_reset} because ${tty_ts}$(display_home_path "${target}")${tty_reset} already exists and ${tty_bold}--force${tty_reset} is not set." return 0 fi case "${source_kind}" in ssh) git_ssh_command="$(build_git_ssh_command_from_ssh_keys "${repo_name}")" log "${tty_tp}cloning${tty_reset} ${tty_ts}${repo_name}${tty_reset} via ssh to ${tty_ts}$(display_home_path "${target}")${tty_reset}" execute env GIT_SSH_COMMAND="${git_ssh_command}" git clone "${ssh_url}" "${target}" ;; local) log "${tty_tp}cloning${tty_reset} ${tty_ts}${repo_name}${tty_reset} from local repo ${tty_ts}${source_value}${tty_reset} to ${tty_ts}$(display_home_path "${target}")${tty_reset}" execute git clone "${source_value}" "${target}" ;; version) archive_tag="$(normalize_release_tag "${source_value}")" archive_url="$(repo_release_archive_url "${release_base_url}" "${archive_prefix}" "${archive_tag}")" archive_path="${BOOT_TMPDIR}/${repo_name}-${archive_tag}.tar.gz" log "${tty_tp}extracting${tty_reset} ${tty_ts}${repo_name}${tty_reset} release ${tty_ts}${archive_tag}${tty_reset} to ${tty_ts}$(display_home_path "${target}")${tty_reset}" execute mkdir -p "${target}" execute "${CURL}" -fsSL "${archive_url}" -o "${archive_path}" execute tar -xzf "${archive_path}" -C "${target}" ;; *) abort "unsupported internal repo source kind ${tty_bold}${source_kind}${tty_reset}." ;; esac } plan_repo_fetch() { local repo_name="$1" local target_path="$2" local source_kind="$3" local source_local_path="$4" local source_version_tag="$5" local target_display target_display="$(display_home_path "${target_path}")" if [[ -e "${target_path}" ]]; then if force_enabled; then plan_action "${tty_tp}replace${tty_reset} existing ${tty_ts}${repo_name}${tty_reset} checkout at ${tty_ts}${target_display}${tty_reset} because ${tty_bold}--force${tty_reset} is set" else plan_action "${tty_tp}skip${tty_reset} fetching ${tty_ts}${repo_name}${tty_reset} because ${tty_ts}${target_display}${tty_reset} already exists and ${tty_bold}--force${tty_reset} is not set" return 0 fi fi case "${source_kind}" in ssh) plan_action "${tty_tp}clone${tty_reset} ${tty_ts}${repo_name}${tty_reset} via ssh to ${tty_ts}${target_display}${tty_reset}" ;; local) plan_action "${tty_tp}clone${tty_reset} ${tty_ts}${repo_name}${tty_reset} from local repo ${tty_ts}${source_local_path}${tty_reset} to ${tty_ts}${target_display}${tty_reset}" ;; version) plan_action "${tty_tp}extract${tty_reset} ${tty_ts}${repo_name}${tty_reset} release ${tty_ts}${source_version_tag}${tty_reset} to ${tty_ts}${target_display}${tty_reset}" ;; esac } plan_me_fetch() { plan_repo_fetch \ "me" \ "${ME_TARGET_PATH}" \ "${ME_SOURCE_KIND}" \ "${ME_SOURCE_LOCAL_PATH}" \ "${ME_SOURCE_VERSION_TAG}" } plan_tanaab_fetch() { plan_repo_fetch \ "tanaab" \ "${TANAAB_TARGET_PATH}" \ "${TANAAB_SOURCE_KIND}" \ "${TANAAB_SOURCE_LOCAL_PATH}" \ "${TANAAB_SOURCE_VERSION_TAG}" } plan_tanaab_plugin_link() { plan_action "${tty_tp}ensure${tty_reset} relative ${tty_ts}tanaab${tty_reset} plugin link at ${tty_ts}$(tanaab_plugin_checkout_display)${tty_reset} points to ${tty_ts}$(tanaab_target_display)${tty_reset}" } plan_me_apply() { plan_action "${tty_tp}run${tty_reset} ${tty_ts}bootbox${tty_reset} against the ${tty_ts}me${tty_reset} checkout at ${tty_ts}$(me_target_display)${tty_reset} using its ${tty_ts}Brewfile${tty_reset} and dotpkgs on ${tty_ts}~${tty_reset}" } run_me_fetch() { fetch_repo_source_to_target \ "me" \ "${ME_SOURCE_LOCAL_PATH:-${ME_SOURCE_VERSION_TAG:-${ME_SOURCE}}}" \ "${ME_TARGET_PATH}" \ "${ME_REPO_SSH_URL}" \ "${ME_REPO_RELEASE_BASE_URL}" \ "${ME_REPO_RELEASE_ARCHIVE_PREFIX}" \ "${ME_SOURCE_KIND}" } run_tanaab_fetch() { fetch_repo_source_to_target \ "tanaab" \ "${TANAAB_SOURCE_LOCAL_PATH:-${TANAAB_SOURCE_VERSION_TAG:-${TANAAB_SOURCE}}}" \ "${TANAAB_TARGET_PATH}" \ "${TANAAB_REPO_SSH_URL}" \ "${TANAAB_REPO_RELEASE_BASE_URL}" \ "${TANAAB_REPO_RELEASE_ARCHIVE_PREFIX}" \ "${TANAAB_SOURCE_KIND}" } run_bootbox_for_me_apply() { local dotpkg local -a bootbox_args=(--brewfile "${ME_APPLY_BREWFILE}") for dotpkg in "${ME_APPLY_DOTPKGS[@]}"; do bootbox_args+=(--dotpkg "${dotpkg}") done bootbox_run_or_abort me "bootbox failed while applying me checkout ${tty_ts}$(me_target_display)${tty_reset}." "${bootbox_args[@]}" } plan_action() { PLANNED_ACTIONS+=("$1") } ssh_key_spec_base() { printf "%s" "${1%%:*}" } ssh_key_spec_filename_override() { if [[ "$1" == *:* ]]; then printf "%s" "${1#*:}" fi } ssh_key_spec_item() { local spec_base spec_base="$(ssh_key_spec_base "$1")" printf "%s" "${spec_base##*/}" } ssh_key_filename() { local filename_override filename_override="$(ssh_key_spec_filename_override "$1")" if [[ -n "${filename_override}" ]]; then printf "%s" "${filename_override}" return 0 fi ssh_key_spec_item "$1" } ssh_key_target_root() { printf "%s" "${PIROME_TARGET:-${HOME}}" } ssh_key_destination_path() { printf "%s/.ssh/%s" "$(ssh_key_target_root)" "$(ssh_key_filename "$1")" } describe_ssh_key_specs() { local first="1" local ssh_key local destination_path if [[ $# -eq 0 ]]; then return 0 fi for ssh_key in "$@"; do if [[ "${first}" == "1" ]]; then first="0" else printf ", " fi destination_path="$(ssh_key_destination_path "${ssh_key}")" printf "%s ${tty_dim}->${tty_reset} ${tty_ts}%s${tty_reset}" "${ssh_key}" "${destination_path}" done } collect_ssh_key_actions() { local ssh_key local destination_path SSH_KEYS_TO_INSTALL=() SSH_KEYS_TO_OVERWRITE=() SSH_KEYS_TO_SKIP=() if [[ "${#SSH_KEYS[@]}" -gt 0 ]]; then for ssh_key in "${SSH_KEYS[@]}"; do destination_path="$(ssh_key_destination_path "${ssh_key}")" if [[ -e "${destination_path}" ]]; then if force_enabled; then SSH_KEYS_TO_OVERWRITE+=("${ssh_key}") else SSH_KEYS_TO_SKIP+=("${ssh_key}") fi else SSH_KEYS_TO_INSTALL+=("${ssh_key}") fi done fi } have_planned_actions() { [[ "${#PLANNED_ACTIONS[@]}" -gt 0 ]] } show_planned_actions() { if ! have_planned_actions; then return 0 fi log "${tty_bold}this script is about to:${tty_reset}" log local action for action in "${PLANNED_ACTIONS[@]}"; do log " - ${action}" done } getc() { local save_state save_state="$(/bin/stty -g)" /bin/stty raw -echo IFS='' read -r -n 1 -d '' "$@" /bin/stty "${save_state}" } wait_for_user() { local c trap 'stty sane; tput sgr0; echo; exit 1' SIGINT echo echo "press ${tty_bold}RETURN${tty_reset}/${tty_bold}ENTER${tty_reset} to continue or any other key to abort:" getc c if ! [[ "${c}" == $'\r' || "${c}" == $'\n' ]]; then exit 1 fi } execute() { debug "${tty_tp}running${tty_reset}" "$@" if ! "$@"; then abort "$(printf "failed during: %s" "$(shell_join "$@")")" fi } cleanup() { if [[ -n "${BOOT_TMPDIR:-}" && -d "${BOOT_TMPDIR}" ]]; then rm -rf "${BOOT_TMPDIR}" fi } validate_inputs() { if [[ -z "${OP_TOKEN:-}" ]]; then abort_multi "$(cat <