#!/bin/bash # Gantt Board Complete CLI # All API operations available to the web UI # Usage: ./gantt.sh [args] set -e # Configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" API_URL="${API_URL:-http://localhost:3000/api}" # 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 } # API call helper api_call() { local method="$1" local endpoint="$2" local data="${3:-}" local url="${API_URL}${endpoint}" local curl_opts=(-s -w "\n%{http_code}" -H "Content-Type: application/json") 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 " 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 [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, author: "assistant"}') 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 project_id="$2" local start_date="${3:-}" local end_date="${4:-}" local goal="${5:-}" if [ -z "$name" ]; then log_error "Usage: sprint create <name> <project-id> [start-date] [end-date] [goal]" exit 1 fi log_info "Creating sprint: $name" local data data=$(jq -n \ --arg name "$name" \ --arg projectId "$project_id" \ --arg startDate "$start_date" \ --arg endDate "$end_date" \ --arg goal "$goal" \ '{ name: $name, projectId: $projectId, startDate: (if $startDate == "" then null else $startDate end), endDate: (if $endDate == "" then null else $endDate end), goal: (if $goal == "" then null else $goal end), status: "planning" }') 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, status, projectId" 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\"}" } #=================== # 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" } #=================== # USER ADMIN OPERATIONS #=================== cmd_user_get() { local user_id="$1" if [ -z "$user_id" ]; then log_error "Usage: user get <user-id>" exit 1 fi log_info "Fetching user $user_id..." api_call GET "/users?id=$user_id" | jq '.user' } cmd_user_update() { local user_id="$1" local field="$2" local value="$3" if [ -z "$user_id" ] || [ -z "$field" ] || [ -z "$value" ]; then log_error "Usage: user update <user-id> <field> <value>" echo "Fields: name, email, avatar_url" exit 1 fi log_info "Updating user $user_id: $field = $value" local data data=$(jq -n --arg id "$user_id" --arg field "$field" --arg value "$value" \ '{id: $id, ($field): $value}') api_call PATCH "/users" "$data" } cmd_user_delete() { local user_id="$1" if [ -z "$user_id" ]; then log_error "Usage: user delete <user-id>" exit 1 fi log_warn "Deleting user $user_id..." api_call DELETE "/users" "{\"id\": \"$user_id\"}" } #=================== # META OPERATIONS #=================== cmd_meta_list() { log_info "Fetching meta entries..." api_call GET "/meta" | jq '.meta' } cmd_meta_get() { local key="$1" if [ -z "$key" ]; then log_error "Usage: meta get <key>" exit 1 fi log_info "Fetching meta key: $key" api_call GET "/meta?key=$key" | jq '.meta | .[0]' } cmd_meta_set() { local key="$1" local value="$2" if [ -z "$key" ] || [ -z "$value" ]; then log_error "Usage: meta set <key> <value>" exit 1 fi log_info "Setting meta $key = $value" api_call POST "/meta" "{\"key\": \"$key\", \"value\": \"$value\"}" } cmd_meta_delete() { local key="$1" if [ -z "$key" ]; then log_error "Usage: meta delete <key>" exit 1 fi log_warn "Deleting meta key: $key" api_call DELETE "/meta" "{\"key\": \"$key\"}" } #=================== # 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> <project-id> [start] [end] [goal] Create new sprint sprint update <id> <field> <val> Update sprint field Fields: name, goal, startDate, endDate, status, projectId 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 USER ADMIN COMMANDS: user list List all users (same as auth users) user get <user-id> Get specific user user update <id> <field> <val> Update user field Fields: name, email, avatar_url user delete <user-id> Delete a user META COMMANDS: meta list List all meta entries meta get <key> Get specific meta value meta set <key> <value> Set meta key-value pair meta delete <key> Delete meta entry 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 "$@" ;; 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 ;; user|users) local subcmd="${1:-list}" shift || true case "$subcmd" in list|ls) cmd_auth_users "$@" ;; # Reuse auth users list get|show) cmd_user_get "$@" ;; update|set|edit) cmd_user_update "$@" ;; delete|rm|remove) cmd_user_delete "$@" ;; *) log_error "Unknown user command: $subcmd"; show_help; exit 1 ;; esac ;; meta) local subcmd="${1:-list}" shift || true case "$subcmd" in list|ls) cmd_meta_list "$@" ;; get|show) cmd_meta_get "$@" ;; set|create|update) cmd_meta_set "$@" ;; delete|rm|remove) cmd_meta_delete "$@" ;; *) log_error "Unknown meta 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 "$@"