496 lines
14 KiB
Bash
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
|