850337530f
Coder's GIT_ASKPASS only knows about github.com; for git.octoturge.com it falls through to a broken interactive prompt, so `git push`/`pull` over HTTPS hangs or fails even after `tea login add` succeeds. A credential helper answering `git credential fill` runs before GIT_ASKPASS is consulted, so wire one up (gated on the wizard's existing DID_GITEA check) that reads the live OAuth token straight out of tea's own credentials.json - verified end-to-end against a running workspace (git ls-remote over HTTPS authenticated successfully). Bump every template's VERSION to publish it.
306 lines
13 KiB
Bash
306 lines
13 KiB
Bash
#!/usr/bin/env bash
|
|
# Coder workspace first-run CLI setup wizard.
|
|
#
|
|
# Meant to be `source`d from a new interactive shell (e.g. via .bashrc). It asks,
|
|
# once per user per workspace, whether to install and log into a few optional
|
|
# AI coding CLIs. It re-runs on every new terminal until the user lets it finish
|
|
# (or explicitly skips it for good), then gets out of the way.
|
|
#
|
|
# VS Code / code-server extensions are intentionally NOT asked about here -
|
|
# they're installed declaratively by the Coder template itself (the
|
|
# `code-server` module's `extensions` input, populated from the matching
|
|
# profile-templates/*.code-profile file at template-push time).
|
|
#
|
|
# Manual re-run: bash /opt/coder/cli-setup-wizard.sh --force
|
|
|
|
set -u
|
|
|
|
WIZARD_DONE_FILE="${HOME}/.cache/coder-cli-wizard/done"
|
|
FORCE=0
|
|
[ "${1:-}" = "--force" ] && FORCE=1
|
|
|
|
# Tracks whether the user actually ended up authenticated against GitHub
|
|
# and/or Gitea below, so the SSH/GPG key step can ask about exactly the
|
|
# host(s) in play (and stay silent - "local git only" - if neither).
|
|
DID_GITHUB=0
|
|
DID_GITEA=0
|
|
|
|
export BUN_INSTALL="${HOME}/.bun"
|
|
export PATH="${BUN_INSTALL}/bin:${HOME}/.local/bin:${PATH}"
|
|
|
|
# Only bother interactive shells with a real terminal attached, and only until
|
|
# the user marks the wizard as done.
|
|
if [ "$FORCE" -ne 1 ]; then
|
|
case "$-" in
|
|
*i*) : ;;
|
|
*) return 0 2>/dev/null || exit 0 ;;
|
|
esac
|
|
[ -t 0 ] || { return 0 2>/dev/null || exit 0; }
|
|
[ -f "$WIZARD_DONE_FILE" ] && { return 0 2>/dev/null || exit 0; }
|
|
fi
|
|
|
|
mkdir -p "$(dirname "$WIZARD_DONE_FILE")"
|
|
|
|
ask_yes_no() {
|
|
local prompt="$1" reply
|
|
read -r -p "$prompt [y/N] " reply
|
|
case "$reply" in
|
|
[Yy]*) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
echo ""
|
|
echo "==================================================================="
|
|
echo " Coder workspace setup wizard"
|
|
echo " Runs once per new terminal until you finish it. Ctrl+C any time"
|
|
echo " to skip for now - it'll ask again next terminal."
|
|
echo "==================================================================="
|
|
|
|
# --- GitHub Copilot CLI ---
|
|
if command -v copilot >/dev/null 2>&1; then
|
|
echo "GitHub Copilot CLI already installed, skipping."
|
|
elif command -v bun >/dev/null 2>&1; then
|
|
if ask_yes_no "Install GitHub Copilot CLI and log in?"; then
|
|
if bun install -g @github/copilot; then
|
|
copilot login || echo "Install succeeded but login didn't complete. Retry any time with: copilot login"
|
|
else
|
|
echo "Copilot CLI install failed. Retry later with: bun install -g @github/copilot && copilot login"
|
|
fi
|
|
fi
|
|
else
|
|
echo "Skipping GitHub Copilot CLI: bun not found on this workspace image."
|
|
fi
|
|
|
|
# --- Google Antigravity CLI (agy) ---
|
|
if command -v agy >/dev/null 2>&1; then
|
|
echo "Antigravity CLI already installed, skipping."
|
|
else
|
|
if ask_yes_no "Install Google Antigravity CLI (agy) and log in?"; then
|
|
if curl -fsSL https://antigravity.google/cli/install.sh | bash; then
|
|
echo "Launching 'agy' once to complete sign-in (exit with /logout or Ctrl+D when done)..."
|
|
agy || echo "Sign-in didn't complete. Retry any time by running: agy"
|
|
else
|
|
echo "Antigravity CLI install failed. Retry later with: curl -fsSL https://antigravity.google/cli/install.sh | bash"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# --- Claude Code CLI ---
|
|
if command -v claude >/dev/null 2>&1; then
|
|
echo "Claude Code CLI already installed, skipping."
|
|
elif command -v bun >/dev/null 2>&1; then
|
|
if ask_yes_no "Install Claude Code CLI and log in?"; then
|
|
if bun install -g @anthropic-ai/claude-code; then
|
|
echo "Launching 'claude' once to complete sign-in (use /login if not prompted; Ctrl+C to exit when done)..."
|
|
claude || echo "Sign-in didn't complete. Retry any time by running: claude"
|
|
else
|
|
echo "Claude Code CLI install failed. Retry later with: bun install -g @anthropic-ai/claude-code"
|
|
fi
|
|
fi
|
|
else
|
|
echo "Skipping Claude Code CLI: bun not found on this workspace image."
|
|
fi
|
|
|
|
# --- GitHub CLI (gh) ---
|
|
if command -v gh >/dev/null 2>&1; then
|
|
echo "GitHub CLI already installed, skipping."
|
|
else
|
|
if ask_yes_no "Install GitHub CLI (gh) and log in?"; then
|
|
if (sudo mkdir -p -m 755 /etc/apt/keyrings \
|
|
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg >/dev/null \
|
|
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list >/dev/null \
|
|
&& sudo apt-get update -qq && sudo apt-get install -y gh); then
|
|
gh auth login || echo "Install succeeded but login didn't complete. Retry any time with: gh auth login"
|
|
else
|
|
echo "GitHub CLI install failed. Retry later with: gh auth login (once gh is installed)"
|
|
fi
|
|
fi
|
|
fi
|
|
command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1 && DID_GITHUB=1
|
|
|
|
# --- Gitea CLI (tea) ---
|
|
if command -v tea >/dev/null 2>&1; then
|
|
echo "Gitea CLI already installed, skipping."
|
|
else
|
|
if ask_yes_no "Install Gitea CLI (tea) and log in?"; then
|
|
TEA_ARCH="$(uname -m)"
|
|
case "$TEA_ARCH" in
|
|
x86_64) TEA_ARCH="amd64" ;;
|
|
aarch64) TEA_ARCH="arm64" ;;
|
|
esac
|
|
TEA_VERSION="$(curl -fsSL https://gitea.com/api/v1/repos/gitea/tea/releases/latest | grep -o '"tag_name":[^,]*' | grep -o 'v[0-9][^"]*')"
|
|
mkdir -p "$HOME/.local/bin"
|
|
if [ -n "$TEA_VERSION" ] \
|
|
&& curl -fsSL "https://gitea.com/gitea/tea/releases/download/${TEA_VERSION}/tea-${TEA_VERSION#v}-linux-${TEA_ARCH}" -o "$HOME/.local/bin/tea" \
|
|
&& chmod +x "$HOME/.local/bin/tea"; then
|
|
echo "Add this Gitea instance now (e.g. https://git.octoturge.com)..."
|
|
tea login add || echo "Login didn't complete. Retry any time with: tea login add"
|
|
else
|
|
echo "Gitea CLI install failed. Retry later from: https://gitea.com/gitea/tea/releases"
|
|
rm -f "$HOME/.local/bin/tea"
|
|
fi
|
|
fi
|
|
fi
|
|
# `tea whoami` succeeds regardless of how the login was done (personal
|
|
# access token or OAuth) - more reliable than parsing tea's own
|
|
# config.yml, whose indentation and fields (no plaintext `token:` at all
|
|
# for an OAuth login) vary by auth method.
|
|
command -v tea >/dev/null 2>&1 && tea whoami >/dev/null 2>&1 && DID_GITEA=1
|
|
|
|
# --- Git credential helper for Gitea, backed by tea's own login ---
|
|
# Coder injects GIT_ASKPASS to answer git's own credential prompts, but it
|
|
# only recognizes external auth providers it knows about (github.com is
|
|
# configured on the Coder deployment itself) - for any other host it falls
|
|
# through to a broken interactive prompt that just hangs a non-interactive
|
|
# git subprocess. A credential helper that successfully answers
|
|
# `git credential fill` runs *before* GIT_ASKPASS is ever consulted, so
|
|
# registering one for git.octoturge.com sidesteps that broken path
|
|
# entirely, without touching how Coder handles github.com.
|
|
#
|
|
# The token itself lives in tea's own store
|
|
# (~/.config/tea/credentials.json), which tea keeps fresh (via its
|
|
# refresh_token) as a side effect of any authenticated call - this helper
|
|
# never keeps its own copy, just re-reads tea's live value every time git
|
|
# asks.
|
|
if [ "$DID_GITEA" -eq 1 ]; then
|
|
GITEA_LOGIN_NAME="$(awk '
|
|
/^[[:space:]]*- name:/ { name = $NF }
|
|
/url: https:\/\/git\.octoturge\.com/ { print name; exit }
|
|
' "$HOME/.config/tea/config.yml" 2>/dev/null)"
|
|
|
|
if [ -n "$GITEA_LOGIN_NAME" ]; then
|
|
mkdir -p "$HOME/.local/bin"
|
|
HELPER="$HOME/.local/bin/git-credential-gitea-tea"
|
|
cat > "$HELPER" <<'HELPER_EOT'
|
|
#!/bin/sh
|
|
# Auto-generated by cli-setup-wizard.sh - re-run the wizard with --force to
|
|
# regenerate this after logging into a different Gitea account.
|
|
LOGIN_NAME="__GITEA_LOGIN_NAME__"
|
|
CRED_FILE="$HOME/.config/tea/credentials.json"
|
|
|
|
case "$1" in
|
|
get)
|
|
# Authenticated no-op call: gives tea a chance to refresh and persist
|
|
# an expired access_token before we read it below.
|
|
tea whoami >/dev/null 2>&1
|
|
token="$(grep -oP "(?<=\"$LOGIN_NAME\":).*" "$CRED_FILE" 2>/dev/null \
|
|
| grep -oP '(?<=access_token\\":\\")[^\\"]*' | head -n1)"
|
|
if [ -n "$token" ]; then
|
|
echo "username=oauth2"
|
|
echo "password=$token"
|
|
fi
|
|
;;
|
|
store|erase)
|
|
# No-op: credentials.json (managed by tea) is the single source of
|
|
# truth, nothing for git to persist or clear on this end.
|
|
;;
|
|
esac
|
|
HELPER_EOT
|
|
sed -i "s/__GITEA_LOGIN_NAME__/$GITEA_LOGIN_NAME/" "$HELPER"
|
|
chmod +x "$HELPER"
|
|
git config --global credential."https://git.octoturge.com".helper "$HELPER"
|
|
echo "Configured git to push/pull https://git.octoturge.com using your tea login."
|
|
fi
|
|
fi
|
|
|
|
# --- SSH + GPG keys for the external git host(s) selected above ---
|
|
# Only asks if the user actually set up GitHub and/or Gitea just now -
|
|
# stays silent for "local git only" (neither was set up).
|
|
if [ "$DID_GITHUB" -eq 1 ] || [ "$DID_GITEA" -eq 1 ]; then
|
|
if [ "$DID_GITHUB" -eq 1 ] && [ "$DID_GITEA" -eq 1 ]; then
|
|
KEY_HOSTS_DESC="GitHub and Gitea"
|
|
elif [ "$DID_GITHUB" -eq 1 ]; then
|
|
KEY_HOSTS_DESC="GitHub"
|
|
else
|
|
KEY_HOSTS_DESC="Gitea"
|
|
fi
|
|
|
|
if ask_yes_no "Auto-generate an SSH key and a GPG signing key, and register them with $KEY_HOSTS_DESC?"; then
|
|
KEY_NAME="${GIT_AUTHOR_NAME:-$(whoami)}"
|
|
KEY_EMAIL="${GIT_AUTHOR_EMAIL:-$(whoami)@$(hostname)}"
|
|
|
|
# SSH key: ed25519, no passphrase (disposable dev workspace convenience;
|
|
# add one manually afterwards with `ssh-keygen -p` if you want one).
|
|
SSH_KEY="$HOME/.ssh/id_ed25519"
|
|
if [ ! -f "$SSH_KEY" ]; then
|
|
mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
|
|
ssh-keygen -t ed25519 -N "" -C "$KEY_EMAIL" -f "$SSH_KEY" -q
|
|
echo "Generated SSH key: ${SSH_KEY}.pub"
|
|
else
|
|
echo "SSH key already exists at ${SSH_KEY}.pub, reusing it."
|
|
fi
|
|
|
|
if [ "$DID_GITHUB" -eq 1 ]; then
|
|
if gh ssh-key add "${SSH_KEY}.pub" --title "coder-$(hostname)" 2>/dev/null; then
|
|
echo "SSH key added to GitHub."
|
|
else
|
|
echo "Couldn't add the SSH key to GitHub automatically (may already be added). Add manually: gh ssh-key add ${SSH_KEY}.pub"
|
|
fi
|
|
fi
|
|
|
|
if [ "$DID_GITEA" -eq 1 ]; then
|
|
if tea ssh-keys add "${SSH_KEY}.pub" --title "coder-$(hostname)" >/dev/null 2>&1; then
|
|
echo "SSH key added to Gitea."
|
|
else
|
|
echo "Couldn't add the SSH key to Gitea automatically (may already be added). Add manually with: tea ssh-keys add ${SSH_KEY}.pub"
|
|
fi
|
|
fi
|
|
|
|
# GPG key: ed25519 signing key, no passphrase, no expiry.
|
|
if gpg --list-secret-keys --with-colons "$KEY_EMAIL" 2>/dev/null | grep -q '^sec'; then
|
|
echo "GPG key for $KEY_EMAIL already exists, reusing it."
|
|
else
|
|
mkdir -p "$HOME/.gnupg" && chmod 700 "$HOME/.gnupg"
|
|
grep -qF "allow-loopback-pinentry" "$HOME/.gnupg/gpg-agent.conf" 2>/dev/null \
|
|
|| echo "allow-loopback-pinentry" >> "$HOME/.gnupg/gpg-agent.conf"
|
|
gpgconf --kill gpg-agent 2>/dev/null
|
|
if gpg --batch --pinentry-mode loopback --passphrase '' --quick-gen-key "$KEY_NAME <$KEY_EMAIL>" ed25519 sign 0 2>/dev/null; then
|
|
echo "Generated GPG signing key for $KEY_EMAIL."
|
|
else
|
|
echo "GPG key generation failed. Generate manually with: gpg --quick-gen-key \"$KEY_NAME <$KEY_EMAIL>\" ed25519 sign 0"
|
|
fi
|
|
fi
|
|
GPG_KEY_ID="$(gpg --list-secret-keys --with-colons "$KEY_EMAIL" 2>/dev/null | awk -F: '/^sec/{print $5; exit}')"
|
|
|
|
if [ -n "$GPG_KEY_ID" ]; then
|
|
git config --global user.signingkey "$GPG_KEY_ID"
|
|
git config --global commit.gpgsign true
|
|
echo "Configured git to sign commits with this key."
|
|
|
|
if [ "$DID_GITHUB" -eq 1 ]; then
|
|
if gpg --armor --export "$GPG_KEY_ID" | gh gpg-key add - 2>/dev/null; then
|
|
echo "GPG key added to GitHub."
|
|
else
|
|
echo "Couldn't add the GPG key to GitHub automatically (may already be added). Add manually: gpg --armor --export $GPG_KEY_ID | gh gpg-key add -"
|
|
fi
|
|
fi
|
|
|
|
if [ "$DID_GITEA" -eq 1 ]; then
|
|
if gpg --armor --export "$GPG_KEY_ID" | tea api -X POST /user/gpg_keys -F armored_public_key=@- >/dev/null 2>&1; then
|
|
echo "GPG key added to Gitea."
|
|
else
|
|
echo "Couldn't add the GPG key to Gitea automatically (may already be added). Add manually: gpg --armor --export $GPG_KEY_ID | tea api -X POST /user/gpg_keys -F armored_public_key=@-"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
elif command -v gh >/dev/null 2>&1 || command -v tea >/dev/null 2>&1; then
|
|
# gh and/or tea are installed but neither is actually logged in yet (login
|
|
# was declined, failed, or never completed) - say why the key-generation
|
|
# question above got skipped instead of just silently not asking.
|
|
echo "Skipping SSH/GPG key setup: not logged in to GitHub or Gitea yet."
|
|
echo "Log in (gh auth login / tea login add) then re-run: bash /opt/coder/cli-setup-wizard.sh --force"
|
|
fi
|
|
|
|
echo ""
|
|
if ask_yes_no "Mark setup wizard as complete so it stops asking on new terminals?"; then
|
|
touch "$WIZARD_DONE_FILE"
|
|
echo "Done. Re-run any time with: bash /opt/coder/cli-setup-wizard.sh --force"
|
|
else
|
|
echo "OK, this'll ask again next time you open a terminal."
|
|
fi
|
|
|
|
return 0 2>/dev/null || exit 0
|