import type { Task, TaskStore } from "../../types.js"; import type { GitHubRepo } from "./remote.js"; import type { SyncResult } from "../sync/registry.js"; /** * Progress callback for sync operations. */ export interface SyncProgress { /** Current task index (1-based) */ current: number; /** Total number of tasks */ total: number; /** Task being processed */ task: Task; /** Current phase of the sync */ phase: "checking" | "creating" | "updating" | "skipped"; } /** * Cached issue data for efficient sync operations. * Contains all data needed for change detection without re-fetching. */ export interface CachedIssue { number: number; title: string; body: string; state: "open" | "closed"; labels: string[]; } export interface GitHubSyncServiceOptions { /** GitHub repository (inferred from git remote) */ repo: GitHubRepo; /** GitHub personal access token */ token: string; /** Label prefix for dex tasks (default: "dex") */ labelPrefix?: string; /** Storage path for task files (default: ".dex") */ storagePath?: string; } export interface SyncAllOptions { /** Callback for progress updates */ onProgress?: (progress: SyncProgress) => void; /** Whether to skip unchanged tasks (default: true) */ skipUnchanged?: boolean; } /** * GitHub Sync Service * * Provides one-way sync of tasks to GitHub Issues. * File storage remains the source of truth. * * Behavior: * - Top-level tasks (no parent_id) → Create/update GitHub Issue * - Subtasks → Embedded in parent issue body as markdown * - Completed tasks → Issue closed only when pushed to remote * - Pending tasks → Issue open * * Sync-on-push: GitHub issues are only closed when the task completion has been * pushed to origin/HEAD. This prevents issues from being prematurely closed * before code changes are pushed. */ export declare class GitHubSyncService { /** Integration ID for the SyncRegistry */ readonly id: "github"; /** Human-readable name for display */ readonly displayName = "GitHub"; private octokit; private owner; private repo; private labelPrefix; private storagePath; constructor(options: GitHubSyncServiceOptions); /** * Get the repository this service syncs to. */ getRepo(): GitHubRepo; /** * Get the full repo string (owner/repo format). */ getRepoString(): string; /** * Get the remote ID (issue number) for a task from its metadata. * Returns null if the task hasn't been synced to GitHub. * Supports both new format (metadata.github.issueNumber) and legacy format (metadata.github_issue_number). */ getRemoteId(task: Task): number | null; /** * Get the URL to the GitHub issue for a task. * Returns null if the task hasn't been synced to GitHub. * Supports both new format (metadata.github.issueUrl) and legacy format. */ getRemoteUrl(task: Task): string | null; /** * Close the GitHub issue for a task (e.g., when the task is deleted locally). * If the task has no associated issue, this is a no-op. */ closeRemote(task: Task): Promise; /** * Sync a single task to GitHub. * For subtasks, syncs the parent issue instead. * Returns sync result with github metadata. */ syncTask(task: Task, store: TaskStore): Promise; /** * Sync all tasks to GitHub. * Returns array of sync results. */ syncAll(store: TaskStore, options?: SyncAllOptions): Promise; /** * Build a SyncResult for an existing issue. */ private buildSyncResult; /** * Sync a parent task (with all descendants) to GitHub. * Returns sync result with github metadata. */ private syncParentTask; /** * Compare issue data against expected values. * Returns true if any field differs (issue needs updating). */ private issueNeedsUpdate; /** * Check if an issue has changed and get its current state. * Returns both change detection result and current state for safe updates. * When we can't fetch the issue, currentState is undefined to preserve remote state. */ private getIssueChangeResult; /** * Fetch only the state of an issue (for when we need to avoid reopening). * Returns undefined if the issue can't be fetched. */ private fetchIssueState; /** * Check if an issue has changed compared to what we would push using cached data. * Synchronous version of hasIssueChanged for use with issue cache. * Returns true if the issue needs updating. */ private hasIssueChangedFromCache; /** * Create a new GitHub issue for a task. * Returns the github metadata for the created issue. * Issue is created as closed if shouldClose is true. */ private createIssue; /** * Update an existing GitHub issue. * * @param currentState - The current state of the issue on GitHub. * undefined means we don't know the current state. * Used to prevent reopening closed issues. */ private updateIssue; /** * Render the issue body with hierarchical task tree. * Includes root task metadata encoded in HTML comments for round-trip support. */ private renderBody; /** * Build labels for a task. */ private buildLabels; /** * Determine if a task should be marked as completed in GitHub. * * - If task has a commit SHA: only mark completed if that commit is pushed to origin * - If task has no commit SHA: don't mark completed (can't verify work is merged) * * This ensures GitHub issues are only closed when the actual work has been pushed. * Tasks completed with --no-commit will remain open in GitHub until manually closed. */ private shouldMarkCompleted; /** * Get the reason why a completed task's issue won't be closed. * Returns undefined if the issue will be closed or the task isn't completed. */ getIssueNotClosingReason(task: Task, store?: TaskStore): string | undefined; private getSubtaskBlockingReason; /** * Reconcile subtasks from the remote issue body. * Compares remote subtask state with local and returns updates for stale local subtasks. * * This handles the scenario where: * 1. Task is completed on Machine A (with subtasks), synced to GitHub * 2. Machine B has stale local state (subtasks not completed) * 3. On sync, Machine B detects remote is newer and pulls subtask state */ private reconcileSubtasksFromRemote; /** * Look up a task by its local ID in GitHub issues. * Used to find existing issues for tasks that don't have metadata yet. * Uses pagination to handle repos with >100 dex issues. */ findIssueByTaskId(taskId: string): Promise; /** * Fetch all dex-labeled issues with pagination support. * Returns a Map keyed by task ID containing all data needed for change detection. */ fetchAllDexIssues(): Promise>; /** * Extract task ID from issue body. * Supports both new format () and legacy format (). */ private extractTaskIdFromBody; } /** * Extract GitHub issue number from task metadata. * Returns null if not synced yet. * Supports both new format (metadata.github.issueNumber) and legacy format (metadata.github_issue_number). */ export declare function getGitHubIssueNumber(task: Task): number | null; //# sourceMappingURL=sync.d.ts.map