- Add api_call_machine() function for GANTT_MACHINE_TOKEN auth - Update api_call() to use machine token when available - Same pattern applied to both api_client.sh and gantt.sh - Allows cron jobs to authenticate without cookie-based login - No breaking changes - cookie auth still works for interactive use
601 lines
16 KiB
Bash
Executable File
601 lines
16 KiB
Bash
Executable File
#!/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"
|
|
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
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 [list|get|create|update|delete|current-sprint|bulk-create] [args...]
|
|
|
|
COMMANDS:
|
|
list [status] List tasks (supports filters)
|
|
get <task-id> Get specific task
|
|
create "Title" [status] [priority] [project] [assignee] [sprint] [type]
|
|
create --title "Title" [flags...] Create task using flags
|
|
update <task-id> <field> <value> Update one field (legacy)
|
|
update <task-id> [flags...] Update task with flags
|
|
delete <task-id> Delete task
|
|
current-sprint Show current sprint ID
|
|
bulk-create <json-file> 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
|
|
|
|
api_call POST "/tasks" "{\"task\": $existing}" | 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
|
|
|
|
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
|