Compare commits
6 Commits
70f4d78c0c
...
7d40459211
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d40459211 | |||
| 42188457da | |||
| b354a0469d | |||
| 2dea56ea39 | |||
| faeee26222 | |||
| 98536e368d |
280
scripts/README.md
Normal file
280
scripts/README.md
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
# Gantt Board CLI Tools
|
||||||
|
|
||||||
|
Complete command-line interface for the Gantt Board. All web UI operations available via CLI.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all tasks
|
||||||
|
./scripts/gantt.sh task list
|
||||||
|
|
||||||
|
# Create a task
|
||||||
|
./scripts/gantt.sh task create "Fix login bug" open high
|
||||||
|
|
||||||
|
# Create task with natural language
|
||||||
|
./scripts/gantt.sh task natural "Research TTS options by Friday, high priority"
|
||||||
|
|
||||||
|
# Update task
|
||||||
|
./scripts/gantt.sh task update <task-id> status done
|
||||||
|
|
||||||
|
# Add comment
|
||||||
|
./scripts/gantt.sh task comment <task-id> "Working on this now"
|
||||||
|
|
||||||
|
# Attach file
|
||||||
|
./scripts/gantt.sh task attach <task-id> ./notes.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Main CLI: `gantt.sh`
|
||||||
|
|
||||||
|
A unified CLI that covers all API operations.
|
||||||
|
|
||||||
|
### Task Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List tasks (optionally filter by status)
|
||||||
|
./scripts/gantt.sh task list
|
||||||
|
./scripts/gantt.sh task list open
|
||||||
|
./scripts/gantt.sh task list in-progress
|
||||||
|
|
||||||
|
# Get specific task
|
||||||
|
./scripts/gantt.sh task get <task-id>
|
||||||
|
|
||||||
|
# Create task
|
||||||
|
./scripts/gantt.sh task create <title> [status] [priority] [project-id]
|
||||||
|
./scripts/gantt.sh task create "Fix bug" open high 1
|
||||||
|
|
||||||
|
# Create from natural language
|
||||||
|
./scripts/gantt.sh task natural "Fix login bug by Friday, assign to Matt, high priority"
|
||||||
|
|
||||||
|
# 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>
|
||||||
|
|
||||||
|
# Delete task
|
||||||
|
./scripts/gantt.sh task delete <task-id>
|
||||||
|
|
||||||
|
# Add comment
|
||||||
|
./scripts/gantt.sh task comment <task-id> "Your comment here"
|
||||||
|
|
||||||
|
# Attach file
|
||||||
|
./scripts/gantt.sh task attach <task-id> <file-path>
|
||||||
|
./scripts/gantt.sh task attach abc-123 ./research.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Project Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all projects
|
||||||
|
./scripts/gantt.sh project list
|
||||||
|
|
||||||
|
# Create project
|
||||||
|
./scripts/gantt.sh project create "Project Name" "Description" "#color"
|
||||||
|
|
||||||
|
# Update project field
|
||||||
|
./scripts/gantt.sh project update <project-id> <field> <value>
|
||||||
|
./scripts/gantt.sh project update abc-123 name "New Name"
|
||||||
|
./scripts/gantt.sh project update abc-123 description "New desc"
|
||||||
|
./scripts/gantt.sh project update abc-123 color "#ff0000"
|
||||||
|
|
||||||
|
# Delete project
|
||||||
|
./scripts/gantt.sh project delete <project-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sprint Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all sprints
|
||||||
|
./scripts/gantt.sh sprint list
|
||||||
|
|
||||||
|
# Create sprint
|
||||||
|
./scripts/gantt.sh sprint create "Sprint 2" <project-id> "2026-02-23" "2026-03-01" "Sprint goal"
|
||||||
|
|
||||||
|
# Update sprint field
|
||||||
|
./scripts/gantt.sh sprint update <sprint-id> <field> <value>
|
||||||
|
./scripts/gantt.sh sprint update abc-123 name "New Sprint Name"
|
||||||
|
./scripts/gantt.sh sprint update abc-123 status active
|
||||||
|
./scripts/gantt.sh sprint update abc-123 startDate "2026-02-25"
|
||||||
|
|
||||||
|
# Delete sprint
|
||||||
|
./scripts/gantt.sh sprint delete <sprint-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/attach-file.sh <task-id> <file-path>
|
||||||
|
```
|
||||||
|
|
||||||
|
Supports: md, txt, json, pdf, png, jpg, gif
|
||||||
|
|
||||||
|
### `view-attachment.sh` - View Attached Files
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/view-attachment.sh <task-id> [index]
|
||||||
|
```
|
||||||
|
|
||||||
|
Displays text files in terminal, saves binary files to `/tmp/`.
|
||||||
|
|
||||||
|
## API Coverage Matrix
|
||||||
|
|
||||||
|
| 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 | ✅ | ✅ | ❌ |
|
||||||
|
| Create project | ✅ | ✅ | ❌ |
|
||||||
|
| Update project | ✅ | ✅ | ❌ |
|
||||||
|
| Delete project | ✅ | ✅ | ❌ |
|
||||||
|
| List sprints | ✅ | ✅ | ❌ |
|
||||||
|
| Create sprint | ✅ | ✅ | ❌ |
|
||||||
|
| Update sprint | ✅ | ✅ | ❌ |
|
||||||
|
| Delete sprint | ✅ | ✅ | ❌ |
|
||||||
|
| Auth login | ✅ | ✅ | ❌ |
|
||||||
|
| Auth logout | ✅ | ✅ | ❌ |
|
||||||
|
| Auth register | ✅ | ✅ | ❌ |
|
||||||
|
| Auth forgot-password | ✅ | ✅ | ❌ |
|
||||||
|
| Auth reset-password | ✅ | ✅ | ❌ |
|
||||||
|
| Auth account update | ✅ | ✅ | ❌ |
|
||||||
|
| Auth list users | ✅ | ✅ | ❌ |
|
||||||
|
| View attachments | ✅ | ❌ | ✅ |
|
||||||
|
|
||||||
|
### Auditing Coverage
|
||||||
|
|
||||||
|
Use the audit script to verify CLI stays in sync with API:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/audit-cli-coverage.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This scans all API routes and checks that each has a matching CLI command. Run this before committing any API changes to ensure Rule 2.5 compliance.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- `curl` - HTTP requests (built into macOS)
|
||||||
|
- `jq` - JSON parsing: `brew install jq`
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Override API URL
|
||||||
|
export API_URL=http://localhost:3001/api
|
||||||
|
./scripts/gantt.sh task list
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Workflows
|
||||||
|
|
||||||
|
### Create Task with Natural Language
|
||||||
|
|
||||||
|
```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
|
||||||
|
|
||||||
|
### "jq: command not found"
|
||||||
|
```bash
|
||||||
|
brew install jq
|
||||||
|
```
|
||||||
|
|
||||||
|
### "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`. Find them:
|
||||||
|
- In the URL when viewing a task
|
||||||
|
- From `task list` output
|
||||||
|
- From `task create` output
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
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
|
||||||
150
scripts/audit-cli-coverage.sh
Executable file
150
scripts/audit-cli-coverage.sh
Executable file
@ -0,0 +1,150 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# CLI Coverage Audit Script
|
||||||
|
# Compares API endpoints to CLI commands to ensure sync
|
||||||
|
# Usage: ./scripts/audit-cli-coverage.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
|
API_DIR="${PROJECT_ROOT}/src/app/api"
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log_info() { echo -e "${GREEN}✓${NC} $1"; }
|
||||||
|
log_warn() { echo -e "${YELLOW}⚠${NC} $1"; }
|
||||||
|
log_error() { echo -e "${RED}✗${NC} $1"; }
|
||||||
|
|
||||||
|
echo "═══════════════════════════════════════════════════════════"
|
||||||
|
echo " CLI Coverage Audit"
|
||||||
|
echo "═══════════════════════════════════════════════════════════"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if API directory exists
|
||||||
|
if [ ! -d "$API_DIR" ]; then
|
||||||
|
log_error "API directory not found: $API_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find all API route files
|
||||||
|
echo "📁 Scanning API routes in $API_DIR..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Extract API endpoints from route.ts files
|
||||||
|
declare -a API_ENDPOINTS=()
|
||||||
|
|
||||||
|
while IFS= read -r -d '' route_file; do
|
||||||
|
# Get the directory path relative to api/
|
||||||
|
rel_path=$(dirname "$route_file" | sed "s|$API_DIR/||")
|
||||||
|
|
||||||
|
# Check for HTTP methods in the file
|
||||||
|
if grep -q "export async function GET" "$route_file"; then
|
||||||
|
API_ENDPOINTS+=("GET /api/$rel_path")
|
||||||
|
fi
|
||||||
|
if grep -q "export async function POST" "$route_file"; then
|
||||||
|
API_ENDPOINTS+=("POST /api/$rel_path")
|
||||||
|
fi
|
||||||
|
if grep -q "export async function PATCH" "$route_file"; then
|
||||||
|
API_ENDPOINTS+=("PATCH /api/$rel_path")
|
||||||
|
fi
|
||||||
|
if grep -q "export async function DELETE" "$route_file"; then
|
||||||
|
API_ENDPOINTS+=("DELETE /api/$rel_path")
|
||||||
|
fi
|
||||||
|
if grep -q "export async function PUT" "$route_file"; then
|
||||||
|
API_ENDPOINTS+=("PUT /api/$rel_path")
|
||||||
|
fi
|
||||||
|
done < <(find "$API_DIR" -name "route.ts" -print0)
|
||||||
|
|
||||||
|
# Sort and dedupe API endpoints
|
||||||
|
IFS=$'\n' API_ENDPOINTS=($(sort <<< "${API_ENDPOINTS[*]}"))
|
||||||
|
unset IFS
|
||||||
|
|
||||||
|
echo "Found ${#API_ENDPOINTS[@]} API endpoint(s):"
|
||||||
|
for endpoint in "${API_ENDPOINTS[@]}"; do
|
||||||
|
echo " • $endpoint"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check CLI coverage
|
||||||
|
echo "🔍 Checking CLI coverage..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Extract CLI commands from gantt.sh (or similar unified CLI)
|
||||||
|
CLI_FILE="${SCRIPT_DIR}/gantt.sh"
|
||||||
|
if [ ! -f "$CLI_FILE" ]; then
|
||||||
|
log_warn "Unified CLI not found at $CLI_FILE"
|
||||||
|
log_info "Checking for individual scripts..."
|
||||||
|
|
||||||
|
# Look for any shell scripts
|
||||||
|
CLI_SCRIPTS=($(find "$SCRIPT_DIR" -name "*.sh" -type f ! -name "audit-cli-coverage.sh" | sort))
|
||||||
|
|
||||||
|
if [ ${#CLI_SCRIPTS[@]} -eq 0 ]; then
|
||||||
|
log_error "No CLI scripts found!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Found CLI scripts:"
|
||||||
|
for script in "${CLI_SCRIPTS[@]}"; do
|
||||||
|
echo " • $(basename "$script")"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Compare API to CLI coverage
|
||||||
|
echo ""
|
||||||
|
echo "═══════════════════════════════════════════════════════════"
|
||||||
|
echo " Coverage Report"
|
||||||
|
echo "═══════════════════════════════════════════════════════════"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# This is project-specific - customize based on your CLI structure
|
||||||
|
# For gantt-board, we check against known CLI commands
|
||||||
|
|
||||||
|
declare -A CLI_MAP=(
|
||||||
|
["GET /api/tasks"]='task list'
|
||||||
|
["POST /api/tasks"]='task create'
|
||||||
|
["DELETE /api/tasks"]='task delete'
|
||||||
|
["POST /api/tasks/natural"]='task natural'
|
||||||
|
)
|
||||||
|
|
||||||
|
MISSING=0
|
||||||
|
COVERED=0
|
||||||
|
|
||||||
|
for endpoint in "${API_ENDPOINTS[@]}"; do
|
||||||
|
if [ -n "${CLI_MAP[$endpoint]}" ]; then
|
||||||
|
log_info "$endpoint → ${CLI_MAP[$endpoint]}"
|
||||||
|
((COVERED++))
|
||||||
|
else
|
||||||
|
log_error "$endpoint → NO CLI COMMAND"
|
||||||
|
((MISSING++))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "═══════════════════════════════════════════════════════════"
|
||||||
|
echo " Summary"
|
||||||
|
echo "═══════════════════════════════════════════════════════════"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ $MISSING -eq 0 ]; then
|
||||||
|
log_info "All API endpoints have CLI coverage!"
|
||||||
|
echo ""
|
||||||
|
echo " Total endpoints: ${#API_ENDPOINTS[@]}"
|
||||||
|
echo " Covered: $COVERED"
|
||||||
|
echo " Missing: $MISSING"
|
||||||
|
echo ""
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
log_error "CLI coverage incomplete!"
|
||||||
|
echo ""
|
||||||
|
echo " Total endpoints: ${#API_ENDPOINTS[@]}"
|
||||||
|
echo " Covered: $COVERED"
|
||||||
|
echo " Missing: $MISSING"
|
||||||
|
echo ""
|
||||||
|
echo "Add CLI commands for missing endpoints before committing."
|
||||||
|
echo "See Rule 2.5 in MEMORY.md for details."
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
663
scripts/gantt.sh
Executable file
663
scripts/gantt.sh
Executable file
@ -0,0 +1,663 @@
|
|||||||
|
#!/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..."
|
||||||
|
api_call GET "/projects" | jq '.projects'
|
||||||
|
}
|
||||||
|
|
||||||
|
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_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"
|
||||||
|
}
|
||||||
|
|
||||||
|
#===================
|
||||||
|
# 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 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 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
|
||||||
|
|
||||||
|
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 "$@" ;;
|
||||||
|
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 "$@" ;;
|
||||||
|
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
|
||||||
|
;;
|
||||||
|
debug) cmd_debug "$@" ;;
|
||||||
|
help|--help|-h) show_help ;;
|
||||||
|
*) log_error "Unknown command: $cmd"; show_help; exit 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@ -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();
|
|
||||||
129
src/app/api/projects/route.ts
Normal file
129
src/app/api/projects/route.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { getServiceSupabase } from "@/lib/supabase/client";
|
||||||
|
import { getAuthenticatedUser } from "@/lib/server/auth";
|
||||||
|
|
||||||
|
export const runtime = "nodejs";
|
||||||
|
|
||||||
|
// GET - fetch all projects
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const user = await getAuthenticatedUser();
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = getServiceSupabase();
|
||||||
|
const { data: projects, error } = await supabase
|
||||||
|
.from("projects")
|
||||||
|
.select("*")
|
||||||
|
.order("created_at", { ascending: true });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return NextResponse.json({ projects: projects || [] });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(">>> API GET /projects error:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to fetch projects" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST - create a new project
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const user = await getAuthenticatedUser();
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const { name, description, color } = body;
|
||||||
|
|
||||||
|
if (!name || typeof name !== "string") {
|
||||||
|
return NextResponse.json({ error: "Missing project name" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = getServiceSupabase();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("projects")
|
||||||
|
.insert({
|
||||||
|
name,
|
||||||
|
description: description || null,
|
||||||
|
color: color || "#3b82f6",
|
||||||
|
created_at: now,
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, project: data });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(">>> API POST /projects error:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to create project" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH - update a project
|
||||||
|
export async function PATCH(request: Request) {
|
||||||
|
try {
|
||||||
|
const user = await getAuthenticatedUser();
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const { id, ...updates } = body;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json({ error: "Missing project id" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = getServiceSupabase();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("projects")
|
||||||
|
.update({
|
||||||
|
...updates,
|
||||||
|
updated_at: now,
|
||||||
|
})
|
||||||
|
.eq("id", id)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, project: data });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(">>> API PATCH /projects error:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to update project" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE - delete a project
|
||||||
|
export async function DELETE(request: Request) {
|
||||||
|
try {
|
||||||
|
const user = await getAuthenticatedUser();
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await request.json();
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json({ error: "Missing project id" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = getServiceSupabase();
|
||||||
|
const { error } = await supabase.from("projects").delete().eq("id", id);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(">>> API DELETE /projects error:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to delete project" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
139
src/app/api/sprints/route.ts
Normal file
139
src/app/api/sprints/route.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { getServiceSupabase } from "@/lib/supabase/client";
|
||||||
|
import { getAuthenticatedUser } from "@/lib/server/auth";
|
||||||
|
|
||||||
|
export const runtime = "nodejs";
|
||||||
|
|
||||||
|
// GET - fetch all sprints
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const user = await getAuthenticatedUser();
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = getServiceSupabase();
|
||||||
|
const { data: sprints, error } = await supabase
|
||||||
|
.from("sprints")
|
||||||
|
.select("*")
|
||||||
|
.order("start_date", { ascending: true });
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return NextResponse.json({ sprints: sprints || [] });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(">>> API GET /sprints error:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to fetch sprints" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST - create a new sprint
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const user = await getAuthenticatedUser();
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const { name, goal, startDate, endDate, status, projectId } = body;
|
||||||
|
|
||||||
|
if (!name || typeof name !== "string") {
|
||||||
|
return NextResponse.json({ error: "Missing sprint name" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = getServiceSupabase();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("sprints")
|
||||||
|
.insert({
|
||||||
|
name,
|
||||||
|
goal: goal || null,
|
||||||
|
start_date: startDate || now,
|
||||||
|
end_date: endDate || now,
|
||||||
|
status: status || "planning",
|
||||||
|
project_id: projectId || null,
|
||||||
|
created_at: now,
|
||||||
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, sprint: data });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(">>> API POST /sprints error:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to create sprint" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH - update a sprint
|
||||||
|
export async function PATCH(request: Request) {
|
||||||
|
try {
|
||||||
|
const user = await getAuthenticatedUser();
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const { id, ...updates } = body;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json({ error: "Missing sprint id" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = getServiceSupabase();
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
// Map camelCase to snake_case for database
|
||||||
|
const dbUpdates: Record<string, unknown> = {};
|
||||||
|
if (updates.name !== undefined) dbUpdates.name = updates.name;
|
||||||
|
if (updates.goal !== undefined) dbUpdates.goal = updates.goal;
|
||||||
|
if (updates.startDate !== undefined) dbUpdates.start_date = updates.startDate;
|
||||||
|
if (updates.endDate !== undefined) dbUpdates.end_date = updates.endDate;
|
||||||
|
if (updates.status !== undefined) dbUpdates.status = updates.status;
|
||||||
|
if (updates.projectId !== undefined) dbUpdates.project_id = updates.projectId;
|
||||||
|
dbUpdates.updated_at = now;
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("sprints")
|
||||||
|
.update(dbUpdates)
|
||||||
|
.eq("id", id)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, sprint: data });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(">>> API PATCH /sprints error:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to update sprint" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE - delete a sprint
|
||||||
|
export async function DELETE(request: Request) {
|
||||||
|
try {
|
||||||
|
const user = await getAuthenticatedUser();
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await request.json();
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return NextResponse.json({ error: "Missing sprint id" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = getServiceSupabase();
|
||||||
|
const { error } = await supabase.from("sprints").delete().eq("id", id);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(">>> API DELETE /sprints error:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to delete sprint" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user