gantt-board/scripts/gantt.sh

756 lines
19 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# Gantt Board Complete CLI
# All API operations available to the web UI
# Usage: ./gantt.sh <command> [args]
set -e
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
API_URL="${API_URL:-https://gantt-board.twisteddevices.com/api}"
COOKIE_FILE="${GANTT_COOKIE_FILE:-$HOME/.config/gantt-board/cookies.txt}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Helper functions
log_info() { echo -e "${BLUE}${NC} $1"; }
log_success() { echo -e "${GREEN}${NC} $1"; }
log_error() { echo -e "${RED}${NC} $1"; }
log_warn() { echo -e "${YELLOW}${NC} $1"; }
# Check dependencies
check_deps() {
if ! command -v jq &> /dev/null; then
log_error "jq is required but not installed. Run: brew install jq"
exit 1
fi
if ! command -v curl &> /dev/null; then
log_error "curl is required but not installed"
exit 1
fi
}
# Machine-to-machine API call (for cron/automation)
api_call_machine() {
local method="$1"
local endpoint="$2"
local data="${3:-}"
local token="${GANTT_MACHINE_TOKEN:-}"
if [ -z "$token" ]; then
log_error "GANTT_MACHINE_TOKEN not set"
return 1
fi
local url="${API_URL}${endpoint}"
local curl_opts=(-s -w "\n%{http_code}" -H "Content-Type: application/json" -H "Authorization: Bearer ${token}")
if [ -n "$data" ]; then
curl_opts+=(-d "$data")
fi
local response
response=$(curl "${curl_opts[@]}" -X "$method" "$url")
local http_code
http_code=$(echo "$response" | tail -n1)
local body
body=$(echo "$response" | sed '$d')
if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
echo "$body"
return 0
else
log_error "API call failed (HTTP $http_code)"
echo "$body" | jq '.' 2>/dev/null || echo "$body"
return 1
fi
}
# API call helper (cookie auth for interactive, machine token for automation)
api_call() {
local method="$1"
local endpoint="$2"
local data="${3:-}"
# Machine token path for automation/cron
if [ -n "${GANTT_MACHINE_TOKEN:-}" ]; then
api_call_machine "$method" "$endpoint" "$data"
return $?
fi
# Cookie auth path for interactive use
local url="${API_URL}${endpoint}"
mkdir -p "$(dirname "$COOKIE_FILE")"
touch "$COOKIE_FILE"
local curl_opts=(-s -w "\n%{http_code}" -H "Content-Type: application/json" -b "$COOKIE_FILE" -c "$COOKIE_FILE")
if [ -n "$data" ]; then
curl_opts+=(-d "$data")
fi
local response
response=$(curl "${curl_opts[@]}" -X "$method" "$url")
local http_code
http_code=$(echo "$response" | tail -n1)
local body
body=$(echo "$response" | sed '$d')
if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
echo "$body"
return 0
else
log_error "API call failed (HTTP $http_code)"
echo "$body" | jq '.' 2>/dev/null || echo "$body"
return 1
fi
}
#===================
# TASK OPERATIONS
#===================
cmd_task_list() {
local filter="${1:-}"
log_info "Fetching tasks..."
local response
response=$(api_call GET "/tasks")
if [ -n "$filter" ]; then
echo "$response" | jq --arg status "$filter" '.tasks | map(select(.status == $status))'
else
echo "$response" | jq '.tasks'
fi
}
cmd_task_get() {
local task_id="$1"
if [ -z "$task_id" ]; then
log_error "Usage: task get <task-id>"
exit 1
fi
log_info "Fetching task $task_id..."
local response
response=$(api_call GET "/tasks")
echo "$response" | jq --arg id "$task_id" '.tasks | map(select(.id == $id)) | .[0]'
}
cmd_task_create() {
local title="$1"
local status="${2:-open}"
local priority="${3:-medium}"
local project_id="${4:-1}"
if [ -z "$title" ]; then
log_error "Usage: task create <title> [status] [priority] [project-id]"
exit 1
fi
log_info "Creating task: $title"
local task_json
task_json=$(jq -n \
--arg title "$title" \
--arg status "$status" \
--arg priority "$priority" \
--arg projectId "$project_id" \
'{
title: $title,
status: $status,
priority: $priority,
projectId: $projectId,
type: "task",
comments: [],
tags: []
}')
api_call POST "/tasks" "{\"task\": $task_json}"
}
cmd_task_natural() {
local text="$1"
if [ -z "$text" ]; then
log_error "Usage: task natural <text-description>"
echo "Example: ./gantt.sh task natural \"Fix login bug by Friday, high priority\""
exit 1
fi
log_info "Creating task from natural language..."
api_call POST "/tasks/natural" "{\"text\": \"$text\"}"
}
cmd_task_update() {
local task_id="$1"
local field="$2"
local value="$3"
if [ -z "$task_id" ] || [ -z "$field" ]; then
log_error "Usage: task update <task-id> <field> <value>"
echo "Fields: status, priority, title, description, assigneeId, sprintId, dueDate"
exit 1
fi
log_info "Updating task $task_id: $field = $value"
# First get the task
local response
response=$(api_call GET "/tasks")
local task
task=$(echo "$response" | jq --arg id "$task_id" '.tasks | map(select(.id == $id)) | .[0]')
if [ "$task" = "null" ] || [ -z "$task" ]; then
log_error "Task not found: $task_id"
exit 1
fi
# Update the field
local updated_task
updated_task=$(echo "$task" | jq --arg field "$field" --arg value "$value" '. + {($field): $value}')
api_call POST "/tasks" "{\"task\": $updated_task}"
}
cmd_task_delete() {
local task_id="$1"
if [ -z "$task_id" ]; then
log_error "Usage: task delete <task-id>"
exit 1
fi
log_warn "Deleting task $task_id..."
api_call DELETE "/tasks" "{\"id\": \"$task_id\"}"
}
cmd_task_comment() {
local task_id="$1"
local text="$2"
if [ -z "$task_id" ] || [ -z "$text" ]; then
log_error "Usage: task comment <task-id> <text>"
exit 1
fi
log_info "Adding comment to task $task_id..."
# Get current task
local response
response=$(api_call GET "/tasks")
local task
task=$(echo "$response" | jq --arg id "$task_id" '.tasks | map(select(.id == $id)) | .[0]')
if [ "$task" = "null" ]; then
log_error "Task not found: $task_id"
exit 1
fi
# Add comment
local comment_id
comment_id=$(date +%s)
local new_comment
new_comment=$(jq -n \
--arg id "$comment_id" \
--arg text "$text" \
--arg createdAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{id: $id, text: $text, createdAt: $createdAt, commentAuthorId: "assistant", replies: []}')
local updated_task
updated_task=$(echo "$task" | jq --argjson comment "$new_comment" '.comments += [$comment]')
api_call POST "/tasks" "{\"task\": $updated_task}"
}
cmd_task_attach() {
local task_id="$1"
local file_path="$2"
if [ -z "$task_id" ] || [ -z "$file_path" ]; then
log_error "Usage: task attach <task-id> <file-path>"
exit 1
fi
if [ ! -f "$file_path" ]; then
log_error "File not found: $file_path"
exit 1
fi
log_info "Attaching file to task $task_id..."
# Get current task
local response
response=$(api_call GET "/tasks")
local task
task=$(echo "$response" | jq --arg id "$task_id" '.tasks | map(select(.id == $id)) | .[0]')
if [ "$task" = "null" ]; then
log_error "Task not found: $task_id"
exit 1
fi
# Create attachment
local filename
filename=$(basename "$file_path")
local mime_type
case "${filename##*.}" in
md|markdown) mime_type="text/markdown" ;;
txt) mime_type="text/plain" ;;
json) mime_type="application/json" ;;
pdf) mime_type="application/pdf" ;;
png) mime_type="image/png" ;;
jpg|jpeg) mime_type="image/jpeg" ;;
gif) mime_type="image/gif" ;;
*) mime_type="application/octet-stream" ;;
esac
local base64_content
base64_content=$(base64 -i "$file_path" | tr -d '\n')
local data_url="data:$mime_type;base64,$base64_content"
local attachment
attachment=$(jq -n \
--arg id "$(date +%s)" \
--arg name "$filename" \
--arg type "$mime_type" \
--argjson size "$(stat -f%z "$file_path" 2>/dev/null || stat -c%s "$file_path")" \
--arg dataUrl "$data_url" \
--arg uploadedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{id: $id, name: $name, type: $type, size: $size, dataUrl: $dataUrl, uploadedAt: $uploadedAt}')
local updated_task
updated_task=$(echo "$task" | jq --argjson att "$attachment" '.attachments = (.attachments // []) + [$att]')
api_call POST "/tasks" "{\"task\": $updated_task}"
}
#===================
# PROJECT OPERATIONS
#===================
cmd_project_list() {
log_info "Fetching projects..."
api_call GET "/projects" | jq '.projects'
}
cmd_project_get() {
local project_id="$1"
if [ -z "$project_id" ]; then
log_error "Usage: project get <project-id>"
exit 1
fi
log_info "Fetching project $project_id..."
api_call GET "/projects/$project_id" | jq '.project'
}
cmd_project_create() {
local name="$1"
local description="${2:-}"
local color="${3:-#3b82f6}"
if [ -z "$name" ]; then
log_error "Usage: project create <name> [description] [color]"
exit 1
fi
log_info "Creating project: $name"
local data
data=$(jq -n --arg name "$name" --arg desc "$description" --arg color "$color" \
'{name: $name, description: (if $desc == "" then null else $desc end), color: $color}')
api_call POST "/projects" "$data"
}
cmd_project_update() {
local project_id="$1"
local field="$2"
local value="$3"
if [ -z "$project_id" ] || [ -z "$field" ]; then
log_error "Usage: project update <id> <field> <value>"
echo "Fields: name, description, color"
exit 1
fi
log_info "Updating project $project_id: $field = $value"
local data
data=$(jq -n --arg id "$project_id" --arg field "$field" --arg value "$value" \
'{id: $id, ($field): $value}')
api_call PATCH "/projects" "$data"
}
cmd_project_delete() {
local project_id="$1"
if [ -z "$project_id" ]; then
log_error "Usage: project delete <id>"
exit 1
fi
log_warn "Deleting project $project_id..."
api_call DELETE "/projects" "{\"id\": \"$project_id\"}"
}
#===================
# SPRINT OPERATIONS
#===================
cmd_sprint_list() {
log_info "Fetching sprints..."
api_call GET "/sprints" | jq '.sprints'
}
cmd_sprint_get() {
local sprint_id="$1"
if [ -z "$sprint_id" ]; then
log_error "Usage: sprint get <sprint-id>"
exit 1
fi
log_info "Fetching sprint $sprint_id..."
api_call GET "/sprints/$sprint_id" | jq '.sprint'
}
cmd_sprint_create() {
local name="$1"
local start_date="${2:-}"
local end_date="${3:-}"
local goal="${4:-}"
if [ -z "$name" ]; then
log_error "Usage: sprint create <name> [start-date] [end-date] [goal]"
exit 1
fi
log_info "Creating sprint: $name"
local data
data=$(jq -n \
--arg name "$name" \
--arg startDate "$start_date" \
--arg endDate "$end_date" \
--arg goal "$goal" \
'{
name: $name,
startDate: (if $startDate == "" then null else $startDate end),
endDate: (if $endDate == "" then null else $endDate end),
goal: (if $goal == "" then null else $goal end)
}')
api_call POST "/sprints" "$data"
}
cmd_sprint_update() {
local sprint_id="$1"
local field="$2"
local value="$3"
if [ -z "$sprint_id" ] || [ -z "$field" ]; then
log_error "Usage: sprint update <id> <field> <value>"
echo "Fields: name, goal, startDate, endDate"
exit 1
fi
if [ "$field" = "status" ]; then
log_error "Sprint status is date-derived. Update startDate/endDate instead."
exit 1
fi
log_info "Updating sprint $sprint_id: $field = $value"
local data
data=$(jq -n --arg id "$sprint_id" --arg field "$field" --arg value "$value" \
'{id: $id, ($field): $value}')
api_call PATCH "/sprints" "$data"
}
cmd_sprint_delete() {
local sprint_id="$1"
if [ -z "$sprint_id" ]; then
log_error "Usage: sprint delete <id>"
exit 1
fi
log_warn "Deleting sprint $sprint_id..."
api_call DELETE "/sprints" "{\"id\": \"$sprint_id\"}"
}
cmd_sprint_close() {
local sprint_id="$1"
if [ -z "$sprint_id" ]; then
log_error "Usage: sprint close <id>"
exit 1
fi
log_info "Closing sprint $sprint_id..."
api_call POST "/sprints/close" "{\"id\": \"$sprint_id\"}"
}
#===================
# AUTH OPERATIONS
#===================
cmd_auth_login() {
local email="$1"
local password="$2"
if [ -z "$email" ] || [ -z "$password" ]; then
log_error "Usage: auth login <email> <password>"
exit 1
fi
log_info "Logging in..."
api_call POST "/auth/login" "{\"email\": \"$email\", \"password\": \"$password\"}"
}
cmd_auth_logout() {
log_info "Logging out..."
api_call POST "/auth/logout" "{}"
}
cmd_auth_session() {
log_info "Checking session..."
api_call GET "/auth/session"
}
cmd_auth_register() {
local email="$1"
local password="$2"
local name="${3:-}"
if [ -z "$email" ] || [ -z "$password" ]; then
log_error "Usage: auth register <email> <password> [name]"
exit 1
fi
log_info "Registering user..."
local data
data=$(jq -n --arg email "$email" --arg password "$password" --arg name "$name" \
'{email: $email, password: $password, name: (if $name == "" then null else $name end)}')
api_call POST "/auth/register" "$data"
}
cmd_auth_forgot_password() {
local email="$1"
if [ -z "$email" ]; then
log_error "Usage: auth forgot-password <email>"
exit 1
fi
log_info "Requesting password reset..."
api_call POST "/auth/forgot-password" "{\"email\": \"$email\"}"
}
cmd_auth_reset_password() {
local token="$1"
local password="$2"
if [ -z "$token" ] || [ -z "$password" ]; then
log_error "Usage: auth reset-password <token> <new-password>"
exit 1
fi
log_info "Resetting password..."
api_call POST "/auth/reset-password" "{\"token\": \"$token\", \"password\": \"$password\"}"
}
cmd_auth_account() {
local field="$1"
local value="$2"
if [ -z "$field" ] || [ -z "$value" ]; then
log_error "Usage: auth account <field> <value>"
echo "Fields: name, email"
exit 1
fi
log_info "Updating account $field..."
local data
data=$(jq -n --arg field "$field" --arg value "$value" '{($field): $value}')
api_call PATCH "/auth/account" "$data"
}
cmd_auth_users() {
log_info "Fetching users..."
api_call GET "/auth/users"
}
#===================
# DEBUG OPERATIONS
#===================
cmd_debug() {
log_info "Calling debug endpoint..."
api_call GET "/debug"
}
#===================
# HELP
#===================
show_help() {
cat << 'EOF'
Gantt Board CLI - Complete API Access
USAGE:
./gantt.sh <command> [subcommand] [args]
TASK COMMANDS:
task list [status] List all tasks (optionally filter by status)
task get <id> Get specific task details
task create <title> [status] [priority] [project-id]
Create a new task
task natural <text> Create task from natural language
Example: "Fix bug by Friday, high priority"
task update <id> <field> <val> Update task field
Fields: status, priority, title, description,
assigneeId, sprintId, dueDate
task delete <id> Delete a task
task comment <id> <text> Add a comment to a task
task attach <id> <file> Attach a file to a task
PROJECT COMMANDS:
project list List all projects
project get <id> Get specific project
project create <name> [desc] [color]
Create new project
project update <id> <field> <val>
Update project field
Fields: name, description, color
project delete <id> Delete a project
SPRINT COMMANDS:
sprint list List all sprints
sprint get <id> Get specific sprint
sprint create <name> [start] [end] [goal]
Create new sprint
sprint update <id> <field> <val>
Update sprint field
Fields: name, goal, startDate, endDate
sprint close <id> Close a sprint
sprint delete <id> Delete a sprint
AUTH COMMANDS:
auth login <email> <pass> Log in
auth logout Log out
auth session Check current session
auth register <email> <pass> Register new account
auth forgot-password <email> Request password reset
auth reset-password <tok> <pass> Reset password with token
auth account <field> <value> Update account (name, email)
auth users List all users
OTHER COMMANDS:
debug Call debug endpoint
help Show this help message
EXAMPLES:
# List open tasks
./gantt.sh task list open
# Create a task naturally
./gantt.sh task natural "Research TTS options by tomorrow, medium priority"
# Update task status
./gantt.sh task update abc-123 status done
# Add comment
./gantt.sh task comment abc-123 "Working on this now"
# Attach file
./gantt.sh task attach abc-123 ./notes.md
ENVIRONMENT:
API_URL Override the API base URL (default: http://localhost:3000/api)
EOF
}
#===================
# MAIN
#===================
main() {
check_deps
local cmd="${1:-help}"
shift || true
case "$cmd" in
task)
local subcmd="${1:-list}"
shift || true
case "$subcmd" in
list|ls) cmd_task_list "$@" ;;
get|show) cmd_task_get "$@" ;;
create|new|add) cmd_task_create "$@" ;;
natural|parse) cmd_task_natural "$@" ;;
update|set|edit) cmd_task_update "$@" ;;
delete|rm|remove) cmd_task_delete "$@" ;;
comment|note) cmd_task_comment "$@" ;;
attach|file) cmd_task_attach "$@" ;;
*) log_error "Unknown task command: $subcmd"; show_help; exit 1 ;;
esac
;;
project|projects)
local subcmd="${1:-list}"
shift || true
case "$subcmd" in
list|ls) cmd_project_list "$@" ;;
get|show) cmd_project_get "$@" ;;
create|new|add) cmd_project_create "$@" ;;
update|set|edit) cmd_project_update "$@" ;;
delete|rm|remove) cmd_project_delete "$@" ;;
*) log_error "Unknown project command: $subcmd"; show_help; exit 1 ;;
esac
;;
sprint|sprints)
local subcmd="${1:-list}"
shift || true
case "$subcmd" in
list|ls) cmd_sprint_list "$@" ;;
get|show) cmd_sprint_get "$@" ;;
create|new|add) cmd_sprint_create "$@" ;;
update|set|edit) cmd_sprint_update "$@" ;;
close|complete) cmd_sprint_close "$@" ;;
delete|rm|remove) cmd_sprint_delete "$@" ;;
*) log_error "Unknown sprint command: $subcmd"; show_help; exit 1 ;;
esac
;;
auth)
local subcmd="${1:-session}"
shift || true
case "$subcmd" in
login) cmd_auth_login "$@" ;;
logout) cmd_auth_logout "$@" ;;
session|whoami) cmd_auth_session "$@" ;;
register|signup) cmd_auth_register "$@" ;;
forgot-password|forgot) cmd_auth_forgot_password "$@" ;;
reset-password|reset) cmd_auth_reset_password "$@" ;;
account|profile) cmd_auth_account "$@" ;;
users|list-users) cmd_auth_users "$@" ;;
*) log_error "Unknown auth command: $subcmd"; show_help; exit 1 ;;
esac
;;
debug) cmd_debug "$@" ;;
help|--help|-h) show_help ;;
*) log_error "Unknown command: $cmd"; show_help; exit 1 ;;
esac
}
main "$@"