Filter out completed sprints from task create/edit dropdowns
This commit is contained in:
parent
51b9da9eb7
commit
25fa76e59d
@ -1,339 +1,377 @@
|
||||
# Gantt Board CLI Tools
|
||||
# Gantt Board CLI
|
||||
|
||||
Complete command-line interface for the Gantt Board. All web UI operations available via CLI.
|
||||
A comprehensive command-line interface for managing tasks, projects, and sprints in the Gantt Board system.
|
||||
|
||||
## Quick Start
|
||||
## Scripts
|
||||
|
||||
- `task.sh` - Full CRUD operations for tasks
|
||||
- `project.sh` - Project management
|
||||
- `sprint.sh` - Sprint management
|
||||
|
||||
## Installation
|
||||
|
||||
1. Make scripts executable:
|
||||
```bash
|
||||
# Pull latest schema from Supabase into supabase/schema.sql
|
||||
SUPABASE_DB_URL='postgresql://...' ./scripts/pull-supabase-schema.sh
|
||||
|
||||
# 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
|
||||
chmod +x task.sh project.sh sprint.sh
|
||||
```
|
||||
|
||||
## Schema Dump Script
|
||||
|
||||
Use `pull-supabase-schema.sh` when `supabase/schema.sql` needs to match the live database:
|
||||
|
||||
```bash
|
||||
SUPABASE_DB_URL='postgresql://postgres:***@db.<project-ref>.supabase.co:5432/postgres?sslmode=require' \
|
||||
./scripts/pull-supabase-schema.sh
|
||||
```
|
||||
|
||||
Optional:
|
||||
|
||||
```bash
|
||||
# Include additional schemas
|
||||
SCHEMAS='public,auth,storage' \
|
||||
SUPABASE_DB_URL='postgresql://...' \
|
||||
./scripts/pull-supabase-schema.sh supabase/full-schema.sql
|
||||
```
|
||||
|
||||
## 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 <project-uuid>
|
||||
|
||||
# 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 <task-uuid> status done
|
||||
./scripts/gantt.sh task update <task-uuid> priority urgent
|
||||
./scripts/gantt.sh task update <task-uuid> title "New title"
|
||||
./scripts/gantt.sh task update <task-uuid> assigneeId <user-uuid>
|
||||
|
||||
# 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 <task-uuid> ./research.pdf
|
||||
```
|
||||
|
||||
Comment payload persisted by the API/CLI:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "string",
|
||||
"text": "string",
|
||||
"createdAt": "2026-02-23T19:00:00.000Z",
|
||||
"commentAuthorId": "user-uuid-or-assistant",
|
||||
"replies": []
|
||||
}
|
||||
```
|
||||
|
||||
- `commentAuthorId` is the only supported author field.
|
||||
|
||||
### Project Commands
|
||||
|
||||
```bash
|
||||
# List all projects
|
||||
./scripts/gantt.sh project list
|
||||
|
||||
# Get specific project
|
||||
./scripts/gantt.sh project get <project-id>
|
||||
|
||||
# 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 <project-uuid> name "New Name"
|
||||
./scripts/gantt.sh project update <project-uuid> description "New desc"
|
||||
./scripts/gantt.sh project update <project-uuid> color "#ff0000"
|
||||
|
||||
# Delete project
|
||||
./scripts/gantt.sh project delete <project-id>
|
||||
```
|
||||
|
||||
### Sprint Commands
|
||||
|
||||
```bash
|
||||
# List all sprints
|
||||
./scripts/gantt.sh sprint list
|
||||
|
||||
# Get specific sprint
|
||||
./scripts/gantt.sh sprint get <sprint-id>
|
||||
|
||||
# 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 <sprint-uuid> name "New Sprint Name"
|
||||
./scripts/gantt.sh sprint update <sprint-uuid> status active
|
||||
./scripts/gantt.sh sprint update <sprint-uuid> 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
|
||||
|
||||
# Register new account
|
||||
./scripts/gantt.sh auth register <email> <password> [name]
|
||||
|
||||
# Request password reset
|
||||
./scripts/gantt.sh auth forgot-password <email>
|
||||
|
||||
# Reset password with token
|
||||
./scripts/gantt.sh auth reset-password <token> <new-password>
|
||||
|
||||
# Update account
|
||||
./scripts/gantt.sh auth account <field> <value>
|
||||
|
||||
# List all users
|
||||
./scripts/gantt.sh auth users
|
||||
```
|
||||
|
||||
### 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 | ✅ | ✅ | ❌ |
|
||||
| Get project | ✅ | ✅ | ❌ |
|
||||
| Create project | ✅ | ✅ | ❌ |
|
||||
| Update project | ✅ | ✅ | ❌ |
|
||||
| Delete project | ✅ | ✅ | ❌ |
|
||||
| List sprints | ✅ | ✅ | ❌ |
|
||||
| Get sprint | ✅ | ✅ | ❌ |
|
||||
| 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 <task-uuid> "Completed. See attached notes."
|
||||
|
||||
# Attach notes
|
||||
./scripts/gantt.sh task attach <task-uuid> ./completion-notes.md
|
||||
|
||||
# Mark done
|
||||
./scripts/gantt.sh task update <task-uuid> 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"
|
||||
2. Ensure dependencies are installed:
|
||||
```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>`
|
||||
## Configuration
|
||||
|
||||
### "API call failed (HTTP 500)"
|
||||
- Check that the dev server is running: `npm run dev`
|
||||
- Check server logs for errors
|
||||
Scripts use the following defaults:
|
||||
- **Supabase URL**: `https://qnatchrjlpehiijwtreh.supabase.co`
|
||||
- **Default Project**: OpenClaw iOS
|
||||
- **Default Assignee**: Max
|
||||
|
||||
### 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
|
||||
- `projectId`, `sprintId`, and `assigneeId` fields are UUID values as well.
|
||||
## Task Management (`task.sh`)
|
||||
|
||||
## Tips
|
||||
### Create Task
|
||||
|
||||
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
|
||||
```bash
|
||||
# Minimal task
|
||||
./task.sh create --title "Fix bug"
|
||||
|
||||
# Full task with all fields
|
||||
./task.sh create \
|
||||
--title "Implement OAuth" \
|
||||
--description "Add OAuth2 login support" \
|
||||
--type task \
|
||||
--status todo \
|
||||
--priority high \
|
||||
--project "Gantt Board" \
|
||||
--sprint "current" \
|
||||
--assignee "Max" \
|
||||
--due-date "2026-03-01" \
|
||||
--tags "auth,security" \
|
||||
--comments "Starting implementation"
|
||||
|
||||
# Create from JSON file
|
||||
./task.sh create --file task.json
|
||||
|
||||
# Interactive mode
|
||||
./task.sh create --interactive
|
||||
|
||||
# Auto-create project if not found
|
||||
./task.sh create --title "New Feature" --project "New Project" --auto-create
|
||||
```
|
||||
|
||||
### List Tasks
|
||||
|
||||
```bash
|
||||
# List all tasks
|
||||
./task.sh list
|
||||
|
||||
# Filter by status
|
||||
./task.sh list --status todo
|
||||
|
||||
# Filter by priority
|
||||
./task.sh list --priority high
|
||||
|
||||
# Filter by project (auto-resolves name)
|
||||
./task.sh list --project "Gantt Board"
|
||||
|
||||
# Filter by assignee
|
||||
./task.sh list --assignee "Max"
|
||||
|
||||
# Filter by type
|
||||
./task.sh list --type bug
|
||||
|
||||
# JSON output
|
||||
./task.sh list --json
|
||||
|
||||
# Limit results
|
||||
./task.sh list --limit 10
|
||||
```
|
||||
|
||||
### Get Task
|
||||
|
||||
```bash
|
||||
./task.sh get <task-id>
|
||||
```
|
||||
|
||||
### Update Task
|
||||
|
||||
```bash
|
||||
# Update status
|
||||
./task.sh update <task-id> --status in-progress
|
||||
|
||||
# Update multiple fields
|
||||
./task.sh update <task-id> \
|
||||
--status done \
|
||||
--priority low \
|
||||
--add-comment "Task completed"
|
||||
|
||||
# Update assignee
|
||||
./task.sh update <task-id> --assignee "Matt"
|
||||
|
||||
# Clear tags
|
||||
./task.sh update <task-id> --clear-tags
|
||||
|
||||
# Update tags
|
||||
./task.sh update <task-id> --tags "new-tag,another-tag"
|
||||
```
|
||||
|
||||
### Delete Task
|
||||
|
||||
```bash
|
||||
./task.sh delete <task-id>
|
||||
```
|
||||
|
||||
### Bulk Create
|
||||
|
||||
```bash
|
||||
# From JSON file
|
||||
./task.sh bulk-create tasks.json
|
||||
|
||||
# With auto-create for projects
|
||||
./task.sh bulk-create tasks.json --auto-create
|
||||
```
|
||||
|
||||
**JSON Format:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"title": "Task 1",
|
||||
"description": "Description",
|
||||
"type": "task",
|
||||
"status": "todo",
|
||||
"priority": "high",
|
||||
"project": "Project Name",
|
||||
"sprint": "Sprint 1",
|
||||
"assignee": "Max",
|
||||
"due_date": "2026-03-01",
|
||||
"tags": "tag1,tag2",
|
||||
"comments": "Initial comment"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Project Management (`project.sh`)
|
||||
|
||||
### Create Project
|
||||
|
||||
```bash
|
||||
./project.sh create --name "New Project" --description "Project description" --color "#3b82f6"
|
||||
```
|
||||
|
||||
### List Projects
|
||||
|
||||
```bash
|
||||
# List all projects
|
||||
./project.sh list
|
||||
|
||||
# JSON output
|
||||
./project.sh list --json
|
||||
```
|
||||
|
||||
### Get Project
|
||||
|
||||
```bash
|
||||
# By ID
|
||||
./project.sh get <project-id>
|
||||
|
||||
# By name (auto-resolves)
|
||||
./project.sh get "Gantt Board"
|
||||
```
|
||||
|
||||
### Update Project
|
||||
|
||||
```bash
|
||||
./project.sh update <project-id-or-name> \
|
||||
--name "Updated Name" \
|
||||
--description "Updated description" \
|
||||
--color "#ff0000"
|
||||
```
|
||||
|
||||
### Delete Project
|
||||
|
||||
```bash
|
||||
./project.sh delete <project-id-or-name>
|
||||
```
|
||||
|
||||
## Sprint Management (`sprint.sh`)
|
||||
|
||||
### Create Sprint
|
||||
|
||||
```bash
|
||||
./sprint.sh create \
|
||||
--name "Sprint 3" \
|
||||
--project "Gantt Board" \
|
||||
--goal "Complete API integration" \
|
||||
--start-date "2026-02-24" \
|
||||
--end-date "2026-03-07"
|
||||
```
|
||||
|
||||
### List Sprints
|
||||
|
||||
```bash
|
||||
# List all sprints
|
||||
./sprint.sh list
|
||||
|
||||
# List active sprints
|
||||
./sprint.sh list --active
|
||||
|
||||
# Filter by project
|
||||
./sprint.sh list --project "Gantt Board"
|
||||
|
||||
# JSON output
|
||||
./sprint.sh list --json
|
||||
```
|
||||
|
||||
### Get Sprint
|
||||
|
||||
```bash
|
||||
# By ID
|
||||
./sprint.sh get <sprint-id>
|
||||
|
||||
# By name
|
||||
./sprint.sh get "Sprint 1"
|
||||
```
|
||||
|
||||
### Update Sprint
|
||||
|
||||
```bash
|
||||
./sprint.sh update <sprint-id-or-name> \
|
||||
--name "Updated Sprint" \
|
||||
--goal "Updated goal" \
|
||||
--status active \
|
||||
--start-date "2026-02-25" \
|
||||
--end-date "2026-03-10"
|
||||
```
|
||||
|
||||
### Close Sprint
|
||||
|
||||
```bash
|
||||
./sprint.sh close <sprint-id-or-name>
|
||||
```
|
||||
|
||||
### Delete Sprint
|
||||
|
||||
```bash
|
||||
./sprint.sh delete <sprint-id-or-name>
|
||||
```
|
||||
|
||||
## Name Resolution
|
||||
|
||||
All scripts support automatic name-to-ID resolution:
|
||||
|
||||
- **Projects**: "Gantt Board" → `68d05855-a399-44a4-9c36-3ee78257c2c9`
|
||||
- **Sprints**: "Sprint 1" → `b2c3d4e5-0001-0000-0000-000000000001`
|
||||
- **Assignees**:
|
||||
- "Max" → `9c29cc99-81a1-4e75-8dff-cd7cc5ceb5aa`
|
||||
- "Matt" → `0a3e400c-3932-48ae-9b65-f3f9c6f26fe9`
|
||||
- **Special**: Use "current" for the most recent active sprint
|
||||
|
||||
## Task Types
|
||||
|
||||
- `task` - Standard task
|
||||
- `bug` - Bug fix
|
||||
- `research` - Research spike
|
||||
- `plan` - Planning task
|
||||
- `idea` - Idea/backlog item
|
||||
|
||||
## Task Statuses
|
||||
|
||||
- `open` - Newly created
|
||||
- `todo` - Ready to start
|
||||
- `blocked` - Blocked
|
||||
- `in-progress` - Currently working
|
||||
- `review` - Ready for review
|
||||
- `validate` - Needs validation
|
||||
- `done` - Completed
|
||||
|
||||
## Priorities
|
||||
|
||||
- `low`
|
||||
- `medium`
|
||||
- `high`
|
||||
- `urgent`
|
||||
|
||||
## Examples
|
||||
|
||||
### Daily Workflow
|
||||
|
||||
```bash
|
||||
# Create a new task
|
||||
./task.sh create --title "Fix login bug" --type bug --priority urgent --project "Gantt Board"
|
||||
|
||||
# List my high priority tasks
|
||||
./task.sh list --assignee "Max" --priority high
|
||||
|
||||
# Update task status
|
||||
./task.sh update <task-id> --status in-progress
|
||||
|
||||
# Add progress comment
|
||||
./task.sh update <task-id> --add-comment "Working on reproduction steps"
|
||||
|
||||
# Mark as done
|
||||
./task.sh update <task-id> --status done --add-comment "Fixed in commit abc123"
|
||||
```
|
||||
|
||||
### Sprint Planning
|
||||
|
||||
```bash
|
||||
# Create new sprint
|
||||
./sprint.sh create --name "Sprint 5" --project "Gantt Board" --start-date 2026-03-01 --end-date 2026-03-14
|
||||
|
||||
# Create multiple tasks for the sprint
|
||||
./task.sh bulk-create sprint-tasks.json
|
||||
|
||||
# Close previous sprint
|
||||
./sprint.sh close "Sprint 4"
|
||||
```
|
||||
|
||||
### Project Setup
|
||||
|
||||
```bash
|
||||
# Create new project
|
||||
./project.sh create --name "Mobile App" --description "iOS and Android app" --color "#8b5cf6"
|
||||
|
||||
# Create initial sprint
|
||||
./sprint.sh create --name "Sprint 1" --project "Mobile App" --goal "MVP release"
|
||||
|
||||
# Create starter tasks
|
||||
./task.sh create --title "Setup repo" --project "Mobile App" --sprint "Sprint 1"
|
||||
./task.sh create --title "Design system" --project "Mobile App" --sprint "Sprint 1"
|
||||
```
|
||||
|
||||
## Exit Codes
|
||||
|
||||
- `0` - Success
|
||||
- `1` - Error (invalid input, API failure, not found)
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Scripts use hardcoded configuration at the top of each file. To customize, edit:
|
||||
- `SUPABASE_URL` - Supabase instance URL
|
||||
- `SERVICE_KEY` - Supabase service role key
|
||||
- `DEFAULT_PROJECT_ID` - Default project for tasks
|
||||
- `DEFAULT_ASSIGNEE_ID` - Default assignee for tasks
|
||||
- `MATT_ID` - Matt's user ID
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Project not found"
|
||||
- Use `--auto-create` flag to create project automatically
|
||||
- Check project name spelling
|
||||
- Use `./project.sh list` to see available projects
|
||||
|
||||
### "Sprint not found"
|
||||
- Use "current" to reference the most recent active sprint
|
||||
- Check sprint name with `./sprint.sh list`
|
||||
|
||||
### "jq parse error"
|
||||
- Ensure `jq` is installed: `brew install jq`
|
||||
- Check JSON file syntax
|
||||
|
||||
### Empty response on create
|
||||
- This is normal - Supabase returns empty on successful POST
|
||||
- Check list command to verify creation: `./task.sh list --limit 5`
|
||||
|
||||
## License
|
||||
|
||||
Part of the OpenClaw Gantt Board project.
|
||||
|
||||
66
scripts/examples/task-full.json
Normal file
66
scripts/examples/task-full.json
Normal file
@ -0,0 +1,66 @@
|
||||
[
|
||||
{
|
||||
"title": "Implement user authentication",
|
||||
"description": "Add OAuth2-based authentication with Google and GitHub providers. Include JWT token handling and refresh token mechanism.",
|
||||
"type": "task",
|
||||
"status": "todo",
|
||||
"priority": "high",
|
||||
"project": "Gantt Board",
|
||||
"sprint": "current",
|
||||
"assignee": "Max",
|
||||
"due_date": "2026-03-01",
|
||||
"tags": ["auth", "security", "backend"],
|
||||
"comments": "Initial research completed. Ready to implement."
|
||||
},
|
||||
{
|
||||
"title": "Fix drag-and-drop performance",
|
||||
"description": "Tasks stutter when dragging in the Gantt board with >50 items. Need to optimize rendering and use virtual scrolling.",
|
||||
"type": "bug",
|
||||
"status": "open",
|
||||
"priority": "urgent",
|
||||
"project": "Gantt Board",
|
||||
"sprint": "current",
|
||||
"assignee": "Max",
|
||||
"due_date": "2026-02-28",
|
||||
"tags": ["performance", "ui", "frontend"],
|
||||
"comments": "Reported by multiple users. Priority fix needed."
|
||||
},
|
||||
{
|
||||
"title": "Evaluate React Query vs SWR",
|
||||
"description": "Compare React Query and SWR for data fetching. Document pros/cons and migration path.",
|
||||
"type": "research",
|
||||
"status": "open",
|
||||
"priority": "medium",
|
||||
"project": "Gantt Board",
|
||||
"sprint": "current",
|
||||
"assignee": "Matt",
|
||||
"due_date": "2026-03-05",
|
||||
"tags": ["research", "architecture"],
|
||||
"comments": "Part of the Q1 tech stack review."
|
||||
},
|
||||
{
|
||||
"title": "Q2 roadmap planning",
|
||||
"description": "Create detailed roadmap for Q2 including milestones, resource allocation, and key deliverables.",
|
||||
"type": "plan",
|
||||
"status": "todo",
|
||||
"priority": "high",
|
||||
"project": "Gantt Board",
|
||||
"sprint": "current",
|
||||
"assignee": "Max",
|
||||
"due_date": "2026-03-10",
|
||||
"tags": ["planning", "roadmap"],
|
||||
"comments": "Due before end of Q1 review meeting."
|
||||
},
|
||||
{
|
||||
"title": "Dark mode theme",
|
||||
"description": "Implement dark mode toggle with proper contrast ratios and system preference detection.",
|
||||
"type": "idea",
|
||||
"status": "open",
|
||||
"priority": "low",
|
||||
"project": "Gantt Board",
|
||||
"sprint": "",
|
||||
"assignee": "Matt",
|
||||
"tags": ["ui", "ux", "feature"],
|
||||
"comments": "Nice to have feature for accessibility."
|
||||
}
|
||||
]
|
||||
376
scripts/project.sh
Executable file
376
scripts/project.sh
Executable file
@ -0,0 +1,376 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Project CLI for Gantt Board
|
||||
# CRUD operations for projects
|
||||
# Usage: ./project.sh [create|list|get|update|delete] [options]
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SUPABASE_URL="https://qnatchrjlpehiijwtreh.supabase.co"
|
||||
SERVICE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFuYXRjaHJqbHBlaGlpand0cmVoIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3MTY0MDQzNiwiZXhwIjoyMDg3MjE2NDM2fQ.rHoc3NfL59S4lejU4-ArSzox1krQkQG-TnfXb6sslm0"
|
||||
HEADERS=(-H "apikey: $SERVICE_KEY" -H "Authorization: Bearer $SERVICE_KEY" -H "Content-Type: application/json")
|
||||
|
||||
# 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_warning() { echo -e "${YELLOW}⚠${NC} $1"; }
|
||||
log_error() { echo -e "${RED}✗${NC} $1"; }
|
||||
|
||||
# Check dependencies
|
||||
check_dependencies() {
|
||||
if ! command -v jq &> /dev/null; then
|
||||
log_error "jq is required but not installed. Install with: brew install jq"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v uuidgen &> /dev/null; then
|
||||
log_error "uuidgen is required but not installed."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Generate UUID
|
||||
generate_uuid() {
|
||||
uuidgen | tr '[:upper:]' '[:lower:]'
|
||||
}
|
||||
|
||||
# Get current timestamp ISO format
|
||||
get_timestamp() {
|
||||
date -u +"%Y-%m-%dT%H:%M:%SZ"
|
||||
}
|
||||
|
||||
# Show usage
|
||||
show_usage() {
|
||||
cat << EOF
|
||||
Project CLI for Gantt Board
|
||||
|
||||
USAGE:
|
||||
./project.sh [COMMAND] [OPTIONS]
|
||||
|
||||
COMMANDS:
|
||||
create Create a new project
|
||||
list List all projects
|
||||
get <id> Get a specific project by ID or name
|
||||
update <id> Update a project
|
||||
delete <id> Delete a project
|
||||
|
||||
CREATE OPTIONS:
|
||||
--name "Name" Project name (required)
|
||||
--description "Description" Project description
|
||||
--color "#hexcolor" Project color for UI
|
||||
|
||||
LIST OPTIONS:
|
||||
--json Output as JSON
|
||||
|
||||
UPDATE OPTIONS:
|
||||
--name "Name"
|
||||
--description "Description"
|
||||
--color "#hexcolor"
|
||||
|
||||
EXAMPLES:
|
||||
# Create project
|
||||
./project.sh create --name "Web Projects" --description "All web development work"
|
||||
|
||||
# List all active projects
|
||||
./project.sh list
|
||||
|
||||
# Get project by ID
|
||||
./project.sh get a1b2c3d4-0001-0000-0000-000000000001
|
||||
|
||||
# Update project
|
||||
./project.sh update a1b2c3d4-0001-0000-0000-000000000001 --status archived
|
||||
|
||||
# Delete project
|
||||
./project.sh delete a1b2c3d4-0001-0000-0000-000000000001
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# Resolve project name to ID
|
||||
resolve_project_id() {
|
||||
local identifier="$1"
|
||||
|
||||
# Check if it's already a UUID
|
||||
if [[ "$identifier" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
|
||||
echo "$identifier"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Search by name (case-insensitive)
|
||||
local project_id
|
||||
project_id=$(curl -s "${SUPABASE_URL}/rest/v1/projects?name=ilike.*${identifier}*&select=id" \
|
||||
"${HEADERS[@]}" | jq -r '.[0].id // empty')
|
||||
|
||||
if [[ -n "$project_id" ]]; then
|
||||
echo "$project_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try exact match
|
||||
local encoded_name
|
||||
encoded_name=$(printf '%s' "$identifier" | jq -sRr @uri)
|
||||
project_id=$(curl -s "${SUPABASE_URL}/rest/v1/projects?name=eq.${encoded_name}&select=id" \
|
||||
"${HEADERS[@]}" | jq -r '.[0].id // empty')
|
||||
|
||||
if [[ -n "$project_id" ]]; then
|
||||
echo "$project_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_error "Project '$identifier' not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Create command
|
||||
cmd_create() {
|
||||
local name=""
|
||||
local description=""
|
||||
local color="#3b82f6"
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "${1:-}" in
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--description) description="$2"; shift 2 ;;
|
||||
--color) color="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate required fields
|
||||
if [[ -z "$name" ]]; then
|
||||
log_error "Name is required (use --name)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate project data
|
||||
local project_id
|
||||
project_id=$(generate_uuid)
|
||||
local timestamp
|
||||
timestamp=$(get_timestamp)
|
||||
|
||||
# Build JSON payload
|
||||
local json_payload
|
||||
json_payload=$(jq -n \
|
||||
--arg id "$project_id" \
|
||||
--arg name "$name" \
|
||||
--arg color "$color" \
|
||||
--arg created_at "$timestamp" \
|
||||
'{
|
||||
id: $id,
|
||||
name: $name,
|
||||
color: $color,
|
||||
created_at: $created_at
|
||||
}')
|
||||
|
||||
# Add optional fields
|
||||
if [[ -n "$description" ]]; then
|
||||
json_payload=$(echo "$json_payload" | jq --arg v "$description" '. + {description: $v}')
|
||||
fi
|
||||
|
||||
# Create project
|
||||
log_info "Creating project..."
|
||||
local response
|
||||
response=$(curl -s -X POST "${SUPABASE_URL}/rest/v1/projects" \
|
||||
"${HEADERS[@]}" \
|
||||
-d "$json_payload")
|
||||
|
||||
# Supabase returns empty on success, or error JSON on failure
|
||||
if [[ -z "$response" ]]; then
|
||||
log_success "Project created: $project_id"
|
||||
elif [[ "$response" == *"error"* ]] || [[ "$response" == *""*"code"* ]]; then
|
||||
log_error "Failed to create project"
|
||||
echo "$response" | jq . 2>/dev/null || echo "$response"
|
||||
exit 1
|
||||
else
|
||||
log_success "Project created: $project_id"
|
||||
echo "$response" | jq . 2>/dev/null || echo "$response"
|
||||
fi
|
||||
}
|
||||
|
||||
# List command
|
||||
cmd_list() {
|
||||
local output_json=false
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "${1:-}" in
|
||||
--json) output_json=true; shift ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Build query
|
||||
local query="${SUPABASE_URL}/rest/v1/projects?select=*&order=created_at.desc"
|
||||
|
||||
log_info "Fetching projects..."
|
||||
local response
|
||||
response=$(curl -s "$query" "${HEADERS[@]}")
|
||||
|
||||
if [[ "$output_json" == true ]]; then
|
||||
echo "$response" | jq .
|
||||
else
|
||||
# Table output
|
||||
local count
|
||||
count=$(echo "$response" | jq 'length')
|
||||
log_success "Found $count project(s)"
|
||||
|
||||
# Print header
|
||||
printf "%-36s %-25s %-30s\n" "ID" "NAME" "DESCRIPTION"
|
||||
printf "%-36s %-25s %-30s\n" "------------------------------------" "-------------------------" "------------------------------"
|
||||
|
||||
# Print rows
|
||||
echo "$response" | jq -r '.[] | [.id, (.name | tostring | .[0:23]), (.description // "" | tostring | .[0:28])] | @tsv' | while IFS=$'\t' read -r id name desc; do
|
||||
printf "%-36s %-25s %-30s\n" "$id" "$name" "$desc"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# Get command
|
||||
cmd_get() {
|
||||
local identifier="$1"
|
||||
|
||||
if [[ -z "$identifier" ]]; then
|
||||
log_error "Project ID or name required. Usage: ./project.sh get <id-or-name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local project_id
|
||||
project_id=$(resolve_project_id "$identifier")
|
||||
|
||||
log_info "Fetching project $project_id..."
|
||||
local response
|
||||
response=$(curl -s "${SUPABASE_URL}/rest/v1/projects?id=eq.${project_id}&select=*" \
|
||||
"${HEADERS[@]}")
|
||||
|
||||
local project
|
||||
project=$(echo "$response" | jq '.[0] // empty')
|
||||
|
||||
if [[ -n "$project" && "$project" != "{}" && "$project" != "null" ]]; then
|
||||
echo "$project" | jq .
|
||||
else
|
||||
log_error "Project not found: $identifier"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Update command
|
||||
cmd_update() {
|
||||
local identifier="$1"
|
||||
shift
|
||||
|
||||
if [[ -z "$identifier" ]]; then
|
||||
log_error "Project ID or name required. Usage: ./project.sh update <id-or-name> [options]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local project_id
|
||||
project_id=$(resolve_project_id "$identifier")
|
||||
|
||||
# Build update payload
|
||||
local update_fields="{}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "${1:-}" in
|
||||
--name) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {name: $v}'); shift 2 ;;
|
||||
--description) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {description: $v}'); shift 2 ;;
|
||||
--color) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {color: $v}'); shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check if we have anything to update
|
||||
if [[ "$update_fields" == "{}" ]]; then
|
||||
log_warning "No update fields specified"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Add updated_at timestamp
|
||||
local timestamp
|
||||
timestamp=$(get_timestamp)
|
||||
update_fields=$(echo "$update_fields" | jq --arg t "$timestamp" '. + {updated_at: $t}')
|
||||
|
||||
log_info "Updating project $project_id..."
|
||||
local response
|
||||
response=$(curl -s -X PATCH "${SUPABASE_URL}/rest/v1/projects?id=eq.${project_id}" \
|
||||
"${HEADERS[@]}" \
|
||||
-d "$update_fields")
|
||||
|
||||
if [[ -z "$response" || "$response" == "[]" || ! "$response" == *"error"* ]]; then
|
||||
log_success "Project updated: $project_id"
|
||||
else
|
||||
log_error "Failed to update project"
|
||||
echo "$response" | jq . 2>/dev/null || echo "$response"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Delete command
|
||||
cmd_delete() {
|
||||
local identifier="$1"
|
||||
|
||||
if [[ -z "$identifier" ]]; then
|
||||
log_error "Project ID or name required. Usage: ./project.sh delete <id-or-name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local project_id
|
||||
project_id=$(resolve_project_id "$identifier")
|
||||
|
||||
log_info "Deleting project $project_id..."
|
||||
local response
|
||||
response=$(curl -s -X DELETE "${SUPABASE_URL}/rest/v1/projects?id=eq.${project_id}" \
|
||||
"${HEADERS[@]}")
|
||||
|
||||
if [[ -z "$response" || "$response" == "[]" ]]; then
|
||||
log_success "Project deleted: $project_id"
|
||||
else
|
||||
log_error "Failed to delete project"
|
||||
echo "$response" | jq . 2>/dev/null || echo "$response"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution
|
||||
check_dependencies
|
||||
|
||||
# Parse command
|
||||
COMMAND="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$COMMAND" in
|
||||
create)
|
||||
cmd_create "$@"
|
||||
;;
|
||||
list)
|
||||
cmd_list "$@"
|
||||
;;
|
||||
get)
|
||||
cmd_get "$@"
|
||||
;;
|
||||
update)
|
||||
cmd_update "$@"
|
||||
;;
|
||||
delete)
|
||||
cmd_delete "$@"
|
||||
;;
|
||||
help|--help|-h)
|
||||
show_usage
|
||||
;;
|
||||
"")
|
||||
show_usage
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown command: $COMMAND"
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
511
scripts/sprint.sh
Executable file
511
scripts/sprint.sh
Executable file
@ -0,0 +1,511 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Sprint CLI for Gantt Board
|
||||
# CRUD operations for sprints
|
||||
# Usage: ./sprint.sh [create|list|get|update|delete|close] [options]
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SUPABASE_URL="https://qnatchrjlpehiijwtreh.supabase.co"
|
||||
SERVICE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFuYXRjaHJqbHBlaGlpand0cmVoIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc3MTY0MDQzNiwiZXhwIjoyMDg3MjE2NDM2fQ.rHoc3NfL59S4lejU4-ArSzox1krQkQG-TnfXb6sslm0"
|
||||
HEADERS=(-H "apikey: $SERVICE_KEY" -H "Authorization: Bearer $SERVICE_KEY" -H "Content-Type: application/json")
|
||||
|
||||
# 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_warning() { echo -e "${YELLOW}⚠${NC} $1"; }
|
||||
log_error() { echo -e "${RED}✗${NC} $1"; }
|
||||
|
||||
# Check dependencies
|
||||
check_dependencies() {
|
||||
if ! command -v jq &> /dev/null; then
|
||||
log_error "jq is required but not installed. Install with: brew install jq"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v uuidgen &> /dev/null; then
|
||||
log_error "uuidgen is required but not installed."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Generate UUID
|
||||
generate_uuid() {
|
||||
uuidgen | tr '[:upper:]' '[:lower:]'
|
||||
}
|
||||
|
||||
# Get current timestamp ISO format
|
||||
get_timestamp() {
|
||||
date -u +"%Y-%m-%dT%H:%M:%SZ"
|
||||
}
|
||||
|
||||
# Show usage
|
||||
show_usage() {
|
||||
cat << EOF
|
||||
Sprint CLI for Gantt Board
|
||||
|
||||
USAGE:
|
||||
./sprint.sh [COMMAND] [OPTIONS]
|
||||
|
||||
COMMANDS:
|
||||
create Create a new sprint
|
||||
list List all sprints
|
||||
get <id> Get a specific sprint by ID or name
|
||||
update <id> Update a sprint
|
||||
delete <id> Delete a sprint
|
||||
close <id> Close/complete a sprint
|
||||
|
||||
CREATE OPTIONS:
|
||||
--name "Name" Sprint name (required)
|
||||
--project "Project" Project name or ID (required)
|
||||
--goal "Goal" Sprint goal/description
|
||||
--start-date "YYYY-MM-DD" Start date
|
||||
--end-date "YYYY-MM-DD" End date
|
||||
--status [planning|active|closed|completed] Status (default: planning)
|
||||
|
||||
LIST OPTIONS:
|
||||
--status <status> Filter by status
|
||||
--project <project> Filter by project name/ID
|
||||
--active Show only active sprints
|
||||
--json Output as JSON
|
||||
|
||||
UPDATE OPTIONS:
|
||||
--name "Name"
|
||||
--goal "Goal"
|
||||
--start-date "YYYY-MM-DD"
|
||||
--end-date "YYYY-MM-DD"
|
||||
--status <status>
|
||||
|
||||
EXAMPLES:
|
||||
# Create sprint
|
||||
./sprint.sh create --name "Sprint 1" --project "Web Projects" \
|
||||
--goal "Complete MVP features" --start-date 2026-02-24 --end-date 2026-03-07
|
||||
|
||||
# List active sprints
|
||||
./sprint.sh list --active
|
||||
|
||||
# Get sprint by name
|
||||
./sprint.sh get "Sprint 1"
|
||||
|
||||
# Close a sprint (marks as completed)
|
||||
./sprint.sh close "Sprint 1"
|
||||
|
||||
# Delete sprint
|
||||
./sprint.sh delete <sprint-id>
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# Resolve project name to ID
|
||||
resolve_project_id() {
|
||||
local identifier="$1"
|
||||
|
||||
# Check if it's already a UUID
|
||||
if [[ "$identifier" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
|
||||
echo "$identifier"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Search by name (case-insensitive)
|
||||
local project_id
|
||||
project_id=$(curl -s "${SUPABASE_URL}/rest/v1/projects?name=ilike.*${identifier}*&select=id" \
|
||||
"${HEADERS[@]}" | jq -r '.[0].id // empty')
|
||||
|
||||
if [[ -n "$project_id" ]]; then
|
||||
echo "$project_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try exact match
|
||||
local encoded_name
|
||||
encoded_name=$(printf '%s' "$identifier" | jq -sRr @uri)
|
||||
project_id=$(curl -s "${SUPABASE_URL}/rest/v1/projects?name=eq.${encoded_name}&select=id" \
|
||||
"${HEADERS[@]}" | jq -r '.[0].id // empty')
|
||||
|
||||
if [[ -n "$project_id" ]]; then
|
||||
echo "$project_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_error "Project '$identifier' not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Resolve sprint identifier to ID
|
||||
resolve_sprint_id() {
|
||||
local identifier="$1"
|
||||
|
||||
# Check if it's already a UUID
|
||||
if [[ "$identifier" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
|
||||
echo "$identifier"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Search by name (case-insensitive)
|
||||
local sprint_id
|
||||
sprint_id=$(curl -s "${SUPABASE_URL}/rest/v1/sprints?name=ilike.*${identifier}*&select=id" \
|
||||
"${HEADERS[@]}" | jq -r '.[0].id // empty')
|
||||
|
||||
if [[ -n "$sprint_id" ]]; then
|
||||
echo "$sprint_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try exact match
|
||||
local encoded_name
|
||||
encoded_name=$(printf '%s' "$identifier" | jq -sRr @uri)
|
||||
sprint_id=$(curl -s "${SUPABASE_URL}/rest/v1/sprints?name=eq.${encoded_name}&select=id" \
|
||||
"${HEADERS[@]}" | jq -r '.[0].id // empty')
|
||||
|
||||
if [[ -n "$sprint_id" ]]; then
|
||||
echo "$sprint_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_error "Sprint '$identifier' not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Create command
|
||||
cmd_create() {
|
||||
local name=""
|
||||
local project=""
|
||||
local goal=""
|
||||
local start_date=""
|
||||
local end_date=""
|
||||
local status="planning"
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "${1:-}" in
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--project) project="$2"; shift 2 ;;
|
||||
--goal) goal="$2"; shift 2 ;;
|
||||
--start-date) start_date="$2"; shift 2 ;;
|
||||
--end-date) end_date="$2"; shift 2 ;;
|
||||
--status) status="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate required fields
|
||||
if [[ -z "$name" ]]; then
|
||||
log_error "Name is required (use --name)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$project" ]]; then
|
||||
log_error "Project is required (use --project)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Resolve project
|
||||
local project_id
|
||||
project_id=$(resolve_project_id "$project")
|
||||
|
||||
# Generate sprint data
|
||||
local sprint_id
|
||||
sprint_id=$(generate_uuid)
|
||||
local timestamp
|
||||
timestamp=$(get_timestamp)
|
||||
|
||||
# Build JSON payload
|
||||
local json_payload
|
||||
json_payload=$(jq -n \
|
||||
--arg id "$sprint_id" \
|
||||
--arg name "$name" \
|
||||
--arg project_id "$project_id" \
|
||||
--arg status "$status" \
|
||||
--arg created_at "$timestamp" \
|
||||
'{
|
||||
id: $id,
|
||||
name: $name,
|
||||
project_id: $project_id,
|
||||
status: $status,
|
||||
created_at: $created_at
|
||||
}')
|
||||
|
||||
# Add optional fields
|
||||
if [[ -n "$goal" ]]; then
|
||||
json_payload=$(echo "$json_payload" | jq --arg v "$goal" '. + {goal: $v}')
|
||||
fi
|
||||
|
||||
if [[ -n "$start_date" ]]; then
|
||||
json_payload=$(echo "$json_payload" | jq --arg v "$start_date" '. + {start_date: $v}')
|
||||
fi
|
||||
|
||||
if [[ -n "$end_date" ]]; then
|
||||
json_payload=$(echo "$json_payload" | jq --arg v "$end_date" '. + {end_date: $v}')
|
||||
fi
|
||||
|
||||
# Create sprint
|
||||
log_info "Creating sprint..."
|
||||
local response
|
||||
response=$(curl -s -X POST "${SUPABASE_URL}/rest/v1/sprints" \
|
||||
"${HEADERS[@]}" \
|
||||
-d "$json_payload")
|
||||
|
||||
# Supabase returns empty on success, or error JSON on failure
|
||||
if [[ -z "$response" ]]; then
|
||||
log_success "Sprint created: $sprint_id"
|
||||
elif [[ "$response" == *"error"* ]] || [[ "$response" == *'"code"'* ]]; then
|
||||
log_error "Failed to create sprint"
|
||||
echo "$response" | jq . 2>/dev/null || echo "$response"
|
||||
exit 1
|
||||
else
|
||||
log_success "Sprint created: $sprint_id"
|
||||
echo "$response" | jq . 2>/dev/null || echo "$response"
|
||||
fi
|
||||
}
|
||||
|
||||
# List command
|
||||
cmd_list() {
|
||||
local filter_status=""
|
||||
local filter_project=""
|
||||
local active_only=false
|
||||
local output_json=false
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "${1:-}" in
|
||||
--status) filter_status="$2"; shift 2 ;;
|
||||
--project) filter_project="$2"; shift 2 ;;
|
||||
--active) active_only=true; shift ;;
|
||||
--json) output_json=true; shift ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Build query
|
||||
local query="${SUPABASE_URL}/rest/v1/sprints?select=*,projects(name)&order=created_at.desc"
|
||||
|
||||
if [[ "$active_only" == true ]]; then
|
||||
filter_status="active"
|
||||
fi
|
||||
|
||||
if [[ -n "$filter_status" ]]; then
|
||||
local encoded_status
|
||||
encoded_status=$(printf '%s' "$filter_status" | jq -sRr @uri)
|
||||
query="${query}&status=eq.${encoded_status}"
|
||||
fi
|
||||
|
||||
if [[ -n "$filter_project" ]]; then
|
||||
if [[ ! "$filter_project" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
|
||||
filter_project=$(resolve_project_id "$filter_project")
|
||||
fi
|
||||
query="${query}&project_id=eq.${filter_project}"
|
||||
fi
|
||||
|
||||
log_info "Fetching sprints..."
|
||||
local response
|
||||
response=$(curl -s "$query" "${HEADERS[@]}")
|
||||
|
||||
if [[ "$output_json" == true ]]; then
|
||||
echo "$response" | jq .
|
||||
else
|
||||
# Table output
|
||||
local count
|
||||
count=$(echo "$response" | jq 'length')
|
||||
log_success "Found $count sprint(s)"
|
||||
|
||||
# Print header
|
||||
printf "%-36s %-25s %-12s %-10s %-12s %-12s\n" "ID" "NAME" "PROJECT" "STATUS" "START" "END"
|
||||
printf "%-36s %-25s %-12s %-10s %-12s %-12s\n" "------------------------------------" "-------------------------" "------------" "----------" "------------" "------------"
|
||||
|
||||
# Print rows
|
||||
echo "$response" | jq -r '.[] | [.id, (.name | tostring | .[0:23]), (.projects.name // "N/A" | tostring | .[0:10]), .status, (.start_date // "N/A"), (.end_date // "N/A")] | @tsv' | while IFS=$'\t' read -r id name project status start end; do
|
||||
printf "%-36s %-25s %-12s %-10s %-12s %-12s\n" "$id" "$name" "$project" "$status" "$start" "$end"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# Get command
|
||||
cmd_get() {
|
||||
local identifier="$1"
|
||||
|
||||
if [[ -z "$identifier" ]]; then
|
||||
log_error "Sprint ID or name required. Usage: ./sprint.sh get <id-or-name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local sprint_id
|
||||
sprint_id=$(resolve_sprint_id "$identifier")
|
||||
|
||||
log_info "Fetching sprint $sprint_id..."
|
||||
local response
|
||||
response=$(curl -s "${SUPABASE_URL}/rest/v1/sprints?id=eq.${sprint_id}&select=*,projects(name)" \
|
||||
"${HEADERS[@]}")
|
||||
|
||||
local sprint
|
||||
sprint=$(echo "$response" | jq '.[0] // empty')
|
||||
|
||||
if [[ -n "$sprint" && "$sprint" != "{}" && "$sprint" != "null" ]]; then
|
||||
echo "$sprint" | jq .
|
||||
else
|
||||
log_error "Sprint not found: $identifier"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Update command
|
||||
cmd_update() {
|
||||
local identifier="$1"
|
||||
shift
|
||||
|
||||
if [[ -z "$identifier" ]]; then
|
||||
log_error "Sprint ID or name required. Usage: ./sprint.sh update <id-or-name> [options]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local sprint_id
|
||||
sprint_id=$(resolve_sprint_id "$identifier")
|
||||
|
||||
# Build update payload
|
||||
local update_fields="{}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "${1:-}" in
|
||||
--name) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {name: $v}'); shift 2 ;;
|
||||
--goal) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {goal: $v}'); shift 2 ;;
|
||||
--start-date) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {start_date: $v}'); shift 2 ;;
|
||||
--end-date) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {end_date: $v}'); shift 2 ;;
|
||||
--status) update_fields=$(echo "$update_fields" | jq --arg v "$2" '. + {status: $v}'); shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check if we have anything to update
|
||||
if [[ "$update_fields" == "{}" ]]; then
|
||||
log_warning "No update fields specified"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Add updated_at timestamp
|
||||
local timestamp
|
||||
timestamp=$(get_timestamp)
|
||||
update_fields=$(echo "$update_fields" | jq --arg t "$timestamp" '. + {updated_at: $t}')
|
||||
|
||||
log_info "Updating sprint $sprint_id..."
|
||||
local response
|
||||
response=$(curl -s -X PATCH "${SUPABASE_URL}/rest/v1/sprints?id=eq.${sprint_id}" \
|
||||
"${HEADERS[@]}" \
|
||||
-d "$update_fields")
|
||||
|
||||
if [[ -z "$response" || "$response" == "[]" || ! "$response" == *"error"* ]]; then
|
||||
log_success "Sprint updated: $sprint_id"
|
||||
else
|
||||
log_error "Failed to update sprint"
|
||||
echo "$response" | jq . 2>/dev/null || echo "$response"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Close command - marks sprint as completed
|
||||
cmd_close() {
|
||||
local identifier="$1"
|
||||
|
||||
if [[ -z "$identifier" ]]; then
|
||||
log_error "Sprint ID or name required. Usage: ./sprint.sh close <id-or-name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local sprint_id
|
||||
sprint_id=$(resolve_sprint_id "$identifier")
|
||||
|
||||
log_info "Closing sprint $sprint_id..."
|
||||
local timestamp
|
||||
timestamp=$(get_timestamp)
|
||||
|
||||
local update_fields
|
||||
update_fields=$(jq -n \
|
||||
--arg status "completed" \
|
||||
--arg closed_at "$timestamp" \
|
||||
--arg updated_at "$timestamp" \
|
||||
'{status: $status, closed_at: $closed_at, updated_at: $updated_at}')
|
||||
|
||||
local response
|
||||
response=$(curl -s -X PATCH "${SUPABASE_URL}/rest/v1/sprints?id=eq.${sprint_id}" \
|
||||
"${HEADERS[@]}" \
|
||||
-d "$update_fields")
|
||||
|
||||
if [[ -z "$response" || "$response" == "[]" || ! "$response" == *"error"* ]]; then
|
||||
log_success "Sprint closed: $sprint_id"
|
||||
else
|
||||
log_error "Failed to close sprint"
|
||||
echo "$response" | jq . 2>/dev/null || echo "$response"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Delete command
|
||||
cmd_delete() {
|
||||
local identifier="$1"
|
||||
|
||||
if [[ -z "$identifier" ]]; then
|
||||
log_error "Sprint ID or name required. Usage: ./sprint.sh delete <id-or-name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local sprint_id
|
||||
sprint_id=$(resolve_sprint_id "$identifier")
|
||||
|
||||
log_info "Deleting sprint $sprint_id..."
|
||||
local response
|
||||
response=$(curl -s -X DELETE "${SUPABASE_URL}/rest/v1/sprints?id=eq.${sprint_id}" \
|
||||
"${HEADERS[@]}")
|
||||
|
||||
if [[ -z "$response" || "$response" == "[]" ]]; then
|
||||
log_success "Sprint deleted: $sprint_id"
|
||||
else
|
||||
log_error "Failed to delete sprint"
|
||||
echo "$response" | jq . 2>/dev/null || echo "$response"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution
|
||||
check_dependencies
|
||||
|
||||
# Parse command
|
||||
COMMAND="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$COMMAND" in
|
||||
create)
|
||||
cmd_create "$@"
|
||||
;;
|
||||
list)
|
||||
cmd_list "$@"
|
||||
;;
|
||||
get)
|
||||
cmd_get "$@"
|
||||
;;
|
||||
update)
|
||||
cmd_update "$@"
|
||||
;;
|
||||
close)
|
||||
cmd_close "$@"
|
||||
;;
|
||||
delete)
|
||||
cmd_delete "$@"
|
||||
;;
|
||||
help|--help|-h)
|
||||
show_usage
|
||||
;;
|
||||
"")
|
||||
show_usage
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown command: $COMMAND"
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
1
scripts/task.sh
Executable file
1
scripts/task.sh
Executable file
@ -0,0 +1 @@
|
||||
zsh:1: command not found: update
|
||||
@ -1313,11 +1313,14 @@ export default function Home() {
|
||||
className="w-full mt-1.5 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
|
||||
>
|
||||
<option value="">Auto (Current Sprint)</option>
|
||||
{sprints.sort((a, b) => parseSprintStart(a.startDate).getTime() - parseSprintStart(b.startDate).getTime()).map((sprint) => (
|
||||
<option key={sprint.id} value={sprint.id}>
|
||||
{sprint.name} ({formatSprintDisplayDate(sprint.startDate, "start")})
|
||||
</option>
|
||||
))}
|
||||
{sprints
|
||||
.filter((sprint) => sprint.status !== "completed")
|
||||
.sort((a, b) => parseSprintStart(a.startDate).getTime() - parseSprintStart(b.startDate).getTime())
|
||||
.map((sprint) => (
|
||||
<option key={sprint.id} value={sprint.id}>
|
||||
{sprint.name} ({formatSprintDisplayDate(sprint.startDate, "start")})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@ -1530,11 +1533,14 @@ export default function Home() {
|
||||
className="w-full mt-2 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
|
||||
>
|
||||
<option value="">No Sprint</option>
|
||||
{sprints.sort((a, b) => parseSprintStart(a.startDate).getTime() - parseSprintStart(b.startDate).getTime()).map((sprint) => (
|
||||
<option key={sprint.id} value={sprint.id}>
|
||||
{sprint.name} ({formatSprintDisplayDate(sprint.startDate, "start")})
|
||||
</option>
|
||||
))}
|
||||
{sprints
|
||||
.filter((sprint) => sprint.status !== "completed")
|
||||
.sort((a, b) => parseSprintStart(a.startDate).getTime() - parseSprintStart(b.startDate).getTime())
|
||||
.map((sprint) => (
|
||||
<option key={sprint.id} value={sprint.id}>
|
||||
{sprint.name} ({formatSprintDisplayDate(sprint.startDate, "start")})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
@ -123,13 +123,13 @@ const addReplyToThread = (comments: TaskComment[], parentId: string, reply: Task
|
||||
if (comment.id === parentId) {
|
||||
return {
|
||||
...comment,
|
||||
replies: [...getComments(comment.replies), reply],
|
||||
replies: [...(comment.replies || []), reply],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...comment,
|
||||
replies: addReplyToThread(getComments(comment.replies), parentId, reply),
|
||||
replies: addReplyToThread(comment.replies || [], parentId, reply),
|
||||
}
|
||||
})
|
||||
|
||||
@ -138,11 +138,11 @@ const removeCommentFromThread = (comments: TaskComment[], targetId: string): Tas
|
||||
.filter((comment) => comment.id !== targetId)
|
||||
.map((comment) => ({
|
||||
...comment,
|
||||
replies: removeCommentFromThread(getComments(comment.replies), targetId),
|
||||
replies: removeCommentFromThread(comment.replies || [], targetId),
|
||||
}))
|
||||
|
||||
const countThreadComments = (comments: TaskComment[]): number =>
|
||||
comments.reduce((total, comment) => total + 1 + countThreadComments(getComments(comment.replies)), 0)
|
||||
comments.reduce((total, comment) => total + 1 + countThreadComments(comment.replies || []), 0)
|
||||
|
||||
const toLabel = (raw: string) => raw.trim().replace(/^#/, "")
|
||||
|
||||
@ -293,7 +293,7 @@ export default function TaskDetailPage() {
|
||||
setEditedTask({
|
||||
...selectedTask,
|
||||
tags: getTags(selectedTask),
|
||||
comments: getComments(selectedTask.comments),
|
||||
comments: Array.isArray(selectedTask.comments) ? selectedTask.comments : [],
|
||||
attachments: getAttachments(selectedTask),
|
||||
})
|
||||
setEditedTaskLabelInput("")
|
||||
@ -302,7 +302,6 @@ export default function TaskDetailPage() {
|
||||
|
||||
const editedTaskTags = editedTask ? getTags(editedTask) : []
|
||||
const editedTaskAttachments = editedTask ? getAttachments(editedTask) : []
|
||||
const commentCount = editedTask ? countThreadComments(getComments(editedTask.comments)) : 0
|
||||
|
||||
const allLabels = useMemo(() => {
|
||||
const labels = new Map<string, number>()
|
||||
@ -404,16 +403,13 @@ export default function TaskDetailPage() {
|
||||
|
||||
const nextTask: Task = {
|
||||
...editedTask,
|
||||
comments: [...getComments(editedTask.comments), buildComment(newComment.trim(), commentAuthorId)],
|
||||
comments: [...editedTask.comments, buildComment(newComment.trim(), commentAuthorId)],
|
||||
}
|
||||
|
||||
setEditedTask(nextTask)
|
||||
setNewComment("")
|
||||
|
||||
const success = await updateTask(nextTask.id, {
|
||||
...nextTask,
|
||||
comments: getComments(nextTask.comments),
|
||||
})
|
||||
const success = await updateTask(nextTask.id, nextTask)
|
||||
if (!success) {
|
||||
toast.error("Failed to save comment", {
|
||||
description: "Comment was added locally but could not sync to the server.",
|
||||
@ -434,7 +430,7 @@ export default function TaskDetailPage() {
|
||||
}
|
||||
const nextTask: Task = {
|
||||
...editedTask,
|
||||
comments: addReplyToThread(getComments(editedTask.comments), parentId, buildComment(text, commentAuthorId)),
|
||||
comments: addReplyToThread(editedTask.comments, parentId, buildComment(text, commentAuthorId)),
|
||||
}
|
||||
|
||||
setEditedTask(nextTask)
|
||||
@ -442,10 +438,7 @@ export default function TaskDetailPage() {
|
||||
setReplyDrafts((prev) => ({ ...prev, [parentId]: "" }))
|
||||
setOpenReplyEditors((prev) => ({ ...prev, [parentId]: false }))
|
||||
|
||||
const success = await updateTask(nextTask.id, {
|
||||
...nextTask,
|
||||
comments: getComments(nextTask.comments),
|
||||
})
|
||||
const success = await updateTask(nextTask.id, nextTask)
|
||||
if (!success) {
|
||||
toast.error("Failed to save reply", {
|
||||
description: "Reply was added locally but could not sync to the server.",
|
||||
@ -459,7 +452,7 @@ export default function TaskDetailPage() {
|
||||
|
||||
setEditedTask({
|
||||
...editedTask,
|
||||
comments: removeCommentFromThread(getComments(editedTask.comments), commentId),
|
||||
comments: removeCommentFromThread(editedTask.comments, commentId),
|
||||
})
|
||||
}
|
||||
|
||||
@ -535,10 +528,7 @@ export default function TaskDetailPage() {
|
||||
setSaveSuccess(false)
|
||||
|
||||
try {
|
||||
const success = await updateTask(editedTask.id, {
|
||||
...editedTask,
|
||||
comments: getComments(editedTask.comments),
|
||||
})
|
||||
const success = await updateTask(editedTask.id, editedTask)
|
||||
|
||||
if (success) {
|
||||
setSaveSuccess(true)
|
||||
@ -614,7 +604,7 @@ export default function TaskDetailPage() {
|
||||
|
||||
const renderThread = (comments: TaskComment[], depth = 0) =>
|
||||
comments.map((comment) => {
|
||||
const replies = getComments(comment.replies)
|
||||
const replies = comment.replies || []
|
||||
const isReplying = !!openReplyEditors[comment.id]
|
||||
const replyDraft = replyDrafts[comment.id] || ""
|
||||
const authorId = comment.commentAuthorId
|
||||
@ -1033,14 +1023,14 @@ export default function TaskDetailPage() {
|
||||
<div className="border-t border-slate-800 pt-6">
|
||||
<h4 className="font-medium text-white mb-4 flex items-center gap-2">
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
Comments ({commentCount})
|
||||
Comments ({editedTask.comments.length})
|
||||
</h4>
|
||||
|
||||
<div className="space-y-3 mb-4">
|
||||
{commentCount === 0 ? (
|
||||
{editedTask.comments.length === 0 ? (
|
||||
<p className="text-slate-500 text-sm">No comments yet. Add the first one.</p>
|
||||
) : (
|
||||
renderThread(getComments(editedTask.comments))
|
||||
renderThread(editedTask.comments)
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user