gitlink-cli/workflows/lib/common.sh

306 lines
11 KiB
Bash

#!/usr/bin/env bash
# Common utilities for gitlink-cli workflow scripts
set -euo pipefail
# Trap SIGPIPE to prevent premature exit when piping through head/truncate
trap '' PIPE
# Ensure jq is available (WinGet installs to non-default PATH on Windows)
if ! command -v jq &>/dev/null; then
for d in "$LOCALAPPDATA/Microsoft/WinGet/Links" "$HOME/AppData/Local/Microsoft/WinGet/Links"; do
[[ -d "$d" ]] && export PATH="$d:$PATH"
done
fi
# Ensure CLAUDE_CODE_GIT_BASH_PATH is set for Windows (needed by claude CLI)
if [[ -z "${CLAUDE_CODE_GIT_BASH_PATH:-}" ]] && command -v cygpath &>/dev/null; then
export CLAUDE_CODE_GIT_BASH_PATH="$(cygpath -w "$(which bash)")"
fi
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
# ── Logging ──────────────────────────────────────────────────────────
log_step() { echo -e "${BLUE}[STEP]${NC} $*"; }
log_ok() { echo -e "${GREEN}[ OK]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_err() { echo -e "${RED}[ ERR]${NC} $*" >&2; }
log_info() { echo -e "${CYAN}[INFO]${NC} $*"; }
log_title(){ echo -e "\n${BOLD}══════ $* ══════${NC}\n"; }
# ── Auth Check ───────────────────────────────────────────────────────
check_auth() {
# Check env var first, then try CLI auth status
if [[ -n "${GITLINK_TOKEN:-}" ]]; then
log_ok "GITLINK_TOKEN is set"
return 0
fi
local status
status=$(gitlink-cli auth status 2>&1)
if echo "$status" | grep -qi "logged in\|✓"; then
log_ok "Authenticated: $(echo "$status" | sed -n 's/.*as //p' | tr -d '[:space:]')"
return 0
fi
log_err "Not authenticated. Please login first:"
log_info " gitlink-cli auth login"
log_info " export GITLINK_TOKEN=\"your-private-token\""
exit 1
}
# ── JSON Helpers ─────────────────────────────────────────────────────
# Extract a field from CLI JSON output (Envelope: {ok, data, ...})
json_ok() {
echo "$1" | jq -r '.ok // false' 2>/dev/null
}
json_data() {
echo "$1" | jq -r '.data' 2>/dev/null
}
json_get() {
echo "$1" | jq -r "$2" 2>/dev/null
}
json_error() {
echo "$1" | jq -r '.error.message // "unknown error"' 2>/dev/null
}
# ── CLI Wrapper ──────────────────────────────────────────────────────
GL="gitlink-cli"
gl_run() {
local output
# Always use JSON format for scripting
output=$("$GL" "$@" --format json 2>&1) || true
echo "$output"
}
gl_check() {
local output
output=$(gl_run "$@")
# Check if output is valid JSON (use here-string to avoid SIGPIPE)
if ! jq empty <<< "$output" 2>/dev/null; then
log_err "Command failed (non-JSON response): $GL $*"
log_err "$output"
return 1
fi
if [[ "$(json_ok "$output")" != "true" ]]; then
log_err "Command failed: $GL $*"
log_err "$(json_error "$output")"
return 1
fi
echo "$output"
}
# ── Owner/Repo Detection ────────────────────────────────────────────
detect_owner_repo() {
local remote_url
remote_url=$(git remote get-url origin 2>/dev/null || echo "")
if [[ -z "$remote_url" ]]; then
log_err "No git remote 'origin' found. Use --owner and --repo flags."
exit 1
fi
# Parse gitlink URL patterns
# https://gitlink.org.cn/owner/repo.git or git@gitlink.org.cn:owner/repo.git
if [[ "$remote_url" =~ gitlink\.org\.cn[:/]([^/]+)/([^/.]+) ]]; then
DETECTED_OWNER="${BASH_REMATCH[1]}"
DETECTED_REPO="${BASH_REMATCH[2]}"
else
log_err "Cannot parse owner/repo from remote: $remote_url"
exit 1
fi
}
require_owner_repo() {
if [[ -z "${OWNER:-}" || -z "${REPO:-}" ]]; then
detect_owner_repo
OWNER="${OWNER:-$DETECTED_OWNER}"
REPO="${REPO:-$DETECTED_REPO}"
fi
log_info "Using: ${OWNER}/${REPO}"
}
# ── Confirmation ─────────────────────────────────────────────────────
confirm() {
local msg="${1:-Proceed?}"
if [[ "${DRY_RUN:-false}" == "true" ]]; then
log_warn "[DRY RUN] Would execute: $msg"
return 1
fi
read -rp "$(echo -e "${YELLOW}$msg [y/N]${NC} ")" answer
[[ "$answer" =~ ^[Yy] ]]
}
# ── Date Helpers ─────────────────────────────────────────────────────
date_today() {
date +%Y-%m-%d
}
date_week_ago() {
date -d "7 days ago" +%Y-%m-%d 2>/dev/null || date -v-7d +%Y-%m-%d 2>/dev/null
}
date_month_ago() {
date -d "30 days ago" +%Y-%m-%d 2>/dev/null || date -v-30d +%Y-%m-%d 2>/dev/null
}
# ── Parameter Parsing ────────────────────────────────────────────────
parse_common_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--owner) OWNER="$2"; shift 2 ;;
--repo) REPO="$2"; shift 2 ;;
--dry-run) DRY_RUN="true"; shift ;;
--help|-h) usage; exit 0 ;;
*) break ;;
esac
done
}
# ── Section Divider ──────────────────────────────────────────────────
divider() {
echo -e "${CYAN}────────────────────────────────────────────────${NC}"
}
# ══════════════════════════════════════════════════════════════════════
# Scientific Research Helpers
# ══════════════════════════════════════════════════════════════════════
# Normalize value against max (returns 0-1, 0 if max is 0)
normalize() {
awk -v val="$1" -v max="$2" 'BEGIN { printf "%.4f", (max > 0) ? val / max : 0 }'
}
# Clamp value between lo and hi
clamp() {
awk -v val="$1" -v lo="$2" -v hi="$3" 'BEGIN { printf "%.4f", (val < lo) ? lo : ((val > hi) ? hi : val) }'
}
# Weighted sum: pass pairs of "value weight" as arguments
weighted_sum() {
awk 'BEGIN { sum=0; for(i=1;i<ARGC;i+=2) sum+=(ARGV[i]*ARGV[i+1]); printf "%.4f", sum }' "$@"
}
# Jaccard similarity between two space-separated strings
jaccard() {
echo "$1 $2" | awk '{
n1 = split($1, a1, " "); n2 = split($2, a2, " ");
delete seen; inter = 0;
for (i in a1) seen[a1[i]] = 1;
for (i in a2) if (seen[a2[i]] == 1) { inter++; seen[a2[i]] = 2; }
union = 0;
for (i in seen) union++;
printf "%.4f", (union > 0) ? inter / union : 0;
}'
}
# Detect programming language from file extension
detect_lang_from_ext() {
local ext="${1##*.}"
case "$ext" in
go) echo "Go" ;;
py|pyx) echo "Python" ;;
js|ts|jsx|tsx|mjs|cjs) echo "JavaScript/TypeScript" ;;
rs) echo "Rust" ;;
java) echo "Java" ;;
kt|kts) echo "Kotlin" ;;
c|cpp|cxx|h|hpp|hxx) echo "C/C++" ;;
r|R) echo "R" ;;
jl) echo "Julia" ;;
m|mm) echo "MATLAB/Objective-C" ;;
swift) echo "Swift" ;;
rb) echo "Ruby" ;;
php) echo "PHP" ;;
scala) echo "Scala" ;;
dart) echo "Dart" ;;
lua) echo "Lua" ;;
ipynb) echo "Jupyter Notebook" ;;
sh|bash|zsh) echo "Shell" ;;
ps1|psm1|psd1) echo "PowerShell" ;;
*) echo "Other" ;;
esac
}
# Cross-platform days between two dates (YYYY-MM-DD format)
days_between() {
local d1 d2 diff
d1=$(date -d "$1" +%s 2>/dev/null || date -jf "%Y-%m-%d" "$1" +%s 2>/dev/null || echo "0")
d2=$(date -d "$2" +%s 2>/dev/null || date -jf "%Y-%m-%d" "$2" +%s 2>/dev/null || echo "0")
diff=$(( (d2 - d1) / 86400 ))
echo "${diff#-}"
}
# Get today, N days ago (cross-platform)
date_days_ago() {
local n="$1"
date -d "$n days ago" +%Y-%m-%d 2>/dev/null || date -v-"$n"d +%Y-%m-%d 2>/dev/null
}
# Extract first N space-separated authors into BibTeX format
# Input: "First Last" "First2 Last2" ...
format_authors_bibtex() {
local names="$1" count=0 result=""
for name in $names; do
count=$((count + 1))
if [[ $count -gt 10 ]]; then
result="${result} and others"
break
fi
local last="${name##* }" first="${name%% *}"
[[ $count -gt 1 ]] && result="${result} and "
result="${result}${last}, ${first}"
done
echo "$result"
}
# Extract organization name from login (try git remote or repo +info)
org_from_owner() {
local owner="$1"
# check if it's an org or user by listing repos
local out
out=$(gl_run repo +list --user "$owner" --limit 1)
if [[ "$(json_ok "$out")" == "true" ]]; then
echo "$owner"
else
echo ""
fi
}
# Min and max helpers for awk
min_val() { awk -v a="$1" -v b="$2" 'BEGIN { print (a < b) ? a : b }'; }
max_val() { awk -v a="$1" -v b="$2" 'BEGIN { print (a > b) ? a : b }'; }
# ── Knowledge Graph Helpers ──────────────────────────────────────────
# Generate a unique node ID
kg_node_id() { echo "${1}:${2}" | tr '/' '_' | tr ' ' '_'; }
# URL-encode a string (basic)
url_encode() {
local str="$1"
echo "$str" | jq -sRr @uri 2>/dev/null || echo "$str"
}
# Escape JSON string value
json_escape() {
echo "$1" | jq -Rsa . 2>/dev/null || echo "\"$1\""
}
# ── Color-coded Severity ──────────────────────────────────────────────
severity_color() {
case "$1" in
Critical|critical|CRITICAL) echo -e "${RED}$1${NC}" ;;
Warning|warning|WARNING) echo -e "${YELLOW}$1${NC}" ;;
Info|info|INFO) echo -e "${CYAN}$1${NC}" ;;
OK|ok|CLEAN) echo -e "${GREEN}$1${NC}" ;;
*) echo "$1" ;;
esac
}