#!/usr/bin/env bash # Install twg. # bash <(curl -fsSL https://teamwork-graph.atlassian.com/cli/install) # TWG_VERSION selects a release for scripted installs. set -euo pipefail PUBLIC_CDN_BASE_URL="https://teamwork-graph.atlassian.com/cli" if [[ -n "${TWG_INSTALL_BASE_URL:-}" ]]; then PUBLIC_CDN_BASE_URL="${TWG_INSTALL_BASE_URL}" fi AGENT_INSTRUCTIONS_URL="${PUBLIC_CDN_BASE_URL}/AGENTS.md" GASV3_ENDPOINT="https://as.atlassian.com/api/v1/batch" DEFAULT_VERSION="1.2.5" # Options SKIP_DOWNLOAD=false SKIP_LOGIN=false SKIP_SKILLS=false WAIT_FOR_PID="" INSTALL_DIR_OVERRIDE="" REQUESTED_VERSION="${TWG_VERSION:-}" ASSUME_YES=false INSTALLER_USE_PAT="${TWG_INSTALLER_PAT:-0}" PLUGIN_AGENT="" print_usage() { cat <<'EOF' Usage: bash install.sh [options] Options: --version Install a specific version. --install-dir Install to a specific directory. --skip-download Skip binary download. --skip-login Skip login after installation. --skip-skills Skip skill installation. --plugin Attribute installation to an agent. --wait-for-pid Wait for a process before replacing the binary. --pat Use a personal access token when supported. -h, --help Show this help. EOF } validate_plugin_agent() { case "$1" in claude-code|codex|copilot|cursor|devin|gemini|hermes|pi|qoder) ;; *) echo "Warning: --plugin '$1' is not a recognized agent; continuing anyway. Known agents: claude-code, codex, copilot, cursor, devin, gemini, hermes, pi, qoder." >&2 ;; esac } normalize_plugin_agent() { case "$1" in claude-code|codex|copilot|cursor|devin|gemini|hermes|pi|qoder) printf '%s\n' "$1" ;; *) printf 'unknown\n' ;; esac } parse_arguments() { while [[ $# -gt 0 ]]; do case "$1" in -h|--help) print_usage exit 0 ;; --skip-download) SKIP_DOWNLOAD=true shift ;; -y|--yes) ASSUME_YES=true shift ;; --skip-login) SKIP_LOGIN=true shift ;; --skip-skills) SKIP_SKILLS=true shift ;; --pat) INSTALLER_USE_PAT=1 shift ;; --plugin) if [[ $# -lt 2 ]]; then echo "Error: --plugin requires a value." >&2 exit 2 fi validate_plugin_agent "$2" PLUGIN_AGENT="$2" shift 2 ;; --plugin=*) PLUGIN_AGENT="${1#--plugin=}" validate_plugin_agent "${PLUGIN_AGENT}" shift ;; --wait-for-pid) if [[ $# -lt 2 ]]; then echo "Error: --wait-for-pid requires a value." >&2 exit 2 fi WAIT_FOR_PID="$2" shift 2 ;; --wait-for-pid=*) WAIT_FOR_PID="${1#--wait-for-pid=}" shift ;; --install-dir) if [[ $# -lt 2 ]]; then echo "Error: --install-dir requires a value." >&2 exit 2 fi INSTALL_DIR_OVERRIDE="$2" shift 2 ;; --install-dir=*) INSTALL_DIR_OVERRIDE="${1#--install-dir=}" if [[ -z "${INSTALL_DIR_OVERRIDE}" ]]; then echo "Error: --install-dir requires a value." >&2 exit 2 fi shift ;; --version) if [[ $# -lt 2 ]]; then echo "Error: --version requires a value." >&2 exit 2 fi REQUESTED_VERSION="$2" shift 2 ;; --version=*) REQUESTED_VERSION="${1#--version=}" shift ;; *) echo "Error: unknown argument '$1'." >&2 exit 2 ;; esac done } parse_arguments "$@" if [[ -n "${PLUGIN_AGENT}" ]]; then PLUGIN_AGENT="$(normalize_plugin_agent "${PLUGIN_AGENT}")" fi # Platform detect_platform() { OS="$(uname -s)" ARCH="$(uname -m)" case "${OS}" in Darwin) PLATFORM="darwin"; ANALYTICS_PLATFORM="mac" ;; Linux) PLATFORM="linux"; ANALYTICS_PLATFORM="linux" ;; *) echo "Error: unsupported OS '${OS}'. Only macOS and Linux are supported." >&2 exit 1 ;; esac case "${ARCH}" in arm64|aarch64) ARCH_SUFFIX="arm64" ;; x86_64) ARCH_SUFFIX="x64" ;; *) echo "Error: unsupported architecture '${ARCH}'. Only arm64 and x86_64 are supported." >&2 exit 1 ;; esac BINARY_NAME="twg-${PLATFORM}-${ARCH_SUFFIX}" } detect_platform # Release configure_release() { if [[ -n "${REQUESTED_VERSION}" ]]; then RELEASE_SELECTOR="version ${REQUESTED_VERSION}" else REQUESTED_VERSION="${DEFAULT_VERSION}" RELEASE_SELECTOR="latest (v${DEFAULT_VERSION})" fi INSTALLED_VERSION_LABEL="${REQUESTED_VERSION}" DOWNLOAD_BINARY_NAME="${BINARY_NAME}-v${REQUESTED_VERSION}" DOWNLOAD_BASE_URL="${PUBLIC_CDN_BASE_URL}" CHECKSUMS_URL="${PUBLIC_CDN_BASE_URL}/SHA256SUMS-v${REQUESTED_VERSION}" FINALIZE_SOURCE="direct-public-installer" INSTALL_DIR="${INSTALL_DIR_OVERRIDE:-${HOME}/.local/bin}" echo "Agent install instructions: ${AGENT_INSTRUCTIONS_URL}" } configure_release TWG_PATH="${INSTALL_DIR}/twg" TWG_BIN_PATH="${INSTALL_DIR}/twg-bin" detect_shell_profile() { case "${SHELL:-}" in */zsh) echo "${HOME}/.zshrc" ;; */bash) if [[ -f "${HOME}/.bash_profile" ]]; then echo "${HOME}/.bash_profile" else echo "${HOME}/.bashrc" fi ;; *) echo "${HOME}/.zshrc" ;; esac } append_install_dir_to_path_profile() { SHELL_PROFILE="$(detect_shell_profile)" PATH_PROFILE_UPDATED=false PATH_PROFILE_SKIPPED=false local confirm_path="" if [[ -f "${SHELL_PROFILE}" ]] && grep -F "export PATH=\"${INSTALL_DIR}:\$PATH\"" "${SHELL_PROFILE}" >/dev/null 2>&1; then PATH_PROFILE_UPDATED=true return fi if { printf '\n' printf '# Added by Teamwork Graph CLI installer\n' printf 'export PATH="%s:$PATH"\n' "${INSTALL_DIR}" } >> "${SHELL_PROFILE}"; then PATH_PROFILE_UPDATED=true else PATH_PROFILE_SKIPPED=true echo "Warning: could not update ${SHELL_PROFILE}; continuing without a persistent PATH update." >&2 fi } validate_prerequisites() { if [[ "${SKIP_DOWNLOAD}" != "true" ]] && ! command -v curl &>/dev/null; then echo "Error: 'curl' not found on PATH." >&2 exit 127 fi } print_install_plan() { echo "Detected platform: ${PLATFORM}-${ARCH_SUFFIX}" echo "Requested release: ${RELEASE_SELECTOR}" echo "Will install twg to: ${TWG_PATH}" echo "" if [[ "${SKIP_DOWNLOAD}" == "true" ]]; then echo "Will reuse existing twg at: ${TWG_PATH}" echo "" fi } validate_prerequisites print_install_plan # Install binary mkdir -p "${INSTALL_DIR}" if [[ -n "${WAIT_FOR_PID}" ]]; then while kill -0 "${WAIT_FOR_PID}" 2>/dev/null; do sleep 1 done fi TWG_BACKUP="" TWG_LEGACY_COPIED=false TWG_INSTALLED_REPLACEMENT=false CHECKSUMS_PATH="" TWG_TMP="" INSTALL_METADATA_PATH="" INSTALL_METADATA_BACKUP="" INSTALL_METADATA_ROLLBACK_ACTION="none" run_without_bun_env() ( for name in "${!BUN_@}"; do unset "${name}" done "$@" ) resolve_installed_twg_version() { local version if [[ ! -x "${TWG_PATH}" ]]; then echo "unknown" return fi version="$(run_without_bun_env "${TWG_PATH}" -v 2>/dev/null | head -n 1)" || version="" case "${version}" in ""|*[!A-Za-z0-9._+-]*) echo "unknown" ;; *) echo "${version}" ;; esac } report_plugin_install() { local stage="$1" local state="$2" local result="$3" local error_code="${4:-}" if [[ "${DO_NOT_TRACK:-0}" == "1" ]]; then return fi # Every interpolated value is constrained to a closed set so the payload # cannot contain arbitrary installer output or local/user data. case "${stage}" in download|auth) ;; *) return ;; esac case "${state}" in started|finished) ;; *) return ;; esac case "${result}" in started|success|error) ;; *) return ;; esac case "${error_code}" in ""|download_failed|login_failed|setup_failed|interrupted) ;; *) return ;; esac local now error_attribute payload metrics_version agent now="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" || return metrics_version="$(resolve_installed_twg_version)" agent="${PLUGIN_AGENT:-unknown}" case "${agent}" in claude-code|codex|copilot|cursor|devin|gemini|hermes|pi|qoder) ;; *) agent="unknown" ;; esac error_attribute="" if [[ -n "${error_code}" ]]; then error_attribute=",\"error_code\":\"${error_code}\"" fi payload="$( cat </dev/null 2>&1 || true } # Dual-emit the binary acquisition step into the unified onboarding funnel. # Keep pluginInstall until downstream dashboards have migrated. DOWNLOAD_SETUP_STEP_REPORTED=false #!/usr/bin/env bash resolve_install_metadata_path() { local config_root if [[ -n "${TWG_CONFIG_DIR:-}" ]]; then config_root="${TWG_CONFIG_DIR}"; elif [[ -n "${XDG_CONFIG_HOME:-}" ]]; then config_root="${XDG_CONFIG_HOME}/twg"; else config_root="${HOME}/.config/twg"; fi printf '%s/install.json\n' "${config_root}" } twg_anonymous_machine_id() { local raw="" seed="" seed_path metadata_path metadata temporary_path machine_id_path if [[ "$(uname -s)" == "Darwin" ]]; then raw="$(ioreg -rd1 -c IOPlatformExpertDevice 2>/dev/null | awk -F'"' '/IOPlatformUUID/{print $4; exit}')" else for machine_id_path in /etc/machine-id /var/lib/dbus/machine-id; do raw="$(cat "${machine_id_path}" 2>/dev/null || true)" [[ -n "${raw}" ]] && break done fi if [[ -z "${raw}" ]]; then metadata_path="$(resolve_install_metadata_path)" seed_path="$(dirname "${metadata_path}")/anonymous-machine-seed" [[ -r "${seed_path}" ]] && seed="$(head -n 1 "${seed_path}" | tr -d '[:space:]')" if [[ ! "${seed}" =~ ^[0-9A-Fa-f-]{36}$ && -r "${metadata_path}" ]]; then metadata="$(grep -Eo '"machineIdSeed"[[:space:]]*:[[:space:]]*"[0-9A-Fa-f-]{36}"' "${metadata_path}" 2>/dev/null | head -n 1 || true)" seed="$(printf '%s' "${metadata}" | sed -E 's/.*"([0-9A-Fa-f-]{36})"/\1/')" fi [[ ! "${seed}" =~ ^[0-9A-Fa-f-]{36}$ ]] && seed="$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid 2>/dev/null || true)" [[ ! "${seed}" =~ ^[0-9A-Fa-f-]{36}$ ]] && return if [[ ! -r "${seed_path}" ]]; then temporary_path="${seed_path}.$$.tmp" if ! (umask 077; mkdir -p "$(dirname "${seed_path}")" && printf '%s\n' "${seed}" > "${temporary_path}" && ln "${temporary_path}" "${seed_path}" && rm -f "${temporary_path}") 2>/dev/null; then rm -f "${temporary_path}" 2>/dev/null || true seed="$(head -n 1 "${seed_path}" 2>/dev/null | tr -d '[:space:]')" fi fi [[ ! "${seed}" =~ ^[0-9A-Fa-f-]{36}$ ]] && return raw="${seed}" fi if command -v sha256sum >/dev/null 2>&1; then printf 'twg-cli/anonymous-machine-id/v1:%s' "${raw}" | sha256sum | awk '{print $1}' elif command -v shasum >/dev/null 2>&1; then printf 'twg-cli/anonymous-machine-id/v1:%s' "${raw}" | shasum -a 256 | awk '{print $1}' fi } report_download_setup_step() { local success="$1" error_code="${2:-}" origin agent now error_attribute agent_attribute payload machine_id machine_attribute [[ "${DO_NOT_TRACK:-0}" == "1" ]] && return case "${success}" in true|false) ;; *) return ;; esac; case "${error_code}" in ""|download_failed|checksum_failed|interrupted) ;; *) return ;; esac origin="direct"; [[ -n "${PLUGIN_AGENT:-}" ]] && origin="plugin"; agent="${PLUGIN_AGENT:-unknown}"; case "${agent}" in claude-code|codex|copilot|cursor|devin|gemini|hermes|pi|qoder) ;; *) agent="unknown" ;; esac now="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" || return; machine_id="$(twg_anonymous_machine_id)"; machine_attribute=""; agent_attribute=""; error_attribute="" if [[ "${origin}" == plugin ]]; then agent_attribute=",\"install_agent\":\"${agent}\""; fi [[ "${machine_id}" =~ ^[a-f0-9]{64}$ ]] && machine_attribute=",\"anonymous_machine_id\":\"${machine_id}\""; [[ -n "${error_code}" ]] && error_attribute=",\"error_code\":\"${error_code}\"" payload="{\"batch\":[{\"type\":\"track\",\"event\":\"cliSetupStep completed\",\"anonymousId\":\"anonymous\",\"properties\":{\"eventType\":\"operational\",\"product\":\"teamworkGraph\",\"source\":\"twg/cli\",\"version\":\"$(resolve_installed_twg_version)\",\"env\":\"prod\",\"origin\":\"console\",\"platform\":\"${ANALYTICS_PLATFORM}\",\"tags\":[\"teamworkGraph\"],\"tenantIdType\":\"none\",\"actionSubject\":\"cliSetupStep\",\"action\":\"completed\",\"attributes\":{\"step\":\"download\",\"success\":\"${success}\",\"flow\":\"setup-finalize\",\"install_origin\":\"${origin}\"${agent_attribute}${machine_attribute}${error_attribute}}},\"timestamp\":\"${now}\",\"sentAt\":\"${now}\"}],\"writeKey\":\"BLANK\",\"sentAt\":\"${now}\"}" curl -fsS --retry 2 --connect-timeout 1 --max-time 3 -X POST -H "Authorization: Basic QkxBTks6" -H "Content-Type: application/json" --data-binary "${payload}" "${GASV3_ENDPOINT}" >/dev/null 2>&1 || true; DOWNLOAD_SETUP_STEP_REPORTED=true } PLUGIN_INSTALL_STAGE="" start_plugin_install_stage() { PLUGIN_INSTALL_STAGE="$1" report_plugin_install "${PLUGIN_INSTALL_STAGE}" started started } finish_plugin_install_stage() { local result="$1" local error_code="${2:-}" if [[ -n "${PLUGIN_INSTALL_STAGE}" ]]; then report_plugin_install "${PLUGIN_INSTALL_STAGE}" finished "${result}" "${error_code}" if [[ "${PLUGIN_INSTALL_STAGE}" == "download" ]]; then if [[ "${DOWNLOAD_SETUP_STEP_REPORTED}" != true ]]; then if [[ "${result}" == "success" ]]; then report_download_setup_step true; else report_download_setup_step false "${error_code}"; fi fi fi PLUGIN_INSTALL_STAGE="" fi } handle_install_interrupt() { finish_plugin_install_stage error interrupted printf "\nInterrupted — installation cancelled.\n" >&2 exit 130 } # Treat Ctrl+C/SIGTERM during setup as cancellation, not a rollback failure. trap handle_install_interrupt INT TERM handle_install_exit() { local status=$? if [[ "${status}" -ne 0 && -n "${PLUGIN_INSTALL_STAGE}" ]]; then if [[ "${PLUGIN_INSTALL_STAGE}" == "download" ]]; then finish_plugin_install_stage error download_failed else finish_plugin_install_stage error setup_failed fi fi if [[ -n "${CHECKSUMS_PATH:-}" ]]; then rm -f "${CHECKSUMS_PATH}" fi if [[ -n "${TWG_TMP:-}" ]]; then rm -f "${TWG_TMP}" fi } trap handle_install_exit EXIT validate_setup_finalize_support() { local candidate_path="$1" if run_without_bun_env "${candidate_path}" setup finalize --help >/dev/null 2>&1; then return 0 fi echo "Error: downloaded twg ${INSTALLED_VERSION_LABEL} does not support 'twg setup finalize'." >&2 echo "Install twg v0.9.2 or newer, or use an installer published with that older release." >&2 return 1 } backup_existing_twg() { if [[ -e "${TWG_PATH}" ]]; then TWG_BACKUP="$(mktemp "${INSTALL_DIR}/.twg.backup.XXXXXX")" || { echo "Error: could not create backup file in ${INSTALL_DIR}." >&2 exit 1 } mv "${TWG_PATH}" "${TWG_BACKUP}" fi } rollback_twg_install() { local status="$1" if [[ -n "${TWG_BACKUP}" && -e "${TWG_BACKUP}" ]]; then rm -f "${TWG_PATH}" 2>/dev/null || true mv "${TWG_BACKUP}" "${TWG_PATH}" 2>/dev/null || true elif [[ "${TWG_LEGACY_COPIED}" == "true" || "${TWG_INSTALLED_REPLACEMENT}" == "true" ]]; then rm -f "${TWG_PATH}" 2>/dev/null || true fi restore_install_metadata exit "${status}" } cleanup_replaced_twg_files() { rm -f "${TWG_BIN_PATH}" 2>/dev/null || true if [[ -n "${TWG_BACKUP}" ]]; then rm -f "${TWG_BACKUP}" 2>/dev/null || true fi if [[ -n "${INSTALL_METADATA_BACKUP}" ]]; then rm -f "${INSTALL_METADATA_BACKUP}" 2>/dev/null || true fi } backup_install_metadata() { INSTALL_METADATA_PATH="$(resolve_install_metadata_path)" if [[ -e "${INSTALL_METADATA_PATH}" ]]; then local metadata_backup metadata_backup="$(mktemp "${TMPDIR:-/tmp}/twg-install-metadata.XXXXXX")" || { echo "Error: could not create install metadata backup." >&2 return 1 } cp "${INSTALL_METADATA_PATH}" "${metadata_backup}" || { rm -f "${metadata_backup}" 2>/dev/null || true return 1 } INSTALL_METADATA_BACKUP="${metadata_backup}" INSTALL_METADATA_ROLLBACK_ACTION="restore" else INSTALL_METADATA_ROLLBACK_ACTION="remove" fi } restore_install_metadata() { if [[ -z "${INSTALL_METADATA_PATH}" ]]; then return fi case "${INSTALL_METADATA_ROLLBACK_ACTION}" in restore) if [[ -n "${INSTALL_METADATA_BACKUP}" && -e "${INSTALL_METADATA_BACKUP}" ]]; then mkdir -p "$(dirname "${INSTALL_METADATA_PATH}")" 2>/dev/null || true cp "${INSTALL_METADATA_BACKUP}" "${INSTALL_METADATA_PATH}" 2>/dev/null || true fi ;; remove) rm -f "${INSTALL_METADATA_PATH}" 2>/dev/null || true ;; none) ;; esac } if [[ "${SKIP_DOWNLOAD}" == "true" ]]; then if [[ ! -x "${TWG_PATH}" && -x "${TWG_BIN_PATH}" ]]; then cp "${TWG_BIN_PATH}" "${TWG_PATH}" chmod +x "${TWG_PATH}" TWG_LEGACY_COPIED=true fi if [[ ! -x "${TWG_PATH}" ]]; then echo "Error: --skip-download requires an existing executable at ${TWG_PATH}." >&2 exit 126 fi echo "Skipping download and reusing existing twg at ${TWG_PATH}" else start_plugin_install_stage download URL="${DOWNLOAD_BASE_URL}/${DOWNLOAD_BINARY_NAME}" CHECKSUMS_PATH="$(mktemp "${TMPDIR:-/tmp}/twg-sha256sums.XXXXXX")" rm -f "${INSTALL_DIR}"/.twg.* 2>/dev/null || true TWG_TMP="$(mktemp "${INSTALL_DIR}/.twg.XXXXXX")" || { echo "Error: could not create temporary file in ${INSTALL_DIR}. Check disk space and permissions." >&2 exit 1 } echo "Downloading ${URL} …" curl -fSL --retry 2 -o "${TWG_TMP}" "${URL}" || { download_status=$? finish_plugin_install_stage error download_failed exit "${download_status}" } echo "Downloading ${CHECKSUMS_URL} -> ${CHECKSUMS_PATH} …" curl -fSL --retry 2 -o "${CHECKSUMS_PATH}" "${CHECKSUMS_URL}" || { download_status=$? finish_plugin_install_stage error download_failed exit "${download_status}" } if [[ ! -s "${CHECKSUMS_PATH}" ]]; then echo "Error: checksum file is empty or download failed. Aborting." >&2 report_download_setup_step false checksum_failed finish_plugin_install_stage error download_failed exit 1 fi expected_sha="$(awk -v file="${DOWNLOAD_BINARY_NAME}" '{ checksum = $1 name = $2 sub(/^\*/, "", name) sub(/\r$/, "", name) if (name == file) { print checksum; exit } }' "${CHECKSUMS_PATH}")" if [[ -z "${expected_sha}" ]]; then echo "Error: checksum entry for ${DOWNLOAD_BINARY_NAME} not found in ${CHECKSUMS_URL}." >&2 report_download_setup_step false checksum_failed finish_plugin_install_stage error download_failed exit 1 fi if command -v sha256sum &>/dev/null; then actual_sha="$(sha256sum "${TWG_TMP}" | awk '{print $1}')" elif command -v shasum &>/dev/null; then actual_sha="$(shasum -a 256 "${TWG_TMP}" | awk '{print $1}')" else echo "Error: neither sha256sum nor shasum is available to verify ${DOWNLOAD_BINARY_NAME}." >&2 exit 127 fi if [[ "${actual_sha}" != "${expected_sha}" ]]; then echo "Error: checksum verification failed for ${DOWNLOAD_BINARY_NAME}." >&2 echo "Expected: ${expected_sha}" >&2 echo "Actual: ${actual_sha}" >&2 report_download_setup_step false checksum_failed finish_plugin_install_stage error download_failed exit 1 fi chmod +x "${TWG_TMP}" if [ ! -x "${TWG_TMP}" ]; then echo "Error: could not make downloaded binary executable." >&2 exit 126 fi validate_setup_finalize_support "${TWG_TMP}" backup_existing_twg mv "${TWG_TMP}" "${TWG_PATH}" || { move_status=$? rollback_twg_install "${move_status}" } TWG_INSTALLED_REPLACEMENT=true TWG_TMP="" finish_plugin_install_stage success if [[ "${OS}" == "Darwin" ]]; then xattr -d com.apple.quarantine "${TWG_PATH}" 2>/dev/null || true fi fi echo "" # PATH PATH_ALREADY_SET=true PATH_PROFILE_UPDATED=false PATH_PROFILE_SKIPPED=false SHELL_PROFILE="" case ":${PATH}:" in *":${INSTALL_DIR}:"*) ;; *) PATH_ALREADY_SET=false export PATH="${INSTALL_DIR}:${PATH}" append_install_dir_to_path_profile ;; esac run_setup_finalize() { local finalize_args local install_origin="${TWG_INSTALL_ORIGIN:-direct}" case "${install_origin}" in direct|source) ;; *) install_origin="direct" ;; esac [[ -n "${PLUGIN_AGENT:-}" ]] && install_origin="plugin" finalize_args=( setup finalize --install-method direct-public-installer --install-channel stable --install-dir "${INSTALL_DIR}" --binary-path "${TWG_PATH}" --installed-version "${INSTALLED_VERSION_LABEL}" --platform "${PLATFORM}" --arch "${ARCH_SUFFIX}" --source "${FINALIZE_SOURCE}" --install-origin "${install_origin}" ) if [[ "${install_origin}" == "plugin" ]]; then finalize_args+=(--install-agent "${PLUGIN_AGENT}") fi finalize_args+=(--detect-agents --allow-login-failure) # Carry explicit consent only for a fully unattended install. if [[ "${ASSUME_YES}" == "true" && "${SKIP_LOGIN}" == "true" && "${SKIP_SKILLS}" == "true" ]]; then finalize_args+=(--yes) fi if [[ "${INSTALLER_USE_PAT}" == "1" ]]; then finalize_args+=(--pat) fi if [[ "${SKIP_SKILLS}" == "true" ]]; then finalize_args+=(--skip-skills) fi if [[ "${SKIP_LOGIN}" == "true" ]]; then finalize_args+=(--skip-login) fi if validate_setup_finalize_support "${TWG_PATH}"; then local finalize_status if [[ "${SKIP_LOGIN}" != "true" ]]; then start_plugin_install_stage auth fi # Pipe installs consume stdin; use the controlling TTY for interactive login. if { : < /dev/tty; } 2>/dev/null; then TWG_SETUP_BITBUCKET_FROM_FINALIZE=1 run_without_bun_env "${TWG_PATH}" "${finalize_args[@]}" < /dev/tty else # Skip optional Bitbucket setup without a TTY. TWG_SKIP_BITBUCKET=1 TWG_SETUP_BITBUCKET_FROM_FINALIZE=1 run_without_bun_env "${TWG_PATH}" "${finalize_args[@]}" fi finalize_status=$? if [[ "${finalize_status}" -eq 130 ]]; then finish_plugin_install_stage error interrupted # Preserve the installed binary so the user can log in later. echo "" >&2 echo "Interrupted. twg is installed — run 'twg login' when you're ready." >&2 exit 130 elif [[ "${finalize_status}" -eq 20 ]]; then finish_plugin_install_stage error login_failed LOGIN_FAILED=true elif [[ "${finalize_status}" -ne 0 ]]; then finish_plugin_install_stage error setup_failed return "${finalize_status}" else finish_plugin_install_stage success fi else return 1 fi } # Finalize LOGIN_FAILED=false if ! backup_install_metadata; then rollback_twg_install 1 fi set +e run_setup_finalize finalize_result=$? set -e if [[ "${finalize_result}" -ne 0 ]]; then rollback_twg_install "${finalize_result}" fi cleanup_replaced_twg_files DISPLAY_VERSION="$(resolve_installed_twg_version)" if [[ "${DISPLAY_VERSION}" == "unknown" ]]; then echo "twg installed at ${TWG_PATH}" else echo "twg ${DISPLAY_VERSION} installed at ${TWG_PATH}" fi # Summary echo "" echo "════════════════════════════════════════════════════════════" echo " ✅ Installed: ${TWG_PATH}" echo "" if [[ "${PATH_ALREADY_SET}" == "false" ]]; then if [[ "${PATH_PROFILE_UPDATED}" == "true" ]]; then echo " ✅ Added ${INSTALL_DIR} to ${SHELL_PROFILE}" echo " Open a new terminal to pick up the PATH update." elif [[ "${PATH_PROFILE_SKIPPED}" == "true" ]]; then echo " ⚠️ ${INSTALL_DIR} was not added to your shell profile." echo " twg is available inside this installer process only until your PATH is updated." fi else echo " ✅ ${INSTALL_DIR} is already in your PATH — you're all set!" fi echo "════════════════════════════════════════════════════════════" echo "" echo "Next steps:" if [[ "${PATH_ALREADY_SET}" == "false" ]]; then echo " \"${TWG_PATH}\" doctor # works before opening a new terminal" echo " twg doctor # after opening a new terminal" else echo " twg doctor" fi if [[ "${SKIP_LOGIN}" == "true" || "${LOGIN_FAILED}" == "true" ]]; then if [[ "${PATH_ALREADY_SET}" == "false" ]]; then echo " \"${TWG_PATH}\" login # (skipped during install; current shell)" else echo " twg login # (skipped during install)" fi fi if [[ "${SKIP_SKILLS}" == "true" ]]; then if [[ "${PATH_ALREADY_SET}" == "false" ]]; then echo " \"${TWG_PATH}\" skills install # (skipped during install; current shell)" else echo " twg skills install # (skipped during install)" fi fi