Wire in GPU optimizations and Gitea↔GitHub sync
Ollama config (all 3 setup scripts): - Models updated from Qwen 2.5 → Qwen 3.5 series (Feb 2026) - Auto-detect multi-GPU: TOTAL_VRAM = per-card × count - KV cache quantization (q8_0) + flash attention enabled by default - Context windows scaled by total VRAM (4K→128K) - RAG server CHAT_MODEL now uses detected model variable Gitea↔GitHub sync (new): - gitea-github-sync.sh: bidirectional mirror with --init wizard - Modes: --pull-only, --push-only, --list (dry run), --repo single - Auto-discovers repos from both platforms via API - Systemd timer: --install-timer [interval] for scheduled sync - MCP tool: gitea_github_sync() for on-demand from Claude/WebUI - Sync script mounted read-only into mcp-server container - .env gets GITEA_URL variable for sync script - curl added to mcp-server container deps https://claude.ai/code/session_01PtYTPherSJaxDEVPgF6Nxu
This commit is contained in:
Executable
+464
@@ -0,0 +1,464 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# Gitea ↔ GitHub Mirror Sync
|
||||||
|
#
|
||||||
|
# Mirrors repos between your local Gitea and GitHub in both directions:
|
||||||
|
# GitHub → Gitea: Pulls repos you own on GitHub into Gitea (backup/offline use)
|
||||||
|
# Gitea → GitHub: Pushes Gitea repos to GitHub (remote backup)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./gitea-github-sync.sh — sync all configured repos
|
||||||
|
# ./gitea-github-sync.sh --pull-only — GitHub → Gitea only
|
||||||
|
# ./gitea-github-sync.sh --push-only — Gitea → GitHub only
|
||||||
|
# ./gitea-github-sync.sh --repo owner/name — sync one specific repo
|
||||||
|
# ./gitea-github-sync.sh --list — list what would sync (dry run)
|
||||||
|
# ./gitea-github-sync.sh --init — interactive first-time setup
|
||||||
|
#
|
||||||
|
# Config: ~/.config/gitea-github-sync/config
|
||||||
|
# Tokens: reads from .env in the same directory as this script (or $SYNC_ENV)
|
||||||
|
#
|
||||||
|
# Schedule: install the systemd timer with --install-timer
|
||||||
|
# ./gitea-github-sync.sh --install-timer — every 6 hours (default)
|
||||||
|
# ./gitea-github-sync.sh --install-timer 1h — custom interval
|
||||||
|
# ./gitea-github-sync.sh --remove-timer — remove the timer
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
|
||||||
|
info() { echo -e "${CYAN}[sync]${NC} $*"; }
|
||||||
|
ok() { echo -e "${GREEN}[ ok ]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[warn]${NC} $*"; }
|
||||||
|
err() { echo -e "${RED}[err ]${NC} $*" >&2; }
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/gitea-github-sync"
|
||||||
|
CONFIG_FILE="$CONFIG_DIR/config"
|
||||||
|
WORK_DIR="$CONFIG_DIR/repos"
|
||||||
|
LOG_FILE="$CONFIG_DIR/sync.log"
|
||||||
|
|
||||||
|
# ── load tokens from .env ───────────────────────────────────────────────────
|
||||||
|
ENV_FILE="${SYNC_ENV:-$SCRIPT_DIR/.env}"
|
||||||
|
if [[ -f "$ENV_FILE" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
set -a; source <(grep -E '^(GITEA_TOKEN|GITHUB_TOKEN|GITEA_URL)=' "$ENV_FILE" | sed 's/ *#.*//'); set +a
|
||||||
|
fi
|
||||||
|
|
||||||
|
GITEA_URL="${GITEA_URL:-http://localhost:3001}"
|
||||||
|
GITEA_TOKEN="${GITEA_TOKEN:-}"
|
||||||
|
GITHUB_TOKEN="${GITHUB_TOKEN:-}"
|
||||||
|
|
||||||
|
# ── parse args ──────────────────────────────────────────────────────────────
|
||||||
|
MODE="all" # all | pull | push | list | init | install-timer | remove-timer
|
||||||
|
SINGLE_REPO=""
|
||||||
|
TIMER_INTERVAL="6h"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--pull-only) MODE="pull"; shift ;;
|
||||||
|
--push-only) MODE="push"; shift ;;
|
||||||
|
--list) MODE="list"; shift ;;
|
||||||
|
--init) MODE="init"; shift ;;
|
||||||
|
--install-timer) MODE="install-timer"; shift; [[ "${1:-}" =~ ^[0-9]+[smhd]$ ]] && { TIMER_INTERVAL="$1"; shift; } ;;
|
||||||
|
--remove-timer) MODE="remove-timer"; shift ;;
|
||||||
|
--repo) shift; SINGLE_REPO="${1:-}"; shift ;;
|
||||||
|
-h|--help)
|
||||||
|
sed -n '2,/^# =====/{ /^# =====/d; s/^# \?//p; }' "$0"; exit 0 ;;
|
||||||
|
*) err "Unknown arg: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── helpers ─────────────────────────────────────────────────────────────────
|
||||||
|
_gitea_api() {
|
||||||
|
local method="$1" path="$2"; shift 2
|
||||||
|
curl -sfL -X "$method" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"$GITEA_URL/api/v1$path" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
_github_api() {
|
||||||
|
local method="$1" path="$2"; shift 2
|
||||||
|
curl -sfL -X "$method" \
|
||||||
|
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
"https://api.github.com$path" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
_log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; }
|
||||||
|
|
||||||
|
# ── config management ──────────────────────────────────────────────────────
|
||||||
|
load_config() {
|
||||||
|
mkdir -p "$CONFIG_DIR" "$WORK_DIR"
|
||||||
|
GITHUB_USER=""
|
||||||
|
GITEA_USER=""
|
||||||
|
SYNC_REPOS=() # explicit list (empty = auto-discover)
|
||||||
|
EXCLUDE_REPOS=() # repos to skip
|
||||||
|
PUSH_PRIVATE=false # push private Gitea repos to GitHub?
|
||||||
|
PULL_PRIVATE=true # pull private GitHub repos to Gitea?
|
||||||
|
PULL_FORKS=false # pull forked repos from GitHub?
|
||||||
|
|
||||||
|
if [[ -f "$CONFIG_FILE" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
save_config() {
|
||||||
|
mkdir -p "$CONFIG_DIR"
|
||||||
|
cat > "$CONFIG_FILE" << EOF
|
||||||
|
# Gitea-GitHub Sync — configuration
|
||||||
|
# Generated $(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
# GitHub username (for discovering repos to pull)
|
||||||
|
GITHUB_USER="$GITHUB_USER"
|
||||||
|
|
||||||
|
# Gitea username (for discovering repos to push)
|
||||||
|
GITEA_USER="$GITEA_USER"
|
||||||
|
|
||||||
|
# Explicit repo list — if set, only these sync. Format: owner/repo
|
||||||
|
# Leave empty () to auto-discover from both platforms.
|
||||||
|
SYNC_REPOS=($(printf '"%s" ' "${SYNC_REPOS[@]}"))
|
||||||
|
|
||||||
|
# Repos to skip (pattern matched against owner/repo)
|
||||||
|
EXCLUDE_REPOS=($(printf '"%s" ' "${EXCLUDE_REPOS[@]}"))
|
||||||
|
|
||||||
|
# Push private Gitea repos to GitHub as private repos?
|
||||||
|
PUSH_PRIVATE=$PUSH_PRIVATE
|
||||||
|
|
||||||
|
# Pull private GitHub repos to Gitea?
|
||||||
|
PULL_PRIVATE=$PULL_PRIVATE
|
||||||
|
|
||||||
|
# Pull forked repos from GitHub?
|
||||||
|
PULL_FORKS=$PULL_FORKS
|
||||||
|
EOF
|
||||||
|
ok "Config saved: $CONFIG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── init (first-time setup) ────────────────────────────────────────────────
|
||||||
|
do_init() {
|
||||||
|
echo -e "\n${BOLD}Gitea ↔ GitHub Sync — First-Time Setup${NC}\n"
|
||||||
|
|
||||||
|
# Check tokens
|
||||||
|
if [[ -z "$GITEA_TOKEN" || "$GITEA_TOKEN" == "your-gitea-token-here" ]]; then
|
||||||
|
err "GITEA_TOKEN not set. Add it to $ENV_FILE first."
|
||||||
|
echo " Generate at: $GITEA_URL/user/settings/applications"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ -z "$GITHUB_TOKEN" || "$GITHUB_TOKEN" == "your-github-token-here" ]]; then
|
||||||
|
err "GITHUB_TOKEN not set. Add it to $ENV_FILE first."
|
||||||
|
echo " Generate at: https://github.com/settings/tokens"
|
||||||
|
echo " Scopes needed: repo (full control)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Discover usernames
|
||||||
|
info "Detecting GitHub user..."
|
||||||
|
GITHUB_USER=$(_github_api GET /user | python3 -c "import sys,json; print(json.load(sys.stdin)['login'])" 2>/dev/null) \
|
||||||
|
|| { err "Failed to reach GitHub API. Check GITHUB_TOKEN."; exit 1; }
|
||||||
|
ok "GitHub user: $GITHUB_USER"
|
||||||
|
|
||||||
|
info "Detecting Gitea user..."
|
||||||
|
GITEA_USER=$(_gitea_api GET /user | python3 -c "import sys,json; print(json.load(sys.stdin)['login'])" 2>/dev/null) \
|
||||||
|
|| { err "Failed to reach Gitea API. Check GITEA_TOKEN and GITEA_URL ($GITEA_URL)."; exit 1; }
|
||||||
|
ok "Gitea user: $GITEA_USER"
|
||||||
|
|
||||||
|
# Ask about sync scope
|
||||||
|
echo ""
|
||||||
|
read -rp "Pull private GitHub repos to Gitea? [Y/n] " ans
|
||||||
|
PULL_PRIVATE=true; [[ "${ans,,}" == "n" ]] && PULL_PRIVATE=false
|
||||||
|
|
||||||
|
read -rp "Pull forked repos from GitHub? [y/N] " ans
|
||||||
|
PULL_FORKS=false; [[ "${ans,,}" == "y" ]] && PULL_FORKS=true
|
||||||
|
|
||||||
|
read -rp "Push private Gitea repos to GitHub? [y/N] " ans
|
||||||
|
PUSH_PRIVATE=false; [[ "${ans,,}" == "y" ]] && PUSH_PRIVATE=true
|
||||||
|
|
||||||
|
save_config
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "Run '$(basename "$0") --list' to preview what would sync."
|
||||||
|
info "Run '$(basename "$0")' to sync now."
|
||||||
|
info "Run '$(basename "$0") --install-timer' to sync automatically."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── discover repos ─────────────────────────────────────────────────────────
|
||||||
|
get_github_repos() {
|
||||||
|
local page=1 repos=()
|
||||||
|
while true; do
|
||||||
|
local batch
|
||||||
|
batch=$(_github_api GET "/user/repos?per_page=100&page=$page&affiliation=owner" \
|
||||||
|
| python3 -c "
|
||||||
|
import sys, json
|
||||||
|
for r in json.load(sys.stdin):
|
||||||
|
if r.get('fork') and not $($PULL_FORKS && echo True || echo False):
|
||||||
|
continue
|
||||||
|
if r.get('private') and not $($PULL_PRIVATE && echo True || echo False):
|
||||||
|
continue
|
||||||
|
print(r['full_name'] + '|' + r['clone_url'] + '|' + str(r.get('private',False)).lower())
|
||||||
|
" 2>/dev/null) || break
|
||||||
|
[[ -z "$batch" ]] && break
|
||||||
|
while IFS= read -r line; do repos+=("$line"); done <<< "$batch"
|
||||||
|
((page++))
|
||||||
|
done
|
||||||
|
printf '%s\n' "${repos[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_gitea_repos() {
|
||||||
|
local page=1 repos=()
|
||||||
|
while true; do
|
||||||
|
local batch
|
||||||
|
batch=$(_gitea_api GET "/repos/search?limit=50&page=$page" \
|
||||||
|
| python3 -c "
|
||||||
|
import sys, json
|
||||||
|
for r in json.load(sys.stdin).get('data', []):
|
||||||
|
if r.get('private') and not $($PUSH_PRIVATE && echo True || echo False):
|
||||||
|
continue
|
||||||
|
print(r['full_name'] + '|' + r['clone_url'] + '|' + str(r.get('private',False)).lower())
|
||||||
|
" 2>/dev/null) || break
|
||||||
|
[[ -z "$batch" ]] && break
|
||||||
|
while IFS= read -r line; do repos+=("$line"); done <<< "$batch"
|
||||||
|
((page++))
|
||||||
|
done
|
||||||
|
printf '%s\n' "${repos[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_excluded() {
|
||||||
|
local repo="$1"
|
||||||
|
for pat in "${EXCLUDE_REPOS[@]}"; do
|
||||||
|
[[ "$repo" == $pat ]] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── sync: GitHub → Gitea (pull) ───────────────────────────────────────────
|
||||||
|
sync_github_to_gitea() {
|
||||||
|
local full_name="$1" clone_url="$2" is_private="$3"
|
||||||
|
local repo_name="${full_name#*/}"
|
||||||
|
local local_path="$WORK_DIR/$full_name"
|
||||||
|
|
||||||
|
# Clone or fetch from GitHub
|
||||||
|
if [[ -d "$local_path" ]]; then
|
||||||
|
info "Fetching $full_name from GitHub..."
|
||||||
|
git -C "$local_path" fetch --all --prune --quiet 2>/dev/null || {
|
||||||
|
err "Failed to fetch $full_name"; return 1; }
|
||||||
|
else
|
||||||
|
info "Cloning $full_name from GitHub..."
|
||||||
|
mkdir -p "$(dirname "$local_path")"
|
||||||
|
local auth_url="${clone_url/https:\/\//https:\/\/$GITHUB_TOKEN@}"
|
||||||
|
git clone --bare --quiet "$auth_url" "$local_path" 2>/dev/null || {
|
||||||
|
err "Failed to clone $full_name"; return 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure repo exists on Gitea
|
||||||
|
local gitea_check
|
||||||
|
gitea_check=$(_gitea_api GET "/repos/$GITEA_USER/$repo_name" 2>/dev/null) || true
|
||||||
|
if ! echo "$gitea_check" | python3 -c "import sys,json; json.load(sys.stdin)['id']" &>/dev/null; then
|
||||||
|
info "Creating $repo_name on Gitea..."
|
||||||
|
_gitea_api POST "/user/repos" \
|
||||||
|
-d "{\"name\":\"$repo_name\",\"private\":$is_private,\"description\":\"Mirror of $full_name from GitHub\"}" \
|
||||||
|
>/dev/null || { err "Failed to create $repo_name on Gitea"; return 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Push to Gitea
|
||||||
|
local gitea_push_url="${GITEA_URL/https:\/\//https:\/\/$GITEA_USER:$GITEA_TOKEN@}"
|
||||||
|
gitea_push_url="${gitea_push_url/http:\/\//http:\/\/$GITEA_USER:$GITEA_TOKEN@}"
|
||||||
|
gitea_push_url="$gitea_push_url/$GITEA_USER/$repo_name.git"
|
||||||
|
|
||||||
|
git -C "$local_path" push --mirror "$gitea_push_url" --quiet 2>/dev/null || {
|
||||||
|
err "Failed to push $full_name to Gitea"; return 1; }
|
||||||
|
ok "GitHub → Gitea: $full_name"
|
||||||
|
_log "PULL $full_name OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── sync: Gitea → GitHub (push) ───────────────────────────────────────────
|
||||||
|
sync_gitea_to_github() {
|
||||||
|
local full_name="$1" clone_url="$2" is_private="$3"
|
||||||
|
local repo_name="${full_name#*/}"
|
||||||
|
local local_path="$WORK_DIR/gitea/$full_name"
|
||||||
|
|
||||||
|
# Clone or fetch from Gitea
|
||||||
|
local gitea_auth_url="${clone_url/https:\/\//https:\/\/$GITEA_USER:$GITEA_TOKEN@}"
|
||||||
|
gitea_auth_url="${gitea_auth_url/http:\/\//http:\/\/$GITEA_USER:$GITEA_TOKEN@}"
|
||||||
|
|
||||||
|
if [[ -d "$local_path" ]]; then
|
||||||
|
info "Fetching $full_name from Gitea..."
|
||||||
|
git -C "$local_path" fetch --all --prune --quiet 2>/dev/null || {
|
||||||
|
err "Failed to fetch $full_name from Gitea"; return 1; }
|
||||||
|
else
|
||||||
|
info "Cloning $full_name from Gitea..."
|
||||||
|
mkdir -p "$(dirname "$local_path")"
|
||||||
|
git clone --bare --quiet "$gitea_auth_url" "$local_path" 2>/dev/null || {
|
||||||
|
err "Failed to clone $full_name from Gitea"; return 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure repo exists on GitHub
|
||||||
|
local gh_check
|
||||||
|
gh_check=$(_github_api GET "/repos/$GITHUB_USER/$repo_name" 2>/dev/null) || true
|
||||||
|
if ! echo "$gh_check" | python3 -c "import sys,json; json.load(sys.stdin)['id']" &>/dev/null; then
|
||||||
|
info "Creating $repo_name on GitHub..."
|
||||||
|
_github_api POST "/user/repos" \
|
||||||
|
-d "{\"name\":\"$repo_name\",\"private\":$is_private,\"description\":\"Mirror from Gitea\"}" \
|
||||||
|
>/dev/null || { err "Failed to create $repo_name on GitHub"; return 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Push to GitHub
|
||||||
|
local github_push_url="https://$GITHUB_TOKEN@github.com/$GITHUB_USER/$repo_name.git"
|
||||||
|
git -C "$local_path" push --mirror "$github_push_url" --quiet 2>/dev/null || {
|
||||||
|
err "Failed to push $full_name to GitHub"; return 1; }
|
||||||
|
ok "Gitea → GitHub: $full_name"
|
||||||
|
_log "PUSH $full_name OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── list (dry run) ─────────────────────────────────────────────────────────
|
||||||
|
do_list() {
|
||||||
|
echo -e "\n${BOLD}Repos that would sync:${NC}\n"
|
||||||
|
|
||||||
|
if [[ ${#SYNC_REPOS[@]} -gt 0 ]]; then
|
||||||
|
echo -e "${CYAN}Explicit list:${NC}"
|
||||||
|
printf ' %s\n' "${SYNC_REPOS[@]}"
|
||||||
|
else
|
||||||
|
if [[ "$MODE" != "push" ]]; then
|
||||||
|
echo -e "${CYAN}GitHub → Gitea (pull):${NC}"
|
||||||
|
get_github_repos | while IFS='|' read -r name url priv; do
|
||||||
|
is_excluded "$name" && echo " $name (excluded)" && continue
|
||||||
|
echo " $name $([ "$priv" = "true" ] && echo "[private]")"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
if [[ "$MODE" != "pull" ]]; then
|
||||||
|
echo -e "${CYAN}Gitea → GitHub (push):${NC}"
|
||||||
|
get_gitea_repos | while IFS='|' read -r name url priv; do
|
||||||
|
is_excluded "$name" && echo " $name (excluded)" && continue
|
||||||
|
echo " $name $([ "$priv" = "true" ] && echo "[private]")"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── main sync ──────────────────────────────────────────────────────────────
|
||||||
|
do_sync() {
|
||||||
|
local pull_count=0 push_count=0 fail_count=0
|
||||||
|
|
||||||
|
_log "=== Sync started (mode=$MODE) ==="
|
||||||
|
|
||||||
|
# GitHub → Gitea
|
||||||
|
if [[ "$MODE" == "all" || "$MODE" == "pull" ]]; then
|
||||||
|
info "Discovering GitHub repos..."
|
||||||
|
while IFS='|' read -r name url priv; do
|
||||||
|
[[ -z "$name" ]] && continue
|
||||||
|
[[ -n "$SINGLE_REPO" && "$name" != "$SINGLE_REPO" ]] && continue
|
||||||
|
is_excluded "$name" && continue
|
||||||
|
if sync_github_to_gitea "$name" "$url" "$priv"; then
|
||||||
|
((pull_count++))
|
||||||
|
else
|
||||||
|
((fail_count++))
|
||||||
|
fi
|
||||||
|
done < <(get_github_repos)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Gitea → GitHub
|
||||||
|
if [[ "$MODE" == "all" || "$MODE" == "push" ]]; then
|
||||||
|
info "Discovering Gitea repos..."
|
||||||
|
while IFS='|' read -r name url priv; do
|
||||||
|
[[ -z "$name" ]] && continue
|
||||||
|
[[ -n "$SINGLE_REPO" && "${name#*/}" != "${SINGLE_REPO#*/}" ]] && continue
|
||||||
|
is_excluded "$name" && continue
|
||||||
|
# Skip repos that came from GitHub (already mirrored)
|
||||||
|
local repo_name="${name#*/}"
|
||||||
|
if [[ -d "$WORK_DIR/$GITHUB_USER/$repo_name" ]]; then
|
||||||
|
info "Skipping $name (already a GitHub mirror)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if sync_gitea_to_github "$name" "$url" "$priv"; then
|
||||||
|
((push_count++))
|
||||||
|
else
|
||||||
|
((fail_count++))
|
||||||
|
fi
|
||||||
|
done < <(get_gitea_repos)
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
ok "Sync complete: ${pull_count} pulled, ${push_count} pushed, ${fail_count} failed"
|
||||||
|
_log "=== Sync complete: pull=$pull_count push=$push_count fail=$fail_count ==="
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── systemd timer ──────────────────────────────────────────────────────────
|
||||||
|
install_timer() {
|
||||||
|
local service_file="/etc/systemd/system/gitea-github-sync.service"
|
||||||
|
local timer_file="/etc/systemd/system/gitea-github-sync.timer"
|
||||||
|
local script_path
|
||||||
|
script_path="$(readlink -f "$0")"
|
||||||
|
|
||||||
|
info "Installing systemd timer (interval: $TIMER_INTERVAL)..."
|
||||||
|
|
||||||
|
sudo tee "$service_file" > /dev/null << EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Gitea-GitHub Mirror Sync
|
||||||
|
After=network-online.target docker.service
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
User=$USER
|
||||||
|
ExecStart=$script_path
|
||||||
|
Environment=HOME=$HOME
|
||||||
|
StandardOutput=append:$LOG_FILE
|
||||||
|
StandardError=append:$LOG_FILE
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sudo tee "$timer_file" > /dev/null << EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Gitea-GitHub Sync Timer
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=5min
|
||||||
|
OnUnitActiveSec=$TIMER_INTERVAL
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now gitea-github-sync.timer
|
||||||
|
ok "Timer installed: every $TIMER_INTERVAL"
|
||||||
|
ok "Check status: systemctl status gitea-github-sync.timer"
|
||||||
|
ok "Run now: sudo systemctl start gitea-github-sync.service"
|
||||||
|
ok "Logs: $LOG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_timer() {
|
||||||
|
info "Removing systemd timer..."
|
||||||
|
sudo systemctl disable --now gitea-github-sync.timer 2>/dev/null || true
|
||||||
|
sudo rm -f /etc/systemd/system/gitea-github-sync.{service,timer}
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
ok "Timer removed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── preflight checks ──────────────────────────────────────────────────────
|
||||||
|
preflight() {
|
||||||
|
local ok=true
|
||||||
|
if [[ -z "$GITEA_TOKEN" || "$GITEA_TOKEN" == "your-gitea-token-here" ]]; then
|
||||||
|
err "GITEA_TOKEN not set. Edit $ENV_FILE"; ok=false
|
||||||
|
fi
|
||||||
|
if [[ -z "$GITHUB_TOKEN" || "$GITHUB_TOKEN" == "your-github-token-here" ]]; then
|
||||||
|
err "GITHUB_TOKEN not set. Edit $ENV_FILE"; ok=false
|
||||||
|
fi
|
||||||
|
if [[ -z "$GITEA_USER" || -z "$GITHUB_USER" ]]; then
|
||||||
|
err "Run --init first to configure usernames"; ok=false
|
||||||
|
fi
|
||||||
|
$ok || exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── main ───────────────────────────────────────────────────────────────────
|
||||||
|
load_config
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
init) do_init ;;
|
||||||
|
install-timer) install_timer ;;
|
||||||
|
remove-timer) remove_timer ;;
|
||||||
|
list) preflight; do_list ;;
|
||||||
|
*) preflight; do_sync ;;
|
||||||
|
esac
|
||||||
+70
-44
@@ -39,30 +39,42 @@ LOCAL_IP=$(ip route get 1.1.1.1 2>/dev/null | grep -oP 'src \K\S+' \
|
|||||||
|
|
||||||
# Models (defaults, may be adjusted below based on VRAM)
|
# Models (defaults, may be adjusted below based on VRAM)
|
||||||
EMBED_MODEL="nomic-embed-text"
|
EMBED_MODEL="nomic-embed-text"
|
||||||
CHAT_MODEL="qwen2.5:14b"
|
CHAT_MODEL="qwen3.5:9b"
|
||||||
CODE_MODEL="qwen2.5-coder:7b"
|
CODE_MODEL="qwen3.5:9b"
|
||||||
FAST_MODEL="qwen2.5:7b"
|
FAST_MODEL="qwen3.5:4b"
|
||||||
|
|
||||||
# ── detect GPU ────────────────────────────────────────────────────────────────
|
# ── detect GPU ────────────────────────────────────────────────────────────────
|
||||||
VRAM_GB=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null \
|
VRAM_GB=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null \
|
||||||
| head -1 | awk '{printf "%d", $1/1024}' 2>/dev/null || echo "0")
|
| head -1 | awk '{printf "%d", $1/1024}' 2>/dev/null || echo "0")
|
||||||
|
GPU_COUNT=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | wc -l || echo "0")
|
||||||
GPU_NAME=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -1 || echo "None")
|
GPU_NAME=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -1 || echo "None")
|
||||||
|
TOTAL_VRAM=$((VRAM_GB * GPU_COUNT))
|
||||||
|
|
||||||
if [[ "$VRAM_GB" -ge 14 ]]; then
|
# Ollama optimization flags (stacked — see docs/gpu-setup-research.md)
|
||||||
CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:14b"
|
OLLAMA_KV_CACHE="q8_0" # halves KV cache VRAM (q4_0 for aggressive)
|
||||||
GPU_TIER="16GB VRAM — 14B models"
|
OLLAMA_FLASH="1" # flash attention: less VRAM, no quality loss
|
||||||
elif [[ "$VRAM_GB" -ge 8 ]]; then
|
|
||||||
CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:7b"
|
if [[ "$TOTAL_VRAM" -ge 40 ]]; then
|
||||||
GPU_TIER="8GB VRAM — 14B chat, 7B code"
|
CHAT_MODEL="qwen3.5:27b"; CODE_MODEL="qwen3.5:27b"
|
||||||
elif [[ "$VRAM_GB" -ge 4 ]]; then
|
CTX=131072; GPU_TIER="${TOTAL_VRAM}GB VRAM — 27B dense, 128K context"
|
||||||
CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b"
|
elif [[ "$TOTAL_VRAM" -ge 28 ]]; then
|
||||||
GPU_TIER="6GB VRAM — 7B models"
|
CHAT_MODEL="qwen3.5-35b-a3b"; CODE_MODEL="qwen3.5-35b-a3b"
|
||||||
elif [[ "$VRAM_GB" -gt 0 ]]; then
|
CTX=131072; GPU_TIER="${TOTAL_VRAM}GB VRAM — 35B MoE, 128K context"
|
||||||
CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b"
|
elif [[ "$TOTAL_VRAM" -ge 14 ]]; then
|
||||||
GPU_TIER="${VRAM_GB}GB VRAM — 7B models"
|
CHAT_MODEL="qwen3.5-35b-a3b"; CODE_MODEL="qwen3.5-35b-a3b"
|
||||||
|
CTX=65536; GPU_TIER="${TOTAL_VRAM}GB VRAM — 35B MoE + KV quant, 64K context"
|
||||||
|
elif [[ "$TOTAL_VRAM" -ge 8 ]]; then
|
||||||
|
CHAT_MODEL="qwen3.5:9b"; CODE_MODEL="qwen3.5:9b"
|
||||||
|
CTX=32768; GPU_TIER="${TOTAL_VRAM}GB VRAM — 9B dense, 32K context"
|
||||||
|
elif [[ "$TOTAL_VRAM" -ge 4 ]]; then
|
||||||
|
CHAT_MODEL="qwen3.5:4b"; CODE_MODEL="qwen3.5:4b"
|
||||||
|
CTX=16384; GPU_TIER="${TOTAL_VRAM}GB VRAM — 4B models, 16K context"
|
||||||
|
elif [[ "$TOTAL_VRAM" -gt 0 ]]; then
|
||||||
|
CHAT_MODEL="qwen3.5:4b"; CODE_MODEL="qwen3.5:4b"
|
||||||
|
CTX=8192; GPU_TIER="${TOTAL_VRAM}GB VRAM — 4B models"
|
||||||
else
|
else
|
||||||
CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b"
|
CHAT_MODEL="qwen3.5:4b"; CODE_MODEL="qwen3.5:4b"
|
||||||
GPU_TIER="CPU only — 7B models (slow)"
|
CTX=4096; OLLAMA_KV_CACHE="q4_0"; GPU_TIER="CPU only — 4B models (slow)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── new vs update ─────────────────────────────────────────────────────────────
|
# ── new vs update ─────────────────────────────────────────────────────────────
|
||||||
@@ -485,19 +497,19 @@ if $INSTALL_AI; then
|
|||||||
printf " %-4s %-8s %-42s %s\n" "3)" "22B" "phi4:14b + codestral:22b" "$(speed_label 13)"
|
printf " %-4s %-8s %-42s %s\n" "3)" "22B" "phi4:14b + codestral:22b" "$(speed_label 13)"
|
||||||
printf " %-4s %-8s %-42s %s\n" "4)" "70B" "llama3.3:70b + codestral:22b" "$(speed_label 41)"
|
printf " %-4s %-8s %-42s %s\n" "4)" "70B" "llama3.3:70b + codestral:22b" "$(speed_label 41)"
|
||||||
;;
|
;;
|
||||||
2) # Performance
|
2) # Performance (Qwen 3.5 — Feb 2026)
|
||||||
_TIER_NAMES=(7B 14B 32B 72B)
|
_TIER_NAMES=(4B 9B 35B 27B)
|
||||||
printf " %-4s %-8s %-42s %s\n" "1)" "7B" "qwen2.5:7b + qwen2.5-coder:7b" "$(speed_label 4)"
|
printf " %-4s %-8s %-42s %s\n" "1)" "4B" "qwen3.5:4b (chat+code)" "$(speed_label 2)"
|
||||||
printf " %-4s %-8s %-42s %s\n" "2)" "14B" "qwen2.5:14b + qwen2.5-coder:14b" "$(speed_label 9)"
|
printf " %-4s %-8s %-42s %s\n" "2)" "9B" "qwen3.5:9b (chat+code)" "$(speed_label 5)"
|
||||||
printf " %-4s %-8s %-42s %s\n" "3)" "32B" "qwen2.5:14b + qwen2.5-coder:32b" "$(speed_label 19)"
|
printf " %-4s %-8s %-42s %s\n" "3)" "35B" "qwen3.5-35b-a3b (MoE, 3B active)" "$(speed_label 12)"
|
||||||
printf " %-4s %-8s %-42s %s\n" "4)" "72B" "qwen2.5:72b + qwen2.5-coder:32b" "$(speed_label 41)"
|
printf " %-4s %-8s %-42s %s\n" "4)" "27B" "qwen3.5:27b (dense, A- quality)" "$(speed_label 17)"
|
||||||
;;
|
;;
|
||||||
3) # Mixed
|
3) # Mixed (Western chat + Qwen 3.5 code)
|
||||||
_TIER_NAMES=(7B 14B 32B 70B)
|
_TIER_NAMES=(7B 14B 35B 70B)
|
||||||
printf " %-4s %-8s %-42s %s\n" "1)" "7B" "mistral:7b + qwen2.5-coder:7b" "$(speed_label 4)"
|
printf " %-4s %-8s %-42s %s\n" "1)" "7B" "mistral:7b + qwen3.5:4b" "$(speed_label 4)"
|
||||||
printf " %-4s %-8s %-42s %s\n" "2)" "14B" "phi4:14b + qwen2.5-coder:14b" "$(speed_label 9)"
|
printf " %-4s %-8s %-42s %s\n" "2)" "14B" "phi4:14b + qwen3.5:9b" "$(speed_label 9)"
|
||||||
printf " %-4s %-8s %-42s %s\n" "3)" "32B" "phi4:14b + qwen2.5-coder:32b" "$(speed_label 19)"
|
printf " %-4s %-8s %-42s %s\n" "3)" "35B" "phi4:14b + qwen3.5-35b-a3b" "$(speed_label 19)"
|
||||||
printf " %-4s %-8s %-42s %s\n" "4)" "70B" "llama3.3:70b + qwen2.5-coder:32b" "$(speed_label 41)"
|
printf " %-4s %-8s %-42s %s\n" "4)" "70B" "llama3.3:70b + qwen3.5-35b-a3b" "$(speed_label 41)"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -535,16 +547,16 @@ if $INSTALL_AI; then
|
|||||||
1:14B) FAST_MODEL="mistral:7b"; CHAT_MODEL="phi4:14b"; CODE_MODEL="starcoder2:15b"; REASON_MODEL="phi4:14b" ;;
|
1:14B) FAST_MODEL="mistral:7b"; CHAT_MODEL="phi4:14b"; CODE_MODEL="starcoder2:15b"; REASON_MODEL="phi4:14b" ;;
|
||||||
1:22B) FAST_MODEL="mistral:7b"; CHAT_MODEL="phi4:14b"; CODE_MODEL="codestral:22b"; REASON_MODEL="phi4:14b" ;;
|
1:22B) FAST_MODEL="mistral:7b"; CHAT_MODEL="phi4:14b"; CODE_MODEL="codestral:22b"; REASON_MODEL="phi4:14b" ;;
|
||||||
1:70B) FAST_MODEL="mistral:7b"; CHAT_MODEL="llama3.3:70b"; CODE_MODEL="codestral:22b"; REASON_MODEL="llama3.3:70b" ;;
|
1:70B) FAST_MODEL="mistral:7b"; CHAT_MODEL="llama3.3:70b"; CODE_MODEL="codestral:22b"; REASON_MODEL="llama3.3:70b" ;;
|
||||||
# Performance-first
|
# Performance-first (Qwen 3.5)
|
||||||
2:7B) FAST_MODEL="qwen2.5:7b"; CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b"; REASON_MODEL="" ;;
|
2:4B) FAST_MODEL="qwen3.5:4b"; CHAT_MODEL="qwen3.5:4b"; CODE_MODEL="qwen3.5:4b"; REASON_MODEL="" ;;
|
||||||
2:14B) FAST_MODEL="qwen2.5:7b"; CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:14b"; REASON_MODEL="deepseek-r1:14b" ;;
|
2:9B) FAST_MODEL="qwen3.5:4b"; CHAT_MODEL="qwen3.5:9b"; CODE_MODEL="qwen3.5:9b"; REASON_MODEL="" ;;
|
||||||
2:32B) FAST_MODEL="qwen2.5:7b"; CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:32b"; REASON_MODEL="deepseek-r1:14b" ;;
|
2:35B) FAST_MODEL="qwen3.5:4b"; CHAT_MODEL="qwen3.5-35b-a3b"; CODE_MODEL="qwen3.5-35b-a3b"; REASON_MODEL="" ;;
|
||||||
2:72B) FAST_MODEL="qwen2.5:7b"; CHAT_MODEL="qwen2.5:72b"; CODE_MODEL="qwen2.5-coder:32b"; REASON_MODEL="deepseek-r1:14b" ;;
|
2:27B) FAST_MODEL="qwen3.5:9b"; CHAT_MODEL="qwen3.5:27b"; CODE_MODEL="qwen3.5:27b"; REASON_MODEL="" ;;
|
||||||
# Mixed
|
# Mixed (Western chat + Qwen 3.5 code)
|
||||||
3:7B) FAST_MODEL="mistral:7b"; CHAT_MODEL="mistral:7b"; CODE_MODEL="qwen2.5-coder:7b"; REASON_MODEL="" ;;
|
3:7B) FAST_MODEL="mistral:7b"; CHAT_MODEL="mistral:7b"; CODE_MODEL="qwen3.5:4b"; REASON_MODEL="" ;;
|
||||||
3:14B) FAST_MODEL="mistral:7b"; CHAT_MODEL="phi4:14b"; CODE_MODEL="qwen2.5-coder:14b"; REASON_MODEL="phi4:14b" ;;
|
3:14B) FAST_MODEL="mistral:7b"; CHAT_MODEL="phi4:14b"; CODE_MODEL="qwen3.5:9b"; REASON_MODEL="phi4:14b" ;;
|
||||||
3:32B) FAST_MODEL="mistral:7b"; CHAT_MODEL="phi4:14b"; CODE_MODEL="qwen2.5-coder:32b"; REASON_MODEL="phi4:14b" ;;
|
3:35B) FAST_MODEL="mistral:7b"; CHAT_MODEL="phi4:14b"; CODE_MODEL="qwen3.5-35b-a3b"; REASON_MODEL="phi4:14b" ;;
|
||||||
3:70B) FAST_MODEL="mistral:7b"; CHAT_MODEL="llama3.3:70b"; CODE_MODEL="qwen2.5-coder:32b"; REASON_MODEL="llama3.3:70b" ;;
|
3:70B) FAST_MODEL="mistral:7b"; CHAT_MODEL="llama3.3:70b"; CODE_MODEL="qwen3.5-35b-a3b"; REASON_MODEL="llama3.3:70b" ;;
|
||||||
*)
|
*)
|
||||||
warn "Unrecognised tier '$TIER_PICK' — keeping detected defaults"
|
warn "Unrecognised tier '$TIER_PICK' — keeping detected defaults"
|
||||||
;;
|
;;
|
||||||
@@ -763,8 +775,11 @@ WEBUI_URL=
|
|||||||
# Gitea — generate at http://$LOCAL_IP:3001/user/settings/applications
|
# Gitea — generate at http://$LOCAL_IP:3001/user/settings/applications
|
||||||
GITEA_TOKEN=your-gitea-token-here
|
GITEA_TOKEN=your-gitea-token-here
|
||||||
|
|
||||||
# GitHub — optional, for GitHub API access via MCP
|
# GitHub — optional, for GitHub API access via MCP and Gitea↔GitHub sync
|
||||||
GITHUB_TOKEN=your-github-token-here
|
GITHUB_TOKEN=your-github-token-here
|
||||||
|
|
||||||
|
# Gitea URL — used by sync script (default: http://localhost:3001)
|
||||||
|
GITEA_URL=http://$LOCAL_IP:3001
|
||||||
ENV
|
ENV
|
||||||
ok "Created .env — add your tokens before using MCP Gitea/GitHub tools"
|
ok "Created .env — add your tokens before using MCP Gitea/GitHub tools"
|
||||||
else
|
else
|
||||||
@@ -816,10 +831,12 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
${OLLAMA_VOLUME_LINE}
|
${OLLAMA_VOLUME_LINE}
|
||||||
environment:
|
environment:
|
||||||
- OLLAMA_NUM_GPU=999 # use all available VRAM (auto-detects GPU size)
|
- OLLAMA_NUM_GPU=999 # use all available VRAM (auto-detects GPU size)
|
||||||
- OLLAMA_NUM_CTX=8192 # lower to 4096 if you hit OOM
|
- OLLAMA_NUM_CTX=$CTX # auto-set by detected VRAM
|
||||||
- OLLAMA_KEEP_ALIVE=24h
|
- OLLAMA_KEEP_ALIVE=24h
|
||||||
- OLLAMA_MAX_LOADED_MODELS=1
|
- OLLAMA_MAX_LOADED_MODELS=1
|
||||||
|
- OLLAMA_KV_CACHE_TYPE=$OLLAMA_KV_CACHE # q8_0 halves KV cache; q4_0 = 1/3 size
|
||||||
|
- OLLAMA_FLASH_ATTENTION=$OLLAMA_FLASH # tiled attention: less VRAM, no quality loss
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
reservations:
|
reservations:
|
||||||
@@ -885,7 +902,7 @@ ${OLLAMA_VOLUME_LINE}
|
|||||||
- OLLAMA_URL=http://ollama:11434
|
- OLLAMA_URL=http://ollama:11434
|
||||||
- CHROMA_URL=http://chromadb:8000
|
- CHROMA_URL=http://chromadb:8000
|
||||||
- EMBED_MODEL=nomic-embed-text
|
- EMBED_MODEL=nomic-embed-text
|
||||||
- CHAT_MODEL=qwen2.5:14b
|
- CHAT_MODEL=$CHAT_MODEL
|
||||||
- PAPERS_DIR=/papers
|
- PAPERS_DIR=/papers
|
||||||
- REPOS_DIR=/repos
|
- REPOS_DIR=/repos
|
||||||
command: >
|
command: >
|
||||||
@@ -913,6 +930,7 @@ ${OLLAMA_VOLUME_LINE}
|
|||||||
- $BASE/repos:/repos
|
- $BASE/repos:/repos
|
||||||
- $BASE/mcp_server.py:/app/mcp_server.py
|
- $BASE/mcp_server.py:/app/mcp_server.py
|
||||||
- $BASE/mcp_requirements.txt:/app/mcp_requirements.txt
|
- $BASE/mcp_requirements.txt:/app/mcp_requirements.txt
|
||||||
|
- $SCRIPT_DIR/gitea-github-sync.sh:/app/gitea-github-sync.sh:ro
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
env_file: $BASE/.env
|
env_file: $BASE/.env
|
||||||
environment:
|
environment:
|
||||||
@@ -922,7 +940,7 @@ ${OLLAMA_VOLUME_LINE}
|
|||||||
- RAG_URL=http://rag-server:8001
|
- RAG_URL=http://rag-server:8001
|
||||||
command: >
|
command: >
|
||||||
bash -c "apt-get update -qq &&
|
bash -c "apt-get update -qq &&
|
||||||
apt-get install -y --no-install-recommends git ripgrep &&
|
apt-get install -y --no-install-recommends git ripgrep curl &&
|
||||||
pip install --no-cache-dir -r mcp_requirements.txt &&
|
pip install --no-cache-dir -r mcp_requirements.txt &&
|
||||||
python mcp_server.py"
|
python mcp_server.py"
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -1360,6 +1378,14 @@ if $INSTALL_AI; then
|
|||||||
echo -e " ${YELLOW}Add API tokens to:${NC} $BASE/.env"
|
echo -e " ${YELLOW}Add API tokens to:${NC} $BASE/.env"
|
||||||
$SVC_RAG && echo -e " ${YELLOW}Drop PDFs into:${NC} $BASE/papers/"
|
$SVC_RAG && echo -e " ${YELLOW}Drop PDFs into:${NC} $BASE/papers/"
|
||||||
echo -e " ${YELLOW}Your workspace:${NC} $BASE/workspace/"
|
echo -e " ${YELLOW}Your workspace:${NC} $BASE/workspace/"
|
||||||
|
$SVC_GITEA && {
|
||||||
|
echo ""
|
||||||
|
echo -e " ${YELLOW}Gitea ↔ GitHub sync:${NC}"
|
||||||
|
echo " First time: $SCRIPT_DIR/gitea-github-sync.sh --init"
|
||||||
|
echo " Sync now: $SCRIPT_DIR/gitea-github-sync.sh"
|
||||||
|
echo " Auto (6h): $SCRIPT_DIR/gitea-github-sync.sh --install-timer"
|
||||||
|
echo " Via MCP: gitea_github_sync(mode='all')"
|
||||||
|
}
|
||||||
fi
|
fi
|
||||||
if $SVC_KIWIX && [[ "$ZIM_CHOICE" == "3" ]]; then
|
if $SVC_KIWIX && [[ "$ZIM_CHOICE" == "3" ]]; then
|
||||||
echo -e " ${YELLOW}ZIM downloads:${NC} ./kiwix_download.sh (not started)"
|
echo -e " ${YELLOW}ZIM downloads:${NC} ./kiwix_download.sh (not started)"
|
||||||
|
|||||||
+30
-14
@@ -23,19 +23,31 @@ IS_UPDATE=false; [[ -f "$BASE/docker-compose.yml" ]] && IS_UPDATE=true
|
|||||||
# ── detect VRAM and set models accordingly ────────────────────────────────────
|
# ── detect VRAM and set models accordingly ────────────────────────────────────
|
||||||
VRAM_GB=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null \
|
VRAM_GB=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null \
|
||||||
| head -1 | awk '{printf "%d", $1/1024}' 2>/dev/null || echo "0")
|
| head -1 | awk '{printf "%d", $1/1024}' 2>/dev/null || echo "0")
|
||||||
|
GPU_COUNT=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | wc -l || echo "0")
|
||||||
|
TOTAL_VRAM=$((VRAM_GB * GPU_COUNT))
|
||||||
|
|
||||||
if [[ "$VRAM_GB" -ge 14 ]]; then
|
# Ollama optimization flags (stacked — see docs/gpu-setup-research.md)
|
||||||
CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:14b"
|
OLLAMA_KV_CACHE="q8_0" # halves KV cache VRAM (q4_0 for aggressive)
|
||||||
CTX=32768; TIER="16GB — 14B models + 32k context"
|
OLLAMA_FLASH="1" # flash attention: less VRAM, no quality loss
|
||||||
elif [[ "$VRAM_GB" -ge 8 ]]; then
|
|
||||||
CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:7b"
|
if [[ "$TOTAL_VRAM" -ge 40 ]]; then
|
||||||
CTX=16384; TIER="8-16GB — 14B chat, 7B code, 16k context"
|
CHAT_MODEL="qwen3.5:27b"; CODE_MODEL="qwen3.5:27b"
|
||||||
elif [[ "$VRAM_GB" -ge 4 ]]; then
|
CTX=131072; TIER="${TOTAL_VRAM}GB — 27B dense, 128K context"
|
||||||
CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b"
|
elif [[ "$TOTAL_VRAM" -ge 28 ]]; then
|
||||||
CTX=8192; TIER="6GB — 7B models, 8k context"
|
CHAT_MODEL="qwen3.5-35b-a3b"; CODE_MODEL="qwen3.5-35b-a3b"
|
||||||
|
CTX=131072; TIER="${TOTAL_VRAM}GB — 35B MoE, 128K context"
|
||||||
|
elif [[ "$TOTAL_VRAM" -ge 14 ]]; then
|
||||||
|
CHAT_MODEL="qwen3.5-35b-a3b"; CODE_MODEL="qwen3.5-35b-a3b"
|
||||||
|
CTX=65536; TIER="${TOTAL_VRAM}GB — 35B MoE + KV quant, 64K context"
|
||||||
|
elif [[ "$TOTAL_VRAM" -ge 8 ]]; then
|
||||||
|
CHAT_MODEL="qwen3.5:9b"; CODE_MODEL="qwen3.5:9b"
|
||||||
|
CTX=32768; TIER="${TOTAL_VRAM}GB — 9B dense, 32K context"
|
||||||
|
elif [[ "$TOTAL_VRAM" -ge 4 ]]; then
|
||||||
|
CHAT_MODEL="qwen3.5:4b"; CODE_MODEL="qwen3.5:4b"
|
||||||
|
CTX=16384; TIER="${TOTAL_VRAM}GB — 4B models, 16K context"
|
||||||
else
|
else
|
||||||
CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b"
|
CHAT_MODEL="qwen3.5:4b"; CODE_MODEL="qwen3.5:4b"
|
||||||
CTX=4096; TIER="CPU-only — 7B models, 4k context"
|
CTX=4096; OLLAMA_KV_CACHE="q4_0"; TIER="CPU-only — 4B models, 4K context"
|
||||||
fi
|
fi
|
||||||
EMBED_MODEL="nomic-embed-text"
|
EMBED_MODEL="nomic-embed-text"
|
||||||
|
|
||||||
@@ -488,6 +500,7 @@ if [[ ! -f "$BASE/.env" ]]; then
|
|||||||
# Local AI Stack — edit to add your API tokens
|
# Local AI Stack — edit to add your API tokens
|
||||||
GITEA_TOKEN=your-gitea-token-here
|
GITEA_TOKEN=your-gitea-token-here
|
||||||
GITHUB_TOKEN=your-github-token-here
|
GITHUB_TOKEN=your-github-token-here
|
||||||
|
GITEA_URL=http://$LOCAL_IP:3001
|
||||||
ENV
|
ENV
|
||||||
ok "Created .env"
|
ok "Created .env"
|
||||||
else
|
else
|
||||||
@@ -527,9 +540,11 @@ services:
|
|||||||
volumes: [ollama-models:/root/.ollama]
|
volumes: [ollama-models:/root/.ollama]
|
||||||
environment:
|
environment:
|
||||||
- OLLAMA_NUM_GPU=999
|
- OLLAMA_NUM_GPU=999
|
||||||
- OLLAMA_NUM_CTX=
|
- OLLAMA_NUM_CTX=$CTX
|
||||||
- OLLAMA_KEEP_ALIVE=24h
|
- OLLAMA_KEEP_ALIVE=24h
|
||||||
- OLLAMA_MAX_LOADED_MODELS=1
|
- OLLAMA_MAX_LOADED_MODELS=1
|
||||||
|
- OLLAMA_KV_CACHE_TYPE=$OLLAMA_KV_CACHE
|
||||||
|
- OLLAMA_FLASH_ATTENTION=$OLLAMA_FLASH
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
reservations:
|
reservations:
|
||||||
@@ -586,7 +601,7 @@ services:
|
|||||||
- OLLAMA_URL=http://ollama:11434
|
- OLLAMA_URL=http://ollama:11434
|
||||||
- CHROMA_URL=http://chromadb:8000
|
- CHROMA_URL=http://chromadb:8000
|
||||||
- EMBED_MODEL=nomic-embed-text
|
- EMBED_MODEL=nomic-embed-text
|
||||||
- CHAT_MODEL=
|
- CHAT_MODEL=$CHAT_MODEL
|
||||||
command: >
|
command: >
|
||||||
bash -c "apt-get update -qq && apt-get install -y --no-install-recommends git &&
|
bash -c "apt-get update -qq && apt-get install -y --no-install-recommends git &&
|
||||||
pip install --no-cache-dir -r requirements.txt &&
|
pip install --no-cache-dir -r requirements.txt &&
|
||||||
@@ -605,6 +620,7 @@ services:
|
|||||||
- $BASE/repos:/repos
|
- $BASE/repos:/repos
|
||||||
- $BASE/mcp_server.py:/app/mcp_server.py
|
- $BASE/mcp_server.py:/app/mcp_server.py
|
||||||
- $BASE/mcp_requirements.txt:/app/mcp_requirements.txt
|
- $BASE/mcp_requirements.txt:/app/mcp_requirements.txt
|
||||||
|
- $SCRIPT_DIR/gitea-github-sync.sh:/app/gitea-github-sync.sh:ro
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
env_file: $BASE/.env
|
env_file: $BASE/.env
|
||||||
environment:
|
environment:
|
||||||
@@ -613,7 +629,7 @@ services:
|
|||||||
- GITEA_URL=http://gitea:3000
|
- GITEA_URL=http://gitea:3000
|
||||||
- RAG_URL=http://rag-server:8001
|
- RAG_URL=http://rag-server:8001
|
||||||
command: >
|
command: >
|
||||||
bash -c "apt-get update -qq && apt-get install -y --no-install-recommends git ripgrep &&
|
bash -c "apt-get update -qq && apt-get install -y --no-install-recommends git ripgrep curl &&
|
||||||
pip install --no-cache-dir -r mcp_requirements.txt &&
|
pip install --no-cache-dir -r mcp_requirements.txt &&
|
||||||
python mcp_server.py"
|
python mcp_server.py"
|
||||||
depends_on: [rag-server]
|
depends_on: [rag-server]
|
||||||
|
|||||||
@@ -180,6 +180,25 @@ def github_api(method: str, endpoint: str, body: str = "") -> str:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return r.text
|
return r.text
|
||||||
|
|
||||||
|
# ── Gitea ↔ GitHub sync ──────────────────────────────────────────────────────
|
||||||
|
@mcp.tool()
|
||||||
|
def gitea_github_sync(mode: str = "all", repo: str = "") -> str:
|
||||||
|
"""Run Gitea↔GitHub mirror sync. mode: all|pull|push|list. repo: optional owner/name."""
|
||||||
|
cmd = ["/app/gitea-github-sync.sh"]
|
||||||
|
if mode == "pull": cmd.append("--pull-only")
|
||||||
|
elif mode == "push": cmd.append("--push-only")
|
||||||
|
elif mode == "list": cmd.append("--list")
|
||||||
|
if repo:
|
||||||
|
cmd.extend(["--repo", repo])
|
||||||
|
try:
|
||||||
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=600,
|
||||||
|
env={**os.environ, "SYNC_ENV": "/app/.env"})
|
||||||
|
return (r.stdout + r.stderr).strip() or "Sync completed (no output)"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return "Sync timed out after 10 minutes"
|
||||||
|
except Exception as e:
|
||||||
|
return f"Sync failed: {e}"
|
||||||
|
|
||||||
# ── RAG ingest ────────────────────────────────────────────────────────────────
|
# ── RAG ingest ────────────────────────────────────────────────────────────────
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def ingest_repo(url: str, name: str = "", branch: str = "main") -> str:
|
def ingest_repo(url: str, name: str = "", branch: str = "main") -> str:
|
||||||
|
|||||||
+26
-12
@@ -1106,19 +1106,31 @@ IS_UPDATE=false; [[ -f "$BASE/docker-compose.yml" ]] && IS_UPDATE=true
|
|||||||
# ── detect VRAM and set models accordingly ────────────────────────────────────
|
# ── detect VRAM and set models accordingly ────────────────────────────────────
|
||||||
VRAM_GB=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null \
|
VRAM_GB=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null \
|
||||||
| head -1 | awk '{printf "%d", $1/1024}' 2>/dev/null || echo "0")
|
| head -1 | awk '{printf "%d", $1/1024}' 2>/dev/null || echo "0")
|
||||||
|
GPU_COUNT=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | wc -l || echo "0")
|
||||||
|
TOTAL_VRAM=$((VRAM_GB * GPU_COUNT))
|
||||||
|
|
||||||
if [[ "$VRAM_GB" -ge 14 ]]; then
|
# Ollama optimization flags (stacked — see docs/gpu-setup-research.md)
|
||||||
CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:14b"
|
OLLAMA_KV_CACHE="q8_0" # halves KV cache VRAM (q4_0 for aggressive)
|
||||||
CTX=32768; TIER="16GB — 14B models + 32k context"
|
OLLAMA_FLASH="1" # flash attention: less VRAM, no quality loss
|
||||||
elif [[ "$VRAM_GB" -ge 8 ]]; then
|
|
||||||
CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:7b"
|
if [[ "$TOTAL_VRAM" -ge 40 ]]; then
|
||||||
CTX=16384; TIER="8-16GB — 14B chat, 7B code, 16k context"
|
CHAT_MODEL="qwen3.5:27b"; CODE_MODEL="qwen3.5:27b"
|
||||||
elif [[ "$VRAM_GB" -ge 4 ]]; then
|
CTX=131072; TIER="${TOTAL_VRAM}GB — 27B dense, 128K context"
|
||||||
CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b"
|
elif [[ "$TOTAL_VRAM" -ge 28 ]]; then
|
||||||
CTX=8192; TIER="6GB — 7B models, 8k context"
|
CHAT_MODEL="qwen3.5-35b-a3b"; CODE_MODEL="qwen3.5-35b-a3b"
|
||||||
|
CTX=131072; TIER="${TOTAL_VRAM}GB — 35B MoE, 128K context"
|
||||||
|
elif [[ "$TOTAL_VRAM" -ge 14 ]]; then
|
||||||
|
CHAT_MODEL="qwen3.5-35b-a3b"; CODE_MODEL="qwen3.5-35b-a3b"
|
||||||
|
CTX=65536; TIER="${TOTAL_VRAM}GB — 35B MoE + KV quant, 64K context"
|
||||||
|
elif [[ "$TOTAL_VRAM" -ge 8 ]]; then
|
||||||
|
CHAT_MODEL="qwen3.5:9b"; CODE_MODEL="qwen3.5:9b"
|
||||||
|
CTX=32768; TIER="${TOTAL_VRAM}GB — 9B dense, 32K context"
|
||||||
|
elif [[ "$TOTAL_VRAM" -ge 4 ]]; then
|
||||||
|
CHAT_MODEL="qwen3.5:4b"; CODE_MODEL="qwen3.5:4b"
|
||||||
|
CTX=16384; TIER="${TOTAL_VRAM}GB — 4B models, 16K context"
|
||||||
else
|
else
|
||||||
CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b"
|
CHAT_MODEL="qwen3.5:4b"; CODE_MODEL="qwen3.5:4b"
|
||||||
CTX=4096; TIER="CPU-only — 7B models, 4k context"
|
CTX=4096; OLLAMA_KV_CACHE="q4_0"; TIER="CPU-only — 4B models, 4K context"
|
||||||
fi
|
fi
|
||||||
EMBED_MODEL="nomic-embed-text"
|
EMBED_MODEL="nomic-embed-text"
|
||||||
|
|
||||||
@@ -1611,9 +1623,11 @@ services:
|
|||||||
volumes: [ollama-models:/root/.ollama]
|
volumes: [ollama-models:/root/.ollama]
|
||||||
environment:
|
environment:
|
||||||
- OLLAMA_NUM_GPU=999
|
- OLLAMA_NUM_GPU=999
|
||||||
- OLLAMA_NUM_CTX=
|
- OLLAMA_NUM_CTX=$CTX
|
||||||
- OLLAMA_KEEP_ALIVE=24h
|
- OLLAMA_KEEP_ALIVE=24h
|
||||||
- OLLAMA_MAX_LOADED_MODELS=1
|
- OLLAMA_MAX_LOADED_MODELS=1
|
||||||
|
- OLLAMA_KV_CACHE_TYPE=$OLLAMA_KV_CACHE
|
||||||
|
- OLLAMA_FLASH_ATTENTION=$OLLAMA_FLASH
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
reservations:
|
reservations:
|
||||||
|
|||||||
Reference in New Issue
Block a user