test-repo/skills/gantt-tasks/lib/tasks.sh

496 lines
14 KiB
Bash

#!/bin/bash
# gantt-tasks library - Full CRUD for Gantt Board tasks
# Location: ~/.agents/skills/gantt-tasks/lib/tasks.sh
# Refactored to use API endpoints with machine token auth
# Configuration
GANTT_API_URL="${GANTT_API_URL:-https://gantt-board.twisteddevices.com/api}"
GANTT_MACHINE_TOKEN="${GANTT_MACHINE_TOKEN:-}"
MAX_ID="9c29cc99-81a1-4e75-8dff-cd7cc5ceb5aa" # Max user ID
# Debug function
debug() {
echo "[DEBUG] $*" >&2
}
# Error handler
error_exit() {
echo "$1" >&2
return 1
}
# Make API call with machine token
_api_call() {
local method="$1"
local endpoint="$2"
local data="${3:-}"
if [[ -z "$GANTT_MACHINE_TOKEN" ]]; then
error_exit "GANTT_MACHINE_TOKEN not set"
return 1
fi
local url="${GANTT_API_URL}${endpoint}"
local curl_opts=(
-s
-H "Content-Type: application/json"
-H "Authorization: Bearer ${GANTT_MACHINE_TOKEN}"
)
if [[ -n "$data" ]]; then
curl_opts+=(-d "$data")
fi
curl "${curl_opts[@]}" -X "$method" "$url" 2>/dev/null
}
# Resolve project name to ID
resolve_project_id() {
local PROJECT_NAME="$1"
# Special case: Research Project
if [[ "$PROJECT_NAME" == "Research" || "$PROJECT_NAME" == "Research Project" ]]; then
echo "a1b2c3d4-0003-0000-0000-000000000003"
return 0
fi
# Query by name via API
local RESULT=$(_api_call "GET" "/projects" | jq -r --arg q "$PROJECT_NAME" '
.projects
| map(select((.name // "") | ascii_downcase | contains($q | ascii_downcase)))
| .[0].id // empty
')
if [[ -n "$RESULT" ]]; then
echo "$RESULT"
return 0
fi
# Return empty if not found
echo ""
return 1
}
# Resolve user name to ID
resolve_user_id() {
local USER_NAME="$1"
# Current mappings
case "$USER_NAME" in
"Max"|"max") echo "9c29cc99-81a1-4e75-8dff-cd7cc5ceb5aa" ;;
"Matt"|"matt"|"Matt Bruce") echo "0a3e400c-3932-48ae-9b65-f3f9c6f26fe9" ;;
*) echo "$MAX_ID" ;; # Default to Max
esac
}
# Get current sprint (contains today's date)
sprint_get_current() {
local TODAY=$(date +%Y-%m-%d)
# Get current sprint via API
local RESULT=$(_api_call "GET" "/sprints" | jq -r --arg today "$TODAY" '
.sprints
| map(select(.startDate <= $today and .endDate >= $today))
| .[0].id // empty
')
if [[ -n "$RESULT" && "$RESULT" != "null" ]]; then
echo "$RESULT"
return 0
fi
# Fallback: get most recent sprint
RESULT=$(_api_call "GET" "/sprints" | jq -r '
.sprints
| sort_by(.startDate)
| reverse
| .[0].id // empty
')
echo "$RESULT"
}
# Resolve sprint name/ID
resolve_sprint_id() {
local SPRINT="$1"
if [[ "$SPRINT" == "current" ]]; then
sprint_get_current
return 0
fi
# If it looks like a UUID, use it directly
if [[ "$SPRINT" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
echo "$SPRINT"
return 0
fi
# Query by name via API
local RESULT=$(_api_call "GET" "/sprints" | jq -r --arg q "$SPRINT" '
.sprints
| map(select((.name // "") | ascii_downcase | contains($q | ascii_downcase)))
| .[0].id // empty
')
echo "$RESULT"
}
# Create a new task
task_create() {
local TITLE=""
local DESCRIPTION=""
local TYPE="task"
local STATUS="open"
local PRIORITY="medium"
local PROJECT=""
local SPRINT=""
local ASSIGNEE="Max"
local DUE_DATE=""
local TAGS=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--title) TITLE="$2"; shift 2 ;;
--description) DESCRIPTION="$2"; shift 2 ;;
--type) TYPE="$2"; shift 2 ;;
--status) STATUS="$2"; shift 2 ;;
--priority) PRIORITY="$2"; shift 2 ;;
--project) PROJECT="$2"; shift 2 ;;
--sprint) SPRINT="$2"; shift 2 ;;
--assignee) ASSIGNEE="$2"; shift 2 ;;
--due-date) DUE_DATE="$2"; shift 2 ;;
--tags) TAGS="$2"; shift 2 ;;
*) shift ;;
esac
done
# Validate
[[ -z "$TITLE" ]] && error_exit "--title is required" && return 1
# Resolve IDs
local PROJECT_ID="a1b2c3d4-0003-0000-0000-000000000003"
if [[ -n "$PROJECT" ]]; then
PROJECT_ID=$(resolve_project_id "$PROJECT")
fi
local SPRINT_ID=""
if [[ -n "$SPRINT" ]]; then
SPRINT_ID=$(resolve_sprint_id "$SPRINT")
fi
local ASSIGNEE_ID=$(resolve_user_id "$ASSIGNEE")
# Build JSON payload
local JSONPayload=$(jq -n \
--arg title "$TITLE" \
--arg type "$TYPE" \
--arg status "$STATUS" \
--arg priority "$PRIORITY" \
--arg assigneeId "$ASSIGNEE_ID" \
--arg projectId "$PROJECT_ID" \
'{
title: $title,
type: $type,
status: $status,
priority: $priority,
assigneeId: $assigneeId,
projectId: $projectId,
comments: [],
tags: []
}')
# Add optional fields
[[ -n "$DESCRIPTION" ]] && JSONPayload=$(echo "$JSONPayload" | jq --arg desc "$DESCRIPTION" '. + {description: $desc}')
[[ -n "$SPRINT_ID" ]] && JSONPayload=$(echo "$JSONPayload" | jq --arg sid "$SPRINT_ID" '. + {sprintId: $sid}')
[[ -n "$DUE_DATE" ]] && JSONPayload=$(echo "$JSONPayload" | jq --arg dd "$DUE_DATE" '. + {dueDate: $dd}')
[[ -n "$TAGS" ]] && JSONPayload=$(echo "$JSONPayload" | jq --argjson tags "[$(echo "$TAGS" | sed 's/,/","/g' | sed 's/^/"/' | sed 's/$/"/')]" '. + {tags: $tags}')
# Create task via API
local Response=$(_api_call "POST" "/tasks" "{\"task\": $JSONPayload}")
local TaskId=$(echo "$Response" | jq -r '.task.id // .id // empty')
if [[ -n "$TaskId" && "$TaskId" != "null" ]]; then
echo "$TaskId"
return 0
fi
error_exit "Failed to create task: $(echo "$Response" | jq -r '.error // "Unknown error"')"
return 1
}
# Get task details
task_get() {
local TASK_ID="$1"
local Response=$(_api_call "GET" "/tasks")
echo "$Response" | jq --arg id "$TASK_ID" '.tasks | map(select(.id == $id)) | .[0] // empty'
}
# List tasks with filters
task_list() {
local StatusFilter=""
local ProjectFilter=""
local SprintFilter=""
local AssigneeFilter=""
local PriorityFilter=""
local JsonOutput=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--status) StatusFilter="$2"; shift 2 ;;
--project) ProjectFilter="$2"; shift 2 ;;
--sprint) SprintFilter="$2"; shift 2 ;;
--assignee) AssigneeFilter="$2"; shift 2 ;;
--priority) PriorityFilter="$2"; shift 2 ;;
--json) JsonOutput=true; shift ;;
*) shift ;;
esac
done
# Get all tasks via API
local Response=$(_api_call "GET" "/tasks")
local Tasks=$(echo "$Response" | jq '.tasks // []')
# Apply filters in jq
local Filtered="$Tasks"
if [[ -n "$StatusFilter" ]]; then
# Handle comma-separated status values
if [[ "$StatusFilter" =~ , ]]; then
Filtered=$(echo "$Filtered" | jq --arg statuses "$StatusFilter" '
[split(",")] as $statusList | map(select(.status as $s | $statusList | index($s)))
')
else
Filtered=$(echo "$Filtered" | jq --arg status "$StatusFilter" 'map(select(.status == $status))')
fi
fi
if [[ -n "$ProjectFilter" ]]; then
local ProjectId=$(resolve_project_id "$ProjectFilter")
[[ -n "$ProjectId" ]] && Filtered=$(echo "$Filtered" | jq --arg pid "$ProjectId" 'map(select(.projectId == $pid))')
fi
if [[ -n "$SprintFilter" ]]; then
local SprintId=$(resolve_sprint_id "$SprintFilter")
[[ -n "$SprintId" ]] && Filtered=$(echo "$Filtered" | jq --arg sid "$SprintId" 'map(select(.sprintId == $sid))')
fi
if [[ -n "$AssigneeFilter" ]]; then
local AssigneeId=$(resolve_user_id "$AssigneeFilter")
Filtered=$(echo "$Filtered" | jq --arg aid "$AssigneeId" 'map(select(.assigneeId == $aid))')
fi
if [[ -n "$PriorityFilter" ]]; then
Filtered=$(echo "$Filtered" | jq --arg pri "$PriorityFilter" 'map(select(.priority == $pri))')
fi
if [[ "$JsonOutput" == true ]]; then
echo "{\"tasks\": $Filtered}"
else
# Pretty print
echo "$Filtered" | jq -r '.[] | "\(.status) | \(.priority) | \(.title) | \(.id)"' 2>/dev/null
fi
}
# Update task
task_update() {
local TASK_ID="$1"
shift
# First get the current task
local CurrentTask=$(task_get "$TASK_ID")
[[ -z "$CurrentTask" ]] && error_exit "Task not found: $TASK_ID" && return 1
# Build updates
local Updates="{}"
while [[ $# -gt 0 ]]; do
case $1 in
--title) Updates=$(echo "$Updates" | jq --arg v "$2" '. + {title: $v}'); shift 2 ;;
--description) Updates=$(echo "$Updates" | jq --arg v "$2" '. + {description: $v}'); shift 2 ;;
--status) Updates=$(echo "$Updates" | jq --arg v "$2" '. + {status: $v}'); shift 2 ;;
--priority) Updates=$(echo "$Updates" | jq --arg v "$2" '. + {priority: $v}'); shift 2 ;;
--due-date) Updates=$(echo "$Updates" | jq --arg v "$2" '. + {dueDate: $v}'); shift 2 ;;
*) shift ;;
esac
done
# Merge current task with updates
local MergedTask=$(echo "$CurrentTask" | jq --argjson updates "$Updates" '. + $updates')
# Send update via API
local Payload=$(jq -n --argjson task "$MergedTask" '{task: $task}')
local Response=$(_api_call "POST" "/tasks" "$Payload")
if echo "$Response" | jq -e '.error' >/dev/null 2>&1; then
error_exit "Update failed: $(echo "$Response" | jq -r '.error')"
return 1
fi
echo "✅ Task updated"
}
# Delete task
task_delete() {
local TASK_ID="$1"
_api_call "DELETE" "/tasks" "{\"id\": \"$TASK_ID\"}" >/dev/null
echo "✅ Task deleted"
}
# Add comment to task
task_add_comment() {
local TASK_ID="$1"
local TEXT="$2"
local UseChecklist=false
local StatusPrefix=""
shift 2
while [[ $# -gt 0 ]]; do
case $1 in
--checklist) UseChecklist=true; shift ;;
--status) StatusPrefix="$2"; shift 2 ;;
*) shift ;;
esac
done
# Format comment
local Now=$(date +"%Y-%m-%d %H:%M")
local StatusEmoji="🔄"
[[ -n "$StatusPrefix" ]] && StatusEmoji="$StatusPrefix"
local CommentText
if [[ "$UseChecklist" == true ]]; then
CommentText=$(cat <<EOF
## [$Now] $StatusEmoji ${StatusPrefix:-Update}
### ✅ Completed
- [x] Previous work
### 🔄 In Progress
- [x] $TEXT
### 📋 Remaining
- [ ] Next step
### 📊 Progress: 1/1 tasks
EOF
)
else
CommentText="[$Now] $TEXT"
fi
# Get current task with comments
local CurrentTask=$(task_get "$TASK_ID")
local CurrentComments=$(echo "$CurrentTask" | jq '.comments // []')
# Build new comment
local NewComment=$(jq -n \
--arg id "$(uuidgen 2>/dev/null || date +%s)" \
--arg text "$CommentText" \
--arg author "$MAX_ID" \
'{
id: $id,
text: $text,
createdAt: now | todate,
commentAuthorId: $author,
replies: []
}')
# Append to comments
local UpdatedComments=$(echo "$CurrentComments" | jq --argjson new "$NewComment" '. + [$new]')
# Merge and update task
local MergedTask=$(echo "$CurrentTask" | jq --argjson comments "$UpdatedComments" '. + {comments: $comments}')
local Payload=$(jq -n --argjson task "$MergedTask" '{task: $task}')
_api_call "POST" "/tasks" "$Payload" >/dev/null
echo "✅ Comment added"
}
# Attach file to task (stores content in attachments field)
task_attach_file() {
local TASK_ID="$1"
local FILE_PATH="$2"
local Description="${3:-Attached file}"
[[ ! -f "$FILE_PATH" ]] && error_exit "File not found: $FILE_PATH" && return 1
# Read file content
local Content=$(cat "$FILE_PATH")
local FileName=$(basename "$FILE_PATH")
# Get current task with attachments
local CurrentTask=$(task_get "$TASK_ID")
local CurrentAttachments=$(echo "$CurrentTask" | jq '.attachments // []')
# Build new attachment
local NewAttachment=$(jq -n \
--arg id "$(uuidgen 2>/dev/null || date +%s)" \
--arg name "$FileName" \
--arg type "$(file -b --mime-type "$FILE_PATH" 2>/dev/null || echo 'application/octet-stream')" \
--arg dataUrl "data:text/plain;base64,$(base64 -i "$FILE_PATH" | tr -d '\n')" \
'{
id: $id,
name: $name,
type: $type,
dataUrl: $dataUrl,
uploadedAt: now | todate
}')
# Append to attachments
local UpdatedAttachments=$(echo "$CurrentAttachments" | jq --argjson new "$NewAttachment" '. + [$new]')
# Merge and update task
local MergedTask=$(echo "$CurrentTask" | jq --argjson attachments "$UpdatedAttachments" '. + {attachments: $attachments}')
local Payload=$(jq -n --argjson task "$MergedTask" '{task: $task}')
_api_call "POST" "/tasks" "$Payload" >/dev/null
echo "✅ File attached: $FileName"
}
# Update an existing comment by ID
task_update_comment() {
local TASK_ID="$1"
local COMMENT_ID="$2"
local NEW_TEXT="$3"
# Get current task
local CurrentTask=$(task_get "$TASK_ID")
local AllComments=$(echo "$CurrentTask" | jq '.comments // []')
# Find and update the specific comment, keep others unchanged
local UpdatedComments=$(echo "$AllComments" | jq --arg cid "$COMMENT_ID" --arg text "$NEW_TEXT" \
'map(if .id == $cid then .text = $text else . end)')
# Merge and update task
local MergedTask=$(echo "$CurrentTask" | jq --argjson comments "$UpdatedComments" '. + {comments: $comments}')
local Payload=$(jq -n --argjson task "$MergedTask" '{task: $task}')
_api_call "POST" "/tasks" "$Payload" >/dev/null
echo "✅ Comment updated"
}
# Export all functions
export -f resolve_project_id
export -f resolve_user_id
export -f sprint_get_current
export -f resolve_sprint_id
export -f task_create
export -f task_get
export -f task_list
export -f task_update
export -f task_delete
export -f task_add_comment
export -f task_update_comment
export -f task_attach_file
export -f _api_call