Initial Commit

This commit is contained in:
FanaticPythoner (Nathan Trudeau)
2026-04-27 15:56:43 -04:00
parent 81b23fb2b2
commit 0c159e91fb
25 changed files with 5729 additions and 0 deletions

314
scripts/setup.sh Executable file
View File

@@ -0,0 +1,314 @@
#!/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
deploy=0
usage() {
cat <<'USAGE'
Usage: just setup [--headless|--no-browser] [--deploy] [--yes|-y]
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).
--deploy After cloning the orchestrator, prompt [Y/n]
(default Y) to exec the orchestrator's
`just operator-setup` runner (reads
operator_setup_steps.json). Equivalent to
`just deploy`. Without --deploy, setup prints the
operator plan and stops after the clone with no
prompt and no auto-run.
--yes, -y Auto-accept every prompt (session reuse, checkout
reuse, handoff confirmation) by setting
FORGE_SETUP_YES=1 for this run. Safe only when
FORGE_GITEA_USERNAME is already set in .env. Does
not relax the --headless guard (a fresh PKCE login
still requires either a browser or a pre-populated
refresh token).
-h, --help Show this message.
Environment:
FORGE_SETUP_YES=1 Same as --yes; honoured even when no flag is given.
USAGE
}
while [ $# -gt 0 ]; do
case "$1" in
--headless|--no-browser) headless=1; shift;;
--deploy) deploy=1; shift;;
--yes|-y) export FORGE_SETUP_YES=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 the Gitea username exactly as it appears in the 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. Recovery options:
1. Drop FORGE_SETUP_YES and re-run 'just setup --headless'
(setup prints the authorisation URL and waits for
browser completion).
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. The Gitea 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 operator manifest)"
# The operator scaffold is pinned to the operator manifest/recipe pair;
# `--deploy` only controls whether to prompt for an automatic handoff.
# The contributor manifest is never referenced from this scaffold.
manifest_name="operator_setup_steps.json"
recipe_name="operator-setup"
manifest_path="$dest/scripts/$manifest_name"
if [ ! -f "$manifest_path" ]; then
warn "orchestrator at $dest does not ship $manifest_name"
warn "(older checkout?). See the orchestrator's README for onboarding:"
note " $dest/README.md"
exit 0
fi
# Authoritative step list: $manifest_path in the orchestrator checkout.
# This welcome repo never hardcodes the sequence.
bash "$here/next_steps.sh" --manifest "$manifest_name" --recipe "$recipe_name" || true
forward=()
[ "$headless" = "1" ] && forward+=("--headless")
[ "${FORGE_SETUP_YES:-0}" = "1" ] && forward+=("--yes")
if [ "$deploy" != "1" ]; then
# `just setup` (no --deploy) prints the plan and stops: no prompt,
# no auto-run. The hint below is informational only.
note "skipping auto-run (no --deploy). To execute the plan later:"
if [ "${#forward[@]}" -gt 0 ]; then
note " just run-next-steps --manifest $manifest_name --recipe $recipe_name ${forward[*]}"
else
note " just run-next-steps --manifest $manifest_name --recipe $recipe_name"
fi
note "or, inside the orchestrator checkout:"
note " cd $dest && just $recipe_name"
exit 0
fi
# --deploy path: prompt [Y/n] (default Y). FORGE_SETUP_YES=1 accepts Y.
reply="$(prompt_choice "Hand off to just $recipe_name now? [Y/n]" "Y")"
case "$reply" in
[Nn]*)
note "skipping auto-run. To execute later:"
if [ "${#forward[@]}" -gt 0 ]; then
note " just run-next-steps --manifest $manifest_name --recipe $recipe_name ${forward[*]}"
else
note " just run-next-steps --manifest $manifest_name --recipe $recipe_name"
fi
note "or, inside the orchestrator checkout:"
note " cd $dest && just $recipe_name"
;;
*)
step "handing off to the orchestrator"
exec bash "$here/next_steps.sh" --run --manifest "$manifest_name" --recipe "$recipe_name" "${forward[@]}"
;;
esac