fei-cli/fei

822 lines
26 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# fei — feiCode (Forgejo) CLI 工具
# 用法: fei <命令> [参数...]
set -euo pipefail
FEI_VERSION="1.1.0"
FEI_HOST="feicode.com"
FEI_API="https://${FEI_HOST}/api/v1"
FEI_SSH_PORT=2222
FEI_TOKEN="${FORGEJO_TOKEN:-}"
# ── 工具函数 ──
_fei_check_token() {
if [[ -z "$FEI_TOKEN" ]]; then
echo "错误: FORGEJO_TOKEN 未设置" >&2
return 1
fi
}
_fei_api() {
local method="$1" path="$2"
shift 2
local tmpfile http_code
tmpfile=$(mktemp)
http_code=$(curl -s -o "$tmpfile" -w '%{http_code}' -X "$method" \
-H "Authorization: token $FEI_TOKEN" \
-H "Content-Type: application/json" \
"$@" "${FEI_API}${path}") || { rm -f "$tmpfile"; echo "错误: 网络请求失败" >&2; return 1; }
if [[ "$http_code" -ge 200 && "$http_code" -lt 300 ]]; then
cat "$tmpfile"
rm -f "$tmpfile"
else
local msg
msg=$(jq -r '.message // empty' < "$tmpfile" 2>/dev/null)
rm -f "$tmpfile"
echo "错误: HTTP ${http_code}${msg:+ — $msg}" >&2
return 1
fi
}
_fei_get() { _fei_api GET "$@"; }
_fei_post() { local p="$1"; shift; _fei_api POST "$p" "$@"; }
_fei_patch() { local p="$1"; shift; _fei_api PATCH "$p" "$@"; }
_fei_delete() { _fei_api DELETE "$@"; }
# 从当前 git 仓库提取 org/repo
_fei_repo_from_git() {
local url
url=$(git remote get-url origin 2>/dev/null) || { echo "错误: 不在 git 仓库中或无 origin remote" >&2; return 1; }
# 支持 ssh://git@host:port/org/repo.git 和 git@host:org/repo.git 和 https
url="${url%.git}"
if [[ "$url" == ssh://* ]]; then
# ssh://git@feicode.com:2222/Org/repo
echo "$url" | sed -E 's#^ssh://[^/]*:[0-9]+/##; s#^ssh://[^/]*/##'
elif [[ "$url" == *@* ]]; then
# git@feicode.com:Org/repo
echo "$url" | sed -E 's#^[^:]+:##'
else
# https://feicode.com/Org/repo
echo "$url" | sed -E 's#^https?://[^/]+/##'
fi
}
_fei_fmt_time() {
# 简化时间显示
echo "$1" | sed -E 's/T/ /; s/\+.*//' | cut -c1-16
}
# ── 命令实现 ──
cmd_help() {
cat << 'EOF'
fei — feiCode CLI 工具
仓库操作:
fei clone <org/repo> 克隆仓库 (SSH)
fei clone --https <org/repo> 克隆仓库 (HTTPS)
fei repos [org] 列出仓库
fei search <关键词> 搜索仓库
fei info [org/repo] 仓库详情 (默认当前仓库)
fei web [org/repo] 浏览器打开仓库页面
fei fork <org/repo> Fork 仓库到当前用户
分支操作:
fei branch list [org/repo] 分支列表
fei branch create <名称> [org/repo] 创建分支 (基于默认分支)
fei branch delete <名称> [org/repo] 删除分支
fei diff <分支1> <分支2> [org/repo] 比较两个分支差异
fei log [org/repo] 最近提交记录
PR 操作:
fei pr list [org/repo] PR 列表
fei pr create <标题> [正文] 创建 PR (当前分支 → main)
fei pr create <标题> -b <base> 指定目标分支
fei pr view <编号> 查看 PR 详情
fei pr merge <编号> 合并 PR
Issue 操作:
fei issue list [org/repo] Issue 列表
fei issue create <标题> [正文] 创建 Issue
fei issue view <编号> 查看 Issue 详情
fei issue close <编号> 关闭 Issue
Release 操作:
fei release list [org/repo] Release 列表
fei release create <tag> <标题> 创建 Release
fei release create <tag> <标题> --asset <文件> 创建并上传附件
标签操作:
fei label list [org/repo] 标签列表
fei label create <名称> <颜色> 创建标签 (颜色如 #ff0000)
通知:
fei notify 未读通知列表
fei notify count 未读通知数量
文件操作:
fei cat <文件路径> [org/repo] 查看文件内容
fei ls [目录] [org/repo] 列出目录内容
推送 (带敏感文件过滤):
fei push [branch] [remote] 推送分支 (自动排除 .git/push-exclude 中的路径)
fei push --tag <tag> [remote] 推送 tag
管理 (需要管理员权限):
fei admin users 用户列表
fei admin dump 触发 Forgejo 备份
fei admin doctor Forgejo 健康检查
Pages:
fei pages create <站点名> 创建 Pages 站点仓库 (自带部署 workflow)
fei pages list 列出所有 Pages 站点
fei pages open <站点名> 浏览器打开站点
其他:
fei whoami 当前认证信息
fei orgs 列出组织
fei help 显示帮助
fei --version 显示版本号
EOF
}
cmd_whoami() {
_fei_check_token || return 1
_fei_get "/user" | jq -r '"用户: \(.login)\n邮箱: \(.email)\n全名: \(.full_name // "未设置")\n管理员: \(.is_admin)"'
}
cmd_orgs() {
_fei_check_token || return 1
_fei_get "/user/orgs" | jq -r '.[] | "\(.username)\t\(.description // "")"' | column -t -s$'\t'
}
cmd_clone() {
local use_https=false
if [[ "${1:-}" == "--https" ]]; then
use_https=true
shift
fi
local repo="${1:?用法: fei clone [--https] <org/repo>}"
if $use_https; then
git clone "https://${FEI_HOST}/${repo}.git"
else
git clone "ssh://git@${FEI_HOST}:${FEI_SSH_PORT}/${repo}.git"
fi
}
cmd_repos() {
_fei_check_token || return 1
local org="" limit=50
while [[ $# -gt 0 ]]; do
case "$1" in
--limit) limit="$2"; shift 2 ;;
*) org="$1"; shift ;;
esac
done
local page=1 path
if [[ -n "$org" ]]; then
path="/orgs/${org}/repos"
else
path="/user/repos"
fi
while true; do
local result
result=$(_fei_get "${path}?limit=${limit}&page=${page}&sort=updated")
local count
count=$(echo "$result" | jq 'length')
[[ "$count" == "0" ]] && break
echo "$result" | jq -r '.[] | "\(.full_name)\t\(.description // "-" | .[0:50])\t\(if .mirror then "镜像" else "" end)"' | column -t -s$'\t'
[[ "$count" -lt "$limit" ]] && break
((page++))
done
}
cmd_search() {
_fei_check_token || return 1
local query="" limit=50
while [[ $# -gt 0 ]]; do
case "$1" in
--limit) limit="$2"; shift 2 ;;
*) query="$1"; shift ;;
esac
done
[[ -z "$query" ]] && { echo "用法: fei search <关键词> [--limit N]" >&2; return 1; }
_fei_get "/repos/search?q=$(printf '%s' "$query" | jq -sRr @uri)&limit=${limit}" | \
jq -r '.data[] | "\(.full_name)\t\(.description // "-" | .[0:50])\t⭐\(.stars_count)"' | column -t -s$'\t'
}
cmd_info() {
_fei_check_token || return 1
local repo="${1:-$(_fei_repo_from_git)}"
_fei_get "/repos/${repo}" | jq -r '
"仓库: \(.full_name)",
"描述: \(.description // "无")",
"网站: \(.website // "无")",
"镜像: \(if .mirror then "是" else "否" end)",
"星标: \(.stars_count) Fork: \(.forks_count) Issue: \(.open_issues_count)",
"默认分支: \(.default_branch)",
"创建: \(.created_at[:10])",
"SSH: ssh://git@'"${FEI_HOST}:${FEI_SSH_PORT}"'/\(.full_name).git",
"HTTP: \(.clone_url)"'
}
cmd_web() {
local repo="${1:-$(_fei_repo_from_git)}"
local url="https://${FEI_HOST}/${repo}"
echo "$url"
xdg-open "$url" 2>/dev/null || open "$url" 2>/dev/null || echo "(请手动打开)"
}
cmd_fork() {
_fei_check_token || return 1
local repo="${1:?用法: fei fork <org/repo>}"
_fei_post "/repos/${repo}/forks" -d '{}' | jq -r '"Fork 完成: \(.full_name)\nSSH: ssh://git@'"${FEI_HOST}:${FEI_SSH_PORT}"'/\(.full_name).git"'
}
# ── 分支 ──
cmd_branch() {
local sub="${1:-list}"
shift 2>/dev/null || true
case "$sub" in
list) _branch_list "$@" ;;
create) _branch_create "$@" ;;
delete) _branch_delete "$@" ;;
*) echo "用法: fei branch {list|create|delete}" ;;
esac
}
_branch_list() {
_fei_check_token || return 1
local repo="${1:-$(_fei_repo_from_git)}"
_fei_get "/repos/${repo}/branches?limit=50" | \
jq -r '.[] | "\(.name)\t\(.commit.id[:8])\t\(.commit.message | split("\n")[0] | .[0:60])"' | column -t -s$'\t'
}
_branch_create() {
_fei_check_token || return 1
local name="${1:?用法: fei branch create <名称> [org/repo]}"
local repo="${2:-$(_fei_repo_from_git)}"
local default_branch
default_branch=$(_fei_get "/repos/${repo}" | jq -r '.default_branch')
_fei_post "/repos/${repo}/branches" \
-d "$(jq -n --arg n "$name" --arg b "$default_branch" '{new_branch_name: $n, old_branch_name: $b}')" | \
jq -r '"分支已创建: \(.name) (基于 '"$default_branch"')"'
}
_branch_delete() {
_fei_check_token || return 1
local name="${1:?用法: fei branch delete <名称> [org/repo]}"
local repo="${2:-$(_fei_repo_from_git)}"
local http_code
http_code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: token $FEI_TOKEN" \
"${FEI_API}/repos/${repo}/branches/${name}")
if [[ "$http_code" == "204" ]]; then
echo "分支已删除: $name"
else
echo "删除失败: HTTP $http_code" >&2; return 1
fi
}
cmd_diff() {
_fei_check_token || return 1
local base="${1:?用法: fei diff <分支1> <分支2> [org/repo]}"
local head="${2:?用法: fei diff <分支1> <分支2> [org/repo]}"
local repo="${3:-$(_fei_repo_from_git)}"
_fei_get "/repos/${repo}/compare/${base}...${head}" | \
jq -r '"比较: \(.base_commit.id[:8])...\(.merge_base_commit.id[:8])",
"提交数: \(.total_commits)",
"文件变更: \(.files | length)",
"---",
(.files[] | "\(.status)\t\(.filename)\t+\(.additions) -\(.deletions)")' | column -t -s$'\t'
}
cmd_log() {
_fei_check_token || return 1
local repo="${1:-$(_fei_repo_from_git)}"
_fei_get "/repos/${repo}/commits?limit=15" | \
jq -r '.[] | "\(.sha[:8])\t\(.commit.author.date[:10])\t\(.commit.author.name)\t\(.commit.message | split("\n")[0] | .[0:60])"' | column -t -s$'\t'
}
# ── 标签 ──
cmd_label() {
local sub="${1:-list}"
shift 2>/dev/null || true
case "$sub" in
list) _label_list "$@" ;;
create) _label_create "$@" ;;
*) echo "用法: fei label {list|create}" ;;
esac
}
_label_list() {
_fei_check_token || return 1
local repo="${1:-$(_fei_repo_from_git)}"
_fei_get "/repos/${repo}/labels?limit=50" | \
jq -r '.[] | "\(.name)\t#\(.color)\t\(.description // "")"' | column -t -s$'\t'
}
_label_create() {
_fei_check_token || return 1
local name="${1:?用法: fei label create <名称> <颜色>}"
local color="${2:?用法: fei label create <名称> <颜色>}"
local repo="${3:-$(_fei_repo_from_git)}"
color="${color#\#}"
_fei_post "/repos/${repo}/labels" \
-d "$(jq -n --arg n "$name" --arg c "$color" '{name: $n, color: ("#" + $c)}')" | \
jq -r '"标签已创建: \(.name) (#\(.color))"'
}
# ── 通知 ──
cmd_notify() {
_fei_check_token || return 1
local sub="${1:-list}"
case "$sub" in
count)
local count
count=$(_fei_get "/notifications/new" | jq -r '.new // 0')
echo "未读通知: ${count}" ;;
list|*)
_fei_get "/notifications?limit=20" | \
jq -r '.[] | "\(if .unread then "●" else "○" end) \(.subject.type)\t\(.subject.title | .[0:60])\t\(.repository.full_name)\t\(.updated_at[:10])"' | column -t -s$'\t'
[[ "$sub" != "list" ]] && true ;;
esac
}
# ── 管理 ──
cmd_admin() {
local sub="${1:-help}"
shift 2>/dev/null || true
case "$sub" in
users) _admin_users "$@" ;;
dump) _admin_dump "$@" ;;
doctor) _admin_doctor "$@" ;;
*) echo "用法: fei admin {users|dump|doctor}" ;;
esac
}
_admin_users() {
_fei_check_token || return 1
_fei_get "/admin/users?limit=50" | \
jq -r '.[] | "\(.id)\t\(.login)\t\(.email)\t\(if .is_admin then "管理员" else "" end)\t\(if .prohibit_login then "禁用" else "活跃" end)\t\(.last_login[:10] // "从未")"' | column -t -s$'\t'
}
_admin_dump() {
echo "触发 Forgejo 备份..."
ssh -o ConnectTimeout=5 feicode-prod \
"docker exec forgejo_FeiCode-1 forgejo dump -f /data/forgejo-dump-\$(date +%Y%m%d-%H%M%S).zip --quiet" 2>&1
echo "备份完成,文件在 feicode-prod 容器 /data/ 目录"
}
_admin_doctor() {
echo "运行 Forgejo 健康检查..."
ssh -o ConnectTimeout=10 feicode-prod \
"docker exec forgejo_FeiCode-1 forgejo doctor check" 2>&1
}
# ── PR ──
cmd_pr() {
local sub="${1:-list}"
shift 2>/dev/null || true
case "$sub" in
list) _pr_list "$@" ;;
create) _pr_create "$@" ;;
view) _pr_view "$@" ;;
merge) _pr_merge "$@" ;;
*) echo "用法: fei pr {list|create|view|merge}" ;;
esac
}
_pr_list() {
_fei_check_token || return 1
local repo="" state="open" limit=50
while [[ $# -gt 0 ]]; do
case "$1" in
--limit) limit="$2"; shift 2 ;;
*) [[ -z "$repo" ]] && repo="$1" || state="$1"; shift ;;
esac
done
repo="${repo:-$(_fei_repo_from_git)}"
_fei_get "/repos/${repo}/pulls?state=${state}&limit=${limit}" | \
jq -r '.[] | "#\(.number)\t\(.title | .[0:50])\t\(.user.login)\t\(.head.label)→\(.base.label)\t\(.created_at[:10])"' | column -t -s$'\t'
}
_pr_create() {
_fei_check_token || return 1
local title="${1:?用法: fei pr create <标题> [正文] [-b <base>]}"
shift
local body="" base="main"
while [[ $# -gt 0 ]]; do
case "$1" in
-b|--base) base="$2"; shift 2 ;;
*) body="$1"; shift ;;
esac
done
local repo head
repo=$(_fei_repo_from_git)
head=$(git branch --show-current)
local payload
payload=$(jq -n --arg t "$title" --arg b "$body" --arg h "$head" --arg base "$base" \
'{title: $t, body: $b, head: $h, base: $base}')
_fei_post "/repos/${repo}/pulls" -d "$payload" | \
jq -r '"PR #\(.number) 已创建: \(.html_url)"'
}
_pr_view() {
_fei_check_token || return 1
local num="${1:?用法: fei pr view <编号>}"
local repo="${2:-$(_fei_repo_from_git)}"
_fei_get "/repos/${repo}/pulls/${num}" | jq -r '
"PR #\(.number): \(.title)",
"状态: \(.state) 可合并: \(.mergeable)",
"作者: \(.user.login) 分支: \(.head.label) → \(.base.label)",
"创建: \(.created_at[:10])",
"",
(.body // "无描述")'
}
_pr_merge() {
_fei_check_token || return 1
local num="${1:?用法: fei pr merge <编号>}"
local repo="${2:-$(_fei_repo_from_git)}"
_fei_post "/repos/${repo}/pulls/${num}/merge" -d '{"Do":"merge"}' | \
jq -r '// "PR #'"$num"' 已合并"'
}
# ── Issue ──
cmd_issue() {
local sub="${1:-list}"
shift 2>/dev/null || true
case "$sub" in
list) _issue_list "$@" ;;
create) _issue_create "$@" ;;
view) _issue_view "$@" ;;
close) _issue_close "$@" ;;
*) echo "用法: fei issue {list|create|view|close}" ;;
esac
}
_issue_list() {
_fei_check_token || return 1
local repo="" state="open" limit=50
while [[ $# -gt 0 ]]; do
case "$1" in
--limit) limit="$2"; shift 2 ;;
*) [[ -z "$repo" ]] && repo="$1" || state="$1"; shift ;;
esac
done
repo="${repo:-$(_fei_repo_from_git)}"
_fei_get "/repos/${repo}/issues?state=${state}&type=issues&limit=${limit}" | \
jq -r '.[] | "#\(.number)\t\(.title | .[0:50])\t\(.user.login)\t\(.labels | map(.name) | join(",") | .[0:20])\t\(.created_at[:10])"' | column -t -s$'\t'
}
_issue_create() {
_fei_check_token || return 1
local title="${1:?用法: fei issue create <标题> [正文]}"
local body="${2:-}"
local repo
repo=$(_fei_repo_from_git)
local payload
payload=$(jq -n --arg t "$title" --arg b "$body" '{title: $t, body: $b}')
_fei_post "/repos/${repo}/issues" -d "$payload" | \
jq -r '"Issue #\(.number) 已创建: \(.html_url)"'
}
_issue_view() {
_fei_check_token || return 1
local num="${1:?用法: fei issue view <编号>}"
local repo="${2:-$(_fei_repo_from_git)}"
_fei_get "/repos/${repo}/issues/${num}" | jq -r '
"Issue #\(.number): \(.title)",
"状态: \(.state) 标签: \(.labels | map(.name) | join(", ") // "无")",
"作者: \(.user.login) 创建: \(.created_at[:10])",
"",
(.body // "无描述")'
}
_issue_close() {
_fei_check_token || return 1
local num="${1:?用法: fei issue close <编号>}"
local repo="${2:-$(_fei_repo_from_git)}"
_fei_patch "/repos/${repo}/issues/${num}" -d '{"state":"closed"}' | \
jq -r '"Issue #\(.number) 已关闭"'
}
# ── Release ──
cmd_release() {
local sub="${1:-list}"
shift 2>/dev/null || true
case "$sub" in
list) _release_list "$@" ;;
create) _release_create "$@" ;;
*) echo "用法: fei release {list|create}" ;;
esac
}
_release_list() {
_fei_check_token || return 1
local repo="${1:-$(_fei_repo_from_git)}"
_fei_get "/repos/${repo}/releases?limit=10" | \
jq -r '.[] | "\(.tag_name)\t\(.name // "-")\t\(.published_at[:10])\t\(.assets | length)个附件"' | column -t -s$'\t'
}
_release_create() {
_fei_check_token || return 1
local tag="${1:?用法: fei release create <tag> <标题> [--asset <文件>]}"
local title="${2:?用法: fei release create <tag> <标题> [--asset <文件>]}"
shift 2
local body="" asset=""
while [[ $# -gt 0 ]]; do
case "$1" in
--asset) asset="${2:?--asset 需要指定文件路径}"; shift 2 ;;
*) body="$1"; shift ;;
esac
done
local repo
repo=$(_fei_repo_from_git)
local payload
payload=$(jq -n --arg t "$tag" --arg n "$title" --arg b "$body" \
'{tag_name: $t, name: $n, body: $b}')
local result
result=$(_fei_post "/repos/${repo}/releases" -d "$payload")
echo "$result" | jq -r '"Release \(.tag_name) 已创建: \(.html_url)"'
if [[ -n "$asset" ]]; then
if [[ ! -f "$asset" ]]; then
echo "错误: 文件不存在: $asset" >&2
return 1
fi
local release_id
release_id=$(echo "$result" | jq -r '.id')
local filename
filename=$(basename "$asset")
echo "上传附件: $filename ..."
curl -s -o /dev/null -w '' -X POST \
-H "Authorization: token $FEI_TOKEN" \
-F "attachment=@${asset}" \
"${FEI_API}/repos/${repo}/releases/${release_id}/assets?name=$(printf '%s' "$filename" | jq -sRr @uri)" \
| jq -r '"附件已上传: \(.name) (\(.size) bytes)"' 2>/dev/null || echo "附件上传完成"
fi
}
# ── 文件操作 ──
cmd_cat() {
_fei_check_token || return 1
local filepath="${1:?用法: fei cat <文件路径> [org/repo]}"
local repo="${2:-$(_fei_repo_from_git)}"
_fei_get "/repos/${repo}/contents/${filepath}" | jq -r '.content' | base64 -d
}
cmd_ls() {
_fei_check_token || return 1
local dir="${1:-.}"
local repo="${2:-$(_fei_repo_from_git)}"
[[ "$dir" == "." ]] && dir=""
_fei_get "/repos/${repo}/contents/${dir}" | \
jq -r '.[] | "\(if .type == "dir" then "📁" else "📄" end) \(.name)\t\(.size // "")"' | column -t -s$'\t'
}
# ── 推送 (集成 wenpai 的 push-feicode 逻辑) ──
cmd_push() {
# 优先使用 push-feicode.sh过滤规则统一
local push_script
for push_script in "$HOME/bin/push-feicode.sh" /usr/local/bin/push-feicode.sh; do
if [[ -x "$push_script" ]]; then
exec "$push_script" "$@"
fi
done
# 内置逻辑(无 push-feicode.sh 时的 fallback
local tag="" branch="main" remote="origin"
if [[ "${1:-}" == "--tag" ]]; then
tag="${2:?用法: fei push --tag <tag> [remote]}"
remote="${3:-origin}"
else
branch="${1:-$(git branch --show-current 2>/dev/null || echo main)}"
remote="${2:-origin}"
fi
local exclude_file=".git/push-exclude"
# 无排除文件,直接推送
if [[ ! -f "$exclude_file" ]]; then
if [[ -n "$tag" ]]; then
git push "$remote" "refs/tags/$tag"
else
git push "$remote" "$branch"
fi
return
fi
# 有排除文件,走过滤推送
if ! command -v git-filter-repo &>/dev/null; then
echo "错误: 需要 git-filter-repo (pip install git-filter-repo)" >&2
return 1
fi
local -a filter_args=()
while IFS= read -r line; do
line=$(echo "$line" | sed 's/#.*//' | xargs)
[[ -z "$line" ]] && continue
filter_args+=(--path "$line")
done < "$exclude_file"
if [[ ${#filter_args[@]} -eq 0 ]]; then
if [[ -n "$tag" ]]; then
git push "$remote" "refs/tags/$tag"
else
git push "$remote" "$branch"
fi
return
fi
local remote_url repo_name tmpdir
remote_url=$(git remote get-url "$remote" 2>/dev/null) || { echo "错误: remote '$remote' 不存在" >&2; return 1; }
repo_name=$(basename "$(git rev-parse --show-toplevel)")
tmpdir=$(mktemp -d "/tmp/fei-push-${repo_name}-XXXXXX")
trap 'rm -rf "$tmpdir"' EXIT
local clone_ref="${tag:-$branch}"
echo ">>> 克隆 ($clone_ref) 到临时目录..."
git clone --no-hardlinks --branch "$clone_ref" "$(git rev-parse --show-toplevel)" "$tmpdir/repo" --quiet
echo ">>> 过滤 ${#filter_args[@]} 个敏感路径..."
cd "$tmpdir/repo"
git filter-repo --invert-paths "${filter_args[@]}" --force --quiet
git remote add target "$remote_url"
if [[ -n "$tag" ]]; then
git tag -f "$tag"
echo ">>> 推送 tag $tag..."
git push target "refs/tags/$tag" --force
local current_branch
current_branch=$(git branch --show-current)
[[ -n "$current_branch" ]] && git push target "$current_branch" --force
else
echo ">>> 推送 $branch..."
git push target "$branch" --force
fi
echo ">>> 完成"
}
# ── Pages ──
_pages_workflow() {
# 生成标准 deploy-pages.yml 内容
cat << 'WORKFLOW'
name: Deploy Pages
on:
push:
branches: [main]
jobs:
deploy:
runs-on: linux-arm64
steps:
- uses: actions/checkout@v4
- name: Deploy to feicode-prod
run: |
SITE_NAME="${GITHUB_REPOSITORY##*/}"
SITE_NAME="${SITE_NAME%-pages}"
PAGES_DIR="/home/www/pages/${SITE_NAME}"
ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 feicode-prod "mkdir -p ${PAGES_DIR}"
rsync -av --delete \
--exclude='.git' \
--exclude='.forgejo' \
--exclude='.gitignore' \
-e "ssh -o StrictHostKeyChecking=no" \
./ feicode-prod:${PAGES_DIR}/
echo "✅ Deployed to feicode-prod:${PAGES_DIR}"
echo "🌐 https://${SITE_NAME}.feibisi.net"
WORKFLOW
}
cmd_pages() {
local sub="${1:-list}"
shift 2>/dev/null || true
case "$sub" in
create) _pages_create "$@" ;;
list) _pages_list "$@" ;;
open) _pages_open "$@" ;;
*) echo "用法: fei pages {create|list|open}" ;;
esac
}
_pages_create() {
_fei_check_token || return 1
local site="${1:?用法: fei pages create <站点名>}"
local repo_name="${site}-pages"
# 创建仓库
local result
result=$(_fei_post "/orgs/feiCode/repos" \
-d "$(jq -n --arg n "$repo_name" --arg d "Pages 站点: ${site}.feibisi.net" \
'{name: $n, description: $d, private: false, auto_init: true, default_branch: "main"}')")
local full_name
full_name=$(echo "$result" | jq -r '.full_name // empty')
if [[ -z "$full_name" ]]; then
echo "创建失败: $(echo "$result" | jq -r '.message // "未知错误"')" >&2
return 1
fi
# 添加 workflow 文件
local workflow_content
workflow_content=$(printf '%s' "$(_pages_workflow)" | base64 -w 0)
curl -sf -X POST \
-H "Authorization: token $FEI_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg c "$workflow_content" '{message: "feat: 添加 Pages 自动部署 workflow", content: $c}')" \
"${FEI_API}/repos/${full_name}/contents/.forgejo/workflows/deploy-pages.yml" > /dev/null
echo "✅ Pages 站点已创建"
echo "仓库: https://${FEI_HOST}/${full_name}"
echo "站点: https://${site}.feibisi.net"
echo ""
echo "克隆后放入静态文件,推送 main 即自动部署:"
echo " fei clone ${full_name}"
}
_pages_list() {
_fei_check_token || return 1
_fei_get "/orgs/feiCode/repos?limit=50" | \
jq -r '.[] | select(.name | endswith("-pages")) | "\(.name | sub("-pages$";""))\thttps://\(.name | sub("-pages$";"")).feibisi.net\t\(.html_url)"' | column -t -s$'\t'
# 也检查 feibisi 用户下的
_fei_get "/user/repos?limit=50" | \
jq -r '.[] | select(.name | endswith("-pages")) | "\(.name | sub("-pages$";""))\thttps://\(.name | sub("-pages$";"")).feibisi.net\t\(.html_url)"' | column -t -s$'\t'
}
_pages_open() {
local site="${1:?用法: fei pages open <站点名>}"
local url="https://${site}.feibisi.net"
echo "$url"
xdg-open "$url" 2>/dev/null || open "$url" 2>/dev/null || echo "(请手动打开)"
}
# ── 主入口 ──
main() {
local cmd="${1:-help}"
shift 2>/dev/null || true
case "$cmd" in
clone) cmd_clone "$@" ;;
repos) cmd_repos "$@" ;;
search) cmd_search "$@" ;;
info) cmd_info "$@" ;;
web) cmd_web "$@" ;;
fork) cmd_fork "$@" ;;
branch) cmd_branch "$@" ;;
diff) cmd_diff "$@" ;;
log) cmd_log "$@" ;;
pr) cmd_pr "$@" ;;
issue) cmd_issue "$@" ;;
release) cmd_release "$@" ;;
label) cmd_label "$@" ;;
notify) cmd_notify "$@" ;;
cat) cmd_cat "$@" ;;
ls) cmd_ls "$@" ;;
push) cmd_push "$@" ;;
pages) cmd_pages "$@" ;;
admin) cmd_admin "$@" ;;
whoami) cmd_whoami "$@" ;;
orgs) cmd_orgs "$@" ;;
help|-h|--help) cmd_help ;;
--version|-V) echo "fei $FEI_VERSION" ;;
*) echo "未知命令: $cmd (fei help 查看帮助)" >&2; return 1 ;;
esac
}
main "$@"