Ghost Shell

JavaScript NOASSERTION

Stars
0
Forks
0
Downloads
N/A
Open Issues
0
Files main

Repository Files

Loading file structure...
scripts/git
#!/usr/bin/env bash

set -e

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="$ROOT/.env"

RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[1;34m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
DIM='\033[2m'
NC='\033[0m'

step() {
    echo
    echo -e "${BLUE}==>${NC} ${1}"
}

info() {
    echo -e " ${DIM}${1}${NC}"
}

ok() {
    echo -e " ${GREEN}✓${NC} ${1}"
}

warn() {
    echo -e " ${YELLOW}!${NC} ${1}"
}

fail() {
    echo -e " ${RED}✗${NC} ${1}"
}

echo -e "${CYAN}Ghost Shell — git commit & push${NC}"
info "Project root: ${ROOT}"

step "Loading configuration"
if [ ! -f "$ENV_FILE" ]; then
    fail "Missing ${ENV_FILE}. Create it with API_MODEL and API_KEY."
    exit 1
fi
ok "Loaded ${ENV_FILE}"

set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a

: "${API_MODEL:?Set API_MODEL in .env}"
: "${API_KEY:?Set API_KEY in .env}"

# OpenRouter Standard Endpoint
API_URL="https://openrouter.ai/api/v1/chat/completions"
API_CONNECT_TIMEOUT="${API_CONNECT_TIMEOUT:-10}"
API_MAX_TIME="${API_MAX_TIME:-30}"
API_MAX_RETRIES="${API_MAX_RETRIES:-3}"
API_RETRY_DELAY="${API_RETRY_DELAY:-5}"
COMPANY_NAME="Ghost Compiler"
DEBUG_API="${DEBUG_API:-0}"

info "API model: ${API_MODEL}"
info "API timeout: connect=${API_CONNECT_TIMEOUT}s, max=${API_MAX_TIME}s"
info "API retries: ${API_MAX_RETRIES} (delay: ${API_RETRY_DELAY}s)"
if [ "${SKIP_API:-0}" = "1" ]; then
    info "SKIP_API=1 (local commit message only)"
fi
if [ "$DEBUG_API" = "1" ]; then
    info "DEBUG_API=1 (raw API responses will be printed)"
fi

step "Checking git repository"
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
    fail "Not a Git repository."
    exit 1
}
ok "Inside git work tree"

CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
info "Current branch: ${CURRENT_BRANCH}"

if git remote get-url origin >/dev/null 2>&1; then
    info "Remote origin: $(git remote get-url origin)"
else
    warn "No origin remote configured yet"
fi

step "Staging all changes"
git add -A
ok "Ran git add -A"

if git diff --cached --quiet; then
    ok "Nothing to commit — working tree clean after staging"
    exit 0
fi

#########################################
# High-level change summary
#########################################

step "Analyzing staged changes"

CHANGED_FILES=$(git diff --cached --name-only)
FILE_COUNT=$(printf '%s\n' "$CHANGED_FILES" | sed '/^$/d' | wc -l | tr -d ' ')
INSERTIONS=$(git diff --cached --numstat | awk '{ ins += ($1 == "-" ? 0 : $1) } END { print ins + 0 }')
DELETIONS=$(git diff --cached --numstat | awk '{ del += ($2 == "-" ? 0 : $2) } END { print del + 0 }')

ok "${FILE_COUNT} file(s) staged (+${INSERTIONS}/-${DELETIONS} lines)"
echo
info "Staged files:"
while IFS= read -r file; do
    [ -z "$file" ] && continue
    echo "      - ${file}"
done <<< "$CHANGED_FILES"

FRONTEND=0
BACKEND=0
CI=0
DOCS=0
CONFIG=0
OTHER=0

while IFS= read -r file; do
    [ -z "$file" ] && continue
    case "$file" in
        src/*|index.html|vite.config.*)
            FRONTEND=$((FRONTEND + 1))
            ;;
        src-tauri/*)
            BACKEND=$((BACKEND + 1))
            ;;
        .github/*)
            CI=$((CI + 1))
            ;;
        *.md|LICENSE)
            DOCS=$((DOCS + 1))
            ;;
        package.json|package-lock.json|src-tauri/Cargo.toml|src-tauri/Cargo.lock|src-tauri/tauri.conf.json)
            CONFIG=$((CONFIG + 1))
            ;;
        *)
            OTHER=$((OTHER + 1))
            ;;
    esac
done <<< "$CHANGED_FILES"

AREAS=()
[ "$FRONTEND" -gt 0 ] && AREAS+=("frontend: ${FRONTEND} files")
[ "$BACKEND" -gt 0 ] && AREAS+=("backend: ${BACKEND} files")
[ "$CI" -gt 0 ] && AREAS+=("ci: ${CI} files")
[ "$DOCS" -gt 0 ] && AREAS+=("docs: ${DOCS} files")
[ "$CONFIG" -gt 0 ] && AREAS+=("config: ${CONFIG} files")
[ "$OTHER" -gt 0 ] && AREAS+=("other: ${OTHER} files")

CHANGE_SUMMARY=$(printf '%s\n' "${AREAS[@]}")
if [ -z "$CHANGE_SUMMARY" ]; then
    CHANGE_SUMMARY="general project files"
fi

echo
info "Change areas:"
while IFS= read -r area; do
    [ -z "$area" ] && continue
    echo "      - ${area}"
done <<< "$CHANGE_SUMMARY"

generate_local_message() {
    if [ "$CI" -gt 0 ] && [ "$((FRONTEND + BACKEND + DOCS + CONFIG + OTHER))" -eq 0 ]; then
        echo "ci: pipeline update"
    elif [ "$DOCS" -gt 0 ] && [ "$((FRONTEND + BACKEND + CI + CONFIG + OTHER))" -eq 0 ]; then
        echo "docs: project documentation update"
    elif [ "$CONFIG" -gt 0 ] && [ "$((FRONTEND + BACKEND + CI + DOCS + OTHER))" -eq 0 ]; then
        echo "chore: project configuration update"
    elif [ "$FRONTEND" -gt 0 ] && [ "$BACKEND" -gt 0 ]; then
        echo "feat: application update"
    elif [ "$FRONTEND" -gt 0 ]; then
        echo "feat: client update"
    elif [ "$BACKEND" -gt 0 ]; then
        echo "feat: backend update"
    else
        echo "chore: project update"
    fi
}

LOCAL_MESSAGE=$(generate_local_message)
info "Local fallback message: ${LOCAL_MESSAGE}"

#########################################
# Prompt
#########################################

DIFF_STAT=$(git diff --cached --stat | head -n 40)

PROMPT=$(cat <<EOF
You are a senior software engineer at $COMPANY_NAME working on Ghost Shell.

Write ONE concise and descriptive Conventional Commit message for the following staged changes.

Context:
Staged changes statistics:
${DIFF_STAT}

Rules:
- Formatted as: <type>: <description> (e.g. feat: resolve terminal reconnect loop)
- The description (after the type prefix) must be a concise, lowercase summary of the actual update (around 20-30 characters long).
- Lowercase type: feat, fix, refactor, docs, style, chore, perf, test, ci, build
- No body. No quotes. No explanation.
- Return ONLY the commit message.
EOF
)

#########################################
# Build request body once (Standard OpenAI format)
#########################################

REQUEST_BODY=$(jq -n \
    --arg model "$API_MODEL" \
    --arg prompt "$PROMPT" \
    '{
      model: $model,
      temperature: 0.1,
      max_tokens: 60,
      messages: [{ role: "user", content: $prompt }]
    }')

#########################################
# Call API with retry
#########################################

call_api() {
    local response_file="$1"
    local http_code

    http_code=$(
        curl -sS \
            --connect-timeout "$API_CONNECT_TIMEOUT" \
            --max-time "$API_MAX_TIME" \
            -o "$response_file" \
            -w "%{http_code}" \
            -X POST "$API_URL" \
            -H "Authorization: Bearer $API_KEY" \
            -H "Content-Type: application/json" \
            -H "HTTP-Referer: https://github.com/ghost-maintainer/ghost-shell" \
            -H "X-Title: Ghost Shell" \
            -d "$REQUEST_BODY"
    )
    echo "$http_code"
}

parse_api_response() {
    local response_file="$1"
    API_RAW=$(jq -r '.choices[0].message.content // empty' "$response_file" 2>/dev/null || true)
    REASONING_RAW=$(jq -r '.choices[0].message.reasoning_content // empty' "$response_file" 2>/dev/null || true)
    FINISH_REASON=$(jq -r '.choices[0].finish_reason // empty' "$response_file" 2>/dev/null || true)
    COMMIT_MESSAGE=$(echo "$API_RAW" | head -n1 | sed 's/^"//;s/"$//' | xargs)
}

#########################################
# Commit message generation
#########################################

COMMIT_MESSAGE=""
MESSAGE_SOURCE="local"
API_RAW=""
REASONING_RAW=""
FINISH_REASON=""

step "Generating commit message"

if [ "${SKIP_API:-0}" = "1" ]; then
    warn "SKIP_API=1 — skipping OpenRouter API"
    COMMIT_MESSAGE="$LOCAL_MESSAGE"
    ok "Using local message"
else
    info "Calling OpenRouter API (${API_URL})"
    info "Waiting up to ${API_MAX_TIME}s per request, up to ${API_MAX_RETRIES} retries..."

    API_RESPONSE_FILE=$(mktemp)
    trap 'rm -f "$API_RESPONSE_FILE"' EXIT

    ATTEMPT=0
    SUCCESS=0

    while [ "$ATTEMPT" -lt "$API_MAX_RETRIES" ]; do
        ATTEMPT=$((ATTEMPT + 1))

        if [ "$ATTEMPT" -gt 1 ]; then
            info "Retry ${ATTEMPT}/${API_MAX_RETRIES} after ${API_RETRY_DELAY}s..."
            sleep "$API_RETRY_DELAY"
        fi

        set +e
        HTTP_CODE=$(call_api "$API_RESPONSE_FILE")
        CURL_EXIT=$?
        set -e

        info "Attempt ${ATTEMPT}: HTTP ${HTTP_CODE:-?}, curl exit ${CURL_EXIT}"

        if [ "$DEBUG_API" = "1" ]; then
            echo
            info "Raw response body (attempt ${ATTEMPT}):"
            if jq . "$API_RESPONSE_FILE" >/dev/null 2>&1; then
                jq . "$API_RESPONSE_FILE" | sed 's/^/      /'
            else
                sed 's/^/      /' "$API_RESPONSE_FILE"
            fi
            echo
        fi

        # Network error
        if [ "$CURL_EXIT" -ne 0 ]; then
            warn "Network error (curl exit ${CURL_EXIT})"
            continue
        fi

        # Rate limited — retry with backoff
        if [ "$HTTP_CODE" = "429" ]; then
            warn "Rate limited (HTTP 429)"
            if [ "$ATTEMPT" -lt "$API_MAX_RETRIES" ]; then
                BACKOFF=$((API_RETRY_DELAY * ATTEMPT))
                info "Backing off ${BACKOFF}s before next retry..."
                sleep "$BACKOFF"
            fi
            continue
        fi

        # Server error — retry
        if [[ "$HTTP_CODE" = 5* ]]; then
            warn "Server error (HTTP ${HTTP_CODE})"
            continue
        fi

        # Success
        if [[ "$HTTP_CODE" = 2* ]]; then
            parse_api_response "$API_RESPONSE_FILE"

            if [ -n "$API_RAW" ] && [ "$API_RAW" != "null" ]; then
                info "API returned: ${API_RAW}"
            fi

            # Empty response
            if [ -z "$COMMIT_MESSAGE" ] || [ "$COMMIT_MESSAGE" = "null" ]; then
                if [ "$FINISH_REASON" = "length" ]; then
                    fail "Response truncated (finish_reason=length)"
                elif [ -n "$REASONING_RAW" ] && [ "$REASONING_RAW" != "null" ]; then
                    fail "Model returned only reasoning, no content"
                    info "Reasoning (truncated): $(echo "$REASONING_RAW" | tr -d '\n' | cut -c1-200)..."
                fi
                warn "No usable message in response"
                continue
            fi


            SUCCESS=1
            break
        fi

        # Other client error (4xx, not 429) — don't retry
        API_ERROR=$(jq -r 'if .error.message then .error.message elif .error.code then (.error.code | tostring) + ": " + .error.message elif .detail then .detail elif .title then .title else empty end' "$API_RESPONSE_FILE" 2>/dev/null || true)
        if [ -n "$API_ERROR" ]; then
            fail "API error: ${API_ERROR}"
        else
            fail "Unexpected HTTP ${HTTP_CODE}: $(tr -d '\n' < "$API_RESPONSE_FILE" | cut -c1-300)"
        fi
        info "Tip: run with DEBUG_API=1 to see the full response body."
        break
    done

    if [ "$SUCCESS" -eq 1 ]; then
        if [ "$ATTEMPT" -gt 1 ]; then
            MESSAGE_SOURCE="api (attempt ${ATTEMPT})"
        else
            MESSAGE_SOURCE="api"
        fi
        ok "Using API message"
    else
        warn "All ${API_MAX_RETRIES} attempt(s) failed — using local fallback"
        COMMIT_MESSAGE="$LOCAL_MESSAGE"
    fi
fi

echo
echo -e " ${GREEN}Commit message (${MESSAGE_SOURCE}):${NC} ${COMMIT_MESSAGE}"

#########################################
# Commit
#########################################

step "Creating commit"
git commit -m "$COMMIT_MESSAGE"
ok "Commit created"

COMMIT_SHA=$(git rev-parse --short HEAD)
info "Commit SHA: ${COMMIT_SHA}"

CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$CURRENT_BRANCH" != "main" ]; then
    step "Renaming branch to main"
    info "Was: ${CURRENT_BRANCH}"
    git branch -M main
    ok "Branch is now: main"
fi

if ! git remote get-url origin >/dev/null 2>&1; then
    echo
    fail "No origin remote configured."
    info "Commit was saved locally (${COMMIT_SHA})."
    info "Add a remote with: git remote add origin <repo-url>"
    exit 1
fi

step "Pushing to origin/main"
info "Running: git push -u origin main"

set +e
git push -u origin main
PUSH_EXIT=$?
set -e

if [ "$PUSH_EXIT" -ne 0 ]; then
    fail "Push failed (exit ${PUSH_EXIT})"
    info "Commit ${COMMIT_SHA} exists locally but was not pushed"
    exit "$PUSH_EXIT"
fi

ok "Push completed"

echo
echo -e "${GREEN}Done!${NC} ${COMMIT_SHA} → origin/main"
echo -e " ${DIM}Message: ${COMMIT_MESSAGE}${NC}"