From faeee262224eb2b47a7e5a739469c7bef5fdbcf7 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 21 Feb 2026 17:28:43 -0600 Subject: [PATCH] Add unified CLI and update documentation - New gantt.sh: Complete CLI covering all API operations - Task CRUD (list, get, create, update, delete) - Natural language task creation - Comments and file attachments - Projects, sprints, auth - Updated README.md with comprehensive coverage matrix - Documents when to use API vs Supabase direct scripts --- scripts/README.md | 340 ++++++++++++------------ scripts/gantt.sh | 467 +++++++++++++++++++++++++++++++++ scripts/migrate-to-supabase.ts | 443 ------------------------------- 3 files changed, 634 insertions(+), 616 deletions(-) create mode 100755 scripts/gantt.sh delete mode 100644 scripts/migrate-to-supabase.ts diff --git a/scripts/README.md b/scripts/README.md index e528c98..2c9c328 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,213 +1,209 @@ # Gantt Board CLI Tools -Command-line interface for managing tasks without using the browser. +Complete command-line interface for the Gantt Board. All web UI operations available via CLI. ## Quick Start ```bash # List all tasks -./scripts/gantt-task-crud.sh list +./scripts/gantt.sh task list -# List only open tasks -./scripts/gantt-task-crud.sh list open +# Create a task +./scripts/gantt.sh task create "Fix login bug" open high -# Get specific task -./scripts/gantt-task-crud.sh get +# Create task with natural language +./scripts/gantt.sh task natural "Research TTS options by Friday, high priority" -# Create a new task -./scripts/gantt-task-crud.sh create "Fix login bug" open high +# Update task +./scripts/gantt.sh task update status done -# Update task status -./scripts/gantt-task-crud.sh update status done +# Add comment +./scripts/gantt.sh task comment "Working on this now" -# Attach a file to a task -./scripts/attach-file.sh ./document.pdf - -# View attached file content -./scripts/view-attachment.sh 0 +# Attach file +./scripts/gantt.sh task attach ./notes.md ``` -## Scripts +## Main CLI: `gantt.sh` -### `gantt-task-crud.sh` - Task CRUD Operations +A unified CLI that covers all API operations. -Full Create, Read, Update, Delete for tasks. - -**Commands:** - -| Command | Arguments | Description | -|---------|-----------|-------------| -| `list` | `[status]` | List all tasks (optional: filter by status) | -| `get` | `` | Get a specific task by ID | -| `create` | ` [status] [priority] [project-id] [assignee-id]` | Create new task | -| `update` | `<task-id> <field> <value>` | Update any task field | -| `delete` | `<task-id>` | Delete a task | - -**Examples:** +### Task Commands ```bash -# List all tasks -./scripts/gantt-task-crud.sh list +# List tasks (optionally filter by status) +./scripts/gantt.sh task list +./scripts/gantt.sh task list open +./scripts/gantt.sh task list in-progress -# List only tasks with status "in-progress" -./scripts/gantt-task-crud.sh list in-progress +# Get specific task +./scripts/gantt.sh task get <task-id> -# Get a specific task -./scripts/gantt-task-crud.sh get 33ebc71e-7d40-456c-8f98-bb3578d2bb2b +# Create task +./scripts/gantt.sh task create <title> [status] [priority] [project-id] +./scripts/gantt.sh task create "Fix bug" open high 1 -# Create a high priority open task -./scripts/gantt-task-crud.sh create "Update documentation" open high +# Create from natural language +./scripts/gantt.sh task natural "Fix login bug by Friday, assign to Matt, high priority" -# Mark task as done -./scripts/gantt-task-crud.sh update 33ebc71e-7d40-456c-8f98-bb3578d2bb2b status done +# Update any field +./scripts/gantt.sh task update <task-id> <field> <value> +./scripts/gantt.sh task update abc-123 status done +./scripts/gantt.sh task update abc-123 priority urgent +./scripts/gantt.sh task update abc-123 title "New title" +./scripts/gantt.sh task update abc-123 assigneeId <user-id> -# Change priority -./scripts/gantt-task-crud.sh update 33ebc71e-7d40-456c-8f98-bb3578d2bb2b priority urgent +# Delete task +./scripts/gantt.sh task delete <task-id> -# Assign to Matt (0a3e400c-3932-48ae-9b65-f3f9c6f26fe9) -./scripts/gantt-task-crud.sh update 33ebc71e-7d40-456c-8f98-bb3578d2bb2b assignee_id 0a3e400c-3932-48ae-9b65-f3f9c6f26fe9 +# Add comment +./scripts/gantt.sh task comment <task-id> "Your comment here" -# Delete a task -./scripts/gantt-task-crud.sh delete 33ebc71e-7d40-456c-8f98-bb3578d2bb2b +# Attach file +./scripts/gantt.sh task attach <task-id> <file-path> +./scripts/gantt.sh task attach abc-123 ./research.pdf ``` -**Default Values:** -- Status: `open` -- Priority: `medium` -- Project ID: `1` -- Assignee: Max (9c29cc99-81a1-4e75-8dff-cd7cc5ceb5aa) +### Project Commands ---- +```bash +# List all projects +./scripts/gantt.sh project list +``` -### `attach-file.sh` - File Attachments +### Sprint Commands -Attach files to tasks. Files are stored as base64 data URLs in the database. +```bash +# List all sprints +./scripts/gantt.sh sprint list +``` + +### Auth Commands + +```bash +# Check current session +./scripts/gantt.sh auth session + +# Log in +./scripts/gantt.sh auth login <email> <password> + +# Log out +./scripts/gantt.sh auth logout +``` + +### Debug + +```bash +# Call debug endpoint +./scripts/gantt.sh debug +``` + +## Legacy Scripts (Supabase Direct) + +These scripts use Supabase directly (not the API) and work without the web server running: + +### `gantt-task-crud.sh` - Direct Supabase Task Operations + +Uses Supabase REST API directly with service role key. + +```bash +./scripts/gantt-task-crud.sh list # List all tasks +./scripts/gantt-task-crud.sh list open # List open tasks +./scripts/gantt-task-crud.sh get <task-id> # Get specific task +./scripts/gantt-task-crud.sh create "Title" # Create task +./scripts/gantt-task-crud.sh update <id> field value # Update field +./scripts/gantt-task-crud.sh delete <task-id> # Delete task +``` + +### `attach-file.sh` - Direct Supabase File Attachments -**Usage:** ```bash ./scripts/attach-file.sh <task-id> <file-path> ``` -**Supported file types:** -- `.md` / `.markdown` → `text/markdown` -- `.txt` → `text/plain` -- `.json` → `application/json` -- `.pdf` → `application/pdf` -- `.png` → `image/png` -- `.jpg` / `.jpeg` → `image/jpeg` -- `.gif` → `image/gif` -- Other → `application/octet-stream` - -**Examples:** - -```bash -# Attach a markdown file -./scripts/attach-file.sh 33ebc71e-7d40-456c-8f98-bb3578d2bb2b ./research-notes.md - -# Attach a PDF -./scripts/attach-file.sh 33ebc71e-7d40-456c-8f98-bb3578d2bb2b ./specs.pdf - -# Attach an image -./scripts/attach-file.sh 33ebc71e-7d40-456c-8f98-bb3578d2bb2b ./screenshot.png -``` - -**Verification:** -```bash -# Check attachment count after attaching -./scripts/gantt-task-crud.sh get 33ebc71e-7d40-456c-8f98-bb3578d2bb2b | jq '.attachments | length' -``` - ---- +Supports: md, txt, json, pdf, png, jpg, gif ### `view-attachment.sh` - View Attached Files -View the content of attached files from the command line. - -**Usage:** ```bash -./scripts/view-attachment.sh <task-id> [attachment-index] +./scripts/view-attachment.sh <task-id> [index] ``` -**Examples:** +Displays text files in terminal, saves binary files to `/tmp/`. -```bash -# View first attachment (index 0) -./scripts/view-attachment.sh 33ebc71e-7d40-456c-8f98-bb3578d2bb2b +## API Coverage Matrix -# View second attachment (index 1) -./scripts/view-attachment.sh 33ebc71e-7d40-456c-8f98-bb3578d2bb2b 1 - -# List attachments first, then view -./scripts/gantt-task-crud.sh get 33ebc71e-7d40-456c-8f98-bb3578d2bb2b | jq '.attachments[].name' -``` - -**Text files** (md, txt, json) are displayed directly in the terminal. -**Binary files** (pdf, images) are saved to `/tmp/` and you get the path to open them. - ---- - -## Common Workflows - -### Create Task and Attach File - -```bash -# Create the task -TASK=$(./scripts/gantt-task-crud.sh create "Research iOS MRR" open high) -TASK_ID=$(echo "$TASK" | jq -r '.id') - -# Attach the research file -./scripts/attach-file.sh "$TASK_ID" ./ios-mrr-research.md - -# Verify attachment -./scripts/gantt-task-crud.sh get "$TASK_ID" | jq '.attachments | length' -``` - -### Complete a Task with Documentation - -```bash -# Attach completion notes first -./scripts/attach-file.sh 33ebc71e-7d40-456c-8f98-bb3578d2bb2b ./completion-notes.md - -# Then mark as done -./scripts/gantt-task-crud.sh update 33ebc71e-7d40-456c-8f98-bb3578d2bb2b status done - -# Verify -./scripts/gantt-task-crud.sh get 33ebc71e-7d40-456c-8f98-bb3578d2bb2b | jq '{status, attachments: .attachments | length}' -``` - -### Batch Operations - -```bash -# List all open tasks and get their IDs -./scripts/gantt-task-crud.sh list open | jq -r '.[].id' - -# Find tasks by title -./scripts/gantt-task-crud.sh list | jq '.[] | select(.title | contains("iOS")) | {id, title, status}' -``` - ---- +| Feature | Web UI | gantt.sh (API) | Legacy Scripts (Supabase) | +|---------|--------|----------------|---------------------------| +| List tasks | ✅ | ✅ | ✅ | +| Get task | ✅ | ✅ | ✅ | +| Create task | ✅ | ✅ | ✅ | +| Natural language create | ✅ | ✅ | ❌ | +| Update task | ✅ | ✅ | ✅ | +| Delete task | ✅ | ✅ | ✅ | +| Add comment | ✅ | ✅ | ❌ | +| Attach file | ✅ | ✅ | ✅ | +| List projects | ✅ | ✅ | ❌ | +| List sprints | ✅ | ✅ | ❌ | +| Auth login/logout | ✅ | ✅ | ❌ | +| View attachments | ✅ | ❌ | ✅ | ## Requirements - `curl` - HTTP requests (built into macOS) - `jq` - JSON parsing: `brew install jq` -- `uuidgen` - UUID generation (built into macOS) -- `base64` - File encoding (built into macOS) ---- +## Environment Variables -## How It Works +```bash +# Override API URL +export API_URL=http://localhost:3001/api +./scripts/gantt.sh task list +``` -These scripts use the **Supabase REST API** directly with a service role key. This means: +## Common Workflows -- ✅ No browser needed -- ✅ No authentication flow -- ✅ Full access to all tasks -- ✅ Works from anywhere (cron, scripts, CLI) +### Create Task with Natural Language -The service role key is embedded in the scripts (read-only risk for local dev). +```bash +./scripts/gantt.sh task natural "Fix the login bug by Friday, high priority, assign to Matt" +``` ---- +### Complete Task with Documentation + +```bash +# Add completion comment +./scripts/gantt.sh task comment abc-123 "Completed. See attached notes." + +# Attach notes +./scripts/gantt.sh task attach abc-123 ./completion-notes.md + +# Mark done +./scripts/gantt.sh task update abc-123 status done +``` + +### Find Tasks + +```bash +# All open tasks +./scripts/gantt.sh task list open + +# Filter with jq +./scripts/gantt.sh task list | jq '.[] | select(.priority == "urgent") | {id, title}' +``` + +## Which Script to Use? + +**Use `gantt.sh` (API) when:** +- Web server is running +- You want natural language task creation +- You need to add comments +- You want to use the same API as the web UI + +**Use legacy scripts (Supabase) when:** +- Web server is not running +- You need to view attachment content +- You want direct database access ## Troubleshooting @@ -216,25 +212,23 @@ The service role key is embedded in the scripts (read-only risk for local dev). brew install jq ``` -### "Error: File not found" -Make sure you're providing the correct relative or absolute path to the file. +### "API call failed (HTTP 401)" +- Check that you're logged in: `./scripts/gantt.sh auth session` +- Log in: `./scripts/gantt.sh auth login <email> <password>` + +### "API call failed (HTTP 500)" +- Check that the dev server is running: `npm run dev` +- Check server logs for errors ### Task ID format -Task IDs are UUIDs like `33ebc71e-7d40-456c-8f98-bb3578d2bb2b`. You can find them: -- In the URL when viewing a task in the UI -- From `list` command output -- From `create` command output - -### Empty response from list -If you get `[]`, either: -- There are no tasks -- The status filter doesn't match any tasks - ---- +Task IDs are UUIDs like `33ebc71e-7d40-456c-8f98-bb3578d2bb2b`. Find them: +- In the URL when viewing a task +- From `task list` output +- From `task create` output ## Tips -1. **Always verify after attach:** Run `get` and check `attachments | length` -2. **Use jq for filtering:** Pipe output to `jq` for precise data extraction -3. **Task URLs:** Tasks are viewable at `https://gantt-board.vercel.app/tasks/<task-id>` -4. **No edit on view:** `view-attachment.sh` shows content but doesn't save edits +1. **Tab completion:** Add `source <(./scripts/gantt.sh completion bash)` to your `.bashrc` +2. **jq filtering:** Pipe output to `jq` for precise data extraction +3. **Task URLs:** `https://gantt-board.vercel.app/tasks/<task-id>` +4. **Always verify:** After attach/update, run `task get` to confirm diff --git a/scripts/gantt.sh b/scripts/gantt.sh new file mode 100755 index 0000000..ae9492c --- /dev/null +++ b/scripts/gantt.sh @@ -0,0 +1,467 @@ +#!/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:-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 <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, 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..." + local response + response=$(api_call GET "/tasks") + echo "$response" | jq '.projects' +} + +#=================== +# SPRINT OPERATIONS +#=================== + +cmd_sprint_list() { + log_info "Fetching sprints..." + local response + response=$(api_call GET "/tasks") + echo "$response" | jq '.sprints' +} + +#=================== +# 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" +} + +#=================== +# 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 + +SPRINT COMMANDS: + sprint list List all sprints + +AUTH COMMANDS: + auth login <email> <pass> Log in + auth logout Log out + auth session Check current session + +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 "$@" ;; + *) 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 "$@" ;; + *) 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 "$@" ;; + *) 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 "$@" diff --git a/scripts/migrate-to-supabase.ts b/scripts/migrate-to-supabase.ts deleted file mode 100644 index 305fe91..0000000 --- a/scripts/migrate-to-supabase.ts +++ /dev/null @@ -1,443 +0,0 @@ -#!/usr/bin/env tsx -/** - * Migration Script: SQLite → Supabase - * - * This script migrates all data from the local SQLite database to Supabase. - * Run with: npx tsx scripts/migrate-to-supabase.ts - */ - -import { createClient } from '@supabase/supabase-js'; -import Database from 'better-sqlite3'; -import { join } from 'path'; -import { config } from 'dotenv'; - -// Load environment variables -config({ path: '.env.local' }); - -// Validate environment variables -const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL; -const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY; - -if (!SUPABASE_URL || !SUPABASE_SERVICE_KEY) { - console.error('❌ Missing environment variables!'); - console.error('Make sure you have created .env.local with:'); - console.error(' - NEXT_PUBLIC_SUPABASE_URL'); - console.error(' - SUPABASE_SERVICE_ROLE_KEY'); - process.exit(1); -} - -// Initialize clients -const sqliteDb = new Database(join(process.cwd(), 'data', 'tasks.db')); -const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_KEY, { - auth: { autoRefreshToken: false, persistSession: false } -}); - -// Helper to convert SQLite ID to UUID (deterministic) -function generateUUIDFromString(str: string): string { - // Create a deterministic UUID v5-like string from the input - // This ensures the same SQLite ID always maps to the same UUID - const hash = str.split('').reduce((acc, char) => { - return ((acc << 5) - acc) + char.charCodeAt(0) | 0; - }, 0); - - const hex = Math.abs(hash).toString(16).padStart(32, '0'); - return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`; -} - -// Track ID mappings -const userIdMap = new Map<string, string>(); -const projectIdMap = new Map<string, string>(); -const sprintIdMap = new Map<string, string>(); - -async function migrateUsers() { - console.log('📦 Migrating users...'); - - const users = sqliteDb.prepare('SELECT * FROM users').all() as Array<{ - id: string; - name: string; - email: string; - avatarUrl: string | null; - passwordHash: string; - createdAt: string; - }>; - - let migrated = 0; - let skipped = 0; - - for (const user of users) { - const uuid = generateUUIDFromString(user.id); - userIdMap.set(user.id, uuid); - - const { error } = await supabase - .from('users') - .upsert({ - id: uuid, - legacy_id: user.id, - name: user.name, - email: user.email.toLowerCase().trim(), - avatar_url: user.avatarUrl, - password_hash: user.passwordHash, - created_at: user.createdAt, - }, { onConflict: 'email' }); - - if (error) { - console.error(` ❌ Failed to migrate user ${user.email}:`, error.message); - } else { - migrated++; - console.log(` ✓ ${user.email}`); - } - } - - console.log(` ✅ Migrated ${migrated} users (${skipped} skipped)\n`); - return migrated; -} - -async function migrateSessions() { - console.log('📦 Migrating sessions...'); - - const sessions = sqliteDb.prepare('SELECT * FROM sessions').all() as Array<{ - id: string; - userId: string; - tokenHash: string; - createdAt: string; - expiresAt: string; - }>; - - let migrated = 0; - - for (const session of sessions) { - const userUuid = userIdMap.get(session.userId); - if (!userUuid) { - console.log(` ⚠️ Skipping session for unknown user: ${session.userId}`); - continue; - } - - const { error } = await supabase - .from('sessions') - .upsert({ - id: generateUUIDFromString(session.id), - user_id: userUuid, - token_hash: session.tokenHash, - created_at: session.createdAt, - expires_at: session.expiresAt, - }, { onConflict: 'token_hash' }); - - if (error) { - console.error(` ❌ Failed to migrate session:`, error.message); - } else { - migrated++; - } - } - - console.log(` ✅ Migrated ${migrated} sessions\n`); - return migrated; -} - -async function migratePasswordResetTokens() { - console.log('📦 Migrating password reset tokens...'); - - const tokens = sqliteDb.prepare('SELECT * FROM password_reset_tokens').all() as Array<{ - id: string; - userId: string; - tokenHash: string; - expiresAt: string; - createdAt: string; - used: number; - }>; - - let migrated = 0; - - for (const token of tokens) { - const userUuid = userIdMap.get(token.userId); - if (!userUuid) { - console.log(` ⚠️ Skipping token for unknown user: ${token.userId}`); - continue; - } - - const { error } = await supabase - .from('password_reset_tokens') - .upsert({ - id: generateUUIDFromString(token.id), - user_id: userUuid, - token_hash: token.tokenHash, - expires_at: token.expiresAt, - created_at: token.createdAt, - used: token.used === 1, - }, { onConflict: 'token_hash' }); - - if (error) { - console.error(` ❌ Failed to migrate token:`, error.message); - } else { - migrated++; - } - } - - console.log(` ✅ Migrated ${migrated} password reset tokens\n`); - return migrated; -} - -async function migrateProjects() { - console.log('📦 Migrating projects...'); - - const projects = sqliteDb.prepare('SELECT * FROM projects').all() as Array<{ - id: string; - name: string; - description: string | null; - color: string; - createdAt: string; - }>; - - let migrated = 0; - - for (const project of projects) { - const uuid = generateUUIDFromString(project.id); - projectIdMap.set(project.id, uuid); - - const { error } = await supabase - .from('projects') - .upsert({ - id: uuid, - legacy_id: project.id, - name: project.name, - description: project.description, - color: project.color, - created_at: project.createdAt, - }, { onConflict: 'legacy_id' }); - - if (error) { - console.error(` ❌ Failed to migrate project ${project.name}:`, error.message); - } else { - migrated++; - console.log(` ✓ ${project.name}`); - } - } - - console.log(` ✅ Migrated ${migrated} projects\n`); - return migrated; -} - -async function migrateSprints() { - console.log('📦 Migrating sprints...'); - - const sprints = sqliteDb.prepare('SELECT * FROM sprints').all() as Array<{ - id: string; - name: string; - goal: string | null; - startDate: string; - endDate: string; - status: string; - projectId: string; - createdAt: string; - }>; - - let migrated = 0; - - for (const sprint of sprints) { - const uuid = generateUUIDFromString(sprint.id); - sprintIdMap.set(sprint.id, uuid); - - const projectUuid = projectIdMap.get(sprint.projectId); - if (!projectUuid) { - console.log(` ⚠️ Skipping sprint ${sprint.name} - unknown project: ${sprint.projectId}`); - continue; - } - - const { error } = await supabase - .from('sprints') - .upsert({ - id: uuid, - legacy_id: sprint.id, - name: sprint.name, - goal: sprint.goal, - start_date: sprint.startDate, - end_date: sprint.endDate, - status: sprint.status, - project_id: projectUuid, - created_at: sprint.createdAt, - }, { onConflict: 'legacy_id' }); - - if (error) { - console.error(` ❌ Failed to migrate sprint ${sprint.name}:`, error.message); - } else { - migrated++; - console.log(` ✓ ${sprint.name}`); - } - } - - console.log(` ✅ Migrated ${migrated} sprints\n`); - return migrated; -} - -async function migrateTasks() { - console.log('📦 Migrating tasks...'); - - const tasks = sqliteDb.prepare('SELECT * FROM tasks').all() as Array<{ - id: string; - title: string; - description: string | null; - type: string; - status: string; - priority: string; - projectId: string; - sprintId: string | null; - createdAt: string; - updatedAt: string; - createdById: string | null; - createdByName: string | null; - createdByAvatarUrl: string | null; - updatedById: string | null; - updatedByName: string | null; - updatedByAvatarUrl: string | null; - assigneeId: string | null; - assigneeName: string | null; - assigneeEmail: string | null; - assigneeAvatarUrl: string | null; - dueDate: string | null; - comments: string | null; - tags: string | null; - attachments: string | null; - }>; - - let migrated = 0; - let failed = 0; - - for (const task of tasks) { - const projectUuid = projectIdMap.get(task.projectId); - if (!projectUuid) { - console.log(` ⚠️ Skipping task ${task.title} - unknown project: ${task.projectId}`); - continue; - } - - const sprintUuid = task.sprintId ? sprintIdMap.get(task.sprintId) : null; - const createdByUuid = task.createdById ? userIdMap.get(task.createdById) : null; - const updatedByUuid = task.updatedById ? userIdMap.get(task.updatedById) : null; - const assigneeUuid = task.assigneeId ? userIdMap.get(task.assigneeId) : null; - - // Parse JSON fields safely - let comments = []; - let tags = []; - let attachments = []; - - try { - comments = task.comments ? JSON.parse(task.comments) : []; - tags = task.tags ? JSON.parse(task.tags) : []; - attachments = task.attachments ? JSON.parse(task.attachments) : []; - } catch (e) { - console.warn(` ⚠️ Failed to parse JSON for task ${task.id}:`, e); - } - - const { error } = await supabase - .from('tasks') - .upsert({ - id: generateUUIDFromString(task.id), - legacy_id: task.id, - title: task.title, - description: task.description, - type: task.type, - status: task.status, - priority: task.priority, - project_id: projectUuid, - sprint_id: sprintUuid, - created_at: task.createdAt, - updated_at: task.updatedAt, - created_by_id: createdByUuid, - created_by_name: task.createdByName, - created_by_avatar_url: task.createdByAvatarUrl, - updated_by_id: updatedByUuid, - updated_by_name: task.updatedByName, - updated_by_avatar_url: task.updatedByAvatarUrl, - assignee_id: assigneeUuid, - assignee_name: task.assigneeName, - assignee_email: task.assigneeEmail, - assignee_avatar_url: task.assigneeAvatarUrl, - due_date: task.dueDate, - comments: comments, - tags: tags, - attachments: attachments, - }, { onConflict: 'legacy_id' }); - - if (error) { - console.error(` ❌ Failed to migrate task "${task.title}":`, error.message); - failed++; - } else { - migrated++; - } - } - - console.log(` ✅ Migrated ${migrated} tasks (${failed} failed)\n`); - return migrated; -} - -async function migrateMeta() { - console.log('📦 Migrating meta data...'); - - const meta = sqliteDb.prepare("SELECT * FROM meta WHERE key = 'lastUpdated'").get() as { - key: string; - value: string; - } | undefined; - - if (meta) { - const { error } = await supabase - .from('meta') - .upsert({ - key: 'lastUpdated', - value: meta.value, - updated_at: new Date().toISOString(), - }, { onConflict: 'key' }); - - if (error) { - console.error(` ❌ Failed to migrate meta:`, error.message); - } else { - console.log(` ✅ Migrated lastUpdated: ${meta.value}\n`); - } - } -} - -async function main() { - console.log('🚀 Starting SQLite → Supabase migration\n'); - console.log(`Supabase URL: ${SUPABASE_URL}\n`); - - try { - // Test connection - const { error: healthError } = await supabase.from('users').select('count').limit(1); - if (healthError && healthError.code !== 'PGRST116') { // PGRST116 = no rows, which is fine - throw new Error(`Cannot connect to Supabase: ${healthError.message}`); - } - console.log('✅ Connected to Supabase\n'); - - // Migration order matters due to foreign keys - const stats = { - users: await migrateUsers(), - sessions: await migrateSessions(), - passwordResetTokens: await migratePasswordResetTokens(), - projects: await migrateProjects(), - sprints: await migrateSprints(), - tasks: await migrateTasks(), - }; - - await migrateMeta(); - - console.log('═══════════════════════════════════════'); - console.log('✅ Migration Complete!'); - console.log('═══════════════════════════════════════'); - console.log(` Users: ${stats.users}`); - console.log(` Sessions: ${stats.sessions}`); - console.log(` Password Reset Tokens: ${stats.passwordResetTokens}`); - console.log(` Projects: ${stats.projects}`); - console.log(` Sprints: ${stats.sprints}`); - console.log(` Tasks: ${stats.tasks}`); - console.log('═══════════════════════════════════════'); - console.log('\nNext steps:'); - console.log(' 1. Update your .env.local with Supabase credentials'); - console.log(' 2. Test the app locally: npm run dev'); - console.log(' 3. Deploy to Vercel with the new environment variables'); - - } catch (error) { - console.error('\n❌ Migration failed:', error); - process.exit(1); - } finally { - sqliteDb.close(); - } -} - -main();