- gantt-tasks: Replace Supabase REST with API calls using GANTT_MACHINE_TOKEN - mission-control-docs: Replace Supabase REST with API calls using MC_MACHINE_TOKEN - Both skills now follow API-centric architecture - Updated SKILL.md documentation for both This ensures consistency with the CLI auth pattern and provides single source of truth through API endpoints.
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.vercel.app/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
|