A per-DataDir background process owns ticket JSON, history, and sync so multiple TUI instances stay consistent; MCP token APIs are ready for phase 2. Co-authored-by: Cursor <cursoragent@cursor.com>
160 lines
4.6 KiB
Go
160 lines
4.6 KiB
Go
package client
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"task/internal/project"
|
|
"task/internal/ticket"
|
|
)
|
|
|
|
// TicketStore implements ticket.Store against the daemon (reads + MarkSynced).
|
|
// Mutations used by the TUI go through Executor; Create/Update/Delete/Move here
|
|
// call the daemon mutation endpoints (with undo) for completeness.
|
|
type TicketStore struct{ c *Client }
|
|
|
|
func (c *Client) TicketStore() *TicketStore { return &TicketStore{c: c} }
|
|
|
|
func (s *TicketStore) List(projectID string) ([]ticket.Ticket, error) {
|
|
var out []ticket.Ticket
|
|
q := url.Values{"projectId": {projectID}}.Encode()
|
|
err := s.c.do(http.MethodGet, "/v1/tickets?"+q, nil, &out)
|
|
if out == nil {
|
|
out = []ticket.Ticket{}
|
|
}
|
|
return out, err
|
|
}
|
|
|
|
func (s *TicketStore) Get(id string) (ticket.Ticket, error) {
|
|
var out ticket.Ticket
|
|
err := s.c.do(http.MethodGet, "/v1/tickets/"+url.PathEscape(id), nil, &out)
|
|
return out, err
|
|
}
|
|
|
|
func (s *TicketStore) Create(t ticket.Ticket) (ticket.Ticket, error) {
|
|
var out ticket.Ticket
|
|
err := s.c.do(http.MethodPost, "/v1/tickets", t, &out)
|
|
return out, err
|
|
}
|
|
|
|
func (s *TicketStore) Update(t ticket.Ticket) error {
|
|
return s.c.do(http.MethodPatch, "/v1/tickets/"+url.PathEscape(t.ID), t, nil)
|
|
}
|
|
|
|
func (s *TicketStore) MarkSynced(id string) error {
|
|
return fmt.Errorf("MarkSynced is daemon-internal (use sync endpoints)")
|
|
}
|
|
|
|
func (s *TicketStore) Move(id, toProjectID string) (ticket.Ticket, error) {
|
|
var out ticket.Ticket
|
|
err := s.c.do(http.MethodPost, "/v1/tickets/"+url.PathEscape(id)+"/move", map[string]string{
|
|
"toProjectId": toProjectID,
|
|
}, &out)
|
|
return out, err
|
|
}
|
|
|
|
func (s *TicketStore) Delete(id string) error {
|
|
return s.c.do(http.MethodDelete, "/v1/tickets/"+url.PathEscape(id), nil, nil)
|
|
}
|
|
|
|
// TimeStore implements ticket.TimeStore.
|
|
type TimeStore struct{ c *Client }
|
|
|
|
func (c *Client) TimeStore() *TimeStore { return &TimeStore{c: c} }
|
|
|
|
func (s *TimeStore) List() ([]ticket.TimeEntry, error) {
|
|
var out []ticket.TimeEntry
|
|
err := s.c.do(http.MethodGet, "/v1/time", nil, &out)
|
|
if out == nil {
|
|
out = []ticket.TimeEntry{}
|
|
}
|
|
return out, err
|
|
}
|
|
|
|
func (s *TimeStore) ListByTicket(ticketID string) ([]ticket.TimeEntry, error) {
|
|
var out []ticket.TimeEntry
|
|
err := s.c.do(http.MethodGet, "/v1/time/by-ticket/"+url.PathEscape(ticketID), nil, &out)
|
|
if out == nil {
|
|
out = []ticket.TimeEntry{}
|
|
}
|
|
return out, err
|
|
}
|
|
|
|
func (s *TimeStore) Create(e ticket.TimeEntry) (ticket.TimeEntry, error) {
|
|
var out ticket.TimeEntry
|
|
err := s.c.do(http.MethodPost, "/v1/time", e, &out)
|
|
return out, err
|
|
}
|
|
|
|
func (s *TimeStore) Update(e ticket.TimeEntry) error {
|
|
return s.c.do(http.MethodPatch, "/v1/time/"+url.PathEscape(e.ID), e, nil)
|
|
}
|
|
|
|
func (s *TimeStore) Delete(id string) error {
|
|
return s.c.do(http.MethodDelete, "/v1/time/"+url.PathEscape(id), nil, nil)
|
|
}
|
|
|
|
func (s *TimeStore) DeleteByProject(projectID string) error {
|
|
// Composite delete is done server-side on project delete; not exposed alone.
|
|
return fmt.Errorf("DeleteByProject is daemon-internal")
|
|
}
|
|
|
|
func (s *TimeStore) RetargetTicket(ticketID, projectID string) error {
|
|
return fmt.Errorf("RetargetTicket is daemon-internal")
|
|
}
|
|
|
|
func (s *TimeStore) SumByTicket(ticketID string) (time.Duration, error) {
|
|
var out struct {
|
|
Duration int64 `json:"duration"`
|
|
}
|
|
err := s.c.do(http.MethodGet, "/v1/time/sum/"+url.PathEscape(ticketID), nil, &out)
|
|
return time.Duration(out.Duration), err
|
|
}
|
|
|
|
func (s *TimeStore) LoadActiveTimer() (*ticket.ActiveTimer, error) {
|
|
var out *ticket.ActiveTimer
|
|
err := s.c.do(http.MethodGet, "/v1/timer", nil, &out)
|
|
return out, err
|
|
}
|
|
|
|
func (s *TimeStore) SaveActiveTimer(t *ticket.ActiveTimer) error {
|
|
if t == nil {
|
|
return s.c.do(http.MethodPut, "/v1/timer", map[string]any{}, nil)
|
|
}
|
|
return s.c.do(http.MethodPut, "/v1/timer", t, nil)
|
|
}
|
|
|
|
// ProjectStore implements project.Store.
|
|
type ProjectStore struct{ c *Client }
|
|
|
|
func (c *Client) ProjectStore() *ProjectStore { return &ProjectStore{c: c} }
|
|
|
|
func (s *ProjectStore) List() ([]project.Project, error) {
|
|
var out []project.Project
|
|
err := s.c.do(http.MethodGet, "/v1/projects", nil, &out)
|
|
if out == nil {
|
|
out = []project.Project{}
|
|
}
|
|
return out, err
|
|
}
|
|
|
|
func (s *ProjectStore) Ensure(id string) error {
|
|
return s.c.do(http.MethodPost, "/v1/projects", map[string]string{"id": id}, nil)
|
|
}
|
|
|
|
func (s *ProjectStore) Delete(id string) error {
|
|
return s.c.do(http.MethodDelete, "/v1/projects/"+url.PathEscape(id), nil, nil)
|
|
}
|
|
|
|
func (s *ProjectStore) LoadMeta(id string) (project.Meta, error) {
|
|
var out project.Meta
|
|
err := s.c.do(http.MethodGet, "/v1/projects/"+url.PathEscape(id)+"/meta", nil, &out)
|
|
return out, err
|
|
}
|
|
|
|
func (s *ProjectStore) SaveMeta(id string, meta project.Meta) error {
|
|
return s.c.do(http.MethodPut, "/v1/projects/"+url.PathEscape(id)+"/meta", meta, nil)
|
|
}
|