Initial commit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
timmy
2026-04-21 13:04:09 +08:00
commit faee15bc78
18 changed files with 3278 additions and 0 deletions

193
scripts/lib/common.sh Normal file
View File

@@ -0,0 +1,193 @@
#!/usr/bin/env bash
# gitea-cli 共用 lib。
# 使用方式(呼叫端):
# SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# source "${SCRIPT_DIR}/lib/common.sh"
# load_config
#
# 重要:所有 API 呼叫走 gitea_api所有 push/clone 走 git_push_with_token / git_clone_with_token。
# token 絕不允許出現在 .git/config、remote URL、log 中。
set -euo pipefail
_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
_SKILL_DIR="$(cd "${_LIB_DIR}/../.." && pwd)"
CONFIG_FILE="${_SKILL_DIR}/config.env"
GITEA_API_RESP_FILE=""
_cleanup_common() {
if [[ -n "${GITEA_API_RESP_FILE:-}" && -f "${GITEA_API_RESP_FILE}" ]]; then
rm -f "${GITEA_API_RESP_FILE}"
fi
}
trap _cleanup_common EXIT
# === logging全部寫 stderrstdout 留給結構化輸出)===
log_info() { printf '▶ %s\n' "$*" >&2; }
log_ok() { printf '✓ %s\n' "$*" >&2; }
log_warn() { printf '⚠ %s\n' "$*" >&2; }
log_err() { printf '❌ %s\n' "$*" >&2; }
log_done() { printf '✅ %s\n' "$*" >&2; }
# === 必要指令檢查 ===
require_cmd() {
local missing=()
for c in "$@"; do
command -v "$c" >/dev/null 2>&1 || missing+=("$c")
done
if [[ ${#missing[@]} -gt 0 ]]; then
log_err "缺少指令:${missing[*]}"
exit 1
fi
}
# === config 載入 ===
load_config() {
require_cmd curl git
if [[ ! -f "$CONFIG_FILE" ]]; then
log_err "找不到設定檔:${CONFIG_FILE}"
{
echo ""
echo "請先建立:"
echo " cp ${_SKILL_DIR}/config.example.env ${CONFIG_FILE}"
echo ""
echo "然後編輯 ${CONFIG_FILE} 填入 GITEA_URL / GITEA_USER / GITEA_TOKEN"
} >&2
exit 1
fi
# shellcheck disable=SC1090
source "$CONFIG_FILE"
: "${GITEA_URL:?GITEA_URL 未設定(請編輯 ${CONFIG_FILE}}"
: "${GITEA_USER:?GITEA_USER 未設定(請編輯 ${CONFIG_FILE}}"
: "${GITEA_TOKEN:?GITEA_TOKEN 未設定(請編輯 ${CONFIG_FILE}}"
if [[ "$GITEA_TOKEN" == "請貼上你的_access_token" ]]; then
log_err "GITEA_TOKEN 仍是預設值,請編輯 ${CONFIG_FILE}"
exit 1
fi
if [[ "$GITEA_URL" == "http://your-gitea.example.com:3000" ]]; then
log_err "GITEA_URL 仍是預設值,請編輯 ${CONFIG_FILE}"
exit 1
fi
if [[ "$GITEA_USER" == "your-username" ]]; then
log_err "GITEA_USER 仍是預設值,請編輯 ${CONFIG_FILE}"
exit 1
fi
GITEA_URL="${GITEA_URL%/}"
export GITEA_URL GITEA_USER GITEA_TOKEN
}
# === Gitea API 呼叫 ===
# gitea_api <METHOD> <path-from-/api/v1> [json-body]
# 回傳後讀:$HTTP_CODE狀態碼字串、$GITEA_API_RESP_FILEbody 檔路徑)
gitea_api() {
# 清掉上一次的 tmpfile防止多次呼叫時洩漏
if [[ -n "${GITEA_API_RESP_FILE:-}" && -f "${GITEA_API_RESP_FILE}" ]]; then
rm -f "${GITEA_API_RESP_FILE}"
fi
local method="$1"
local path="$2"
local body="${3:-}"
local url="${GITEA_URL}/api/v1${path}"
GITEA_API_RESP_FILE="$(mktemp)"
local -a curl_args=(
-sS
-o "$GITEA_API_RESP_FILE"
-w '%{http_code}'
-X "$method"
-H "Authorization: token ${GITEA_TOKEN}"
)
if [[ -n "$body" ]]; then
curl_args+=(-H "Content-Type: application/json" -d "$body")
fi
HTTP_CODE="$(curl "${curl_args[@]}" "$url")"
export HTTP_CODE GITEA_API_RESP_FILE
}
# 一次性帶 token 的 git push / clone
# 注意token 透過 -c 參數傳入,在 git 子程序存活期間短暫出現於 process 列表
# /proc/PID/cmdline。這是此模式固有取捨不會持久化到 .git/config / remote URL / log。
_git_with_token() {
git \
-c 'credential.helper=' \
-c "credential.helper=!f() { echo username=${GITEA_USER}; echo password=${GITEA_TOKEN}; }; f" \
"$@"
}
git_push_with_token() {
local branch="$1"
_git_with_token push -u origin "$branch"
}
git_push_tag_with_token() {
local tag="$1"
_git_with_token push origin "$tag"
}
git_clone_with_token() {
local clean_url="$1"
local target_dir="$2"
_git_with_token clone "$clean_url" "$target_dir"
# 再確保 origin 是乾淨 URLclone 本來就是用 clean_url這裡是防呆
git -C "$target_dir" remote set-url origin "$clean_url"
}
# 從 cwd origin 推斷 repo 名稱
# 成功時 echo "owner/repo"timmy/my-project並回傳 0失敗回傳非 0
# 呼叫端若只需要 repo 名稱repo_name="${result##*/}"
# 只支援 http(s):// 形式 URLssh (git@) 不支援(刻意 YAGNI
infer_repo_from_cwd() {
local url
if ! url="$(git remote get-url origin 2>/dev/null)"; then
return 1
fi
# 支援 http(s)://host[:port]/owner/repo[.git]
local path="${url#*://}"
path="${path#*/}"
path="${path%.git}"
[[ -z "$path" ]] && return 1
echo "$path"
}
# === 確認提示 ===
# confirm_destructive <prompt>
# 若 GITEA_YES=1 則直接通過;否則 y/N 互動
confirm_destructive() {
local prompt="$1"
if [[ "${GITEA_YES:-0}" == "1" ]]; then
return 0
fi
if [[ ! -r /dev/tty ]]; then
log_err "需要確認但無 TTY請加 -y / --yes 或設定 GITEA_YES=1"
exit 1
fi
local ans
printf '%s [y/N] ' "$prompt" >&2
read -r ans < /dev/tty
case "$ans" in
y|Y|yes|YES) return 0 ;;
*) log_err "取消"; exit 1 ;;
esac
}
# confirm_exact_match <expected>
# 要求使用者重新輸入完整字串才通過GITEA_YES=1 可略過
confirm_exact_match() {
local expected="$1"
if [[ "${GITEA_YES:-0}" == "1" ]]; then
return 0
fi
if [[ ! -r /dev/tty ]]; then
log_err "需要確認但無 TTY請加 -y / --yes 或設定 GITEA_YES=1"
exit 1
fi
local ans
printf '請完整輸入 "%s" 以確認:' "$expected" >&2
read -r ans < /dev/tty
if [[ "$ans" == "$expected" ]]; then
return 0
fi
log_err "輸入不符合,取消"
exit 1
}