#!/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 # --- 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