#!/bin/bash # # Sprint CLI for Gantt Board # CRUD operations for sprints # Usage: ./sprint.sh [create|list|get|update|delete|close] [options] set -e # Configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SUPABASE_URL="https://qnatchrjlpehiijwtreh.supabase.co" SERVICE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFuYXRjaHJqbHBlaGlpand0cmVoIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3MTY0MDQzNiwiZXhwIjoyMDg3MjE2NDM2fQ.rHoc3NfL59S4lejU4-ArSzox1krQkQG-TnfXb6sslm0" HEADERS=(-H "apikey: $SERVICE_KEY" -H "Authorization: Bearer $SERVICE_KEY" -H "Content-Type: application/json") # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Helper Functions log_info() { echo -e "${BLUE}ℹ${NC} $1"; } log_success() { echo -e "${GREEN}✓${NC} $1"; } log_warning() { echo -e "${YELLOW}⚠${NC} $1"; } log_error() { echo -e "${RED}✗${NC} $1"; } # Check dependencies check_dependencies() { if ! command -v jq &> /dev/null; then log_error "jq is required but not installed. Install with: brew install jq" exit 1 fi if ! command -v uuidgen &> /dev/null; then log_error "uuidgen is required but not installed." exit 1 fi } # Generate UUID generate_uuid() { uuidgen | tr '[:upper:]' '[:lower:]' } # Get current timestamp ISO format get_timestamp() { date -u +"%Y-%m-%dT%H:%M:%SZ" } # Show usage show_usage() { cat << EOF Sprint CLI for Gantt Board USAGE: ./sprint.sh [COMMAND] [OPTIONS] COMMANDS: create Create a new sprint list List all sprints get Get a specific sprint by ID or name update Update a sprint delete Delete a sprint close Close/complete a sprint CREATE OPTIONS: --name "Name" Sprint name (required) --project "Project" Project name or ID (required) --goal "Goal" Sprint goal/description --start-date "YYYY-MM-DD" Start date --end-date "YYYY-MM-DD" End date --status [planning|active|closed|completed] Status (default: planning) LIST OPTIONS: --status Filter by status --project Filter by project name/ID --active Show only active sprints --json Output as JSON UPDATE OPTIONS: --name "Name" --goal "Goal" --start-date "YYYY-MM-DD" --end-date "YYYY-MM-DD" --status EXAMPLES: # Create sprint ./sprint.sh create --name "Sprint 1" --project "Web Projects" \ --goal "Complete MVP features" --start-date 2026-02-24 --end-date 2026-03-07 # List active sprints ./sprint.sh list --active # Get sprint by name ./sprint.sh get "Sprint 1" # Close a sprint (marks as completed) ./sprint.sh close "Sprint 1" # Delete sprint ./sprint.sh delete EOF } # Resolve project name to ID resolve_project_id() { local identifier="$1" # Check if it's already a UUID if [[ "$identifier" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then echo "$identifier" return 0 fi # Search by name (case-insensitive) local project_id project_id=$(curl -s "${SUPABASE_URL}/rest/v1/projects?name=ilike.*${identifier}*&select=id" \ "${HEADERS[@]}" | jq -r '.[0].id // empty') if [[ -n "$project_id" ]]; then echo "$project_id" return 0 fi # Try exact match local encoded_name encoded_name=$(printf '%s' "$identifier" | jq -sRr @uri) project_id=$(curl -s "${SUPABASE_URL}/rest/v1/projects?name=eq.${encoded_name}&select=id" \ "${HEADERS[@]}" | jq -r '.[0].id // empty') if [[ -n "$project_id" ]]; then echo "$project_id" return 0 fi log_error "Project '$identifier' not found" return 1 } # Resolve sprint identifier to ID resolve_sprint_id() { local identifier="$1" # Check if it's already a UUID if [[ "$identifier" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then echo "$identifier" return 0 fi # Search by name (case-insensitive) local sprint_id sprint_id=$(curl -s "${SUPABASE_URL}/rest/v1/sprints?name=ilike.*${identifier}*&select=id" \ "${HEADERS[@]}" | jq -r '.[0].id // empty') if [[ -n "$sprint_id" ]]; then echo "$sprint_id" return 0 fi # Try exact match local encoded_name encoded_name=$(printf '%s' "$identifier" | jq -sRr @uri) sprint_id=$(curl -s "${SUPABASE_URL}/rest/v1/sprints?name=eq.${encoded_name}&select=id" \ "${HEADERS[@]}" | jq -r '.[0].id // empty') if [[ -n "$sprint_id" ]]; then echo "$sprint_id" return 0 fi log_error "Sprint '$identifier' not found" return 1 } # Create command cmd_create() { local name="" local project="" local goal="" local start_date="" local end_date="" local status="planning" # Parse arguments while [[ $# -gt 0 ]]; do case "${1:-}" in --name) name="$2"; shift 2 ;; --project) project="$2"; shift 2 ;; --goal) goal="$2"; shift 2 ;; --start-date) start_date="$2"; shift 2 ;; --end-date) end_date="$2"; shift 2 ;; --status) status="$2"; shift 2 ;; *) shift ;; esac done # Validate required fields if [[ -z "$name" ]]; then log_error "Name is required (use --name)" exit 1 fi if [[ -z "$project" ]]; then log_error "Project is required (use --project)" exit 1 fi # Resolve project local project_id project_id=$(resolve_project_id "$project") # Generate sprint data local sprint_id sprint_id=$(generate_uuid) local timestamp timestamp=$(get_timestamp) # Build JSON payload local json_payload json_payload=$(jq -n \ --arg id "$sprint_id" \ --arg name "$name" \ --arg project_id "$project_id" \ --arg status "$status" \ --arg created_at "$timestamp" \ '{ id: $id, name: $name, project_id: $project_id, status: $status, created_at: $created_at }') # Add optional fields if [[ -n "$goal" ]]; then json_payload=$(echo "$json_payload" | jq --arg v "$goal" '. + {goal: $v}') fi if [[ -n "$start_date" ]]; then json_payload=$(echo "$json_payload" | jq --arg v "$start_date" '. + {start_date: $v}') fi if [[ -n "$end_date" ]]; then json_payload=$(echo "$json_payload" | jq --arg v "$end_date" '. + {end_date: $v}') fi # Create sprint log_info "Creating sprint..." local response response=$(curl -s -X POST "${SUPABASE_URL}/rest/v1/sprints" \ "${HEADERS[@]}" \ -d "$json_payload") # Supabase returns empty on success, or error JSON on failure if [[ -z "$response" ]]; then log_success "Sprint created: $sprint_id" elif [[ "$response" == *"error"* ]] || [[ "$response" == *'"code"'* ]]; then log_error "Failed to create sprint" echo "$response" | jq . 2>/dev/null || echo "$response" exit 1 else log_success "Sprint created: $sprint_id" echo "$response" | jq . 2>/dev/null || echo "$response" fi } # List command cmd_list() { local filter_status="" local filter_project="" local active_only=false local output_json=false # Parse arguments while [[ $# -gt 0 ]]; do case "${1:-}" in --status) filter_status="$2"; shift 2 ;; --project) filter_project="$2"; shift 2 ;; --active) active_only=true; shift ;; --json) output_json=true; shift ;; *) shift ;; esac done # Build query local query="${SUPABASE_URL}/rest/v1/sprints?select=*,projects(name)&order=created_at.desc" if [[ "$active_only" == true ]]; then filter_status="active" fi if [[ -n "$filter_status" ]]; then local encoded_status encoded_status=$(printf '%s' "$filter_status" | jq -sRr @uri) query="${query}&status=eq.${encoded_status}" fi if [[ -n "$filter_project" ]]; then if [[ ! "$filter_project" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then filter_project=$(resolve_project_id "$filter_project") fi query="${query}&project_id=eq.${filter_project}" fi log_info "Fetching sprints..." local response response=$(curl -s "$query" "${HEADERS[@]}") if [[ "$output_json" == true ]]; then echo "$response" | jq . else # Table output local count count=$(echo "$response" | jq 'length') log_success "Found $count sprint(s)" # Print header printf "%-36s %-25s %-12s %-10s %-12s %-12s\n" "ID" "NAME" "PROJECT" "STATUS" "START" "END" printf "%-36s %-25s %-12s %-10s %-12s %-12s\n" "------------------------------------" "-------------------------" "------------" "----------" "------------" "------------" # Print rows echo "$response" | jq -r '.[] | [.id, (.name | tostring | .[0:23]), (.projects.name // "N/A" | tostring | .[0:10]), .status, (.start_date // "N/A"), (.end_date // "N/A")] | @tsv' | while IFS=$'\t' read -r id name project status start end; do printf "%-36s %-25s %-12s %-10s %-12s %-12s\n" "$id" "$name" "$project" "$status" "$start" "$end" done fi } # Get command cmd_get() { local identifier="$1" if [[ -z "$identifier" ]]; then log_error "Sprint ID or name required. Usage: ./sprint.sh get " exit 1 fi local sprint_id sprint_id=$(resolve_sprint_id "$identifier") log_info "Fetching sprint $sprint_id..." local response response=$(curl -s "${SUPABASE_URL}/rest/v1/sprints?id=eq.${sprint_id}&select=*,projects(name)" \ "${HEADERS[@]}") local sprint sprint=$(echo "$response" | jq '.[0] // empty') if [[ -n "$sprint" && "$sprint" != "{}" && "$sprint" != "null" ]]; then echo "$sprint" | jq . else log_error "Sprint not found: $identifier" exit 1 fi } # Update command cmd_update() { local identifier="$1" shift if [[ -z "$identifier" ]]; then log_error "Sprint ID or name required. Usage: ./sprint.sh update [options]" exit 1 fi local sprint_id sprint_id=$(resolve_sprint_id "$identifier") # Build update payload local update_fields="{}" while [[ $# -gt 0 ]]; do case "${1:-}" in --name) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {name: $v}'); shift 2 ;; --goal) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {goal: $v}'); shift 2 ;; --start-date) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {start_date: $v}'); shift 2 ;; --end-date) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {end_date: $v}'); shift 2 ;; --status) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {status: $v}'); shift 2 ;; *) shift ;; esac done # Check if we have anything to update if [[ "$update_fields" == "{}" ]]; then log_warning "No update fields specified" exit 0 fi # Add updated_at timestamp local timestamp timestamp=$(get_timestamp) update_fields=$(echo "$update_fields" | jq --arg t "$timestamp" '. + {updated_at: $t}') log_info "Updating sprint $sprint_id..." local response response=$(curl -s -X PATCH "${SUPABASE_URL}/rest/v1/sprints?id=eq.${sprint_id}" \ "${HEADERS[@]}" \ -d "$update_fields") if [[ -z "$response" || "$response" == "[]" || ! "$response" == *"error"* ]]; then log_success "Sprint updated: $sprint_id" else log_error "Failed to update sprint" echo "$response" | jq . 2>/dev/null || echo "$response" exit 1 fi } # Close command - marks sprint as completed cmd_close() { local identifier="$1" if [[ -z "$identifier" ]]; then log_error "Sprint ID or name required. Usage: ./sprint.sh close " exit 1 fi local sprint_id sprint_id=$(resolve_sprint_id "$identifier") log_info "Closing sprint $sprint_id..." local timestamp timestamp=$(get_timestamp) local update_fields update_fields=$(jq -n \ --arg status "completed" \ --arg closed_at "$timestamp" \ --arg updated_at "$timestamp" \ '{status: $status, closed_at: $closed_at, updated_at: $updated_at}') local response response=$(curl -s -X PATCH "${SUPABASE_URL}/rest/v1/sprints?id=eq.${sprint_id}" \ "${HEADERS[@]}" \ -d "$update_fields") if [[ -z "$response" || "$response" == "[]" || ! "$response" == *"error"* ]]; then log_success "Sprint closed: $sprint_id" else log_error "Failed to close sprint" echo "$response" | jq . 2>/dev/null || echo "$response" exit 1 fi } # Delete command cmd_delete() { local identifier="$1" if [[ -z "$identifier" ]]; then log_error "Sprint ID or name required. Usage: ./sprint.sh delete " exit 1 fi local sprint_id sprint_id=$(resolve_sprint_id "$identifier") log_info "Deleting sprint $sprint_id..." local response response=$(curl -s -X DELETE "${SUPABASE_URL}/rest/v1/sprints?id=eq.${sprint_id}" \ "${HEADERS[@]}") if [[ -z "$response" || "$response" == "[]" ]]; then log_success "Sprint deleted: $sprint_id" else log_error "Failed to delete sprint" echo "$response" | jq . 2>/dev/null || echo "$response" exit 1 fi } # Main execution check_dependencies # Parse command COMMAND="${1:-}" shift || true case "$COMMAND" in create) cmd_create "$@" ;; list) cmd_list "$@" ;; get) cmd_get "$@" ;; update) cmd_update "$@" ;; close) cmd_close "$@" ;; delete) cmd_delete "$@" ;; help|--help|-h) show_usage ;; "") show_usage ;; *) log_error "Unknown command: $COMMAND" show_usage exit 1 ;; esac