Initial Commit
This commit is contained in:
116
scripts/common.sh
Executable file
116
scripts/common.sh
Executable file
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Shared helpers sourced by every script in this directory.
|
||||
# . "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common.sh"
|
||||
#
|
||||
# Requires bash 4+ and coreutils.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---- paths ----------------------------------------------------------
|
||||
LOCAL_BIN="${HOME}/.local/bin"
|
||||
CRED_HELPER="${LOCAL_BIN}/git-credential-forge"
|
||||
|
||||
# Canonical OAuth auth store: matches
|
||||
# forge-stack-devpi-gateway-gitea/client_auth.auth_store_path() exactly.
|
||||
FORGE_AUTH_DIR="${FSDGG_RUNTIME_DIR:-${HOME}/.forge-stack-devpi-gateway-gitea}"
|
||||
FORGE_AUTH_FILE="${FSDGG_AUTH_STORE_PATH:-${FORGE_AUTH_DIR}/client-auth.json}"
|
||||
|
||||
# ---- colors / logging ----------------------------------------------
|
||||
# ANSI enabled only when stderr is a TTY, NO_COLOR is unset, and
|
||||
# TERM != "dumb"; empty strings otherwise (piped output byte-identical).
|
||||
if [ -t 2 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-dumb}" != "dumb" ]; then
|
||||
_FC_RESET=$'\e[0m'
|
||||
_FC_BOLD=$'\e[1m'
|
||||
_FC_DIM=$'\e[2m'
|
||||
_FC_RED=$'\e[31m'
|
||||
_FC_GREEN=$'\e[32m'
|
||||
_FC_YELLOW=$'\e[33m'
|
||||
_FC_CYAN=$'\e[36m'
|
||||
_FC_MAGENTA=$'\e[35m'
|
||||
else
|
||||
_FC_RESET=''; _FC_BOLD=''; _FC_DIM=''
|
||||
_FC_RED=''; _FC_GREEN=''; _FC_YELLOW=''
|
||||
_FC_CYAN=''; _FC_MAGENTA=''
|
||||
fi
|
||||
|
||||
# Width-6 tag inside ANSI envelope: pads "[ok]", "[err]" etc. to 6
|
||||
# visible columns so subsequent %-wNs fields stay aligned.
|
||||
_fc_tag() { printf '%s%-6s%s' "$1" "[$2]" "$_FC_RESET"; }
|
||||
|
||||
info() { printf '%s %s\n' "$(_fc_tag "$_FC_CYAN" info)" "$*" >&2; }
|
||||
ok() { printf '%s %s\n' "$(_fc_tag "$_FC_GREEN" ok )" "$*" >&2; }
|
||||
warn() { printf '%s %s\n' "$(_fc_tag "$_FC_YELLOW" warn)" "$*" >&2; }
|
||||
err() { printf '%s %s\n' "$(_fc_tag "$_FC_RED" err )" "$*" >&2; }
|
||||
step() { printf '\n%s==>%s %s%s%s\n' "$_FC_BOLD" "$_FC_RESET" "$_FC_BOLD" "$*" "$_FC_RESET" >&2; }
|
||||
note() { printf '%s%s%s\n' "$_FC_DIM" "$*" "$_FC_RESET" >&2; }
|
||||
die() { err "$@"; exit 1; }
|
||||
|
||||
# ---- repo locator ---------------------------------------------------
|
||||
repo_root() {
|
||||
local here
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[1]:-$0}")" && pwd -P)"
|
||||
local d="$here"
|
||||
for _ in 1 2 3; do
|
||||
if [ -f "$d/Justfile" ]; then
|
||||
printf '%s\n' "$d"
|
||||
return 0
|
||||
fi
|
||||
d="$(dirname "$d")"
|
||||
done
|
||||
die "cannot locate repo root (Justfile) from $here"
|
||||
}
|
||||
|
||||
# Load $repo/.env into the environment.
|
||||
# Existing environment values take precedence (matches just's dotenv-load).
|
||||
load_env() {
|
||||
local root env line key rest
|
||||
root="$(repo_root)"
|
||||
env="$root/.env"
|
||||
[ -f "$env" ] || return 0
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
# Strip CR from CRLF files and leading whitespace.
|
||||
line="${line%$'\r'}"
|
||||
line="${line#"${line%%[![:space:]]*}"}"
|
||||
case "$line" in
|
||||
''|'#'*) continue ;;
|
||||
*=*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
# Optional leading `export `.
|
||||
line="${line#export }"
|
||||
key="${line%%=*}"
|
||||
rest="${line#*=}"
|
||||
# Skip malformed keys.
|
||||
case "$key" in
|
||||
*[!A-Za-z0-9_]*|'') continue ;;
|
||||
esac
|
||||
# Existing env values win.
|
||||
if [ -n "${!key+x}" ]; then
|
||||
continue
|
||||
fi
|
||||
# Strip matching surrounding single or double quotes.
|
||||
case "$rest" in
|
||||
\"*\") rest="${rest#\"}"; rest="${rest%\"}" ;;
|
||||
\'*\') rest="${rest#\'}"; rest="${rest%\'}" ;;
|
||||
esac
|
||||
export "$key=$rest"
|
||||
done < "$env"
|
||||
}
|
||||
|
||||
# ---- prereq checks --------------------------------------------------
|
||||
require_cmd() {
|
||||
local cmd="$1" hint="${2:-}"
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
[ -n "$hint" ] && warn "install hint: $hint"
|
||||
die "required command not found: $cmd"
|
||||
fi
|
||||
}
|
||||
|
||||
require_env() {
|
||||
local name="$1"
|
||||
if [ -z "${!name:-}" ]; then
|
||||
die "environment variable '$name' is unset. Run 'just init-env' and fill .env."
|
||||
fi
|
||||
}
|
||||
221
scripts/doctor.sh
Executable file
221
scripts/doctor.sh
Executable file
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Verify prerequisites for every recipe. For each missing item, prints the
|
||||
# EXACT command to install it on the detected platform. Exits non-zero if
|
||||
# any hard requirement is missing; warns but continues for PATH hygiene
|
||||
# and the web-browser check (headless systems can still use login-headless).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
# shellcheck disable=SC1091
|
||||
. "$here/common.sh"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Platform detection
|
||||
# ---------------------------------------------------------------------------
|
||||
uname_s="$(uname -s 2>/dev/null || echo unknown)"
|
||||
pm="" # one of: apt dnf pacman zypper brew ""
|
||||
case "$uname_s" in
|
||||
Linux)
|
||||
if [ -r /etc/os-release ]; then
|
||||
# shellcheck disable=SC1091
|
||||
. /etc/os-release
|
||||
id_all=" ${ID:-} ${ID_LIKE:-} "
|
||||
case "$id_all" in
|
||||
*' debian '*|*' ubuntu '*) pm=apt ;;
|
||||
*' fedora '*|*' rhel '*|*' centos '*) pm=dnf ;;
|
||||
*' arch '*) pm=pacman ;;
|
||||
*' opensuse '*|*' suse '*) pm=zypper ;;
|
||||
esac
|
||||
fi
|
||||
if [ -z "$pm" ]; then
|
||||
for candidate in apt-get dnf pacman zypper; do
|
||||
if command -v "$candidate" >/dev/null 2>&1; then
|
||||
case "$candidate" in
|
||||
apt-get) pm=apt ;;
|
||||
dnf) pm=dnf ;;
|
||||
pacman) pm=pacman ;;
|
||||
zypper) pm=zypper ;;
|
||||
esac
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
;;
|
||||
Darwin)
|
||||
pm=brew
|
||||
;;
|
||||
esac
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Render a package-install command for the current platform.
|
||||
# pkg_cmd <apt> <dnf> <pacman> <brew> <upstream-url>
|
||||
# ---------------------------------------------------------------------------
|
||||
pkg_cmd() {
|
||||
local apt_pkg="$1" dnf_pkg="$2" pac_pkg="$3" brew_pkg="$4" url="$5"
|
||||
case "$pm" in
|
||||
apt) echo "sudo apt-get update && sudo apt-get install -y $apt_pkg" ;;
|
||||
dnf) echo "sudo dnf install -y $dnf_pkg" ;;
|
||||
pacman) echo "sudo pacman -S --noconfirm $pac_pkg" ;;
|
||||
zypper) echo "sudo zypper install -y $dnf_pkg" ;;
|
||||
brew) echo "brew install $brew_pkg" ;;
|
||||
*) echo "# install manually from: $url" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Accumulators
|
||||
# ---------------------------------------------------------------------------
|
||||
missing=0
|
||||
fix_commands=()
|
||||
|
||||
record_miss() {
|
||||
local name="$1" detail="$2" fix="$3"
|
||||
printf ' %s %-12s -> %s\n' "$(_fc_tag "$_FC_RED" miss)" "$name" "$detail"
|
||||
printf ' fix: %s\n' "$fix"
|
||||
fix_commands+=("$fix")
|
||||
missing=$((missing + 1))
|
||||
}
|
||||
|
||||
check_cmd() {
|
||||
local cmd="$1" fix="$2"
|
||||
if command -v "$cmd" >/dev/null 2>&1; then
|
||||
printf ' %s %-12s -> %s\n' "$(_fc_tag "$_FC_GREEN" ok)" "$cmd" "$(command -v "$cmd")"
|
||||
else
|
||||
record_miss "$cmd" "not found on PATH" "$fix"
|
||||
fi
|
||||
}
|
||||
|
||||
info 'checking prerequisites...'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Required tools
|
||||
# ---------------------------------------------------------------------------
|
||||
check_cmd git "$(pkg_cmd git git git git https://git-scm.com/)"
|
||||
check_cmd bash "$(pkg_cmd bash bash bash bash https://www.gnu.org/software/bash/)"
|
||||
check_cmd curl "$(pkg_cmd curl curl curl curl https://curl.se/)"
|
||||
check_cmd install "$(pkg_cmd coreutils coreutils coreutils coreutils https://www.gnu.org/software/coreutils/)"
|
||||
check_cmd python3 "$(pkg_cmd python3 python3 python python3 https://www.python.org/)"
|
||||
|
||||
# just and uv do not ship in most package managers, so prefer the upstream
|
||||
# installers (no sudo, land binaries in ~/.local/bin / ~/.cargo/bin).
|
||||
check_cmd just 'curl --proto "=https" --tlsv1.2 -LsSf https://just.systems/install.sh | bash -s -- --to "$HOME/.local/bin"'
|
||||
check_cmd uv 'curl -LsSf https://astral.sh/uv/install.sh | sh'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Web browser (warning only: headless is supported via login-headless)
|
||||
# ---------------------------------------------------------------------------
|
||||
if command -v xdg-open >/dev/null 2>&1 || command -v open >/dev/null 2>&1; then
|
||||
printf ' %s %-12s -> (detected)\n' "$(_fc_tag "$_FC_GREEN" ok)" 'web browser'
|
||||
else
|
||||
printf ' %s %-12s -> no xdg-open/open on PATH.\n' "$(_fc_tag "$_FC_YELLOW" warn)" 'web browser'
|
||||
printf ' if this is a headless machine, run: just login-headless\n'
|
||||
case "$pm" in
|
||||
apt) printf ' otherwise install: sudo apt-get install -y xdg-utils\n' ;;
|
||||
dnf) printf ' otherwise install: sudo dnf install -y xdg-utils\n' ;;
|
||||
pacman) printf ' otherwise install: sudo pacman -S --noconfirm xdg-utils\n' ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Python 3.11+ : accepted via system python3 OR uv-managed interpreter
|
||||
# ---------------------------------------------------------------------------
|
||||
py_ok=0
|
||||
py_via=""
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
if python3 - <<'PY' >/dev/null 2>&1
|
||||
import sys
|
||||
sys.exit(0 if sys.version_info >= (3, 11) else 1)
|
||||
PY
|
||||
then
|
||||
py_ok=1
|
||||
py_via="$(python3 -V 2>&1)"
|
||||
fi
|
||||
fi
|
||||
if [ "$py_ok" -eq 0 ] && command -v uv >/dev/null 2>&1; then
|
||||
if uv_py_path="$(uv python find '>=3.11' 2>/dev/null)" && [ -n "$uv_py_path" ]; then
|
||||
py_ok=1
|
||||
py_via="$uv_py_path (via uv)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$py_ok" -eq 1 ]; then
|
||||
printf ' %s %-12s -> %s\n' "$(_fc_tag "$_FC_GREEN" ok)" 'python>=3.11' "$py_via"
|
||||
else
|
||||
detail='python3 missing'
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
detail="python3 is $(python3 -V 2>&1 | awk '{print $2}'); need 3.11+"
|
||||
fi
|
||||
# Primary fix: uv-managed Python. Works on every platform, no sudo, and
|
||||
# the orchestrator (a uv project) will resolve it automatically via
|
||||
# `uv run` / `uv sync`.
|
||||
py_fix='uv python install 3.11'
|
||||
# Per-distro alternative (commented so users can copy one or the other):
|
||||
case "$pm" in
|
||||
apt)
|
||||
py_alt='sudo add-apt-repository -y ppa:deadsnakes/ppa && sudo apt-get update && sudo apt-get install -y python3.11 python3.11-venv'
|
||||
;;
|
||||
dnf)
|
||||
py_alt='sudo dnf install -y python3.11'
|
||||
;;
|
||||
pacman)
|
||||
py_alt='sudo pacman -S --noconfirm python'
|
||||
;;
|
||||
zypper)
|
||||
py_alt='sudo zypper install -y python311'
|
||||
;;
|
||||
brew)
|
||||
py_alt='brew install python@3.11'
|
||||
;;
|
||||
*)
|
||||
py_alt=''
|
||||
;;
|
||||
esac
|
||||
printf ' %s %-12s -> %s\n' "$(_fc_tag "$_FC_RED" miss)" 'python>=3.11' "$detail"
|
||||
printf ' fix: %s\n' "$py_fix"
|
||||
if [ -n "$py_alt" ]; then
|
||||
printf ' alt: %s\n' "$py_alt"
|
||||
fi
|
||||
fix_commands+=("$py_fix")
|
||||
missing=$((missing + 1))
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PATH hygiene: warning, not a hard miss.
|
||||
# ---------------------------------------------------------------------------
|
||||
case ":$PATH:" in
|
||||
*":$LOCAL_BIN:"*)
|
||||
printf ' %s %-12s -> %s is on PATH\n' "$(_fc_tag "$_FC_GREEN" ok)" 'PATH' "$LOCAL_BIN"
|
||||
;;
|
||||
*)
|
||||
rc='~/.bashrc'
|
||||
[ -n "${ZSH_VERSION:-}" ] && rc='~/.zshrc'
|
||||
printf ' %s %-12s -> %s is NOT on PATH.\n' "$(_fc_tag "$_FC_YELLOW" warn)" 'PATH' "$LOCAL_BIN"
|
||||
# shellcheck disable=SC2016
|
||||
printf " fix: echo 'export PATH=\"%s:\$PATH\"' >> %s && exec \$SHELL -l\n" "$LOCAL_BIN" "$rc"
|
||||
;;
|
||||
esac
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
if [ "$missing" -gt 0 ]; then
|
||||
printf '\n'
|
||||
warn "$missing prerequisite(s) missing. Run the following to fix them:"
|
||||
printf '\n'
|
||||
# De-duplicate while preserving order.
|
||||
seen=''
|
||||
for c in "${fix_commands[@]}"; do
|
||||
case "$seen" in
|
||||
*"<<$c>>"*) continue ;;
|
||||
esac
|
||||
seen="$seen<<$c>>"
|
||||
printf ' %s\n' "$c"
|
||||
done
|
||||
printf '\n'
|
||||
warn 'then re-run: just doctor'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info 'all prerequisites present'
|
||||
1041
scripts/forge_auth.py
Executable file
1041
scripts/forge_auth.py
Executable file
File diff suppressed because it is too large
Load Diff
29
scripts/forge_login.sh
Executable file
29
scripts/forge_login.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Run the interactive PKCE OAuth2 login against Gitea, then install
|
||||
# the git credential helper so subsequent git operations against
|
||||
# FORGE_GITEA_URL are silent. Idempotent.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
# shellcheck disable=SC1091
|
||||
. "$here/common.sh"
|
||||
|
||||
load_env
|
||||
require_env FORGE_GITEA_URL
|
||||
require_env FSDGG_CLI_CLIENT_ID
|
||||
require_cmd python3
|
||||
|
||||
# Forward every useful flag. --force re-runs PKCE even if an existing
|
||||
# live token is present. --no-browser prints the URL but skips the
|
||||
# webbrowser.open call (for headless / SSH sessions).
|
||||
python3 "$here/forge_auth.py" login "$@"
|
||||
|
||||
# Install the credential helper. Safe to run every time.
|
||||
"$here/install-git-credential-helper.sh"
|
||||
|
||||
ok "login complete"
|
||||
note "git operations against your Gitea server now authenticate silently."
|
||||
note "Test with:"
|
||||
note " git ls-remote \"\$FORGE_ORCHESTRATOR_REPO_URL\""
|
||||
222
scripts/git-credential-forge.py
Executable file
222
scripts/git-credential-forge.py
Executable file
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Git credential helper backed by the codevalet OAuth auth file.
|
||||
|
||||
Git invokes credential helpers with one of three verbs on argv[1]:
|
||||
|
||||
get : read key=value fields from stdin; emit credentials on stdout
|
||||
store : persist credentials (no-op here; tokens are OAuth-owned)
|
||||
erase : forget credentials (no-op here; ``just logout`` owns that)
|
||||
|
||||
On every ``get`` the helper:
|
||||
|
||||
1. Reads the auth file (same path the gateway uses:
|
||||
``~/.forge-stack-devpi-gateway-gitea/client-auth.json`` by default,
|
||||
or whatever ``FSDGG_AUTH_STORE_PATH`` / ``FSDGG_RUNTIME_DIR`` point
|
||||
at). If the file is missing, the helper passes git's input through
|
||||
unchanged so the normal prompt chain continues.
|
||||
2. Checks that the git request host matches the host recorded in
|
||||
``_forge_gitea_base_url`` (or ``FORGE_GITEA_URL``). If not, passes
|
||||
through: this ensures we never hand OAuth tokens to unrelated
|
||||
hosts even if git mis-scopes its lookup.
|
||||
3. If ``gitea_access_token`` is live, emits
|
||||
``username=<stored-user>`` and ``password=<gitea_access_token>``.
|
||||
4. If the token is expired and a refresh token is present, runs the
|
||||
OAuth refresh flow (``forge_auth.run_refresh``) and retries once.
|
||||
Refresh failures are surfaced by exiting non-zero; git then falls
|
||||
through to the user's configured helper chain (normally a prompt).
|
||||
|
||||
Security notes
|
||||
--------------
|
||||
* The helper never writes to stdout except the credential key=value
|
||||
block. Logs go to stderr.
|
||||
* On OAuth refresh failure we exit **non-zero** rather than silently
|
||||
returning stale credentials.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
|
||||
def _load_forge_auth_module():
|
||||
"""Import ``forge_auth`` from the same directory as this script.
|
||||
|
||||
This works both when the helper is invoked directly from
|
||||
``scripts/`` (development) and after installation under
|
||||
``~/.local/bin`` (because the installer copies ``forge_auth.py``
|
||||
alongside ``git-credential-forge``).
|
||||
"""
|
||||
here = Path(__file__).resolve().parent
|
||||
if str(here) not in sys.path:
|
||||
sys.path.insert(0, str(here))
|
||||
import forge_auth # noqa: E402
|
||||
|
||||
return forge_auth
|
||||
|
||||
|
||||
forge_auth = _load_forge_auth_module()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# git credential protocol I/O
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
def read_git_fields(stream) -> dict[str, str]:
|
||||
fields: dict[str, str] = {}
|
||||
for line in stream:
|
||||
line = line.rstrip("\n")
|
||||
if not line:
|
||||
break
|
||||
if "=" not in line:
|
||||
raise ValueError(f"unexpected git credential input line: {line!r}")
|
||||
key, _, value = line.partition("=")
|
||||
fields[key] = value
|
||||
return fields
|
||||
|
||||
|
||||
def emit(fields: dict[str, str]) -> None:
|
||||
for key, value in fields.items():
|
||||
sys.stdout.write(f"{key}={value}\n")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Host matching
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
def _configured_host() -> tuple[str, str, int | None] | None:
|
||||
"""Return (scheme, host, port) of the host this helper is allowed
|
||||
to produce credentials for.
|
||||
|
||||
Priority:
|
||||
1. The ``_forge_gitea_base_url`` field inside the stored auth
|
||||
file: that is the host we actually authenticated against.
|
||||
2. The ``FORGE_GITEA_URL`` env var (pre-login override).
|
||||
Returns None if neither is set; the helper then passes through.
|
||||
"""
|
||||
store = forge_auth.auth_store_path()
|
||||
state = forge_auth.AuthFile.read(store)
|
||||
stored_url = str(state.raw.get("_forge_gitea_base_url") or "").strip()
|
||||
if stored_url:
|
||||
parsed = urlsplit(stored_url)
|
||||
return parsed.scheme.lower(), (parsed.hostname or "").lower(), parsed.port
|
||||
env_url = os.environ.get("FORGE_GITEA_URL", "").strip()
|
||||
if env_url:
|
||||
parsed = urlsplit(env_url)
|
||||
return parsed.scheme.lower(), (parsed.hostname or "").lower(), parsed.port
|
||||
return None
|
||||
|
||||
|
||||
def _request_matches(
|
||||
fields: dict[str, str], configured: tuple[str, str, int | None]
|
||||
) -> bool:
|
||||
scheme, host, port = configured
|
||||
git_scheme = fields.get("protocol", "").lower()
|
||||
git_host = fields.get("host", "").lower()
|
||||
# git passes host:port as "host" when the URL carried a port.
|
||||
if ":" in git_host:
|
||||
host_part, _, port_str = git_host.partition(":")
|
||||
try:
|
||||
git_port = int(port_str)
|
||||
except ValueError:
|
||||
return False
|
||||
git_host = host_part
|
||||
else:
|
||||
git_port = None
|
||||
if git_scheme != scheme:
|
||||
return False
|
||||
if git_host != host:
|
||||
return False
|
||||
if port is not None and git_port != port:
|
||||
return False
|
||||
if port is None and git_port is not None:
|
||||
# Stored URL had no explicit port (default 443) but request does;
|
||||
# only match the default-HTTPS case.
|
||||
if scheme == "https" and git_port != 443:
|
||||
return False
|
||||
if scheme == "http" and git_port != 80:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Commands
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
|
||||
def cmd_get(fields: dict[str, str]) -> int:
|
||||
configured = _configured_host()
|
||||
if configured is None:
|
||||
emit(fields) # pass-through: we don't know who we answer for
|
||||
return 0
|
||||
if not _request_matches(fields, configured):
|
||||
emit(fields) # pass-through: request is for a different host
|
||||
return 0
|
||||
|
||||
state = forge_auth.AuthFile.read(forge_auth.auth_store_path())
|
||||
|
||||
# Fast path: stored access token is still live.
|
||||
if state.has_live_gitea_token():
|
||||
_emit_credentials(fields, state)
|
||||
return 0
|
||||
|
||||
# Slow path: try to refresh.
|
||||
try:
|
||||
config = forge_auth.ForgeAuthConfig.from_env()
|
||||
except forge_auth.AuthError:
|
||||
forge_auth.cli_warn(
|
||||
"stored token is expired and FORGE_GITEA_URL / "
|
||||
"FSDGG_CLI_CLIENT_ID are not set in the environment; "
|
||||
"cannot refresh. Run 'just login' to re-authenticate."
|
||||
)
|
||||
emit(fields)
|
||||
return 0
|
||||
try:
|
||||
refreshed = forge_auth.run_refresh(config, must_refresh=True)
|
||||
except forge_auth.AuthError as exc:
|
||||
forge_auth.cli_warn(
|
||||
f"token refresh failed: {exc}. "
|
||||
f"Run 'just login' to re-authenticate."
|
||||
)
|
||||
emit(fields)
|
||||
return 0
|
||||
_emit_credentials(fields, refreshed)
|
||||
return 0
|
||||
|
||||
|
||||
def _emit_credentials(fields: dict[str, str], state: forge_auth.AuthFile) -> None:
|
||||
token = state.gitea_access_token
|
||||
if not token:
|
||||
emit(fields)
|
||||
return
|
||||
out = dict(fields)
|
||||
out["username"] = state.username or fields.get("username") or "oauth"
|
||||
out["password"] = token
|
||||
emit(out)
|
||||
|
||||
|
||||
def cmd_consume_noop(stream) -> int:
|
||||
for _ in stream:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
if len(argv) < 2:
|
||||
print("usage: git-credential-forge <get|store|erase>", file=sys.stderr)
|
||||
return 2
|
||||
action = argv[1]
|
||||
if action == "get":
|
||||
fields = read_git_fields(sys.stdin)
|
||||
return cmd_get(fields)
|
||||
if action in ("store", "erase"):
|
||||
return cmd_consume_noop(sys.stdin)
|
||||
print(f"git-credential-forge: unsupported action: {action}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
52
scripts/install-git-credential-helper.sh
Executable file
52
scripts/install-git-credential-helper.sh
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Install ~/.local/bin/git-credential-forge (copies both
|
||||
# git-credential-forge.py and its forge_auth.py companion module) and
|
||||
# wire `git config --global credential.<FORGE_GITEA_URL>.*` so git
|
||||
# calls the helper for every matching URL. Idempotent.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
# shellcheck disable=SC1091
|
||||
. "$here/common.sh"
|
||||
|
||||
load_env
|
||||
require_env FORGE_GITEA_URL
|
||||
require_cmd git
|
||||
require_cmd python3
|
||||
|
||||
helper_src="$here/git-credential-forge.py"
|
||||
module_src="$here/forge_auth.py"
|
||||
[ -f "$helper_src" ] || die "missing helper source: $helper_src"
|
||||
[ -f "$module_src" ] || die "missing module source: $module_src"
|
||||
|
||||
mkdir -p "$LOCAL_BIN"
|
||||
install -m 0755 "$helper_src" "$CRED_HELPER"
|
||||
install -m 0644 "$module_src" "$LOCAL_BIN/forge_auth.py"
|
||||
info "installed credential helper -> $CRED_HELPER"
|
||||
info "installed module -> $LOCAL_BIN/forge_auth.py"
|
||||
|
||||
# Syntax check both installed artefacts before the first git invocation.
|
||||
python3 -c 'import sys; compile(open(sys.argv[1]).read(), sys.argv[1], "exec")' "$CRED_HELPER"
|
||||
python3 -c 'import sys; compile(open(sys.argv[1]).read(), sys.argv[1], "exec")' "$LOCAL_BIN/forge_auth.py"
|
||||
|
||||
# Scope to FORGE_GITEA_URL only. git-config(1): credential.<URL>.*
|
||||
# applies only when git's resolved credential URL matches. Other hosts
|
||||
# are not affected.
|
||||
key="credential.${FORGE_GITEA_URL}.helper"
|
||||
use_path_key="credential.${FORGE_GITEA_URL}.useHttpPath"
|
||||
|
||||
# Wipe any previous helpers we may have set so we do not stack them.
|
||||
git config --global --unset-all "$key" 2>/dev/null || true
|
||||
git config --global --unset-all "$use_path_key" 2>/dev/null || true
|
||||
|
||||
# Username is derived from the OAuth token at runtime by the helper; we
|
||||
# deliberately do NOT set credential.<URL>.username here, to avoid
|
||||
# pinning an old login. The helper emits username=<stored> on every
|
||||
# `get`, which git respects.
|
||||
git config --global --add "$key" "$CRED_HELPER"
|
||||
git config --global "$use_path_key" true
|
||||
|
||||
info "git config --global $key -> $CRED_HELPER"
|
||||
info "git config --global $use_path_key -> true"
|
||||
134
scripts/next_steps.sh
Executable file
134
scripts/next_steps.sh
Executable file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Render or execute the contributor-setup plan defined by the
|
||||
# orchestrator's scripts/contributor_setup_steps.json. The welcome
|
||||
# repo does not hardcode step lists.
|
||||
#
|
||||
# Modes:
|
||||
# default : print the plan.
|
||||
# --run : exec `just contributor-setup` inside the orchestrator.
|
||||
#
|
||||
# Flags (forwarded to the orchestrator under --run):
|
||||
# --headless, --no-browser headless mode
|
||||
# --yes non-interactive
|
||||
# --dry-run, --plan-only plan-only execution
|
||||
# --skip-optional skip optional steps
|
||||
# --only ID[,ID...] run the listed step ids
|
||||
# -h, --help usage
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
# shellcheck disable=SC1091
|
||||
. "$here/common.sh"
|
||||
|
||||
load_env
|
||||
|
||||
MODE="print" # "print" or "run"
|
||||
forward_args=()
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: just next-steps [--run] [flags]
|
||||
just run-next-steps [flags]
|
||||
|
||||
Flags: --headless, --no-browser, --yes, --dry-run, --skip-optional,
|
||||
--only ID[,ID...]
|
||||
|
||||
Step list source: <orchestrator>/scripts/contributor_setup_steps.json.
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--run) MODE=run; shift ;;
|
||||
--headless|--no-browser) forward_args+=("--headless"); shift ;;
|
||||
--yes) forward_args+=("--yes"); shift ;;
|
||||
--dry-run|--plan-only) forward_args+=("--dry-run"); shift ;;
|
||||
--skip-optional) forward_args+=("--skip-optional"); shift ;;
|
||||
--only) shift; [ $# -gt 0 ] || die "--only needs an argument"; forward_args+=("--only" "$1"); shift ;;
|
||||
--only=*) forward_args+=("--only" "${1#--only=}"); shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--) shift; break ;;
|
||||
*) die "unexpected argument: $1 (try --help)" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Locate the orchestrator checkout.
|
||||
workspace_root="${FORGE_WORKSPACE_ROOT:-.}"
|
||||
repo_url="${FORGE_ORCHESTRATOR_REPO_URL:-}"
|
||||
if [ -z "$repo_url" ]; then
|
||||
die "FORGE_ORCHESTRATOR_REPO_URL is unset; run 'just init-env' and edit .env"
|
||||
fi
|
||||
repo_name="$(basename "$repo_url" .git)"
|
||||
orchestrator="$workspace_root/$repo_name"
|
||||
|
||||
if [ ! -d "$orchestrator/.git" ]; then
|
||||
err "orchestrator checkout not found at $orchestrator"
|
||||
note "run 'just clone-orchestrator' (or 'just setup') first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
manifest="$orchestrator/scripts/contributor_setup_steps.json"
|
||||
if [ ! -f "$manifest" ]; then
|
||||
warn "orchestrator checkout at $orchestrator does not ship"
|
||||
warn " scripts/contributor_setup_steps.json (expected in newer versions)."
|
||||
note "falling back to the orchestrator README; open:"
|
||||
note " $orchestrator/README.md"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
step "next steps (source: orchestrator manifest)"
|
||||
note "source: $manifest"
|
||||
note "orchestrator: $orchestrator"
|
||||
printf '\n' >&2
|
||||
|
||||
python3 - "$manifest" >&2 <<'PY'
|
||||
import json, sys, textwrap
|
||||
path = sys.argv[1]
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if data.get("schema_version") != 1:
|
||||
sys.stderr.write(
|
||||
f"[err] unsupported contributor_setup_steps schema: "
|
||||
f"{data.get('schema_version')!r}\n"
|
||||
)
|
||||
sys.exit(2)
|
||||
steps = data.get("steps", [])
|
||||
total = len(steps)
|
||||
for i, s in enumerate(steps, 1):
|
||||
sid = s["id"]
|
||||
title = s.get("title", sid)
|
||||
desc = s.get("desc", "")
|
||||
cmd = " ".join(s["cmd"])
|
||||
opt = "" if "optional" in title.lower() else (" [optional]" if s.get("optional") else "")
|
||||
sys.stderr.write(f" ({i}/{total}) {title}{opt}\n")
|
||||
sys.stderr.write(f" id: {sid}\n")
|
||||
if desc:
|
||||
for line in textwrap.wrap(desc, width=76,
|
||||
initial_indent=" ",
|
||||
subsequent_indent=" "):
|
||||
sys.stderr.write(line + "\n")
|
||||
sys.stderr.write(f" run: {cmd}\n\n")
|
||||
PY
|
||||
|
||||
if [ "$MODE" != "run" ]; then
|
||||
# ``${arr:+...}`` on an empty array triggers set -u; build explicitly.
|
||||
fwd_suffix=""
|
||||
if [ "${#forward_args[@]}" -gt 0 ]; then
|
||||
fwd_suffix=" ${forward_args[*]}"
|
||||
fi
|
||||
note "Execute via:"
|
||||
note " just run-next-steps${fwd_suffix}"
|
||||
note "Execute inside the orchestrator:"
|
||||
note " cd $orchestrator && just contributor-setup${fwd_suffix}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
step "exec just contributor-setup (cwd=$orchestrator)"
|
||||
if ! command -v just >/dev/null 2>&1; then
|
||||
die "just not on PATH; install via 'just doctor' fix hint"
|
||||
fi
|
||||
cd "$orchestrator"
|
||||
exec just contributor-setup "${forward_args[@]}"
|
||||
276
scripts/setup.sh
Executable file
276
scripts/setup.sh
Executable file
@@ -0,0 +1,276 @@
|
||||
#!/usr/bin/env bash
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Interactive onboarding driver. Sequences doctor, init-env,
|
||||
# check-gitea, login, check-access, and clone-orchestrator, prompting
|
||||
# only when a decision requires a human.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
# shellcheck disable=SC1091
|
||||
. "$here/common.sh"
|
||||
|
||||
root="$(repo_root)"
|
||||
cd "$root"
|
||||
|
||||
headless=0
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: just setup [--headless|--no-browser]
|
||||
|
||||
Options:
|
||||
--headless Do not open the browser during login. Prints the
|
||||
--no-browser authorisation URL to stderr instead; paste it into
|
||||
any browser that can reach the loopback callback
|
||||
port (typically via SSH port-forward, see README).
|
||||
-h, --help Show this message.
|
||||
|
||||
Environment:
|
||||
FORGE_SETUP_YES=1 Accept every default; do not prompt. Safe only when
|
||||
FORGE_GITEA_USERNAME is already set in .env.
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--headless|--no-browser) headless=1; shift;;
|
||||
-h|--help) usage; exit 0;;
|
||||
--) shift; break;;
|
||||
-*) die "unknown option: $1 (try 'just setup --help')";;
|
||||
*) die "unexpected positional argument: $1 (try 'just setup --help')";;
|
||||
esac
|
||||
done
|
||||
|
||||
# info/ok/warn/err/step/note/die: see scripts/common.sh.
|
||||
|
||||
# Stdout = chosen reply; stderr = prompt/echo.
|
||||
prompt_choice() {
|
||||
local msg="$1" default="$2" reply
|
||||
if [ "${FORGE_SETUP_YES:-0}" = "1" ] || [ ! -t 0 ]; then
|
||||
printf '%s %s [%s] (auto: %s)\n' \
|
||||
"$(_fc_tag "$_FC_MAGENTA" '?')" "$msg" "$default" "$default" >&2
|
||||
reply="$default"
|
||||
else
|
||||
printf '%s %s [%s] ' \
|
||||
"$(_fc_tag "$_FC_MAGENTA" '?')" "$msg" "$default" >/dev/tty
|
||||
IFS= read -r reply </dev/tty || reply=""
|
||||
[ -z "$reply" ] && reply="$default"
|
||||
fi
|
||||
printf '%s' "$reply"
|
||||
}
|
||||
|
||||
prompt_line() {
|
||||
local msg="$1" reply
|
||||
if [ "${FORGE_SETUP_YES:-0}" = "1" ] || [ ! -t 0 ]; then
|
||||
die "non-interactive mode requested ($msg) but no default is safe; run 'just setup' in a terminal"
|
||||
fi
|
||||
printf '%s %s: ' "$(_fc_tag "$_FC_MAGENTA" '?')" "$msg" >/dev/tty
|
||||
IFS= read -r reply </dev/tty || reply=""
|
||||
printf '%s' "$reply"
|
||||
}
|
||||
|
||||
step "1/7 checking prerequisites (just doctor)"
|
||||
bash "$here/doctor.sh"
|
||||
|
||||
step "2/7 preparing .env"
|
||||
if [ ! -f "$root/.env" ]; then
|
||||
cp "$root/.env.example" "$root/.env"
|
||||
ok "wrote $root/.env from template"
|
||||
else
|
||||
ok ".env already present"
|
||||
fi
|
||||
|
||||
load_env
|
||||
|
||||
if [ -z "${FORGE_GITEA_USERNAME:-}" ]; then
|
||||
note "FORGE_GITEA_USERNAME is blank. Enter your Gitea username exactly as it appears in your profile URL."
|
||||
new_user="$(prompt_line "Gitea username")"
|
||||
if [ -z "$new_user" ]; then
|
||||
die "no username entered: aborting. Edit .env by hand and re-run 'just setup'."
|
||||
fi
|
||||
python3 - "$root/.env" "$new_user" <<'PY'
|
||||
import pathlib, sys
|
||||
path = pathlib.Path(sys.argv[1])
|
||||
value = sys.argv[2]
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
touched = False
|
||||
for i, line in enumerate(lines):
|
||||
stripped = line.lstrip()
|
||||
if stripped.startswith("FORGE_GITEA_USERNAME=") or stripped.startswith("export FORGE_GITEA_USERNAME="):
|
||||
prefix = "export " if stripped.startswith("export ") else ""
|
||||
lines[i] = f'{prefix}FORGE_GITEA_USERNAME="{value}"'
|
||||
touched = True
|
||||
break
|
||||
if not touched:
|
||||
lines.append(f'FORGE_GITEA_USERNAME="{value}"')
|
||||
tmp = path.with_suffix(path.suffix + ".tmp")
|
||||
tmp.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
tmp.replace(path)
|
||||
PY
|
||||
export FORGE_GITEA_USERNAME="$new_user"
|
||||
ok "set FORGE_GITEA_USERNAME=$new_user in .env"
|
||||
else
|
||||
ok "FORGE_GITEA_USERNAME=$FORGE_GITEA_USERNAME"
|
||||
fi
|
||||
|
||||
step "3/7 checking Gitea reachability (just check-gitea)"
|
||||
curl -fsS --max-time 10 ${FORGE_INSECURE_TLS:+-k} \
|
||||
"${FORGE_GITEA_URL}/api/v1/version" \
|
||||
| python3 -c 'import json,sys; d=json.load(sys.stdin); print("[check-gitea] Gitea version:", d.get("version","?"))'
|
||||
ok "reached $FORGE_GITEA_URL"
|
||||
|
||||
step "4/7 authenticating with Gitea (just login)"
|
||||
existing_user=""
|
||||
have_live=0
|
||||
have_stored=0
|
||||
if [ -f "$FORGE_AUTH_FILE" ]; then
|
||||
have_stored=1
|
||||
existing_user="$(python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(open(sys.argv[1]))
|
||||
print(d.get("username") or "")
|
||||
except Exception:
|
||||
print("")
|
||||
' "$FORGE_AUTH_FILE" 2>/dev/null || true)"
|
||||
if python3 "$here/forge_auth.py" status >/dev/null 2>&1; then
|
||||
have_live=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$have_stored" = "1" ] && [ "$have_live" = "0" ] \
|
||||
&& [ -n "$existing_user" ] && [ -n "${FORGE_GITEA_USERNAME:-}" ] \
|
||||
&& [ "${existing_user,,}" = "${FORGE_GITEA_USERNAME,,}" ]; then
|
||||
note "stored session for '$existing_user' is not live; attempting silent refresh..."
|
||||
if python3 "$here/forge_auth.py" refresh --force >/dev/null 2>&1; then
|
||||
ok "refreshed stored session without a browser"
|
||||
have_live=1
|
||||
else
|
||||
note "refresh failed (refresh token likely expired); a fresh login is required"
|
||||
fi
|
||||
fi
|
||||
|
||||
run_login=1
|
||||
if [ "$have_live" = "1" ] && [ -n "$existing_user" ]; then
|
||||
if [ -n "${FORGE_GITEA_USERNAME:-}" ] \
|
||||
&& [ "${existing_user,,}" != "${FORGE_GITEA_USERNAME,,}" ]; then
|
||||
note "stored token is for '$existing_user' but .env configures '$FORGE_GITEA_USERNAME'"
|
||||
reply="$(prompt_choice "Log out and sign in as '$FORGE_GITEA_USERNAME'? [Y/n]" "Y")"
|
||||
case "$reply" in
|
||||
[Nn]*) run_login=0; note "keeping the existing '$existing_user' session (forge_login may still reject it)";;
|
||||
*) python3 "$here/forge_auth.py" logout >/dev/null; ok "cleared stored Gitea tokens";;
|
||||
esac
|
||||
else
|
||||
ok "stored token is live for '$existing_user'"
|
||||
reply="$(prompt_choice "Reuse this session, or log out and re-authenticate? [R]euse / [L]ogout [R]" "R")"
|
||||
case "$reply" in
|
||||
[Ll]*) python3 "$here/forge_auth.py" logout >/dev/null; ok "cleared stored Gitea tokens";;
|
||||
*) run_login=0; ok "reusing the stored session";;
|
||||
esac
|
||||
fi
|
||||
elif [ "$have_stored" = "1" ] && [ -n "$existing_user" ]; then
|
||||
note "stored session for '$existing_user' is stale and could not be refreshed silently; re-authenticating."
|
||||
fi
|
||||
|
||||
if [ "$run_login" = "1" ]; then
|
||||
if [ "$headless" = "1" ] && [ "${FORGE_SETUP_YES:-0}" = "1" ]; then
|
||||
die "cannot complete a fresh login under --headless + FORGE_SETUP_YES=1:
|
||||
a browser-based OAuth step is required but both flags
|
||||
forbid any interaction. Choose one:
|
||||
1. Drop FORGE_SETUP_YES and re-run 'just setup --headless'
|
||||
(setup will print the authorisation URL and wait
|
||||
while you paste it into a browser).
|
||||
2. Run 'just login' once on a machine with a browser
|
||||
to populate the stored refresh token, then re-run
|
||||
'just setup --headless' from the headless host.
|
||||
3. Run 'just setup' (with a browser available) instead."
|
||||
fi
|
||||
if [ "$headless" = "1" ]; then
|
||||
bash "$here/forge_login.sh" --no-browser
|
||||
else
|
||||
bash "$here/forge_login.sh"
|
||||
fi
|
||||
else
|
||||
bash "$here/install-git-credential-helper.sh" >/dev/null
|
||||
fi
|
||||
|
||||
step "5/7 confirming silent git access (just check-access)"
|
||||
GIT_TERMINAL_PROMPT=0 GCM_INTERACTIVE=Never \
|
||||
GIT_ASKPASS="" SSH_ASKPASS="" \
|
||||
VSCODE_GIT_ASKPASS_MAIN="" VSCODE_GIT_ASKPASS_NODE="" \
|
||||
VSCODE_GIT_ASKPASS_EXTRA_ARGS="" VSCODE_GIT_IPC_HANDLE="" \
|
||||
DISPLAY="" WAYLAND_DISPLAY="" \
|
||||
git ls-remote "$FORGE_ORCHESTRATOR_REPO_URL" HEAD >/dev/null 2>&1 \
|
||||
|| die "git ls-remote failed. Your account may not yet be in the org, or FORGE_ORCHESTRATOR_REPO_URL is wrong."
|
||||
ok "silent clone access confirmed"
|
||||
|
||||
step "6/7 cloning the orchestrator (just clone-orchestrator)"
|
||||
workspace_root="${FORGE_WORKSPACE_ROOT:-.}"
|
||||
repo_name="$(basename "$FORGE_ORCHESTRATOR_REPO_URL" .git)"
|
||||
dest="$workspace_root/$repo_name"
|
||||
|
||||
if [ -d "$dest/.git" ]; then
|
||||
note "orchestrator already present at $dest"
|
||||
reply="$(prompt_choice "Reuse existing checkout, or wipe and re-clone? [R]euse / [W]ipe [R]" "R")"
|
||||
case "$reply" in
|
||||
[Ww]*)
|
||||
note "removing $dest"
|
||||
rm -rf "$dest"
|
||||
;;
|
||||
*)
|
||||
ok "keeping existing checkout"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ ! -d "$dest/.git" ]; then
|
||||
mkdir -p "$workspace_root"
|
||||
export GIT_TERMINAL_PROMPT=0 GCM_INTERACTIVE=Never
|
||||
unset GIT_ASKPASS SSH_ASKPASS \
|
||||
VSCODE_GIT_ASKPASS_MAIN VSCODE_GIT_ASKPASS_NODE \
|
||||
VSCODE_GIT_ASKPASS_EXTRA_ARGS VSCODE_GIT_IPC_HANDLE \
|
||||
DISPLAY WAYLAND_DISPLAY
|
||||
if [ -n "${FORGE_ORCHESTRATOR_BRANCH:-}" ]; then
|
||||
git clone --branch "$FORGE_ORCHESTRATOR_BRANCH" \
|
||||
"$FORGE_ORCHESTRATOR_REPO_URL" "$dest"
|
||||
else
|
||||
git clone "$FORGE_ORCHESTRATOR_REPO_URL" "$dest"
|
||||
fi
|
||||
ok "cloned into $dest"
|
||||
fi
|
||||
|
||||
step "7/7 next steps (from the orchestrator's manifest)"
|
||||
manifest_path="$dest/scripts/contributor_setup_steps.json"
|
||||
if [ -f "$manifest_path" ]; then
|
||||
# Authoritative step list: $manifest_path in the orchestrator checkout.
|
||||
# This welcome repo never hardcodes the sequence.
|
||||
bash "$here/next_steps.sh" || true
|
||||
|
||||
# Offer to run the plan now (respect --headless and FORGE_SETUP_YES).
|
||||
forward=()
|
||||
[ "$headless" = "1" ] && forward+=("--headless")
|
||||
[ "${FORGE_SETUP_YES:-0}" = "1" ] && forward+=("--yes")
|
||||
|
||||
reply="$(prompt_choice "Run the orchestrator's contributor-setup now? [y/N]" "N")"
|
||||
case "$reply" in
|
||||
[Yy]*)
|
||||
step "handing off to the orchestrator"
|
||||
exec bash "$here/next_steps.sh" --run "${forward[@]}"
|
||||
;;
|
||||
*)
|
||||
note "skipping auto-run. To execute later:"
|
||||
if [ "${#forward[@]}" -gt 0 ]; then
|
||||
note " just run-next-steps ${forward[*]}"
|
||||
else
|
||||
note " just run-next-steps"
|
||||
fi
|
||||
note "or, inside the orchestrator checkout:"
|
||||
note " cd $dest && just contributor-setup"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
warn "orchestrator at $dest does not ship contributor_setup_steps.json"
|
||||
warn "(older checkout?). See the orchestrator's README for onboarding:"
|
||||
note " $dest/README.md"
|
||||
fi
|
||||
40
scripts/uninstall.sh
Executable file
40
scripts/uninstall.sh
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Reverse every side effect `just login` produced. Safe to re-run.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
# shellcheck disable=SC1091
|
||||
. "$here/common.sh"
|
||||
|
||||
load_env
|
||||
|
||||
# 1. Log out at the OAuth layer (clears our managed fields from
|
||||
# client-auth.json and keeps any fields owned by the orchestrator
|
||||
# gateway untouched).
|
||||
if [ -f "$here/forge_auth.py" ]; then
|
||||
python3 "$here/forge_auth.py" logout || true
|
||||
fi
|
||||
|
||||
# 2. Remove git credential helper + git config entries scoped to the
|
||||
# Gitea URL.
|
||||
if [ -n "${FORGE_GITEA_URL:-}" ]; then
|
||||
for k in helper username useHttpPath; do
|
||||
git config --global --unset-all "credential.${FORGE_GITEA_URL}.$k" 2>/dev/null || true
|
||||
done
|
||||
git config --global --remove-section "credential.${FORGE_GITEA_URL}" 2>/dev/null || true
|
||||
info "cleared git credential.${FORGE_GITEA_URL}.*"
|
||||
fi
|
||||
|
||||
# 3. Remove installed artefacts.
|
||||
for f in "$CRED_HELPER" "$LOCAL_BIN/forge_auth.py"; do
|
||||
if [ -e "$f" ]; then
|
||||
rm -f "$f"
|
||||
info "removed $f"
|
||||
fi
|
||||
done
|
||||
|
||||
info 'uninstall complete.'
|
||||
info "note: $FORGE_AUTH_FILE is left in place if the orchestrator wrote it;"
|
||||
info ' delete it manually if you want a completely clean state.'
|
||||
Reference in New Issue
Block a user