512 lines
14 KiB
Bash
Executable File
512 lines
14 KiB
Bash
Executable File
#!/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 <id> Get a specific sprint by ID or name
|
||
update <id> Update a sprint
|
||
delete <id> Delete a sprint
|
||
close <id> 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 <status> Filter by status
|
||
--project <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 <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 <sprint-id>
|
||
|
||
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 <id-or-name>"
|
||
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 <id-or-name> [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 <id-or-name>"
|
||
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 <id-or-name>"
|
||
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
|