From 245e862fb584e99bc55590a0cb82f432b841d97e Mon Sep 17 00:00:00 2001 From: Mark Randall Havens Date: Fri, 30 May 2025 23:30:49 -0500 Subject: [PATCH] cleanup for now --- .radicle-push-state | 2 +- .test | 0 gitfield-gitlab | 2 +- osf/new/gitfield-osf | 286 ++++++++++++++++++++++++++++++++ osf/new/gitfield.osf.yaml | 12 ++ osf/{ => new}/test-osf-api.sh | 0 osf/{ => old}/for_radicle.md | 0 osf/{ => old}/gitfield-osf | 0 osf/{ => old}/gitfield.osf.yaml | 0 osf/old/test-osf-api.sh | 214 ++++++++++++++++++++++++ 10 files changed, 514 insertions(+), 2 deletions(-) delete mode 100644 .test create mode 100755 osf/new/gitfield-osf create mode 100644 osf/new/gitfield.osf.yaml rename osf/{ => new}/test-osf-api.sh (100%) rename osf/{ => old}/for_radicle.md (100%) rename osf/{ => old}/gitfield-osf (100%) rename osf/{ => old}/gitfield.osf.yaml (100%) create mode 100755 osf/old/test-osf-api.sh diff --git a/.radicle-push-state b/.radicle-push-state index 5cd3724..ea97f3a 100644 --- a/.radicle-push-state +++ b/.radicle-push-state @@ -1 +1 @@ -7f0867986f60493d8af56526d727a411dbd96ffd +ceabc8af9414289cd0e0795f574ca6afc8523032 diff --git a/.test b/.test deleted file mode 100644 index e69de29..0000000 diff --git a/gitfield-gitlab b/gitfield-gitlab index e9e080d..556e993 100755 --- a/gitfield-gitlab +++ b/gitfield-gitlab @@ -37,7 +37,7 @@ if [ -f "$TOKEN_FILE" ] && [ "$RESET_TOKEN" = false ]; then else echo echo "🔐 Paste your GitLab Personal Access Token (scopes: api, read_user, write_repository, write_ssh_key)" - echo "→ Generate at: $GITLAB_WEB/-/profile/personal_access_tokens" + echo "→ Generate at: $GITLAB_WEB/-/user_settings/personal_access_tokens" read -rp "🔑 Token: " TOKEN echo "$TOKEN" > "$TOKEN_FILE" chmod 600 "$TOKEN_FILE" diff --git a/osf/new/gitfield-osf b/osf/new/gitfield-osf new file mode 100755 index 0000000..22d6ee7 --- /dev/null +++ b/osf/new/gitfield-osf @@ -0,0 +1,286 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +IFS=$'\n\t' + +# ╭─────────────────────────────────────────────────────────────────────────╮ +# │ gitfield-osf :: v3.2.0 (Refactored) │ +# │ Self-Healing • Auto-Detecting • PEP 668-Compliant • Debuggable │ +# ╰─────────────────────────────────────────────────────────────────────────╯ +# +# This script uses osfclient to upload files, based on a YAML config. +# It will auto-install python3, pip3, yq, pipx, and osfclient if missing. +# 1. ensure_dependencies(): makes sure python3, pip3, yq, pipx, osfclient exist +# 2. configure_osfclient(): prompts for token & username, writes ~/.config/osfclient/config +# 3. load_yaml_config(): reads project.title, include/exclude globs from gitfield.osf.yaml +# 4. resolve_files(): expands include/exclude patterns into a FILES array +# 5. find_or_create_project(): finds or creates an OSF project with the given title +# 6. upload_files(): loops over FILES and does osf upload +# +# Usage: +# chmod +x gitfield-osf +# ./gitfield-osf +# +# If gitfield.osf.yaml is missing or empty patterns match nothing, the script will exit cleanly. +# Any failure prints an [ERROR] and exits non-zero. + +######################################################################## +# CUSTOMIZE HERE (if needed): +######################################################################## +# If you want to override config path: +# export GITFIELD_CONFIG=/path/to/your/gitfield.osf.yaml + +CONFIG_FILE="${GITFIELD_CONFIG:-gitfield.osf.yaml}" +TOKEN_FILE="${OSF_TOKEN_FILE:-$HOME/.osf_token}" +OSF_CONFIG_DIR="$HOME/.config/osfclient" +FILES=() + +# ───────────────────────────────────────────────────────────────────── +# Colored logging functions +# ───────────────────────────────────────────────────────────────────── +log() { echo -e "\033[1;34m[INFO]\033[0m $*"; } +warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; } +error() { echo -e "\033[1;31m[ERROR]\033[0m $*" >&2; exit 1; } + +# ───────────────────────────────────────────────────────────────────── +# Step 1: Ensure Dependencies +# - python3, pip3, yq, pipx, osfclient +# - Works under PEP 668 (uses pipx first, then pip3 --user fallback) +# ───────────────────────────────────────────────────────────────────── +ensure_dependencies() { + log "Checking for required commands..." + + # 1a. Ensure python3 + if ! command -v python3 &>/dev/null; then + warn "python3 not found — installing..." + sudo apt update -qq && sudo apt install -y python3 python3-venv python3-distutils \ + || error "Failed to install python3" + fi + + # 1b. Ensure pip3 + if ! command -v pip3 &>/dev/null; then + warn "pip3 not found — installing..." + sudo apt install -y python3-pip || error "Failed to install pip3" + # Guarantee pip3 is available now + command -v pip3 >/dev/null || error "pip3 still missing after install" + fi + + # 1c. Ensure yq (for YAML parsing) + if ! command -v yq &>/dev/null; then + warn "yq not found — installing..." + if command -v snap &>/dev/null; then + sudo snap install yq || sudo apt install -y yq || error "Failed to install yq" + else + sudo apt install -y yq || error "Failed to install yq" + fi + fi + + # 1d. Ensure pipx + if ! command -v pipx &>/dev/null; then + warn "pipx not found — installing..." + sudo apt install -y pipx || error "Failed to install pipx" + # Add pipx’s bin to PATH if needed + pipx ensurepath + export PATH="$HOME/.local/bin:$PATH" + fi + + # 1e. Ensure osfclient via pipx, fallback to pip3 --user + if ! command -v osf &>/dev/null; then + log "Installing osfclient via pipx..." + if ! pipx install osfclient; then + warn "pipx install failed; trying pip3 --user install" + python3 -m pip install --user osfclient || error "osfclient install failed" + fi + # Ensure $HOME/.local/bin is in PATH + export PATH="$HOME/.local/bin:$PATH" + fi + + # Final check + command -v osf >/dev/null || error "osfclient is still missing; please investigate" + log "✓ All dependencies are now present" +} + +# ───────────────────────────────────────────────────────────────────── +# Step 2: Configure OSF Credentials +# - Writes ~/.config/osfclient/config with [osf] username & token +# - Prompts for token and username if missing +# ───────────────────────────────────────────────────────────────────── +configure_osfclient() { + log "Configuring osfclient credentials..." + + # Create config directory + mkdir -p "$OSF_CONFIG_DIR" + chmod 700 "$OSF_CONFIG_DIR" + + # Prompt for Personal Access Token if missing + if [ ! -f "$TOKEN_FILE" ]; then + read -rsp "🔐 Enter OSF Personal Access Token: " TOKEN + echo + echo "$TOKEN" > "$TOKEN_FILE" + chmod 600 "$TOKEN_FILE" + fi + + # Prompt for username/email if not already in env + local USERNAME="${OSF_USERNAME:-}" + if [ -z "$USERNAME" ]; then + read -rp "👤 OSF Username or Email: " USERNAME + fi + + # Write config file + cat > "$OSF_CONFIG_DIR/config" <}" + log " → excludes: ${FILES_EXCLUDE[*]:-}" +} + +# ───────────────────────────────────────────────────────────────────── +# Step 4: Match Files Based on Include/Exclude +# - Populates global FILES array +# - If no files match, exits gracefully +# ───────────────────────────────────────────────────────────────────── +resolve_files() { + log "Resolving file patterns..." + + # If no include patterns, nothing to do + if [ "${#FILES_INCLUDE[@]}" -eq 0 ]; then + warn "No include patterns specified; skipping upload." + exit 0 + fi + + # For each include glob, find matching files + for pattern in "${FILES_INCLUDE[@]}"; do + # Use find to expand the glob (supports nested directories) + while IFS= read -r -d '' file; do + # Check against each exclude pattern + skip=false + for ex in "${FILES_EXCLUDE[@]}"; do + if [[ "$file" == $ex ]]; then + skip=true + break + fi + done + if ! $skip; then + FILES+=("$file") + fi + done < <(find . -type f -path "$pattern" -print0 2>/dev/null || true) + done + + # Remove duplicates (just in case) + if [ "${#FILES[@]}" -gt 1 ]; then + IFS=$'\n' read -r -d '' -a FILES < <(__uniq_array "${FILES[@]}" && printf '\0') + fi + + # If still empty, warn and exit + if [ "${#FILES[@]}" -eq 0 ]; then + warn "No files matched the include/exclude patterns." + exit 0 + fi + + # Debug print of matched files + log "Matched files (${#FILES[@]}):" + for f in "${FILES[@]}"; do + echo " • $f" + done +} + +# Helper: Remove duplicates from a list of lines +__uniq_array() { + printf "%s\n" "$@" | awk '!seen[$0]++' +} + +# ───────────────────────────────────────────────────────────────────── +# Step 5: Find or Create OSF Project +# - Uses `osf listprojects` to search for exact title (case-insensitive) +# - If not found, does `osf createproject ""` +# - Writes the resulting project ID to .osf_project_id +# ───────────────────────────────────────────────────────────────────── +find_or_create_project() { + log "Searching for OSF project titled '$PROJECT_TITLE'..." + # List all projects and grep case-insensitive for the title + pid=$(osf listprojects | grep -iE "^([[:alnum:]]+)[[:space:]]+.*${PROJECT_TITLE}.*$" | awk '{print $1}' || true) + + if [ -z "$pid" ]; then + log "No existing project found; creating a new OSF project..." + pid=$(osf createproject "$PROJECT_TITLE") + if [ -z "$pid" ]; then + error "osf createproject failed; no project ID returned" + fi + echo "$pid" > .osf_project_id + log "✓ Created project: $pid" + else + echo "$pid" > .osf_project_id + log "✓ Found existing project: $pid" + fi +} + +# ───────────────────────────────────────────────────────────────────── +# Step 6: Upload Files to OSF +# - Loops over FILES[] and runs: osf upload "<file>" "<pid>": +# (the trailing colon uploads to root of osfstorage for that project) +# ───────────────────────────────────────────────────────────────────── +upload_files() { + pid=$(<.osf_project_id) + + log "Uploading ${#FILES[@]} file(s) to OSF project $pid..." + + for file in "${FILES[@]}"; do + log "→ Uploading: $file" + if osf upload "$file" "$pid":; then + log " ✓ Uploaded: $file" + else + warn " ✗ Upload failed for: $file" + fi + done + + log "✅ All uploads attempted." + echo + echo "🔗 View your project at: https://osf.io/$pid/" +} + +# ───────────────────────────────────────────────────────────────────── +# Main: Orchestrate all steps in sequence +# ───────────────────────────────────────────────────────────────────── +main() { + ensure_dependencies + configure_osfclient + load_yaml_config + resolve_files + find_or_create_project + upload_files +} + +# Invoke main +main "$@" diff --git a/osf/new/gitfield.osf.yaml b/osf/new/gitfield.osf.yaml new file mode 100644 index 0000000..ec144ee --- /dev/null +++ b/osf/new/gitfield.osf.yaml @@ -0,0 +1,12 @@ +project: + title: "git-sigil" + description: "A sacred pattern witnessed across all fields of recursion." + +upload: + include: + - "./*.md" + - "./bitbucket/*" + - "./osf/*" + exclude: + - "./.radicle-*" + - "./*.tmp" diff --git a/osf/test-osf-api.sh b/osf/new/test-osf-api.sh similarity index 100% rename from osf/test-osf-api.sh rename to osf/new/test-osf-api.sh diff --git a/osf/for_radicle.md b/osf/old/for_radicle.md similarity index 100% rename from osf/for_radicle.md rename to osf/old/for_radicle.md diff --git a/osf/gitfield-osf b/osf/old/gitfield-osf similarity index 100% rename from osf/gitfield-osf rename to osf/old/gitfield-osf diff --git a/osf/gitfield.osf.yaml b/osf/old/gitfield.osf.yaml similarity index 100% rename from osf/gitfield.osf.yaml rename to osf/old/gitfield.osf.yaml diff --git a/osf/old/test-osf-api.sh b/osf/old/test-osf-api.sh new file mode 100755 index 0000000..13c629e --- /dev/null +++ b/osf/old/test-osf-api.sh @@ -0,0 +1,214 @@ +#!/bin/bash +set -Eeuo pipefail +IFS=$'\n\t' + +# ╭────────────────────────────────────────────╮ +# │ test-osf-api.sh :: Diagnostic Tool │ +# │ v2.7 — Cosmic. Resilient. Divine. │ +# ╰────────────────────────────────────────────╯ + +CONFIG_FILE="${GITFIELD_CONFIG:-gitfield.osf.yaml}" +TOKEN_FILE="${OSF_TOKEN_FILE:-$HOME/.osf_token}" +OSF_API="${OSF_API_URL:-https://api.osf.io/v2}" +DEBUG_LOG="${GITFIELD_LOG:-$HOME/.test_osf_api_debug.log}" +CURL_TIMEOUT="${CURL_TIMEOUT:-10}" +CURL_RETRIES="${CURL_RETRIES:-3}" +RETRY_DELAY="${RETRY_DELAY:-2}" +RATE_LIMIT_DELAY="${RATE_LIMIT_DELAY:-1}" +VERBOSE="${VERBOSE:-false}" + +# Initialize Debug Log +mkdir -p "$(dirname "$DEBUG_LOG")" +touch "$DEBUG_LOG" +chmod 600 "$DEBUG_LOG" + +trap 'last_command=$BASH_COMMAND; echo -e "\n[ERROR] ❌ Failure at line $LINENO: $last_command" >&2; diagnose; exit 1' ERR + +# Logging Functions +info() { + echo -e "\033[1;34m[INFO]\033[0m $*" >&2 + [ "$VERBOSE" = "true" ] && [ -n "$DEBUG_LOG" ] && debug "INFO: $*" +} +warn() { echo -e "\033[1;33m[WARN]\033[0m $*" >&2; debug "WARN: $*"; } +error() { echo -e "\033[1;31m[ERROR]\033[0m $*" >&2; debug "ERROR: $*"; exit 1; } +debug() { + local msg="$1" lvl="${2:-DEBUG}" + local json_output + json_output=$(jq -n --arg ts "$(date '+%Y-%m-%d %H:%M:%S')" --arg lvl "$lvl" --arg msg "$msg" \ + '{timestamp: $ts, level: $lvl, message: $msg}' 2>/dev/null) || { + echo "[FALLBACK $lvl] $(date '+%Y-%m-%d %H:%M:%S') $msg" >> "$DEBUG_LOG" + return 1 + } + echo "$json_output" >> "$DEBUG_LOG" +} + +debug "Started test-osf-api (v2.7)" + +# ── Diagnostic Function +diagnose() { + info "Running diagnostics..." + debug "Diagnostics started" + echo -e "\n🔍 Diagnostic Report:" + echo -e "1. Network Check:" + if ping -c 1 api.osf.io >/dev/null 2>&1; then + echo -e " ✓ api.osf.io reachable" + else + echo -e " ❌ api.osf.io unreachable. Check network or DNS." + fi + echo -e "2. Curl Version:" + curl --version | head -n 1 + echo -e "3. Debug Log: $DEBUG_LOG" + echo -e "4. Curl Error Log: $DEBUG_LOG.curlerr" + [ -s "$DEBUG_LOG.curlerr" ] && echo -e " Last curl error: $(cat "$DEBUG_LOG.curlerr")" + echo -e "5. Token File: $TOKEN_FILE" + [ -s "$TOKEN_FILE" ] && echo -e " Token exists: $(head -c 4 "$TOKEN_FILE")..." + echo -e "6. Suggestions:" + echo -e " - Check token scopes at https://osf.io/settings/tokens (needs 'nodes' and 'osf.storage')" + echo -e " - Test API: curl -v -H 'Authorization: Bearer \$(cat $TOKEN_FILE)' '$OSF_API/users/me/'" + echo -e " - Test project search: curl -v -H 'Authorization: Bearer \$(cat $TOKEN_FILE)' '$OSF_API/users/me/nodes/?filter\[title\]=git-sigil&page\[size\]=100'" + echo -e " - Increase timeout: CURL_TIMEOUT=30 ./test-osf-api.sh" + debug "Diagnostics completed" +} + +# ── Dependency Check (Parallel) +require_tool() { + local tool=$1 + if ! command -v "$tool" >/dev/null 2>&1; then + warn "$tool not found — attempting to install..." + sudo apt update -qq && sudo apt install -y "$tool" || { + warn "apt failed — trying snap..." + sudo snap install "$tool" || error "Failed to install $tool" + } + fi + debug "$tool path: $(command -v "$tool")" +} + +info "Checking dependencies..." +declare -A dep_pids +for tool in curl jq yq python3; do + require_tool "$tool" & + dep_pids[$tool]=$! +done +for tool in "${!dep_pids[@]}"; do + wait "${dep_pids[$tool]}" || error "Dependency check failed for $tool" +done +info "✓ All dependencies verified" + +# ── Load Token +if [ ! -f "$TOKEN_FILE" ]; then + read -rsp "🔐 Enter OSF Personal Access Token (with 'nodes' and 'osf.storage' scopes): " TOKEN + echo + echo "$TOKEN" > "$TOKEN_FILE" + chmod 600 "$TOKEN_FILE" + info "OSF token saved to $TOKEN_FILE" +fi +TOKEN=$(<"$TOKEN_FILE") +[[ -z "$TOKEN" ]] && error "Empty OSF token in $TOKEN_FILE" + +# ── Validate Token +info "Validating OSF token..." +execute_curl() { + local url=$1 method=${2:-GET} data=${3:-} is_upload=${4:-false} attempt=1 max_attempts=$CURL_RETRIES + local response http_code curl_err + while [ $attempt -le "$max_attempts" ]; do + debug "Curl attempt $attempt/$max_attempts: $method $url" + if [ "$is_upload" = "true" ]; then + response=$(curl -s -S -w "%{http_code}" --connect-timeout "$CURL_TIMEOUT" \ + -X "$method" -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/octet-stream" --data-binary "$data" "$url" 2> "$DEBUG_LOG.curlerr") + else + response=$(curl -s -S -w "%{http_code}" --connect-timeout "$CURL_TIMEOUT" \ + -X "$method" -H "Authorization: Bearer $TOKEN" \ + ${data:+-H "Content-Type: application/json" -d "$data"} "$url" 2> "$DEBUG_LOG.curlerr") + fi + http_code="${response: -3}" + curl_err=$(cat "$DEBUG_LOG.curlerr") + [ -s "$DEBUG_LOG.curlerr" ] && debug "Curl error: $curl_err" + if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then + echo "${response:: -3}" + return 0 + elif [ "$http_code" = "401" ]; then + warn "Invalid token (HTTP 401). Please provide a valid OSF token." + read -rsp "🔐 Enter OSF Personal Access Token (with 'nodes' and 'osf.storage' scopes): " NEW_TOKEN + echo + echo "$NEW_TOKEN" > "$TOKEN_FILE" + chmod 600 "$TOKEN_FILE" + TOKEN="$NEW_TOKEN" + info "New token saved. Retrying..." + elif [ "$http_code" = "429" ]; then + warn "Rate limit hit, retrying after $((RETRY_DELAY * attempt)) seconds..." + sleep $((RETRY_DELAY * attempt)) + elif [ "$http_code" = "403" ]; then + warn "Forbidden (HTTP 403). Possible token scope issue." + [ $attempt -eq "$max_attempts" ] && { + read -rsp "🔐 Re-enter OSF token with 'nodes' and 'osf.storage' scopes: " NEW_TOKEN + echo + echo "$NEW_TOKEN" > "$TOKEN_FILE" + chmod 600 "$TOKEN_FILE" + TOKEN="$NEW_TOKEN" + info "New token saved. Retrying..." + } + elif [[ "$curl_err" == *"bad range in URL"* ]]; then + error "Malformed URL: $url. Ensure query parameters are escaped (e.g., filter\[title\])." + else + debug "API response (HTTP $http_code): ${response:: -3}" + [ $attempt -eq "$max_attempts" ] && error "API request failed (HTTP $http_code): ${response:: -3}" + fi + sleep $((RETRY_DELAY * attempt)) + ((attempt++)) + done +} + +RESPONSE=$(execute_curl "$OSF_API/users/me/") +USER_ID=$(echo "$RESPONSE" | jq -r '.data.id // empty') +[[ -z "$USER_ID" ]] && error "Could not extract user ID" +info "✓ OSF token validated for user ID: $USER_ID" + +# ── Load Config +[[ ! -f "$CONFIG_FILE" ]] && error "Missing config: $CONFIG_FILE" +PROJECT_TITLE=$(yq -r '.project.title // empty' "$CONFIG_FILE") +PROJECT_DESCRIPTION=$(yq -r '.project.description // empty' "$CONFIG_FILE") +[[ -z "$PROJECT_TITLE" ]] && error "Missing project title in $CONFIG_FILE" +debug "Parsed config: title=$PROJECT_TITLE, description=$PROJECT_DESCRIPTION" + +# ── Project Search +build_url() { + local base="$1" title="$2" + local escaped_title + escaped_title=$(python3 -c "import urllib.parse; print(urllib.parse.quote('''$title'''))") + echo "$base/users/me/nodes/?filter\[title\]=$escaped_title&page\[size\]=100" +} + +PROJECT_ID="" +NEXT_URL=$(build_url "$OSF_API" "$PROJECT_TITLE") + +info "Searching for project '$PROJECT_TITLE'..." +while [ -n "$NEXT_URL" ]; do + debug "Querying: $NEXT_URL" + RESPONSE=$(execute_curl "$NEXT_URL") + PROJECT_ID=$(echo "$RESPONSE" | jq -r --arg TITLE "$PROJECT_TITLE" \ + '.data[] | select(.attributes.title == $TITLE) | .id // empty' || true) + if [ -n "$PROJECT_ID" ]; then + debug "Found project ID: $PROJECT_ID" + break + fi + NEXT_URL=$(echo "$RESPONSE" | jq -r '.links.next // empty' | sed 's/filter\[title\]/filter\\\[title\\\]/g;s/page\[size\]/page\\\[size\\\]/g' || true) + debug "Next URL: $NEXT_URL" + [ -n "$NEXT_URL" ] && info "Fetching next page..." && sleep "$RATE_LIMIT_DELAY" +done + +# ── Create Project if Not Found +if [ -z "$PROJECT_ID" ]; then + info "Project not found. Attempting to create '$PROJECT_TITLE'..." + JSON=$(jq -n --arg title="$PROJECT_TITLE" --arg desc="$PROJECT_DESCRIPTION" \ + '{data: {type: "nodes", attributes: {title: $title, category: "project", description: $desc}}}') + RESPONSE=$(execute_curl "$OSF_API/nodes/" POST "$JSON") + PROJECT_ID=$(echo "$RESPONSE" | jq -r '.data.id // empty') + [[ -z "$PROJECT_ID" || "$PROJECT_ID" == "null" ]] && error "Could not extract project ID" + info "✅ Project created: $PROJECT_ID" +else + info "✓ Found project ID: $PROJECT_ID" +fi + +echo -e "\n🔗 View project: https://osf.io/$PROJECT_ID/" +debug "Test completed successfully"