Skip to content

Latest commit

 

History

History
580 lines (462 loc) · 13 KB

File metadata and controls

580 lines (462 loc) · 13 KB

API Reference

Technical documentation for the Obsidian Linear Sync Plugin v1.0.0

📋 Overview

The plugin consists of three core services and a settings interface, all built on the Obsidian Plugin API. This document provides comprehensive API documentation for developers.

🏗️ Architecture

classDiagram
    class LinearSyncPlugin {
        +settings: LinearSyncSettings
        +linearApi: LinearApiService
        +directorySync: DirectorySyncService
        +kanbanSync: KanbanSyncService
        +onload()
        +onunload()
        +loadSettings()
        +saveSettings()
    }
    
    class LinearApiService {
        -plugin: LinearSyncPlugin
        -cache: Map
        -cacheTimeout: number
        +query(query, variables)
        +fetchAllIssues()
        +fetchWorkflowStates()
        +updateIssueState(issueId, stateId)
        +createIssue(data)
        -getCached(key)
        -setCached(key, value)
    }
    
    class DirectorySyncService {
        -plugin: LinearSyncPlugin
        -app: App
        +syncAll(issues)
        +syncIssue(issue)
        +generatePhaseFiles(issue, path, files)
        +createFileWithTemplater(template, output)
        +findIssueDirectory(identifier)
        +moveIssueDirectory(identifier, newState)
        +updateDirectoryMetadata(path, state, phase)
        -preprocessTemplate(content)
    }
    
    class KanbanSyncService {
        -plugin: LinearSyncPlugin
        -app: App
        +updateBoard(issues)
        +handleKanbanMove(identifier, column)
        +parseTrackerFile()
        +writeTrackerFile(content)
        -generateBoardContent(issues)
    }
    
    LinearSyncPlugin --> LinearApiService
    LinearSyncPlugin --> DirectorySyncService
    LinearSyncPlugin --> KanbanSyncService
Loading

📦 Core Services

LinearApiService

Handles all communication with Linear's GraphQL API.

Constructor

new LinearApiService(plugin: LinearSyncPlugin)

Methods

query(query: string, variables?: object): Promise<any>

Execute a GraphQL query against Linear API.

Parameters:

  • query - GraphQL query string
  • variables - Optional query variables

Returns: Promise resolving to query data

Example:

const data = await linearApi.query(`
  query GetIssue($id: String!) {
    issue(id: $id) {
      identifier
      title
      state { name }
    }
  }
`, { id: "abc-123" });
fetchAllIssues(): Promise<Issue[]>

Fetch all active issues (excludes canceled/duplicate).

Returns: Promise resolving to array of issues

Cache: Results cached for 5 minutes

fetchWorkflowStates(): Promise<WorkflowState[]>

Fetch all workflow states for the team.

Returns: Promise resolving to array of workflow states

updateIssueState(issueId: string, stateId: string): Promise<Issue>

Update an issue's workflow state.

Parameters:

  • issueId - Linear issue ID
  • stateId - Target workflow state ID

Returns: Promise resolving to updated issue

createIssue(data: IssueCreateInput): Promise<Issue>

Create a new Linear issue.

Parameters:

  • data - Issue creation data
    • title: string - Issue title
    • description?: string - Issue description (Markdown)
    • teamId: string - Team ID
    • stateId?: string - Initial state ID
    • priority?: number - Priority (1-4)
    • assigneeId?: string - Assignee user ID

Returns: Promise resolving to created issue

DirectorySyncService

Manages file system operations and template processing.

Constructor

new DirectorySyncService(plugin: LinearSyncPlugin, app: App)

Methods

syncAll(issues: Issue[]): Promise<void>

Synchronize all issues to vault directories.

Parameters:

  • issues - Array of Linear issues

Process:

  1. Creates phase directories if missing
  2. Generates required files per phase
  3. Updates existing file metadata
syncIssue(issue: Issue): Promise<void>

Synchronize a single issue.

Parameters:

  • issue - Linear issue object
generatePhaseFiles(issue: Issue, folderPath: string, requiredFiles: string[]): Promise<void>

Generate template files for an issue phase.

Parameters:

  • issue - Linear issue data
  • folderPath - Target directory path
  • requiredFiles - Array of template filenames

Example:

await directorySync.generatePhaseFiles(
  issue,
  "docs/kanban/03-in-progress/ENG-123",
  ["01-prd.md", "02-architecture.md", "03-task.md"]
);
createFileWithTemplater(templateName: string, outputPath: string): Promise<void>

Process a template file using Templater.

Parameters:

  • templateName - Template filename
  • outputPath - Output file path

Process:

  1. Loads template from configured folder
  2. Injects Linear data into context
  3. Processes Templater syntax
  4. Writes rendered content
findIssueDirectory(identifier: string): Promise<string | null>

Find existing directory for an issue.

Parameters:

  • identifier - Issue identifier (e.g., "ENG-123")

Returns: Directory path or null if not found

moveIssueDirectory(identifier: string, newState: string): Promise<void>

Move issue directory to new phase.

Parameters:

  • identifier - Issue identifier
  • newState - Target Linear state name
updateDirectoryMetadata(directoryPath: string, newState: string, newPhase: string): Promise<void>

Update metadata in all files within a directory.

Parameters:

  • directoryPath - Directory to update
  • newState - New Linear state
  • newPhase - New phase identifier

KanbanSyncService

Manages TRACKER.md kanban board synchronization.

Constructor

new KanbanSyncService(plugin: LinearSyncPlugin, app: App)

Methods

updateBoard(issues: Issue[]): Promise<void>

Update TRACKER.md with current issues.

Parameters:

  • issues - Array of Linear issues

Format:

## Backlog
- [ ] [[ENG-123]] Issue Title #tag

## In Progress
- [ ] [[ENG-124]] Another Issue #priority-high
handleKanbanMove(issueIdentifier: string, newColumn: string): Promise<void>

Handle issue movement on kanban board.

Parameters:

  • issueIdentifier - Issue identifier
  • newColumn - Target column name

Triggers:

  1. Updates Linear state
  2. Moves directory to new phase
  3. Generates new phase files
parseTrackerFile(): Promise<KanbanData>

Parse TRACKER.md content.

Returns: Parsed kanban structure

writeTrackerFile(content: string): Promise<void>

Write updated content to TRACKER.md.

Parameters:

  • content - New tracker content

🔧 Plugin Configuration

LinearSyncSettings

Plugin settings interface.

interface LinearSyncSettings {
  // API Configuration
  apiKey: string;              // Linear API key
  
  // Sync Configuration
  syncInterval: number;        // Minutes between syncs
  realTimeSync: boolean;       // Enable auto-sync
  syncWhenUnfocused: boolean;  // Sync when window unfocused
  
  // UI Configuration
  showStatusBar: boolean;      // Show sync status
  showSyncNotifications: boolean;
  
  // Directory Configuration
  syncDirectories: boolean;    // Auto-organize files
  autoMoveDirectories: boolean;
  confirmDirectoryMoves: boolean;
  lockDirectoryStructure: boolean;
  
  // Field Sync Configuration
  syncPriority: boolean;       // Sync priority field
  syncEstimate: boolean;       // Sync estimate points
  syncDueDate: boolean;        // Sync due dates
  
  // Template Configuration
  templatePath: string;        // Template folder path
  phaseFileGeneration: {
    enabled: boolean;
    progressiveTemplateAddition: boolean;
    templateInheritance: boolean;
    phaseTemplateMapping: PhaseMapping;
  };
}

Phase Mapping Configuration

interface PhaseMapping {
  [key: string]: {
    phase: string;
    states: string[];
    requiredFiles: string[];
    addedFiles: string[];
    description: string;
  }
}

📊 Data Models

Linear Issue Model

interface LinearIssue {
  id: string;
  identifier: string;          // e.g., "ENG-123"
  title: string;
  description?: string;
  url: string;
  
  // State
  state: {
    id: string;
    name: string;
    type: string;              // "backlog", "unstarted", "started", "completed", "canceled"
  };
  
  // Metadata
  priority: number;            // 0-4 (0=none, 1=urgent, 4=low)
  estimate?: number;           // Story points
  dueDate?: string;           // ISO date
  
  // Relations
  assignee?: {
    id: string;
    name: string;
    email: string;
  };
  team: {
    id: string;
    key: string;
    name: string;
  };
  project?: {
    id: string;
    name: string;
  };
  labels: {
    nodes: Array<{
      id: string;
      name: string;
      color: string;
    }>;
  };
  
  // Timestamps
  createdAt: string;
  updatedAt: string;
  completedAt?: string;
}

File Frontmatter Model

interface FileFrontmatter {
  // Linear metadata
  linear_id: string;
  linear_identifier: string;
  linear_url: string;
  linear_state: string;
  linear_state_id: string;
  linear_priority?: number;
  linear_estimate?: number;
  linear_due_date?: string;
  linear_team_id: string;
  linear_assignee_id?: string;
  
  // Sync configuration
  auto_sync: boolean;
  last_sync: string;           // ISO timestamp
  
  // Template metadata
  template_type: string;
  template_version: string;
  phase: string;
  state: string;
  created: string;
  modified: string;
}

🔌 Plugin Lifecycle

Initialization

class LinearSyncPlugin extends Plugin {
  async onload() {
    // 1. Load settings
    await this.loadSettings();
    
    // 2. Initialize services
    this.linearApi = new LinearApiService(this);
    this.directorySync = new DirectorySyncService(this, this.app);
    this.kanbanSync = new KanbanSyncService(this, this.app);
    
    // 3. Register commands
    this.addCommand({
      id: 'sync-from-linear',
      name: 'Sync from Linear',
      callback: () => this.syncFromLinear()
    });
    
    // 4. Setup auto-sync
    if (this.settings.realTimeSync) {
      this.registerInterval(
        window.setInterval(
          () => this.autoSync(),
          this.settings.syncInterval * 60 * 1000
        )
      );
    }
    
    // 5. Add settings tab
    this.addSettingTab(new LinearSyncSettingTab(this.app, this));
  }
  
  async onunload() {
    // Cleanup resources
  }
}

🎯 Command Registration

Available Commands

// Sync from Linear
this.addCommand({
  id: 'sync-from-linear',
  name: 'Sync from Linear',
  callback: async () => {
    const issues = await this.linearApi.fetchAllIssues();
    await this.directorySync.syncAll(issues);
    await this.kanbanSync.updateBoard(issues);
  }
});

// Sync to Linear
this.addCommand({
  id: 'sync-to-linear',
  name: 'Sync to Linear',
  callback: async () => {
    await this.syncLocalChangesToLinear();
  }
});

// Batch sync
this.addCommand({
  id: 'batch-sync',
  name: 'Batch sync all kanban files',
  callback: async () => {
    await this.batchSyncKanbanFiles();
  }
});

// Create issue from note
this.addCommand({
  id: 'create-linear-issue',
  name: 'Create Linear issue from note',
  callback: async () => {
    const file = this.app.workspace.getActiveFile();
    await this.createIssueFromFile(file);
  }
});

// Test connection
this.addCommand({
  id: 'test-connection',
  name: 'Test Linear connection',
  callback: async () => {
    await this.testLinearConnection();
  }
});

🔄 Event Handlers

File Events

// File moved
this.registerEvent(
  this.app.vault.on('rename', async (file, oldPath) => {
    if (file instanceof TFile) {
      await this.handleFileMove(file, oldPath);
    }
  })
);

// File modified
this.registerEvent(
  this.app.vault.on('modify', async (file) => {
    if (file instanceof TFile && this.shouldSyncFile(file)) {
      await this.syncFileToLinear(file);
    }
  })
);

🐛 Error Handling

Error Types

class LinearApiError extends Error {
  constructor(message: string, public statusCode?: number) {
    super(message);
    this.name = 'LinearApiError';
  }
}

class TemplaterError extends Error {
  constructor(message: string, public template?: string) {
    super(message);
    this.name = 'TemplaterError';
  }
}

class SyncError extends Error {
  constructor(message: string, public issueId?: string) {
    super(message);
    this.name = 'SyncError';
  }
}

Error Recovery

async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  delay = 1000
): Promise<T> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
    }
  }
  throw new Error('Max retries exceeded');
}

📚 References


Last Updated: 2024-08-29 | Plugin Version: 3.0.0