publish_osf.sh
1 #!/usr/bin/env bash 2 set -uo pipefail 3 4 # === Constants and Paths === 5 BASEDIR="$(pwd)" 6 OSF_YAML="$BASEDIR/osf.yaml" 7 GITFIELD_DIR="$BASEDIR/.gitfield" 8 LOG_DIR="$GITFIELD_DIR/logs" 9 SCAN_LOG_INIT="$GITFIELD_DIR/scan_log.json" 10 SCAN_LOG_PUSH="$GITFIELD_DIR/push_log.json" 11 TMP_JSON_TOKEN="$GITFIELD_DIR/tmp_token.json" 12 TMP_JSON_PROJECT="$GITFIELD_DIR/tmp_project.json" 13 TOKEN_PATH="$HOME/.local/gitfieldlib/osf.token" 14 mkdir -p "$GITFIELD_DIR" "$LOG_DIR" "$(dirname "$TOKEN_PATH")" 15 16 # === Logging === 17 log() { 18 local level="$1" msg="$2" 19 echo "[$(date -Iseconds)] [$level] $msg" >> "$LOG_DIR/gitfield_$(date +%Y%m%d).log" 20 if [[ "$level" == "ERROR" || "$level" == "INFO" || "$VERBOSE" == "true" ]]; then 21 echo "[$(date -Iseconds)] [$level] $msg" >&2 22 fi 23 } 24 25 error() { 26 log "ERROR" "$1" 27 exit 1 28 } 29 30 # === Dependency Check === 31 require_yq() { 32 if ! command -v yq &>/dev/null || ! yq --version 2>/dev/null | grep -q 'version v4'; then 33 log "INFO" "Installing 'yq' (Go version)..." 34 YQ_BIN="/usr/local/bin/yq" 35 ARCH=$(uname -m) 36 case $ARCH in 37 x86_64) ARCH=amd64 ;; 38 aarch64) ARCH=arm64 ;; 39 *) error "Unsupported architecture: $ARCH" ;; 40 esac 41 curl -sL "https://github.com/mikefarah/yq/releases/download/v4.43.1/yq_linux_${ARCH}" -o yq \ 42 && chmod +x yq && sudo mv yq "$YQ_BIN" 43 log "INFO" "'yq' installed to $YQ_BIN" 44 fi 45 } 46 47 require_jq() { 48 if ! command -v jq &>/dev/null; then 49 log "INFO" "Installing 'jq'..." 50 sudo apt update && sudo apt install -y jq 51 log "INFO" "'jq' installed" 52 fi 53 } 54 55 require_yq 56 require_jq 57 58 # === Token Retrieval === 59 get_token() { 60 if [[ -z "${OSF_TOKEN:-}" ]]; then 61 if [[ -f "$TOKEN_PATH" ]]; then 62 OSF_TOKEN=$(<"$TOKEN_PATH") 63 else 64 echo -n "🔐 Enter your OSF_TOKEN: " >&2 65 read -rs OSF_TOKEN 66 echo >&2 67 echo "$OSF_TOKEN" > "$TOKEN_PATH" 68 chmod 600 "$TOKEN_PATH" 69 log "INFO" "Token saved to $TOKEN_PATH" 70 fi 71 fi 72 RESPONSE=$(curl -s -w "\n%{http_code}" -o "$TMP_JSON_TOKEN" "https://api.osf.io/v2/users/me/" \ 73 -H "Authorization: Bearer $OSF_TOKEN") 74 HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) 75 [[ "$HTTP_CODE" == "200" ]] || error "Invalid OSF token (HTTP $HTTP_CODE)" 76 } 77 78 # === Auto-Generate osf.yaml === 79 init_mode() { 80 log "INFO" "Scanning project directory..." 81 mapfile -t ALL_FILES < <(find "$BASEDIR" -type f \( \ 82 -name '*.md' -o -name '*.pdf' -o -name '*.tex' -o -name '*.csv' -o -name '*.txt' \ 83 -o -name '*.rtf' -o -name '*.doc' -o -name '*.docx' -o -name '*.odt' \ 84 -o -name '*.xls' -o -name '*.xlsx' -o -name '*.ods' -o -name '*.ppt' -o -name '*.pptx' \ 85 -o -name '*.odp' -o -name '*.jpg' -o -name '*.jpeg' -o -name '*.png' -o -name '*.gif' \ 86 -o -name '*.svg' -o -name '*.tiff' -o -name '*.bmp' -o -name '*.webp' \ 87 -o -name '*.sh' -o -name '*.py' -o -name '*.rb' -o -name '*.pl' -o -name '*.js' \ 88 -o -name '*.yaml' -o -name '*.yml' -o -name '*.json' -o -name '*.xml' \ 89 -o -name 'LICENSE*' -o -name 'COPYING*' \ 90 \) ! -path "*/.git/*" ! -path "*/.gitfield/*" ! -path "*/.legacy-gitfield/*" | sort -u) 91 92 if [[ ${#ALL_FILES[@]} -gt 0 ]]; then 93 IGNORED_FILES=$(git check-ignore "${ALL_FILES[@]}" 2>/dev/null || true) 94 if [[ -n "$IGNORED_FILES" ]]; then 95 log "INFO" "Ignored files due to .gitignore: $IGNORED_FILES" 96 mapfile -t ALL_FILES < <(printf '%s\n' "${ALL_FILES[@]}" | grep -vF "$IGNORED_FILES" | sort -u) 97 fi 98 fi 99 100 [[ ${#ALL_FILES[@]} -gt 0 ]] || log "WARN" "No files detected in the repository!" 101 log "INFO" "Files detected: ${ALL_FILES[*]}" 102 103 detect_file() { 104 local keywords=("$@") 105 for file in "${ALL_FILES[@]}"; do 106 for kw in "${keywords[@]}"; do 107 if [[ "${file,,}" == *"${kw,,}"* ]]; then 108 echo "$file" 109 return 0 110 fi 111 done 112 done 113 } 114 115 WIKI_PATH=$(detect_file "wiki.md" "wiki" "home.md") 116 README_PATH=$(detect_file "readme.md" "README.md") 117 PAPER_PATH=$(detect_file "main.pdf" "theory.pdf" "paper.pdf" "manuscript.pdf") 118 119 DOCS=() 120 ESSAYS=() 121 IMAGES=() 122 SCRIPTS=() 123 DATA=() 124 FILES=() 125 for f in "${ALL_FILES[@]}"; do 126 case "$f" in 127 "$WIKI_PATH"|"$README_PATH"|"$PAPER_PATH") continue ;; 128 esac 129 130 if [[ "$f" =~ \.(jpg|jpeg|png|gif|svg|tiff|bmp|webp)$ ]]; then 131 IMAGES+=("$f") 132 elif [[ "$f" =~ \.(sh|py|rb|pl|js)$ ]]; then 133 SCRIPTS+=("$f") 134 elif [[ "$f" =~ \.(csv|json|xml|yaml|yml)$ ]]; then 135 DATA+=("$f") 136 elif [[ "$f" =~ \.(md|pdf|tex|doc|docx|odt|xls|xlsx|ods|ppt|pptx|odp|txt|rtf)$ ]] || [[ "$(basename "$f")" =~ ^(LICENSE|COPYING) ]]; then 137 if [[ "$f" =~ /docs/ ]] || [[ "${f,,}" =~ (guide|tutorial|howto|manual|documentation|workflow|readme) ]]; then 138 DOCS+=("$f") 139 elif [[ "$f" =~ /essays/|/notes/ ]] || [[ "${f,,}" =~ (essay|note|draft|reflection) ]]; then 140 ESSAYS+=("$f") 141 else 142 FILES+=("$f") 143 fi 144 fi 145 done 146 147 log "INFO" "Generating osf.yaml..." 148 { 149 echo "# osf.yaml - Configuration for publishing to OSF" 150 echo "# Generated on $(date -Iseconds)" 151 echo "# Edit this file to customize what gets published to OSF." 152 echo 153 echo "title: \"$(basename "$BASEDIR")\"" 154 echo "description: \"Auto-generated by GitField OSF publisher on $(date -Iseconds)\"" 155 echo "category: \"project\"" 156 echo "public: false" 157 echo "tags: [gitfield, auto-generated]" 158 159 echo 160 echo "# Wiki: Main wiki page for your OSF project (wiki.md, home.md)." 161 if [[ -n "$WIKI_PATH" ]]; then 162 echo "wiki:" 163 echo " path: \"${WIKI_PATH#$BASEDIR/}\"" 164 echo " overwrite: true" 165 else 166 echo "# wiki: Not found. Place a 'wiki.md' in your repository to auto-detect." 167 fi 168 169 echo 170 echo "# Readme: Main README file (readme.md, README.md)." 171 if [[ -n "$README_PATH" ]]; then 172 echo "readme:" 173 echo " path: \"${README_PATH#$BASEDIR/}\"" 174 else 175 echo "# readme: Not found. Place a 'README.md' in your repository root." 176 fi 177 178 echo 179 echo "# Paper: Primary academic paper (main.pdf, paper.pdf)." 180 if [[ -n "$PAPER_PATH" ]]; then 181 echo "paper:" 182 echo " path: \"${PAPER_PATH#$BASEDIR/}\"" 183 echo " name: \"$(basename "$PAPER_PATH")\"" 184 else 185 echo "# paper: Not found. Place a PDF (e.g., 'main.pdf') in your repository." 186 fi 187 188 if ((${#DOCS[@]})); then 189 echo 190 echo "# Docs: Documentation files (.md, .pdf, etc.) in docs/ or with keywords like 'guide'." 191 echo "docs:" 192 for doc in "${DOCS[@]}"; do 193 relative_path="${doc#$BASEDIR/}" 194 echo " - path: \"$relative_path\"" 195 echo " name: \"$relative_path\"" 196 done 197 fi 198 199 if ((${#ESSAYS[@]})); then 200 echo 201 echo "# Essays: Written essays (.md, .pdf, etc.) in essays/ or with keywords like 'essay'." 202 echo "essays:" 203 for essay in "${ESSAYS[@]}"; do 204 relative_path="${essay#$BASEDIR/}" 205 echo " - path: \"$relative_path\"" 206 echo " name: \"$relative_path\"" 207 done 208 fi 209 210 if ((${#IMAGES[@]})); then 211 echo 212 echo "# Images: Image files (.jpg, .png, etc.)." 213 echo "images:" 214 for image in "${IMAGES[@]}"; do 215 relative_path="${image#$BASEDIR/}" 216 echo " - path: \"$relative_path\"" 217 echo " name: \"$relative_path\"" 218 done 219 fi 220 221 if ((${#SCRIPTS[@]})); then 222 echo 223 echo "# Scripts: Executable scripts (.sh, .py, etc.) in bin/, scripts/, or tools/." 224 echo "scripts:" 225 for script in "${SCRIPTS[@]}"; do 226 relative_path="${script#$BASEDIR/}" 227 echo " - path: \"$relative_path\"" 228 echo " name: \"$relative_path\"" 229 done 230 fi 231 232 if ((${#DATA[@]})); then 233 echo 234 echo "# Data: Structured data files (.csv, .yaml, etc.)." 235 echo "data:" 236 for datum in "${DATA[@]}"; do 237 relative_path="${datum#$BASEDIR/}" 238 echo " - path: \"$relative_path\"" 239 echo " name: \"$relative_path\"" 240 done 241 fi 242 243 if ((${#FILES[@]})); then 244 echo 245 echo "# Files: Miscellaneous files (.md, LICENSE, etc.)." 246 echo "files:" 247 for file in "${FILES[@]}"; do 248 relative_path="${file#$BASEDIR/}" 249 echo " - path: \"$relative_path\"" 250 echo " name: \"$relative_path\"" 251 done 252 fi 253 } > "$OSF_YAML" 254 255 log "INFO" "Wiki: $WIKI_PATH, Readme: $README_PATH, Paper: $PAPER_PATH" 256 log "INFO" "Docs: ${DOCS[*]}" 257 log "INFO" "Essays: ${ESSAYS[*]}" 258 log "INFO" "Images: ${IMAGES[*]}" 259 log "INFO" "Scripts: ${SCRIPTS[*]}" 260 log "INFO" "Data: ${DATA[*]}" 261 log "INFO" "Files: ${FILES[*]}" 262 263 jq -n \ 264 --argjson all "$(printf '%s\n' "${ALL_FILES[@]}" | jq -R . | jq -s .)" \ 265 --argjson docs "$(printf '%s\n' "${DOCS[@]}" | jq -R . | jq -s .)" \ 266 --argjson files "$(printf '%s\n' "${FILES[@]}" | jq -R . | jq -s .)" \ 267 --argjson scripts "$(printf '%s\n' "${SCRIPTS[@]}" | jq -R . | jq -s .)" \ 268 --arg osf_yaml "$OSF_YAML" \ 269 '{detected_files: $all, classified: {docs: $docs, files: $files, scripts: $scripts}, osf_yaml_path: $osf_yaml}' > "$SCAN_LOG_INIT" 270 271 log "INFO" "Generated $OSF_YAML and scan log" 272 echo "✅ osf.yaml generated at $OSF_YAML." >&2 273 } 274 275 # === Generate Default Wiki with Links === 276 generate_wiki() { 277 local wiki_path 278 wiki_path=$(yq e '.wiki.path' "$OSF_YAML") 279 if [[ "$wiki_path" != "null" && ! -f "$wiki_path" ]]; then 280 log "INFO" "Generating default wiki at $wiki_path..." 281 mkdir -p "$(dirname "$wiki_path")" 282 { 283 echo "# Auto-Generated Wiki for $(yq e '.title' "$OSF_YAML")" 284 echo 285 echo "## Project Overview" 286 echo "$(yq e '.description' "$OSF_YAML")" 287 echo 288 echo "## Repository Info" 289 echo "- **Last Commit**: $(git log -1 --pretty=%B 2>/dev/null || echo "No git commits")" 290 echo "- **Commit Hash**: $(git rev-parse HEAD 2>/dev/null || echo "N/A")" 291 if [[ -f "$(yq e '.readme.path' "$OSF_YAML")" ]]; then 292 echo 293 echo "## README Preview" 294 head -n 10 "$(yq e '.readme.path' "$OSF_YAML")" 295 fi 296 echo 297 echo "## Internal Documents" 298 echo "Links to documents uploaded to OSF (will be populated after --push/--overwrite):" 299 for section in docs essays images scripts data files; do 300 local count 301 count=$(yq e ".${section} | length" "$OSF_YAML") 302 if [[ "$count" != "0" && "$count" != "null" ]]; then 303 echo 304 echo "### $(echo "$section" | tr '[:lower:]' '[:upper:]')" 305 for ((i = 0; i < count; i++)); do 306 local name 307 name=$(yq e ".${section}[$i].name" "$OSF_YAML") 308 echo "- [$name](https://osf.io/{NODE_ID}/files/osfstorage/$name)" 309 done 310 fi 311 done 312 } > "$wiki_path" 313 log "INFO" "Default wiki generated at $wiki_path" 314 fi 315 } 316 317 # === Validate YAML === 318 validate_yaml() { 319 log "INFO" "Validating $OSF_YAML..." 320 [[ -f "$OSF_YAML" ]] || init_mode 321 for field in title description category public; do 322 [[ $(yq e ".$field" "$OSF_YAML") != "null" ]] || error "Missing field: $field in $OSF_YAML" 323 done 324 } 325 326 # === Validate and Read push_log.json === 327 read_project_id() { 328 if [[ ! -f "$SCAN_LOG_PUSH" ]] || ! jq -e '.' "$SCAN_LOG_PUSH" >/dev/null 2>&1; then 329 log "WARN" "No valid push_log.json found" 330 echo "" 331 return 332 fi 333 NODE_ID=$(jq -r '.project_id // ""' "$SCAN_LOG_PUSH") 334 echo "$NODE_ID" 335 } 336 337 # === Search for Existing Project by Title === 338 find_project_by_title() { 339 local title="$1" 340 log "INFO" "Searching for project: $title" 341 if [[ "$DRY_RUN" == "true" ]]; then 342 echo "dry-run-$(uuidgen)" 343 return 344 fi 345 ENCODED_TITLE=$(jq -r -n --arg title "$title" '$title|@uri') 346 RESPONSE=$(curl -s -w "\n%{http_code}" -o "$TMP_JSON_PROJECT" "https://api.osf.io/v2/nodes/?filter[title]=$ENCODED_TITLE" \ 347 -H "Authorization: Bearer $OSF_TOKEN") 348 HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) 349 if [[ "$HTTP_CODE" != "200" ]]; then 350 log "WARN" "Failed to search for project (HTTP $HTTP_CODE)" 351 echo "" 352 return 353 fi 354 NODE_ID=$(jq -r '.data[0].id // ""' "$TMP_JSON_PROJECT") 355 [[ -n "$NODE_ID" ]] && log "INFO" "Found project '$title': $NODE_ID" 356 echo "$NODE_ID" 357 } 358 359 # === Upload Helpers === 360 sanitize_filename() { 361 local name="$1" 362 echo "$name" | tr -d '\n' | sed 's/[^[:alnum:]._-]/_/g' 363 } 364 365 upload_file() { 366 local path="$1" name="$2" 367 local sanitized_name encoded_name 368 sanitized_name=$(sanitize_filename "$name") 369 encoded_name=$(jq -r -n --arg name "$sanitized_name" '$name|@uri') 370 log "INFO" "Uploading $name (sanitized: $sanitized_name) from $path" 371 if [[ "$DRY_RUN" == "true" ]]; then 372 return 0 373 fi 374 375 CHECK_URL="https://api.osf.io/v2/nodes/$NODE_ID/files/osfstorage/?filter[name]=$encoded_name" 376 RESPONSE=$(curl -s -w "\n%{http_code}" -o "$TMP_JSON_PROJECT" "$CHECK_URL" \ 377 -H "Authorization: Bearer $OSF_TOKEN") 378 HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) 379 380 if [[ -z "$HTTP_CODE" ]]; then 381 log "WARN" "No HTTP status for $sanitized_name check. Assuming file does not exist." 382 elif [[ "$HTTP_CODE" == "200" ]]; then 383 FILE_ID=$(jq -r '.data[0].id // ""' "$TMP_JSON_PROJECT") 384 if [[ -n "$FILE_ID" ]]; then 385 if [[ "$MODE" == "overwrite" ]]; then 386 log "INFO" "Deleting existing file $sanitized_name (ID: $FILE_ID)..." 387 DEL_RESPONSE=$(curl -s -w "%{http_code}" -X DELETE "https://api.osf.io/v2/files/$FILE_ID/" \ 388 -H "Authorization: Bearer $OSF_TOKEN") 389 [[ "$DEL_RESPONSE" == "204" ]] || log "WARN" "Failed to delete $sanitized_name (HTTP $DEL_RESPONSE)" 390 else 391 log "WARN" "File $sanitized_name exists. Use --overwrite to replace." 392 return 1 393 fi 394 fi 395 elif [[ "$HTTP_CODE" != "404" ]]; then 396 log "WARN" "Check for $sanitized_name failed (HTTP $HTTP_CODE)" 397 fi 398 399 UPLOAD_URL="https://files.osf.io/v1/resources/$NODE_ID/providers/osfstorage/?kind=file&name=$encoded_name" 400 RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT "$UPLOAD_URL" \ 401 -H "Authorization: Bearer $OSF_TOKEN" \ 402 -F "file=@$path") 403 HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) 404 if [[ "$HTTP_CODE" != "201" ]]; then 405 log "WARN" "Failed to upload $name (HTTP $HTTP_CODE)" 406 return 1 407 fi 408 echo "📤 Uploaded $name to https://osf.io/$NODE_ID/" >&2 409 UPLOADED_FILES+=("$name") 410 return 0 411 } 412 413 upload_group() { 414 local section="$1" 415 local count 416 count=$(yq e ".${section} | length" "$OSF_YAML") 417 log "INFO" "Uploading $section group ($count items)" 418 if [[ "$count" == "0" || "$count" == "null" ]]; then 419 return 0 420 fi 421 local success_count=0 422 for ((i = 0; i < count; i++)); do 423 local path name 424 path=$(yq e ".${section}[$i].path" "$OSF_YAML") 425 name=$(yq e ".${section}[$i].name" "$OSF_YAML") 426 if [[ -f "$BASEDIR/$path" ]]; then 427 upload_file "$BASEDIR/$path" "$name" && ((success_count++)) 428 else 429 log "WARN" "File $path not found, skipping" 430 fi 431 done 432 log "INFO" "Uploaded $success_count/$count items in $section" 433 return 0 434 } 435 436 upload_wiki() { 437 local wiki_path 438 wiki_path=$(yq e '.wiki.path' "$OSF_YAML") 439 if [[ "$wiki_path" != "null" && -f "$BASEDIR/$wiki_path" ]]; then 440 log "INFO" "Pushing wiki from $wiki_path" 441 if [[ "$DRY_RUN" == "true" ]]; then 442 return 0 443 fi 444 # Update wiki content with actual OSF links 445 local wiki_content 446 wiki_content=$(cat "$BASEDIR/$wiki_path") 447 for file in "${UPLOADED_FILES[@]}"; do 448 wiki_content=$(echo "$wiki_content" | sed "s|https://osf.io/{NODE_ID}/files/osfstorage/$file|https://osf.io/$NODE_ID/files/osfstorage/$file|g") 449 done 450 echo "$wiki_content" > "$BASEDIR/$wiki_path.updated" 451 CONTENT=$(jq -Rs . < "$BASEDIR/$wiki_path.updated") 452 RESPONSE=$(curl -s -w "\n%{http_code}" -X PATCH "https://api.osf.io/v2/nodes/$NODE_ID/wikis/home/" \ 453 -H "Authorization: Bearer $OSF_TOKEN" \ 454 -H "Content-Type: application/vnd.api+json" \ 455 -d @- <<EOF 456 { 457 "data": { 458 "type": "wikis", 459 "attributes": { 460 "content": $CONTENT 461 } 462 } 463 } 464 EOF 465 ) 466 HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) 467 if [[ "$HTTP_CODE" != "200" ]]; then 468 log "WARN" "Failed to upload wiki (HTTP $HTTP_CODE)" 469 return 1 470 fi 471 echo "📜 Pushed wiki to https://osf.io/$NODE_ID/" >&2 472 rm -f "$BASEDIR/$wiki_path.updated" 473 return 0 474 fi 475 log "INFO" "No wiki to upload" 476 return 0 477 } 478 479 # === Push Mode === 480 push_mode() { 481 local MODE="$1" 482 validate_yaml 483 generate_wiki 484 get_token 485 486 local title description category public 487 title=$(yq e '.title' "$OSF_YAML") 488 description=$(yq e '.description' "$OSF_YAML") 489 category=$(yq e '.category' "$OSF_YAML") 490 public=$(yq e '.public' "$OSF_YAML" | grep -E '^(true|false)$' || error "Invalid 'public' value") 491 492 NODE_ID="" 493 if [[ "$MODE" == "overwrite" || "$MODE" == "push" ]]; then 494 NODE_ID=$(read_project_id) 495 if [[ -n "$NODE_ID" ]]; then 496 log "INFO" "Using existing OSF project ID: $NODE_ID" 497 RESPONSE=$(curl -s -w "\n%{http_code}" -o "$TMP_JSON_PROJECT" "https://api.osf.io/v2/nodes/$NODE_ID/" \ 498 -H "Authorization: Bearer $OSF_TOKEN") 499 HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) 500 if [[ "$HTTP_CODE" != "200" ]]; then 501 log "WARN" "Project $NODE_ID not found (HTTP $HTTP_CODE)" 502 NODE_ID="" 503 fi 504 fi 505 fi 506 507 if [[ -z "$NODE_ID" ]] && [[ "$MODE" == "overwrite" || "$MODE" == "push" ]]; then 508 NODE_ID=$(find_project_by_title "$title") 509 fi 510 511 if [[ -z "$NODE_ID" ]]; then 512 log "INFO" "Creating new OSF project..." 513 if [[ "$DRY_RUN" == "true" ]]; then 514 NODE_ID="dry-run-$(uuidgen)" 515 else 516 RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "https://api.osf.io/v2/nodes/" \ 517 -H "Authorization: Bearer $OSF_TOKEN" \ 518 -H "Content-Type: application/vnd.api+json" \ 519 -d @- <<EOF 520 { 521 "data": { 522 "type": "nodes", 523 "attributes": { 524 "title": "$title", 525 "description": "$description", 526 "category": "$category", 527 "public": $public 528 } 529 } 530 } 531 EOF 532 ) 533 HTTP_CODE=$(echo "$RESPONSE" | tail -n 1) 534 [[ "$HTTP_CODE" == "201" ]] || error "Project creation failed (HTTP $HTTP_CODE)" 535 NODE_ID=$(jq -r '.data.id' "$TMP_JSON_PROJECT") 536 [[ "$NODE_ID" != "null" && -n "$NODE_ID" ]] || error "No valid OSF project ID returned" 537 log "INFO" "Project created: $NODE_ID" 538 fi 539 fi 540 541 [[ -n "$NODE_ID" ]] || error "Failed to determine OSF project ID" 542 543 log "INFO" "Starting file uploads to project $NODE_ID" 544 declare -a UPLOADED_FILES 545 local overall_success=0 546 if [[ $(yq e '.readme.path' "$OSF_YAML") != "null" ]]; then 547 path=$(yq e '.readme.path' "$OSF_YAML") 548 [[ -f "$BASEDIR/$path" ]] && upload_file "$BASEDIR/$path" "$(basename "$path")" && overall_success=1 549 fi 550 if [[ $(yq e '.paper.path' "$OSF_YAML") != "null" ]]; then 551 path=$(yq e '.paper.path' "$OSF_YAML") 552 name=$(yq e '.paper.name' "$OSF_YAML") 553 [[ -f "$BASEDIR/$path" ]] && upload_file "$BASEDIR/$path" "$name" && overall_success=1 554 fi 555 upload_group "docs" && overall_success=1 556 upload_group "essays" && overall_success=1 557 upload_group "images" && overall_success=1 558 upload_group "scripts" && overall_success=1 559 upload_group "data" && overall_success=1 560 upload_group "files" && overall_success=1 561 upload_wiki && overall_success=1 562 563 if [[ "$DRY_RUN" != "true" ]]; then 564 jq -n \ 565 --arg node_id "$NODE_ID" \ 566 --arg title "$title" \ 567 --arg pushed_at "$(date -Iseconds)" \ 568 '{project_id: $node_id, project_title: $title, pushed_at: $pushed_at}' > "$SCAN_LOG_PUSH" 569 fi 570 571 if [[ "$overall_success" -eq 1 ]]; then 572 log "INFO" "OSF Push Complete! View project: https://osf.io/$NODE_ID/" 573 echo "✅ OSF Push Complete! View project: https://osf.io/$NODE_ID/" >&2 574 else 575 error "OSF Push Failed: No files uploaded" 576 fi 577 } 578 579 # === Validate Mode === 580 validate_mode() { 581 validate_yaml 582 log "INFO" "Checking file existence..." 583 for section in readme paper docs essays images scripts data files wiki; do 584 if [[ "$section" == "docs" || "$section" == "essays" || "$section" == "images" || "$section" == "scripts" || "$section" == "data" || "$section" == "files" ]]; then 585 local count 586 count=$(yq e ".${section} | length" "$OSF_YAML") 587 for ((i = 0; i < count; i++)); do 588 local path 589 path=$(yq e ".${section}[$i].path" "$OSF_YAML") 590 [[ -f "$BASEDIR/$path" ]] || log "WARN" "File $path in $section not found" 591 done 592 elif [[ "$section" != "wiki" ]]; then 593 local path 594 path=$(yq e ".${section}.path" "$OSF_YAML") 595 if [[ "$path" != "null" && -n "$path" && ! -f "$BASEDIR/$path" ]]; then 596 log "WARN" "File $path in $section not found" 597 fi 598 fi 599 done 600 log "INFO" "Validation complete" 601 echo "✅ Validation complete. Check logs: $LOG_DIR/gitfield_$(date +%Y%m%d).log" >&2 602 } 603 604 # === Clean Mode === 605 clean_mode() { 606 log "INFO" "Cleaning .gitfield directory..." 607 rm -rf "$GITFIELD_DIR" 608 mkdir -p "$GITFIELD_DIR" "$LOG_DIR" 609 log "INFO" "Cleaned .gitfield directory" 610 echo "✅ Cleaned .gitfield directory" >&2 611 } 612 613 # === Help Menu === 614 show_help() { 615 local verbose="$1" 616 if [[ "$verbose" == "true" ]]; then 617 cat <<EOF 618 Usage: $0 [OPTION] 619 620 Publish content from a Git repository to OSF. 621 622 Options: 623 --init Generate osf.yaml and scan log without pushing to OSF 624 --push Push to existing OSF project or create new 625 --overwrite Reuse existing OSF project and overwrite files 626 --force Alias for --overwrite 627 --dry-run Simulate actions (use with --push or --overwrite) 628 --validate Check osf.yaml and file existence without pushing 629 --clean Remove .gitfield logs and start fresh 630 --help Show this help message (--help --verbose for more details) 631 632 Examples: 633 $0 --init # Create osf.yaml based on repo contents 634 $0 --push # Push to OSF 635 $0 --overwrite # Push to OSF, overwriting files 636 $0 --dry-run --push # Simulate a push 637 638 Repository Structure and Supported Files: 639 - Wiki: wiki.md, home.md (root or docs/) 640 - Readme: readme.md, README.md (root) 641 - Paper: main.pdf, paper.pdf (root or docs/) 642 - Docs: .md, .pdf, etc., in docs/ or with keywords like 'guide' 643 - Essays: .md, .pdf, etc., in essays/ or with keywords like 'essay' 644 - Images: .jpg, .png, etc., in any directory 645 - Scripts: .sh, .py, etc., in bin/, scripts/, or tools/ 646 - Data: .csv, .yaml, etc., in any directory 647 - Files: Miscellaneous files (.md, LICENSE, etc.) 648 649 After running --init, open osf.yaml to customize. 650 EOF 651 else 652 cat <<EOF 653 Usage: $0 [OPTION] 654 655 Publish content from a Git repository to OSF. 656 657 Options: 658 --init Generate osf.yaml 659 --push Push to OSF 660 --overwrite Push to OSF, overwrite files 661 --force Alias for --overwrite 662 --dry-run Simulate actions (with --push/--overwrite) 663 --validate Check osf.yaml and files 664 --clean Remove .gitfield logs 665 --help Show this help (--help --verbose for more) 666 667 Examples: 668 $0 --init # Create osf.yaml 669 $0 --push # Push to OSF 670 EOF 671 fi 672 } 673 674 # === CLI Dispatcher === 675 DRY_RUN="false" 676 VERBOSE="false" 677 MODE="" 678 while [[ $# -gt 0 ]]; do 679 case "$1" in 680 --init) MODE="init" ;; 681 --push) MODE="push" ;; 682 --overwrite|--force) MODE="overwrite" ;; 683 --dry-run) DRY_RUN="true" ;; 684 --validate) MODE="validate" ;; 685 --clean) MODE="clean" ;; 686 --verbose) VERBOSE="true" ;; 687 --help) show_help "$VERBOSE"; exit 0 ;; 688 *) echo "Unknown option: $1" >&2; show_help "false"; exit 1 ;; 689 esac 690 shift 691 done 692 693 case "$MODE" in 694 init) init_mode ;; 695 push|overwrite) push_mode "$MODE" ;; 696 validate) validate_mode ;; 697 clean) clean_mode ;; 698 *) show_help "false"; exit 0 ;; 699 esac