Add YouTrack pull/push sync with project settings and time tracking.
CI / test-and-build (push) Failing after 1m19s
CI / demo (push) Skipped

Wire manual Y menu and CLI sync for issues and work items, persist
per-project credentials, and keep local edits until an explicit push.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-24 00:17:51 +02:00
co-authored by Cursor
parent 19ae1f1b62
commit 98d2253f44
23 changed files with 2874 additions and 44 deletions
+38
View File
@@ -134,6 +134,8 @@ func (s *JSONStore) Update(t Ticket) error {
for i := range tickets {
if tickets[i].ID == t.ID {
t.CreatedAt = tickets[i].CreatedAt
// Always bump UpdatedAt on local edits so NeedsPush sees dirty tickets.
// Use MarkSynced after a successful remote sync to clear the dirty flag.
t.UpdatedAt = time.Now().UTC()
tickets[i] = t
found = true
@@ -146,6 +148,42 @@ func (s *JSONStore) Update(t Ticket) error {
return s.writeUnlocked(t.ProjectID, tickets)
}
// MarkSynced sets SyncedAt = UpdatedAt without changing UpdatedAt.
func (s *JSONStore) MarkSynced(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
if id == "" {
return fmt.Errorf("ticket id is required")
}
projects, err := os.ReadDir(filepath.Join(s.dataDir, "projects"))
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("ticket %s: not found", id)
}
return fmt.Errorf("list projects: %w", err)
}
for _, p := range projects {
if !p.IsDir() {
continue
}
tickets, err := s.readUnlocked(p.Name())
if err != nil {
return err
}
for i := range tickets {
if tickets[i].ID != id {
continue
}
tickets[i].SyncedAt = tickets[i].UpdatedAt
return s.writeUnlocked(p.Name(), tickets)
}
}
return fmt.Errorf("ticket %s: not found", id)
}
// Move relocates a ticket to another project directory.
func (s *JSONStore) Move(id, toProjectID string) (Ticket, error) {
s.mu.Lock()