#!/bin/bash # Task CLI for Gantt Board (API passthrough) # Usage: ./task.sh [list|get|create|update|delete|current-sprint|bulk-create] [args...] set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=./lib/api_client.sh source "$SCRIPT_DIR/lib/api_client.sh" # Color support - disable with --no-color or NO_COLOR env var NO_COLOR="${NO_COLOR:-}" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # Disable colors if requested if [[ -n "$NO_COLOR" ]]; then RED='' GREEN='' YELLOW='' BLUE='' NC='' fi log_info() { echo -e "${BLUE}i${NC} $1"; } log_success() { echo -e "${GREEN}ok${NC} $1"; } log_warning() { echo -e "${YELLOW}warn${NC} $1"; } log_error() { echo -e "${RED}error${NC} $1"; } UUID_PATTERN='^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' DEFAULT_PROJECT_ID="${DEFAULT_PROJECT_ID:-}" DEFAULT_ASSIGNEE_ID="${DEFAULT_ASSIGNEE_ID:-}" check_dependencies() { if ! command -v jq >/dev/null 2>&1; then log_error "jq is required. Install with: brew install jq" exit 1 fi if ! command -v uuidgen >/dev/null 2>&1; then log_error "uuidgen is required" exit 1 fi } show_usage() { cat << 'USAGE' Task CLI for Gantt Board USAGE: ./task.sh [--no-color] [list|get|create|update|delete|current-sprint|bulk-create] [args...] GLOBAL FLAGS: --no-color Disable colored output (also respects NO_COLOR env var) COMMANDS: list [status] List tasks (supports filters) get Get specific task create "Title" [status] [priority] [project] [assignee] [sprint] [type] create --title "Title" [flags...] Create task using flags update Update one field (legacy) update [flags...] Update task with flags delete Delete task current-sprint Show current sprint ID bulk-create Bulk create from JSON array USAGE } resolve_project_id() { local identifier="$1" if [[ -z "$identifier" ]]; then if [[ -n "$DEFAULT_PROJECT_ID" ]]; then echo "$DEFAULT_PROJECT_ID" return 0 fi local response response=$(api_call GET "/projects") echo "$response" | jq -r '.projects[0].id // empty' return 0 fi if [[ "$identifier" =~ $UUID_PATTERN ]]; then echo "$identifier" return 0 fi local response response=$(api_call GET "/projects") local project_id project_id=$(echo "$response" | jq -r --arg q "$identifier" ' .projects | map(select((.name // "") | ascii_downcase | contains($q | ascii_downcase))) | .[0].id // empty ') if [[ -n "$project_id" ]]; then echo "$project_id" return 0 fi log_error "Project '$identifier' not found" return 1 } resolve_assignee_id() { local identifier="$1" if [[ -z "$identifier" ]]; then if [[ -n "$DEFAULT_ASSIGNEE_ID" ]]; then echo "$DEFAULT_ASSIGNEE_ID" else echo "" fi return 0 fi if [[ "$identifier" =~ $UUID_PATTERN ]]; then echo "$identifier" return 0 fi local response response=$(api_call GET "/auth/users") local user_id user_id=$(echo "$response" | jq -r --arg q "$identifier" ' .users | map(select( ((.name // "") | ascii_downcase | contains($q | ascii_downcase)) or ((.email // "") | ascii_downcase == ($q | ascii_downcase)) )) | .[0].id // empty ') if [[ -n "$user_id" ]]; then echo "$user_id" return 0 fi log_error "Assignee '$identifier' not found" return 1 } resolve_sprint_id() { local identifier="$1" if [[ -z "$identifier" ]]; then echo "" return 0 fi if [[ "$identifier" == "current" ]]; then local response response=$(api_call GET "/sprints/current") echo "$response" | jq -r '.sprint.id // empty' return 0 fi if [[ "$identifier" =~ $UUID_PATTERN ]]; then echo "$identifier" return 0 fi local response response=$(api_call GET "/sprints") local sprint_id sprint_id=$(echo "$response" | jq -r --arg q "$identifier" ' .sprints | map(select((.name // "") | ascii_downcase | contains($q | ascii_downcase))) | .[0].id // empty ') if [[ -n "$sprint_id" ]]; then echo "$sprint_id" return 0 fi log_error "Sprint '$identifier' not found" return 1 } get_current_sprint() { local response response=$(api_call GET "/sprints/current") echo "$response" | jq -r '.sprint.id // empty' } to_tag_array() { local tags_csv="$1" if [[ -z "$tags_csv" ]]; then echo '[]' return fi jq -cn --arg csv "$tags_csv" '$csv | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))' } to_comment_array() { local comment_text="$1" if [[ -z "$comment_text" ]]; then echo '[]' return fi local now now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") jq -cn --arg id "$(date +%s)-$RANDOM" --arg text "$comment_text" --arg createdAt "$now" ' [{ id: $id, text: $text, createdAt: $createdAt, commentAuthorId: "assistant", replies: [] }] ' } list_tasks() { local positional_status="${1:-}" local status_filter="" local priority_filter="" local project_filter="" local assignee_filter="" local type_filter="" local limit="" local output_json=false if [[ -n "$positional_status" && "$positional_status" != --* ]]; then status_filter="$positional_status" shift || true fi while [[ $# -gt 0 ]]; do case "${1:-}" in --status) status_filter="${2:-}"; shift 2 ;; --priority) priority_filter="${2:-}"; shift 2 ;; --project) project_filter="${2:-}"; shift 2 ;; --assignee) assignee_filter="${2:-}"; shift 2 ;; --type) type_filter="${2:-}"; shift 2 ;; --limit) limit="${2:-}"; shift 2 ;; --json) output_json=true; shift ;; *) shift ;; esac done local response response=$(api_call GET "/tasks?scope=active-sprint") local tasks tasks=$(echo "$response" | jq '.tasks') if [[ -n "$status_filter" ]]; then tasks=$(echo "$tasks" | jq --arg v "$status_filter" ' ($v | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))) as $statuses | map(select(.status as $s | ($statuses | index($s)))) ') fi if [[ -n "$priority_filter" ]]; then tasks=$(echo "$tasks" | jq --arg v "$priority_filter" 'map(select(.priority == $v))') fi if [[ -n "$type_filter" ]]; then tasks=$(echo "$tasks" | jq --arg v "$type_filter" 'map(select(.type == $v))') fi if [[ -n "$project_filter" ]]; then local project_id project_id=$(resolve_project_id "$project_filter") tasks=$(echo "$tasks" | jq --arg v "$project_id" 'map(select(.projectId == $v))') fi if [[ -n "$assignee_filter" ]]; then local assignee_id assignee_id=$(resolve_assignee_id "$assignee_filter") tasks=$(echo "$tasks" | jq --arg v "$assignee_id" 'map(select(.assigneeId == $v))') fi if [[ -n "$limit" ]]; then tasks=$(echo "$tasks" | jq --argjson n "$limit" '.[0:$n]') fi if [[ "$output_json" == true ]]; then echo "$tasks" | jq . return fi local count count=$(echo "$tasks" | jq 'length') if [[ "$count" -eq 0 ]]; then local current_sprint_id current_sprint_id="$(get_current_sprint 2>/dev/null || true)" if [[ -z "$current_sprint_id" ]]; then log_warning "No current sprint found. Returning 0 tasks." fi fi log_success "Found $count task(s)" printf "%-36s %-34s %-12s %-10s\n" "ID" "TITLE" "STATUS" "PRIORITY" printf "%-36s %-34s %-12s %-10s\n" "------------------------------------" "----------------------------------" "------------" "----------" echo "$tasks" | jq -r '.[] | [.id, (.title // "" | tostring | .[0:32]), (.status // ""), (.priority // "")] | @tsv' \ | while IFS=$'\t' read -r id title status priority; do printf "%-36s %-34s %-12s %-10s\n" "$id" "$title" "$status" "$priority" done } get_task() { local task_id="${1:-}" if [[ -z "$task_id" ]]; then log_error "Task ID required" exit 1 fi local response response=$(api_call GET "/tasks?taskId=$(urlencode "$task_id")&include=detail") echo "$response" | jq '.tasks[0]' } create_from_payload() { local task_payload="$1" api_call POST "/tasks" "{\"task\": $task_payload}" | jq . } create_task() { local title="" local description="" local task_type="task" local status="open" local priority="medium" local project="" local sprint="" local assignee="" local due_date="" local tags_csv="" local comments_text="" local file_input="" if [[ $# -gt 0 && "${1:-}" != --* ]]; then # Legacy positional form title="${1:-}" status="${2:-$status}" priority="${3:-$priority}" project="${4:-$project}" assignee="${5:-$assignee}" sprint="${6:-$sprint}" task_type="${7:-$task_type}" fi while [[ $# -gt 0 ]]; do case "${1:-}" in --title) title="${2:-}"; shift 2 ;; --description) description="${2:-}"; shift 2 ;; --type) task_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_csv="${2:-}"; shift 2 ;; --comments) comments_text="${2:-}"; shift 2 ;; --file) file_input="${2:-}"; shift 2 ;; --interactive) log_error "Interactive mode is not supported in API passthrough mode" exit 1 ;; *) shift ;; esac done if [[ -n "$file_input" ]]; then if [[ ! -f "$file_input" ]]; then log_error "File not found: $file_input" exit 1 fi title=$(jq -r '.title // ""' "$file_input") description=$(jq -r '.description // ""' "$file_input") task_type=$(jq -r '.type // "task"' "$file_input") status=$(jq -r '.status // "open"' "$file_input") priority=$(jq -r '.priority // "medium"' "$file_input") project=$(jq -r '.project // ""' "$file_input") sprint=$(jq -r '.sprint // ""' "$file_input") assignee=$(jq -r '.assignee // ""' "$file_input") due_date=$(jq -r '.due_date // ""' "$file_input") tags_csv=$(jq -r '.tags // ""' "$file_input") comments_text=$(jq -r '.comments // ""' "$file_input") fi if [[ -z "$title" ]]; then log_error "Title is required" exit 1 fi local project_id project_id=$(resolve_project_id "$project") if [[ -z "$project_id" ]]; then log_error "Could not resolve project" exit 1 fi local assignee_id assignee_id=$(resolve_assignee_id "$assignee") local sprint_id sprint_id=$(resolve_sprint_id "$sprint") local tags_json tags_json=$(to_tag_array "$tags_csv") local comments_json comments_json=$(to_comment_array "$comments_text") local task_id task_id=$(uuidgen | tr '[:upper:]' '[:lower:]') local now now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") local task_payload task_payload=$(jq -n \ --arg id "$task_id" \ --arg title "$title" \ --arg description "$description" \ --arg type "$task_type" \ --arg status "$status" \ --arg priority "$priority" \ --arg projectId "$project_id" \ --arg sprintId "$sprint_id" \ --arg assigneeId "$assignee_id" \ --arg dueDate "$due_date" \ --arg createdAt "$now" \ --arg updatedAt "$now" \ --argjson tags "$tags_json" \ --argjson comments "$comments_json" \ '{ id: $id, title: $title, description: (if $description == "" then null else $description end), type: $type, status: $status, priority: $priority, projectId: $projectId, sprintId: (if $sprintId == "" then null else $sprintId end), assigneeId: (if $assigneeId == "" then null else $assigneeId end), dueDate: (if $dueDate == "" then null else $dueDate end), createdAt: $createdAt, updatedAt: $updatedAt, tags: $tags, comments: $comments, attachments: [] }') create_from_payload "$task_payload" } update_task() { local task_id="${1:-}" shift || true if [[ -z "$task_id" ]]; then log_error "Task ID required" exit 1 fi local existing existing=$(api_call GET "/tasks?taskId=$(urlencode "$task_id")&include=detail" | jq '.tasks[0]') if [[ "$existing" == "null" || -z "$existing" ]]; then log_error "Task not found: $task_id" exit 1 fi if [[ $# -ge 2 && "${1:-}" != --* ]]; then local field="${1:-}" local value="${2:-}" existing=$(echo "$existing" | jq --arg f "$field" --arg v "$value" '. + {($f): $v}') shift 2 || true fi local add_comment="" local clear_tags=false local set_tags="" while [[ $# -gt 0 ]]; do case "${1:-}" in --status) existing=$(echo "$existing" | jq --arg v "${2:-}" '.status = $v'); shift 2 ;; --priority) existing=$(echo "$existing" | jq --arg v "${2:-}" '.priority = $v'); shift 2 ;; --title) existing=$(echo "$existing" | jq --arg v "${2:-}" '.title = $v'); shift 2 ;; --description) existing=$(echo "$existing" | jq --arg v "${2:-}" '.description = $v'); shift 2 ;; --type) existing=$(echo "$existing" | jq --arg v "${2:-}" '.type = $v'); shift 2 ;; --project) local project_id project_id=$(resolve_project_id "${2:-}") existing=$(echo "$existing" | jq --arg v "$project_id" '.projectId = $v') shift 2 ;; --assignee) local assignee_id assignee_id=$(resolve_assignee_id "${2:-}") existing=$(echo "$existing" | jq --arg v "$assignee_id" '.assigneeId = (if $v == "" then null else $v end)') shift 2 ;; --sprint) local sprint_input="${2:-}" local sprint_id sprint_id=$(resolve_sprint_id "$sprint_input") existing=$(echo "$existing" | jq --arg v "$sprint_id" '.sprintId = (if $v == "" then null else $v end)') shift 2 ;; --due-date) existing=$(echo "$existing" | jq --arg v "${2:-}" '.dueDate = (if $v == "" then null else $v end)'); shift 2 ;; --add-comment) add_comment="${2:-}"; shift 2 ;; --clear-tags) clear_tags=true; shift ;; --tags) set_tags="${2:-}"; shift 2 ;; *) shift ;; esac done if [[ "$clear_tags" == true ]]; then existing=$(echo "$existing" | jq '.tags = []') fi if [[ -n "$set_tags" ]]; then local tags_json tags_json=$(to_tag_array "$set_tags") existing=$(echo "$existing" | jq --argjson tags "$tags_json" '.tags = $tags') fi if [[ -n "$add_comment" ]]; then local comment comment=$(to_comment_array "$add_comment" | jq '.[0]') existing=$(echo "$existing" | jq --argjson c "$comment" '.comments = (.comments // []) + [$c]') fi # Use temporary file for large payloads to avoid "Argument list too long" errors local temp_file temp_file=$(mktemp) echo "{\"task\": $existing}" > "$temp_file" # Use curl directly with --data @file to avoid argument length limits local response response=$(curl -sS -X POST "${API_URL}/tasks" \ -H "Content-Type: application/json" \ -b "$COOKIE_FILE" -c "$COOKIE_FILE" \ --data @"$temp_file") # Clean up temp file rm -f "$temp_file" echo "$response" | jq . } delete_task() { local task_id="${1:-}" if [[ -z "$task_id" ]]; then log_error "Task ID required" exit 1 fi api_call DELETE "/tasks" "{\"id\": \"$task_id\"}" | jq . } bulk_create() { local file="${1:-}" if [[ -z "$file" || ! -f "$file" ]]; then log_error "bulk-create requires a valid JSON file" exit 1 fi jq -c '.[]' "$file" | while IFS= read -r row; do local title title=$(echo "$row" | jq -r '.title // empty') if [[ -z "$title" ]]; then log_warning "Skipping entry without title" continue fi local task_type status priority project sprint assignee due_date tags comments description task_type=$(echo "$row" | jq -r '.type // "task"') status=$(echo "$row" | jq -r '.status // "open"') priority=$(echo "$row" | jq -r '.priority // "medium"') project=$(echo "$row" | jq -r '.project // ""') sprint=$(echo "$row" | jq -r '.sprint // ""') assignee=$(echo "$row" | jq -r '.assignee // ""') due_date=$(echo "$row" | jq -r '.due_date // ""') tags=$(echo "$row" | jq -r '.tags // ""') comments=$(echo "$row" | jq -r '.comments // ""') description=$(echo "$row" | jq -r '.description // ""') create_task \ --title "$title" \ --description "$description" \ --type "$task_type" \ --status "$status" \ --priority "$priority" \ --project "$project" \ --sprint "$sprint" \ --assignee "$assignee" \ --due-date "$due_date" \ --tags "$tags" \ --comments "$comments" done } check_dependencies # Handle global flags before commands while [[ $# -gt 0 ]]; do case "${1:-}" in --no-color) RED='' GREEN='' YELLOW='' BLUE='' NC='' shift ;; --) shift break ;; *) break ;; esac done case "${1:-}" in list) shift list_tasks "$@" ;; get) get_task "${2:-}" ;; create) shift create_task "$@" ;; update) shift update_task "$@" ;; delete) delete_task "${2:-}" ;; current-sprint) shift get_current_sprint ;; bulk-create) shift bulk_create "${1:-}" ;; help|--help|-h|"") show_usage ;; *) log_error "Unknown command: ${1:-}" show_usage exit 1 ;; esac