diff --git a/.github/workflows/fix-scripts.yml b/.github/workflows/fix-scripts.yml new file mode 100644 index 000000000000..849e2597838b --- /dev/null +++ b/.github/workflows/fix-scripts.yml @@ -0,0 +1,97 @@ +# Installs the previous stable desktop release, then runs the fix script and +# checks it moved the install to the newest one. The scripts only run during +# incidents, so this keeps them from rotting. +name: Fix scripts + +on: + pull_request: + paths: + - scripts/fix.sh + - scripts/fix.ps1 + - .github/workflows/fix-scripts.yml + workflow_dispatch: + +permissions: + contents: read + +jobs: + unix: + name: ${{ matrix.name }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + - { name: macOS, runner: macos-15, kind: mac } + - { name: Linux AppImage, runner: ubuntu-24.04, kind: appimage } + - { name: Linux deb, runner: ubuntu-24.04, kind: deb } + env: + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v6 + + - name: Resolve versions + run: | + echo "NEWEST=$(gh release list -R pingdotgg/t3code --exclude-pre-releases --exclude-drafts -L 2 --json tagName -q '.[0].tagName' | sed 's/^v//')" >> "$GITHUB_ENV" + echo "PREVIOUS=$(gh release list -R pingdotgg/t3code --exclude-pre-releases --exclude-drafts -L 2 --json tagName -q '.[1].tagName' | sed 's/^v//')" >> "$GITHUB_ENV" + + - name: Install the previous release + run: | + if [ "${{ matrix.kind }}" = deb ]; then + gh release download "v${PREVIOUS}" -R pingdotgg/t3code -p "T3-Code-${PREVIOUS}-amd64.deb" -D /tmp + sudo apt-get install -y "/tmp/T3-Code-${PREVIOUS}-amd64.deb" + else + T3CODE_VERSION="$PREVIOUS" sh scripts/fix.sh + fi + + - name: Fix + run: sh scripts/fix.sh + + - name: Check the newest release is installed + run: | + case "${{ matrix.kind }}" in + mac) installed="$(defaults read "/Applications/T3 Code (Beta).app/Contents/Info" CFBundleShortVersionString)" ;; + deb) installed="$(dpkg-query -W -f='${Version}' t3code)" ;; + appimage) + test ! -e "$HOME/Applications/T3-Code-${PREVIOUS}-x86_64.AppImage" + test -x "$HOME/Applications/T3-Code-${NEWEST}-x86_64.AppImage" + installed="$NEWEST" + ;; + esac + echo "installed=$installed newest=$NEWEST previous=$PREVIOUS" + test "$installed" = "$NEWEST" + + windows: + name: Windows + runs-on: windows-2025 + timeout-minutes: 20 + env: + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v6 + + - name: Resolve versions + shell: bash + run: | + echo "NEWEST=$(gh release list -R pingdotgg/t3code --exclude-pre-releases --exclude-drafts -L 2 --json tagName -q '.[0].tagName' | sed 's/^v//')" >> "$GITHUB_ENV" + echo "PREVIOUS=$(gh release list -R pingdotgg/t3code --exclude-pre-releases --exclude-drafts -L 2 --json tagName -q '.[1].tagName' | sed 's/^v//')" >> "$GITHUB_ENV" + + # Windows PowerShell 5.1, the oldest host the script supports. + - name: Install the previous release + shell: powershell + run: | + $env:T3CODE_VERSION = $env:PREVIOUS + ./scripts/fix.ps1 + + - name: Fix + shell: powershell + run: ./scripts/fix.ps1 + + - name: Check the newest release is installed + shell: powershell + run: | + $install = Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*", "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName -like "T3 Code*" } | Select-Object -First 1 + Write-Host "installed=$($install.DisplayVersion) newest=$env:NEWEST previous=$env:PREVIOUS" + if ($install.DisplayVersion -ne $env:NEWEST) { exit 1 } diff --git a/apps/marketing/.gitignore b/apps/marketing/.gitignore index 254b88f1a73f..c0057feb9b2f 100644 --- a/apps/marketing/.gitignore +++ b/apps/marketing/.gitignore @@ -1,2 +1,4 @@ /public/install.sh /public/install.ps1 +/public/fix.sh +/public/fix.ps1 diff --git a/apps/marketing/scripts/stage-install-scripts.mjs b/apps/marketing/scripts/stage-install-scripts.mjs index bac757976a23..1598c7c3ce42 100644 --- a/apps/marketing/scripts/stage-install-scripts.mjs +++ b/apps/marketing/scripts/stage-install-scripts.mjs @@ -6,6 +6,6 @@ const marketingDir = NodePath.dirname(NodePath.dirname(NodeURL.fileURLToPath(imp const repoRoot = NodePath.dirname(NodePath.dirname(marketingDir)); const publicDir = NodePath.join(marketingDir, "public"); NodeFS.mkdirSync(publicDir, { recursive: true }); -for (const name of ["install.sh", "install.ps1"]) { +for (const name of ["install.sh", "install.ps1", "fix.sh", "fix.ps1"]) { NodeFS.copyFileSync(NodePath.join(repoRoot, "scripts", name), NodePath.join(publicDir, name)); } diff --git a/apps/marketing/vercel.ts b/apps/marketing/vercel.ts index e785d6f6b99a..77cbe8ea0ca6 100644 --- a/apps/marketing/vercel.ts +++ b/apps/marketing/vercel.ts @@ -10,14 +10,14 @@ export const config: VercelConfig = { // curl | sh requires plain, uncompressed scripts with no cache across deploys. headers: [ { - source: "/install.sh", + source: "/(install|fix).sh", headers: [ { key: "Content-Type", value: "text/x-shellscript; charset=utf-8" }, { key: "Cache-Control", value: "public, max-age=300" }, ], }, { - source: "/install.ps1", + source: "/(install|fix).ps1", headers: [ { key: "Content-Type", value: "text/plain; charset=utf-8" }, { key: "Cache-Control", value: "public, max-age=300" }, diff --git a/docs/operations/release.md b/docs/operations/release.md index 6f1c39539748..ddae48cc6764 100644 --- a/docs/operations/release.md +++ b/docs/operations/release.md @@ -472,6 +472,16 @@ available. - `electron-updater` reads `latest-mac.yml` on stable and `nightly-mac.yml` on nightly, for both Intel and Apple Silicon. - The workflow merges the per-arch mac manifests into one channel-specific mac manifest before publishing the GitHub Release. +### Recovering from a broken release + +A desktop build that crashes before its UI loads cannot install its own update. +Publish the fixed release first, then point users at `curl -fsSL https://t3.codes/fix.sh | sh` +(`irm https://t3.codes/fix.ps1 | iex` on Windows). The scripts (`scripts/fix.sh`, `scripts/fix.ps1`) +run no T3 Code code: they reinstall the newest release on each installed app's channel and +verify the download against the channel's `latest*.yml` or `nightly*.yml`. The marketing site +serves them, so changes to them go live with the next nightly. `.github/workflows/fix-scripts.yml` +exercises them against the two newest stable releases. + ### Windows payload topology and update validation Windows packages the bundled server and only its runtime-external/native diff --git a/docs/user/install.md b/docs/user/install.md index 64b1f19fac54..4783494afe5b 100644 --- a/docs/user/install.md +++ b/docs/user/install.md @@ -77,6 +77,26 @@ The `.deb` updates itself like the other desktop builds. It asks for your password to install each update. If your desktop has no password prompt, the update fails. Download the new `.deb` and install it the same way. +### If the desktop app won't start + +If T3 Code crashes or shows a blank window after an update, reinstall the newest +release from a terminal. Your threads and settings stay where they are. + +```bash +curl -fsSL https://t3.codes/fix.sh | sh +``` + +On Windows, in PowerShell: + +```powershell +irm https://t3.codes/fix.ps1 | iex +``` + +It quits T3 Code, reinstalls the newest release on the channel you already use +(stable or nightly), and opens it again. It asks only when it can't tell, such +as when both are installed. Set `T3CODE_CHANNEL=nightly` to switch channels. +Arch Linux packages update through your AUR helper instead. + ### The `t3` command The desktop app includes the `t3` command-line tool. To run it from any diff --git a/scripts/fix.ps1 b/scripts/fix.ps1 new file mode 100644 index 000000000000..b268a49afed2 --- /dev/null +++ b/scripts/fix.ps1 @@ -0,0 +1,134 @@ +# Reinstalls the T3 Code desktop app from the newest GitHub Release on Windows. +# It runs no T3 Code code, so it works however broken the installed app is. +# Needs only PowerShell 5.1+. +# +# irm https://t3.codes/fix.ps1 | iex +# +# Environment: +# T3CODE_CHANNEL stable or nightly (default: the installed app's +# channel, or stable when none is installed) +# T3CODE_VERSION exact version to install (overrides the newest release) +# T3CODE_RELEASE_BASE_URL mirror for releases/download (default: GitHub) +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +$repo = "pingdotgg/t3code" +$baseUrl = if ($env:T3CODE_RELEASE_BASE_URL) { $env:T3CODE_RELEASE_BASE_URL.TrimEnd("/") } else { "https://github.com/$repo/releases/download" } +$headers = @{ "User-Agent" = "t3-fix" } + +function Fail([string] $message) { + Write-Error "t3 fix: $message" + exit 1 +} +function Step([string] $message) { [Console]::Error.WriteLine(" $message") } + +function Find-Install { + $keys = @( + "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" + ) + Get-ItemProperty $keys -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName -like "T3 Code*" -and $_.InstallLocation } | + Select-Object -First 1 +} + +# PROCESSOR_ARCHITEW6432 bypasses WOW64 emulation. +$rawArch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } +$arch = switch ($rawArch) { + "AMD64" { "x64" } + "ARM64" { "arm64" } + default { Fail "unsupported architecture $rawArch" } +} + +[Console]::Error.WriteLine("`n T3 Code fix: reinstalling the newest desktop app`n") + +$install = Find-Install +$channel = $env:T3CODE_CHANNEL +if (-not $channel -and $env:T3CODE_VERSION) { + $channel = if ($env:T3CODE_VERSION -match "-nightly\.") { "nightly" } else { "stable" } +} +if (-not $channel -and $install) { + $channel = if ("$($install.DisplayName) $($install.DisplayVersion)" -match "nightly") { "nightly" } else { "stable" } +} +if (-not $channel) { + $channel = "stable" + if (-not [Console]::IsInputRedirected -and [Environment]::UserInteractive) { + do { + $answer = Read-Host " No T3 Code desktop app found. Install stable or nightly? [stable]" + if (-not $answer) { $answer = "stable" } + } until ($answer -eq "stable" -or $answer -eq "nightly") + $channel = $answer + } +} +if ($channel -ne "stable" -and $channel -ne "nightly") { Fail "T3CODE_CHANNEL must be stable or nightly" } + +$version = $env:T3CODE_VERSION +if (-not $version) { + if ($channel -eq "stable") { + # release.yml marks only plain stable releases latest. + $release = Invoke-RestMethod -Uri "https://github.com/$repo/releases/latest" -Headers ($headers + @{ "Accept" = "application/json" }) + $tag = ($release | Where-Object { $_.tag_name -match '^v\d+\.\d+\.\d+$' }).tag_name + } else { + $tag = $null + for ($page = 1; -not $tag -and $page -le 10; $page++) { + $releases = Invoke-RestMethod -Uri "https://api.github.com/repos/$repo/releases?per_page=100&page=$page" -Headers $headers + $tag = ($releases | Where-Object { -not $_.draft -and $_.tag_name -match '^v\d+\.\d+\.\d+-nightly\.\d+\.\d+$' } | Select-Object -First 1).tag_name + if (-not $releases) { break } + } + } + if (-not $tag) { Fail "could not find a $channel release" } + $version = $tag.Substring(1) +} + +$work = Join-Path ([IO.Path]::GetTempPath()) ("t3-fix-" + [IO.Path]::GetRandomFileName()) +New-Item -ItemType Directory -Path $work | Out-Null +try { + $asset = "T3-Code-$version-$arch.exe" + $manifest = if ($channel -eq "stable") { "latest.yml" } else { "nightly.yml" } + $installer = Join-Path $work $asset + Step "Downloading $asset..." + $manifestText = (Invoke-WebRequest -Uri "$baseUrl/v$version/$manifest" -Headers $headers -UseBasicParsing).Content + if ($manifestText -is [byte[]]) { $manifestText = [Text.Encoding]::UTF8.GetString($manifestText) } + Invoke-WebRequest -Uri "$baseUrl/v$version/$asset" -Headers $headers -OutFile $installer -UseBasicParsing + + # electron-builder's update manifest records each file's base64 SHA-512. + $expected = $null + $url = $null + foreach ($line in $manifestText -split "`n") { + if ($line -match '^\s*-\s*url:\s*(\S+)') { $url = $Matches[1] } + elseif ($line -match '^\s*sha512:\s*(\S+)' -and $url -eq $asset) { $expected = $Matches[1]; break } + } + if (-not $expected) { Fail "$asset is not listed in $manifest" } + $stream = [IO.File]::OpenRead($installer) + try { $actual = [Convert]::ToBase64String([Security.Cryptography.SHA512]::Create().ComputeHash($stream)) } + finally { $stream.Dispose() } + if ($actual -ne $expected) { Fail "checksum mismatch for $asset" } + + if ($install) { + $installDir = $install.InstallLocation.TrimEnd("\") + $running = @(Get-Process | Where-Object { $_.Path -and $_.Path.StartsWith("$installDir\", [StringComparison]::OrdinalIgnoreCase) }) + if ($running.Count -gt 0) { + Step "Quitting $($install.DisplayName)..." + $running | ForEach-Object { $_.CloseMainWindow() | Out-Null } + $running | Wait-Process -Timeout 10 -ErrorAction SilentlyContinue + $running | Where-Object { -not $_.HasExited } | Stop-Process -Force -ErrorAction SilentlyContinue + $running | Wait-Process -Timeout 10 -ErrorAction SilentlyContinue + } + } + + Step "Installing T3 Code $version..." + $process = Start-Process -FilePath $installer -ArgumentList "/S" -Wait -PassThru + if ($process.ExitCode -ne 0) { Fail "the installer exited with code $($process.ExitCode)" } +} finally { + Remove-Item $work -Recurse -Force -ErrorAction SilentlyContinue +} + +$install = Find-Install +if ($install) { + $exe = Get-ChildItem -Path $install.InstallLocation -Filter "T3 Code*.exe" -File | Where-Object { $_.Name -notlike "Uninstall*" } | Select-Object -First 1 + if ($exe) { Start-Process -FilePath $exe.FullName } +} +[Console]::Error.WriteLine(" Reinstalled T3 Code $version`n") +[Console]::Error.WriteLine(" If T3 Code is still broken, run 'npx t3@nightly unfuck' for a guided recovery, or share the logs in ~\.t3\userdata\logs on GitHub or Discord.`n") diff --git a/scripts/fix.sh b/scripts/fix.sh new file mode 100755 index 000000000000..316989fa8460 --- /dev/null +++ b/scripts/fix.sh @@ -0,0 +1,350 @@ +#!/bin/sh +# Reinstalls the T3 Code desktop app from the newest GitHub Release. It runs no +# T3 Code code, so it works however broken the installed app is. Needs only sh, +# curl or wget, and openssl or sha512sum. +# +# curl -fsSL https://t3.codes/fix.sh | sh +# +# Environment: +# T3CODE_CHANNEL stable or nightly (default: each installed app's own +# channel, or stable when none is installed) +# T3CODE_VERSION exact version to install (overrides the newest release) +# T3CODE_RELEASE_BASE_URL mirror for releases/download (default: GitHub) +set -eu + +repo="pingdotgg/t3code" +base_url="${T3CODE_RELEASE_BASE_URL:-https://github.com/${repo}/releases/download}" + +fail() { + printf '\nt3 fix: %s\n' "$1" >&2 + exit 1 +} +step() { printf ' %s\n' "$1" >&2; } + +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +trap 'exit 130' INT +trap 'exit 143' TERM + +fetch() { + if command -v curl >/dev/null 2>&1; then + curl -fsSL -H "Accept: ${3:-*/*}" "$1" -o "$2" + elif command -v wget >/dev/null 2>&1; then + wget -q --header="Accept: ${3:-*/*}" "$1" -O "$2" + else + fail "curl or wget is required" + fi +} + +sha512_base64() { + if command -v openssl >/dev/null 2>&1; then + openssl dgst -sha512 -binary "$1" | base64 | tr -d '\n' + elif command -v sha512sum >/dev/null 2>&1 && command -v xxd >/dev/null 2>&1; then + sha512sum "$1" | cut -d' ' -f1 | xxd -r -p | base64 | tr -d '\n' + else + fail "openssl, or sha512sum and xxd, is required to verify the download" + fi +} + +case "$(uname -s)" in + Darwin) platform="mac" ;; + Linux) platform="linux" ;; + *) fail "unsupported operating system $(uname -s); on Windows run: irm https://t3.codes/fix.ps1 | iex" ;; +esac +case "$(uname -m)" in + arm64 | aarch64) arch="arm64" ;; + x86_64 | amd64) arch="x64" ;; + *) fail "unsupported architecture $(uname -m)" ;; +esac +# A shell under Rosetta reports x86_64 on Apple silicon. +if [ "$platform" = mac ] && [ "$(sysctl -n hw.optional.arm64 2>/dev/null || true)" = 1 ]; then + arch="arm64" +fi + +newest_version() { + if [ -n "${T3CODE_VERSION:-}" ]; then + printf '%s\n' "$T3CODE_VERSION" + return + fi + index="${work}/releases.json" + found= + if [ "$1" = stable ]; then + # release.yml marks only plain stable releases latest. + fetch "https://github.com/${repo}/releases/latest" "$index" application/json || fail "could not reach GitHub" + found="$(sed -n 's/.*"tag_name": *"v\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)".*/\1/p' "$index" | head -n 1)" + else + page=1 + while [ -z "$found" ] && [ "$page" -le 10 ]; do + fetch "https://api.github.com/repos/${repo}/releases?per_page=100&page=${page}" "$index" || fail "could not reach GitHub" + found="$(sed -n 's/.*"tag_name": *"v\([0-9][^"]*-nightly\.[0-9]*\.[0-9]*\)".*/\1/p' "$index" | head -n 1)" + grep -q '"tag_name"' "$index" || break + page=$((page + 1)) + done + fi + [ -n "$found" ] || fail "could not find a ${1} release" + printf '%s\n' "$found" +} + +channel_of() { + case "$1" in + *-nightly.* | *Nightly* | *nightly*) printf 'nightly\n' ;; + *) printf 'stable\n' ;; + esac +} + +# Downloads a release asset into $work and checks it against electron-builder's update manifest. +download_asset() { # channel version asset + manifest_prefix=latest + [ "$1" = stable ] || manifest_prefix=nightly + case "$platform-$arch" in + mac-*) manifest="${manifest_prefix}-mac.yml" ;; + linux-x64) manifest="${manifest_prefix}-linux.yml" ;; + linux-arm64) manifest="${manifest_prefix}-linux-arm64.yml" ;; + esac + step "Downloading $3..." + fetch "${base_url}/v${2}/${manifest}" "${work}/${manifest}" || fail "could not download ${manifest} for ${2}" + fetch "${base_url}/v${2}/${3}" "${work}/${3}" || fail "could not download ${3}" + expected="$(awk -v want="$3" '$1 == "-" && $2 == "url:" { url = $3 } $1 == "sha512:" && url == want { print $2; exit }' "${work}/${manifest}")" + [ -n "$expected" ] || fail "${3} is not listed in ${manifest}" + [ "$(sha512_base64 "${work}/${3}")" = "$expected" ] || fail "checksum mismatch for ${3}" +} + +# Writes into $dir with sudo only when the user cannot. +as_owner_of() { + dir="$1" + shift + if [ -w "$dir" ]; then "$@"; else sudo "$@"; fi +} + +wait_for_exit() { + n=0 + while [ "$n" -lt 20 ] && [ -n "$("$@")" ]; do + sleep 0.5 + n=$((n + 1)) + done +} + +can_launch() { + [ "$(id -u)" != 0 ] && { [ "$platform" = mac ] || [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ]; } +} + +mac_app_name() { + if [ "$1" = nightly ]; then printf 'T3 Code (Nightly)\n'; else printf 'T3 Code (Beta)\n'; fi +} + +# pgrep takes a regex; app names contain parentheses. +mac_pids() { pgrep -f "$(printf '%s' "$1/Contents/" | sed 's/[][().*^$+?{}|\\]/\\&/g')" || true; } + +fix_mac() { # channel app_path + name="$(basename "$2" .app)" + version="$(newest_version "$1")" + asset="T3-Code-${version}-${arch}.zip" + download_asset "$1" "$version" "$asset" + step "Unpacking..." + mkdir -p "${work}/unpacked" + ditto -x -k "${work}/${asset}" "${work}/unpacked" + [ -d "${work}/unpacked/${name}.app" ] || fail "${asset} does not contain ${name}.app" + + if [ -n "$(mac_pids "$2")" ]; then + step "Quitting ${name}..." + osascript -e "tell application \"${name}\" to quit" >/dev/null 2>&1 || true + wait_for_exit mac_pids "$2" + mac_pids "$2" | xargs kill 2>/dev/null || true + wait_for_exit mac_pids "$2" + mac_pids "$2" | xargs kill -9 2>/dev/null || true + fi + + step "Installing ${name} ${version}..." + parent="$(dirname "$2")" + as_owner_of "$parent" mkdir -p "$parent" + if [ -e "$2" ]; then + as_owner_of "$parent" mv "$2" "${2}.old-$$" + fi + as_owner_of "$parent" ditto "${work}/unpacked/${name}.app" "$2" + as_owner_of "$parent" rm -rf "${2}.old-$$" + rm -rf "${work}/unpacked" "${work:?}/${asset}" + + if can_launch; then open "$2" || true; fi + printf ' Reinstalled %s %s\n' "$name" "$version" >&2 +} + +appimage_pids() { + for environ in /proc/[0-9]*/environ; do + [ -O "$environ" ] || continue + if tr '\0' '\n' 2>/dev/null < "$environ" | grep -qxF "APPIMAGE=$1"; then + pid="${environ#/proc/}" + printf '%s\n' "${pid%/environ}" + fi + done +} + +stop_pids() { # description pid_lister args... + description="$1" + shift + [ -n "$("$@")" ] || return 0 + step "Quitting ${description}..." + "$@" | xargs kill 2>/dev/null || true + wait_for_exit "$@" + "$@" | xargs kill -9 2>/dev/null || true +} + +fix_appimage() { # channel path + version="$(newest_version "$1")" + asset="T3-Code-${version}-$([ "$arch" = x64 ] && echo x86_64 || echo arm64).AppImage" + download_asset "$1" "$version" "$asset" + stop_pids "T3 Code" appimage_pids "$2" + + dir="$(dirname "$2")" + # Keep the user's own filename; refresh ours so it names the version inside. + case "$(basename "$2")" in + T3-Code-*.AppImage) target="${dir}/${asset}" ;; + *) target="$2" ;; + esac + step "Installing ${target}..." + mkdir -p "$dir" + chmod +x "${work}/${asset}" + mv "${work}/${asset}" "${target}.part-$$" + mv "${target}.part-$$" "$target" + [ "$target" = "$2" ] || rm -f "$2" + + if can_launch; then nohup "$target" >/dev/null 2>&1 & fi + printf ' Reinstalled T3 Code %s at %s\n' "$version" "$target" >&2 +} + +deb_pids() { pgrep -f "/opt/T3 Code" || true; } + +fix_deb() { # channel + version="$(newest_version "$1")" + asset="T3-Code-${version}-$([ "$arch" = x64 ] && echo amd64 || echo arm64).deb" + download_asset "$1" "$version" "$asset" + stop_pids "T3 Code" deb_pids + step "Installing ${asset} (needs sudo)..." + chmod a+r "${work}/${asset}" + chmod a+rx "$work" + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get install -y --allow-downgrades "${work}/${asset}" + else + sudo dpkg -i "${work}/${asset}" + fi + if can_launch && command -v t3code >/dev/null 2>&1; then nohup t3code >/dev/null 2>&1 & fi + printf ' Reinstalled T3 Code %s\n' "$version" >&2 +} + +# The app records its AppImage path in a desktop entry each time it starts. +recorded_appimage() { + entry="${XDG_DATA_HOME:-$HOME/.local/share}/applications/com.t3tools.T3Code.desktop" + [ -f "$entry" ] || return 0 + path="$(sed -n 's/^Exec=\(.*\) %U$/\1/p' "$entry" | head -n 1)" + path="${path#\"}" + path="${path%\"}" + case "$path" in *.AppImage) [ -f "$path" ] && printf '%s\n' "$path" ;; esac + return 0 +} + +found_appimages() { + recorded_appimage + for dir in "$HOME/Applications" "$HOME/.local/bin" "$HOME/Downloads" "$HOME/Desktop" "$HOME/bin" /opt; do + [ -d "$dir" ] || continue + find "$dir" -maxdepth 1 -type f -name 'T3-Code-*.AppImage' 2>/dev/null + done +} + +# `curl | sh` puts this script on stdin, so questions go to the terminal; without one, take the default. +choose() { # question default choices... + question="$1" + default="$2" + shift 2 + if ! (exec /dev/null; then + printf '%s\n' "$default" + return + fi + while :; do + printf ' %s [%s] ' "$question" "$default" >/dev/tty + read -r answer /dev/null 2>&1 && + [ "$(dpkg-query -W -f='${Status}' t3code 2>/dev/null || true)" = "install ok installed" ]; then + printf '%s\tdeb\t-\n' "$(channel_of "$(dpkg-query -W -f='${Version}' t3code)")" + fi + found_appimages | sort -u | while IFS= read -r path; do + printf '%s\tappimage\t%s\n' "$(channel_of "$(basename "$path")")" "$path" + done +} + +fix_install() { # channel kind path + case "$2" in + mac) fix_mac "$1" "$3" ;; + deb) fix_deb "$1" ;; + appimage) fix_appimage "$1" "$3" ;; + esac +} + +fresh_install() { # channel + if [ "$platform" = mac ]; then + fix_mac "$1" "/Applications/$(mac_app_name "$1").app" + else + fix_appimage "$1" "$HOME/Applications/T3-Code-latest.AppImage" + fi +} + +main() { + printf '\n T3 Code fix: reinstalling the newest desktop app\n\n' >&2 + requested="${T3CODE_CHANNEL:-}" + [ -n "$requested" ] || [ -z "${T3CODE_VERSION:-}" ] || requested="$(channel_of "$T3CODE_VERSION")" + case "$requested" in + "" | stable | nightly) ;; + *) fail "T3CODE_CHANNEL must be stable or nightly" ;; + esac + if [ "$platform" = linux ] && command -v pacman >/dev/null 2>&1; then + for package in t3code-bin t3code-nightly-bin; do + if pacman -Q "$package" >/dev/null 2>&1; then + fail "T3 Code is installed from the AUR package ${package}; update it with your AUR helper, for example: yay -S ${package}" + fi + done + fi + + list_installs > "${work}/installs" + channels="$(cut -f1 "${work}/installs" | sort -u | tr '\n' ' ')" + if [ -z "$requested" ]; then + case "$channels" in + "") requested="$(choose "No T3 Code desktop app found. Install stable or nightly?" stable stable nightly)" ;; + *nightly*stable* | *stable*nightly*) + answer="$(choose "Both stable and nightly are installed. Reinstall stable, nightly, or both?" both stable nightly both)" + [ "$answer" = both ] || requested="$answer" + ;; + esac + fi + + fixed=0 + while IFS="$(printf '\t')" read -r channel kind path; do + [ -z "$requested" ] || [ "$requested" = "$channel" ] || continue + fix_install "$channel" "$kind" "$path" &2 +} + +# Parse the whole script before running anything; `curl | sh` feeds it through stdin. +main