#!/usr/bin/env bash # # cloudshell.sh - run OpenCode's web UI when you are ALREADY inside a Cloud # Shell terminal. # # bash <(curl -fsSL https://.pages.dev/cloudshell.sh) # # This is the "I am already in the cloud interface" variant of install.sh. It # assumes it is running ON the Cloud Shell VM and never tries to tunnel # anywhere - no gcloud, no ssh, no mode detection. It starts opencode in a tmux # session on this VM and points you at Cloud Shell's Web Preview. # # cloudshell.sh start fresh, then attach so you can watch it # cloudshell.sh --attach attach to the running instance # cloudshell.sh --status say whether it is running, and print the URL # cloudshell.sh --stop stop it # cloudshell.sh --help # # Why tmux: opencode has to outlive your terminal. Run in the foreground, it # dies the moment you close the tab, press Ctrl-C, or your session blips - and # it takes any agent run in flight with it. # # Optional environment variables: # PORT Port to serve on. [default: 4096] # OC_VERSION Version of the opencode-ai npm package. [default: 1.18.35] # GH_TOKEN GitHub token for this VM. When set, it is piped into # `gh auth login --with-token` over stdin - never in argv, never # in a file, never in shell history. # # NEVER add `set -x` to this script: it would echo GH_TOKEN. set -euo pipefail PORT="${PORT:-4096}" OC_VERSION="${OC_VERSION:-1.18.35}" TMUX_SESSION="opencode" # Cloud Shell ships Node inside nvm, and nvm is absent from the PATH of every # non-interactive shell there - plain, interactive and login shells alike. So # npm and opencode are "command not found" here until this runs. The leading # `true;` is a no-op that keeps the compound command legal if anything ever # prepends an assignment to it. NODE_PRELUDE='true; for d in /usr/local/nvm/versions/node/*/bin; do [ -d "$d" ] || continue; PATH="$d:$PATH"; done; export PATH' log() { printf '%s\n' "$*"; } warn() { printf 'warning: %s\n' "$*" >&2; } die() { printf 'error: %s\n' "$*" >&2; exit 1; } usage() { cat <<'EOF' Run OpenCode's web UI from inside a Cloud Shell terminal. bash <(curl -fsSL /cloudshell.sh) (no args) start a fresh instance, then attach so you can watch it --attach attach to the instance already running --status say whether it is running, and print the URL --stop stop it --help this text Ctrl-B then D detaches from the tmux session without stopping opencode. Environment variables: PORT Port to serve on. (default: 4096) OC_VERSION Version of the opencode-ai npm package. (default: 1.18.35) GH_TOKEN GitHub token; when set, this VM is logged into GitHub non-interactively. When unset, the step is skipped. EOF } action="start" case "${1:-}" in ""|--start) action="start" ;; -a|--attach) action="attach" ;; --status) action="status" ;; -s|--stop) action="stop" ;; -h|--help) usage; exit 0 ;; *) die "unknown argument '$1' (try --help)" ;; esac case "$PORT" in ''|*[!0-9]*) die "PORT must be a number, got '$PORT'." ;; esac [ "$PORT" -ge 1 ] && [ "$PORT" -le 65535 ] || die "PORT must be 1-65535, got $PORT." case "$OC_VERSION" in ''|*[!0-9.A-Za-z-]*) die "OC_VERSION must look like a version, got '$OC_VERSION'." ;; esac # This variant is deliberately for the in-Cloud-Shell case only. Sending someone # to the wrong script is a much better failure than a half-working tunnel. if [ ! -d /google/devshell ] && [ -z "${DEVSHELL_PROJECT_ID:-}" ]; then die "this variant is meant to be pasted into a Cloud Shell terminal, and you are not in one (no /google/devshell here). From your own machine, use install.sh instead - it starts Cloud Shell and tunnels the port back to you: bash <(curl -fsSL /install.sh)" fi eval "$NODE_PRELUDE" command -v tmux >/dev/null 2>&1 || die "tmux is missing on this VM, so opencode cannot be kept alive." preview_url="" if [ -n "${WEB_HOST:-}" ]; then preview_url="https://$PORT-$WEB_HOST" fi print_url() { log "" if [ -n "$preview_url" ]; then log "Open it in your browser:" log " $preview_url" log " (or click Web Preview in the toolbar -> Change port -> $PORT)" else log "Use Cloud Shell's Web Preview button, port $PORT." fi log "" } session_running() { tmux has-session -t "$TMUX_SESSION" 2>/dev/null; } serving() { curl -fsS -o /dev/null -m 3 "http://127.0.0.1:$PORT/" 2>/dev/null; } # ------------------------------------------------------------------- actions -- case "$action" in stop) if session_running; then tmux kill-session -t "$TMUX_SESSION" log "opencode stopped." else log "Nothing was running." fi exit 0 ;; status) if session_running && serving; then log "opencode is running (tmux session '$TMUX_SESSION') on 127.0.0.1:$PORT." print_url elif session_running; then warn "the tmux session exists but nothing is answering on port $PORT." log "--- last lines from the pane ---" tmux capture-pane -p -t "$TMUX_SESSION" 2>/dev/null | tail -n 25 exit 1 else log "opencode is not running." exit 1 fi exit 0 ;; attach) if ! session_running; then die "nothing is running to attach to. Start one with: bash <(curl -fsSL /cloudshell.sh)" fi if [ -t 0 ]; then log "Attaching to tmux session '$TMUX_SESSION'. Detach with Ctrl-B then D." exec tmux attach -t "$TMUX_SESSION" fi log "Not attached (no terminal on stdin)." print_url exit 0 ;; esac # --------------------------------------------------------------------- start -- # opencode keeps its sessions on the persistent $HOME disk. When that disk is # nearly full, sessions silently fail to save - worth saying out loud. home_avail_kb="$(df -Pk "$HOME" 2>/dev/null | awk 'NR==2 {print $4}')" if [ -n "$home_avail_kb" ] && [ "$home_avail_kb" -lt 204800 ] 2>/dev/null; then warn "only $((home_avail_kb / 1024)) MB free on \$HOME ($HOME)." warn "opencode stores sessions there and may fail to save them." warn "see what is using it: du -sh ~/.cache ~/.npm ~/.local/share/opencode 2>/dev/null | sort -h" fi if ! command -v opencode >/dev/null 2>&1; then log "Installing opencode-ai@$OC_VERSION (once per VM, ~20s)..." npm i -g --allow-scripts=opencode-ai "opencode-ai@$OC_VERSION" fi command -v opencode >/dev/null 2>&1 || die "opencode is still not on PATH after installing." log "opencode $(opencode --version 2>/dev/null | head -n1) on $(node -v)" if [ -n "${GH_TOKEN:-}" ]; then if command -v gh >/dev/null 2>&1; then if printf '%s\n' "$GH_TOKEN" | gh auth login --with-token 2>/dev/null; then gh auth setup-git >/dev/null 2>&1 \ && log "GitHub: authenticated, git credentials configured." \ || warn "gh auth setup-git failed - git push over HTTPS may not use the token." else warn "gh auth login --with-token failed - the token was rejected." fi else warn "the gh CLI is not available on this VM - skipping GitHub auth." fi fi # Fresh instance every run, as chosen. --attach reaches the running one instead. log "Starting a fresh opencode in tmux session '$TMUX_SESSION'..." tmux kill-session -t "$TMUX_SESSION" 2>/dev/null || true tmux new-session -d -s "$TMUX_SESSION" "$NODE_PRELUDE; exec opencode web --port $PORT --hostname 127.0.0.1" ready=0 for _ in $(seq 1 40); do if serving; then ready=1; break; fi session_running || break sleep 1 done if [ "$ready" != "1" ]; then log "--- last lines from the opencode pane ---" tmux capture-pane -p -t "$TMUX_SESSION" 2>/dev/null | tail -n 25 die "opencode did not come up on port $PORT." fi log "opencode is running on this VM and will keep running." print_url log "Watch the logs: bash <(curl -fsSL /cloudshell.sh) --attach (or: tmux attach -t $TMUX_SESSION)" log "Stop it: bash <(curl -fsSL /cloudshell.sh) --stop" log "" # Attach so this is not a fire-and-forget setup. Ctrl-B D leaves it running. if [ -t 0 ]; then log "Attaching now - detach with Ctrl-B then D, and opencode keeps running." sleep 1 exec tmux attach -t "$TMUX_SESSION" fi log "Not attaching (no terminal on stdin). opencode is up regardless."