From 1d2e80e55218bd00a2fe4f13c54d877d1c396964 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Thu, 17 Jul 2025 09:12:43 +0200 Subject: [PATCH] cleanup --- .../sync-md/src/background/background.ts | 24 +- .../sync-md/src/background/old/background.ts | 167 ------------ .../src/background/old/config.const.ts | 29 -- .../background/old/generateTaskOperations.ts | 171 ------------ .../background/old/markdown-parser.test.ts | 176 ------------ .../src/background/old/markdown-parser.ts | 170 ------------ .../src/background/old/message-handler.ts | 106 -------- .../old/models/file-operations.model.ts | 25 -- .../background/old/models/markdown.model.ts | 23 -- .../background/old/models/message.model.ts | 10 - .../src/background/old/models/sync.model.ts | 20 -- .../src/background/old/simple-file-watcher.ts | 81 ------ .../src/background/old/sync-coordinator.ts | 257 ------------------ .../src/background/old/sync-manager.ts | 120 -------- .../sync-md/src/shared/plugin-api.ts | 14 - 15 files changed, 10 insertions(+), 1383 deletions(-) delete mode 100644 packages/plugin-dev/sync-md/src/background/old/background.ts delete mode 100644 packages/plugin-dev/sync-md/src/background/old/config.const.ts delete mode 100644 packages/plugin-dev/sync-md/src/background/old/generateTaskOperations.ts delete mode 100644 packages/plugin-dev/sync-md/src/background/old/markdown-parser.test.ts delete mode 100644 packages/plugin-dev/sync-md/src/background/old/markdown-parser.ts delete mode 100644 packages/plugin-dev/sync-md/src/background/old/message-handler.ts delete mode 100644 packages/plugin-dev/sync-md/src/background/old/models/file-operations.model.ts delete mode 100644 packages/plugin-dev/sync-md/src/background/old/models/markdown.model.ts delete mode 100644 packages/plugin-dev/sync-md/src/background/old/models/message.model.ts delete mode 100644 packages/plugin-dev/sync-md/src/background/old/models/sync.model.ts delete mode 100644 packages/plugin-dev/sync-md/src/background/old/simple-file-watcher.ts delete mode 100644 packages/plugin-dev/sync-md/src/background/old/sync-coordinator.ts delete mode 100644 packages/plugin-dev/sync-md/src/background/old/sync-manager.ts delete mode 100644 packages/plugin-dev/sync-md/src/shared/plugin-api.ts diff --git a/packages/plugin-dev/sync-md/src/background/background.ts b/packages/plugin-dev/sync-md/src/background/background.ts index 16e0960ce..c20d758c0 100644 --- a/packages/plugin-dev/sync-md/src/background/background.ts +++ b/packages/plugin-dev/sync-md/src/background/background.ts @@ -3,19 +3,15 @@ import { initUiBridge } from './ui-bridge'; import { loadLocalConfig } from './local-config'; import { log } from '../shared/logger'; -export const initPlugin = (): void => { - log.log('initPlugin called'); +// Initialize UI bridge to handle messages +initUiBridge(); +log.log('UI bridge initialized'); - // Initialize UI bridge to handle messages - initUiBridge(); - log.log('UI bridge initialized'); +// Load saved config from local storage and start sync if enabled +const config = loadLocalConfig(); +log.log('Loaded config:', config); - // Load saved config from local storage and start sync if enabled - const config = loadLocalConfig(); - log.log('Loaded config:', config); - - if (config?.filePath) { - // Transform config to match sync-manager expectations - initSyncManager(config); - } -}; +if (config?.filePath) { + // Transform config to match sync-manager expectations + initSyncManager(config); +} diff --git a/packages/plugin-dev/sync-md/src/background/old/background.ts b/packages/plugin-dev/sync-md/src/background/old/background.ts deleted file mode 100644 index af5d6a829..000000000 --- a/packages/plugin-dev/sync-md/src/background/old/background.ts +++ /dev/null @@ -1,167 +0,0 @@ -/** - * Sync-MD Plugin Background Script - * Initializes and coordinates all plugin components - */ - -import { SyncConfig } from '../shared/types'; -import { SimpleFileWatcher } from './simple-file-watcher'; -import { SyncManager } from './sync-manager'; -import { setupMessageHandler } from './message-handler'; -import { IGNORED_TASK_ACTIONS, LOG_PREFIX, REGISTERED_HOOKS } from './config.const'; -import { PluginHooks } from '@super-productivity/plugin-api'; - -// Global instances -let fileWatcher: SimpleFileWatcher | null = null; -const syncManager = new SyncManager(); - -/** - * Initialize the sync-md plugin - */ -const initPlugin = async (): Promise => { - console.log(`${LOG_PREFIX.PLUGIN} Initializing...`); - - if (typeof PluginAPI === 'undefined') { - console.error(`${LOG_PREFIX.PLUGIN} PluginAPI not available`); - return; - } - - // Setup window focus tracking - setupWindowFocusTracking(); - - // Setup message handling - setupMessageHandler({ - onConfigUpdated: handleConfigUpdate, - onSyncNow: () => syncManager.requestSync('Manual sync'), - }); - - // Setup hooks - setupHooks(); - - // Load saved config - await loadSavedConfig(); - - console.log(`${LOG_PREFIX.PLUGIN} Initialization complete`); -}; - -/** - * Setup window focus tracking - */ -const setupWindowFocusTracking = (): void => { - if (!PluginAPI.onWindowFocusChange) return; - - PluginAPI.onWindowFocusChange((isFocused: boolean) => { - syncManager?.setWindowFocused(isFocused); - console.log( - `${LOG_PREFIX.SYNC} Window focus: ${isFocused ? 'focused' : 'unfocused'}`, - ); - - if (!isFocused && syncManager) { - syncManager.requestSync('Window focus lost'); - } - }); -}; - -/** - * Setup hooks for task updates - */ -const setupHooks = (): void => { - if (!PluginAPI.registerHook) { - console.warn(`${LOG_PREFIX.PLUGIN} No registerHook API`); - return; - } - - // Register anyTaskUpdate hook - PluginAPI.registerHook(PluginHooks.ANY_TASK_UPDATE, async (payload) => { - const typedPayload = payload as { action?: string }; - console.log(`${LOG_PREFIX.HOOK} Task update: ${typedPayload.action}`); - - // Filter ignored actions - const shouldIgnore = IGNORED_TASK_ACTIONS.some((ignored) => - typedPayload.action?.includes(ignored), - ); - if (shouldIgnore) { - console.log(`${LOG_PREFIX.HOOK} Ignoring: ${typedPayload.action}`); - return; - } - - syncManager?.requestSync(`Task update: ${typedPayload.action}`); - }); - - // Register other hooks - const otherHooks = REGISTERED_HOOKS.filter((h) => h !== 'anyTaskUpdate'); - for (const hookName of otherHooks) { - try { - PluginAPI.registerHook(PluginHooks.PROJECT_LIST_UPDATE, async (payload) => { - const typedPayload = payload as { action?: string }; - console.log( - `${LOG_PREFIX.HOOK} ${hookName}: ${typedPayload.action || 'unknown'}`, - ); - syncManager?.requestSync(`${hookName}: ${typedPayload.action || 'unknown'}`); - }); - } catch (e) { - // Hook might not exist - } - } -}; - -/** - * Handle configuration updates - */ -const handleConfigUpdate = async (config: SyncConfig): Promise => { - console.log(`${LOG_PREFIX.BACKGROUND} Config updated:`, config); - - // Stop existing watcher - if (fileWatcher) { - fileWatcher.stop(); - fileWatcher = null; - } - - if (!config.enabled) { - return; - } - if (!syncManager) { - return; - } - - fileWatcher = new SimpleFileWatcher({ - filePath: config.filePath, - onFileChange: (modifiedTime) => syncManager.handleFileChange(modifiedTime), - onError: (error) => { - console.error(`${LOG_PREFIX.FILE_WATCHER} Error:`, error); - PluginAPI.showSnack({ - msg: `File watch error: ${error.message}`, - type: 'ERROR', - }); - }, - }); - - await fileWatcher.start(); - await syncManager.requestSync('Initial configuration'); -}; - -/** - * Load saved configuration - */ -const loadSavedConfig = async (): Promise => { - const savedData = await PluginAPI.loadSyncedData?.(); - if (!savedData) return; - - try { - const config: SyncConfig = JSON.parse(savedData); - if (config.enabled && typeof PluginAPI.executeNodeScript === 'function') { - await handleConfigUpdate(config); - } - } catch (error) { - console.error('Failed to load saved config:', error); - } -}; - -// Start plugin -if (typeof PluginAPI !== 'undefined') { - console.log(`${LOG_PREFIX.PLUGIN} PluginAPI detected`); - Promise.resolve().then(() => initPlugin()); -} else { - console.warn(`${LOG_PREFIX.PLUGIN} PluginAPI not found`); -} - -export { initPlugin }; diff --git a/packages/plugin-dev/sync-md/src/background/old/config.const.ts b/packages/plugin-dev/sync-md/src/background/old/config.const.ts deleted file mode 100644 index e3d475dbb..000000000 --- a/packages/plugin-dev/sync-md/src/background/old/config.const.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Configuration constants for the sync-md plugin - */ - -// Sync timing configuration -export const SYNC_DEBOUNCE_TIME_FOCUSED = 500; // 0.5 seconds when SP is focused -export const SYNC_DEBOUNCE_TIME_UNFOCUSED = 2000; // 2 seconds when editing markdown -export const MIN_SYNC_INTERVAL = 5000; // 5 seconds minimum between syncs - -// Plugin logging prefixes -export const LOG_PREFIX = { - SYNC: '[Sync]', - PLUGIN: '[Plugin]', - BACKGROUND: '[Background]', - FILE_WATCHER: '[FileWatcher]', - HOOK: '[Hook]', - PLUGIN_BUILD: '[Plugin Build]', -} as const; - -// Hook names to register -export const REGISTERED_HOOKS = [ - 'anyTaskUpdate', - 'projectUpdate', - 'projectListUpdate', - 'workContextUpdate', -] as const; - -// Actions to ignore in task updates -export const IGNORED_TASK_ACTIONS = ['timeTracking'] as const; diff --git a/packages/plugin-dev/sync-md/src/background/old/generateTaskOperations.ts b/packages/plugin-dev/sync-md/src/background/old/generateTaskOperations.ts deleted file mode 100644 index d9a313851..000000000 --- a/packages/plugin-dev/sync-md/src/background/old/generateTaskOperations.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { ParsedTask } from './models/markdown.model'; -import { SpTask } from './models/sync.model'; -import { BatchOperation } from '@super-productivity/plugin-api'; - -/** - * Generate batch operations to sync markdown tasks to Super Productivity - */ -export const generateTaskOperations = ( - mdTasks: ParsedTask[], - spTasks: SpTask[], - projectId: string, -): BatchOperation[] => { - const operations: BatchOperation[] = []; - - // Create maps for easier lookup - const spById = new Map(); - const spByTitle = new Map(); - const mdById = new Map(); - const mdByTitle = new Map(); - - // Build SP maps - spTasks.forEach((task) => { - spById.set(task.id, task); - // Only map by title if no duplicate titles - if (!spByTitle.has(task.title)) { - spByTitle.set(task.title, task); - } - }); - - // Build MD maps and check for duplicates - const duplicateIds = new Set(); - const firstOccurrence = new Map(); - - mdTasks.forEach((mdTask) => { - const checkId = mdTask.id; - if (checkId) { - if (firstOccurrence.has(checkId)) { - duplicateIds.add(checkId); - } else { - firstOccurrence.set(checkId, mdTask.line); - mdById.set(checkId, mdTask); - } - } - if (!mdByTitle.has(mdTask.title)) { - mdByTitle.set(mdTask.title, mdTask); - } - }); - - // Track which SP tasks we've seen - const processedSpIds = new Set(); - - // First pass: process MD tasks - mdTasks.forEach((mdTask) => { - // Skip subtasks - they'll be handled with their parents - if (mdTask.isSubtask) { - return; - } - - // Check if this task has a duplicate ID - const effectiveId = mdTask.id && !duplicateIds.has(mdTask.id) ? mdTask.id : null; - - if (effectiveId) { - const spTask = spById.get(effectiveId); - if (spTask) { - processedSpIds.add(spTask.id); - - // Update existing task - const updates: Record = {}; - if (spTask.title !== mdTask.title) { - updates.title = mdTask.title; - } - if (spTask.isDone !== mdTask.completed) { - updates.isDone = mdTask.completed; - } - - // Handle notes - const mdNotes = mdTask.noteLines ? mdTask.noteLines.join('\n') : ''; - if ((spTask.notes || '') !== mdNotes) { - updates.notes = mdNotes; - } - - if (Object.keys(updates).length > 0) { - operations.push({ - type: 'update', - taskId: spTask.id, - updates, - } as BatchTaskUpdate); - } - } else { - // Create new task with the specified ID - operations.push({ - type: 'create', - tempId: `temp_${mdTask.line}`, - data: { - id: effectiveId, - title: mdTask.title, - isDone: mdTask.completed, - notes: mdTask.noteLines ? mdTask.noteLines.join('\n') : undefined, - projectId, - }, - } as BatchTaskCreate); - } - } else { - // No ID - try to match by title - const spTask = spByTitle.get(mdTask.title); - if (spTask && !processedSpIds.has(spTask.id)) { - processedSpIds.add(spTask.id); - - // Update existing task - const updates: Record = {}; - if (spTask.isDone !== mdTask.completed) { - updates.isDone = mdTask.completed; - } - - const mdNotes = mdTask.noteLines ? mdTask.noteLines.join('\n') : ''; - if ((spTask.notes || '') !== mdNotes) { - updates.notes = mdNotes; - } - - if (Object.keys(updates).length > 0) { - operations.push({ - type: 'update', - taskId: spTask.id, - updates, - } as BatchTaskUpdate); - } - } else { - // Create new task - operations.push({ - type: 'create', - tempId: `temp_${mdTask.line}`, - data: { - title: mdTask.title, - isDone: mdTask.completed, - notes: mdTask.noteLines ? mdTask.noteLines.join('\n') : undefined, - projectId, - }, - } as BatchTaskCreate); - } - } - }); - - // Second pass: delete tasks that exist in SP but not in MD - spTasks.forEach((spTask) => { - if (!processedSpIds.has(spTask.id) && !spTask.parentId) { - operations.push({ - type: 'delete', - taskId: spTask.id, - } as BatchTaskDelete); - } - }); - - // Third pass: handle reordering if needed - const mdRootTasks = mdTasks.filter((t) => !t.isSubtask); - const mdTaskIds = mdRootTasks.map((t) => { - if (t.id && spById.has(t.id)) { - return t.id; - } - const spTask = spByTitle.get(t.title); - return spTask ? spTask.id : `temp_${t.line}`; - }); - - if (mdTaskIds.length > 0) { - operations.push({ - type: 'reorder', - taskIds: mdTaskIds, - } as BatchTaskReorder); - } - - return operations; -}; diff --git a/packages/plugin-dev/sync-md/src/background/old/markdown-parser.test.ts b/packages/plugin-dev/sync-md/src/background/old/markdown-parser.test.ts deleted file mode 100644 index 46d4c508d..000000000 --- a/packages/plugin-dev/sync-md/src/background/old/markdown-parser.test.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { parseMarkdownTasks } from '../../markdown-parser'; - -describe('markdown-parser', () => { - describe('parseMarkdownTasks', () => { - it('should parse simple tasks', () => { - const content = ` -- [ ] Task 1 -- [x] Task 2 completed -- [ ] Task with ID - `.trim(); - - const result = parseMarkdownTasks(content); - - expect(result.errors).toEqual([]); - expect(result.tasks).toHaveLength(3); - expect(result.tasks[0]).toEqual({ - line: 0, - indent: 0, - completed: false, - id: null, - title: 'Task 1', - originalLine: '- [ ] Task 1', - parentId: null, - isSubtask: false, - originalId: null, - depth: 0, - }); - expect(result.tasks[1]).toEqual({ - line: 1, - indent: 0, - completed: true, - id: null, - title: 'Task 2 completed', - originalLine: '- [x] Task 2 completed', - parentId: null, - isSubtask: false, - originalId: null, - depth: 0, - }); - expect(result.tasks[2]).toEqual({ - line: 2, - indent: 0, - completed: false, - id: '123', - title: 'Task with ID', - originalLine: '- [ ] Task with ID', - parentId: null, - isSubtask: false, - originalId: '123', - depth: 0, - }); - }); - - it('should handle nested tasks', () => { - const content = ` -- [ ] Parent task - - [ ] Child task 1 - - [x] Child task 2 - - [ ] Sub-child task -- [ ] Another parent - `.trim(); - - const result = parseMarkdownTasks(content); - - expect(result.errors).toEqual([]); - expect(result.tasks).toHaveLength(4); // Sub-child task is now treated as notes - - // Parent task - expect(result.tasks[0]).toEqual({ - line: 0, - indent: 0, - completed: false, - id: 'parent1', - title: 'Parent task', - originalLine: '- [ ] Parent task', - parentId: null, - isSubtask: false, - originalId: 'parent1', - depth: 0, - }); - - // Child task 1 - expect(result.tasks[1]).toEqual({ - line: 1, - indent: 2, - completed: false, - id: null, - title: 'Child task 1', - originalLine: ' - [ ] Child task 1', - parentId: 'parent1', - isSubtask: true, - originalId: null, - depth: 1, - }); - - // Child task 2 (now has notes from the sub-child) - expect(result.tasks[2]).toEqual({ - line: 2, - indent: 2, - completed: true, - id: 'child2', - title: 'Child task 2', - originalLine: ' - [x] Child task 2', - parentId: 'parent1', - isSubtask: true, - originalId: 'child2', - depth: 1, - noteLines: [' - [ ] Sub-child task'], - }); - - // Another parent (sub-child is now notes, so this is index 3) - expect(result.tasks[3]).toEqual({ - line: 4, - indent: 0, - completed: false, - id: null, - title: 'Another parent', - originalLine: '- [ ] Another parent', - parentId: null, - isSubtask: false, - originalId: null, - depth: 0, - }); - }); - - it('should skip empty task titles', () => { - const content = ` -- [ ] -- [x] Valid task -- [ ] - `.trim(); - - const result = parseMarkdownTasks(content); - - expect(result.errors).toEqual([ - 'Skipping task with empty title at line 1', - 'Skipping task with empty title at line 3', - ]); - expect(result.tasks).toHaveLength(1); - expect(result.tasks[0].title).toBe('Valid task'); - }); - - it('should ignore non-task lines', () => { - const content = ` -# Heading -Some text -- [ ] Task 1 -More text -- [x] Task 2 - `.trim(); - - const result = parseMarkdownTasks(content); - - expect(result.errors).toEqual([]); - expect(result.tasks).toHaveLength(2); - expect(result.tasks[0].title).toBe('Task 1'); - expect(result.tasks[1].title).toBe('Task 2'); - }); - - it('should handle various ID formats', () => { - const content = ` -- [ ] Task 1 -- [x] Task 2 -- [ ] Task 3 - `.trim(); - - const result = parseMarkdownTasks(content); - - expect(result.errors).toEqual([]); - expect(result.tasks).toHaveLength(3); - expect(result.tasks[0].id).toBe('simple-id'); - expect(result.tasks[1].id).toBe('123-456-789'); - expect(result.tasks[2].id).toBe('uuid-like-id-here'); - }); - }); -}); diff --git a/packages/plugin-dev/sync-md/src/background/old/markdown-parser.ts b/packages/plugin-dev/sync-md/src/background/old/markdown-parser.ts deleted file mode 100644 index a2d2f9511..000000000 --- a/packages/plugin-dev/sync-md/src/background/old/markdown-parser.ts +++ /dev/null @@ -1,170 +0,0 @@ -// Pure markdown parsing utilities - easily testable -import { ParsedTask, TaskParseResult } from './models/markdown.model'; - -/** - * Detect the base indentation size used in the markdown - * Returns the smallest non-zero indentation found - */ -const detectIndentSize = (lines: string[]): number => { - let minIndent = Infinity; - - for (const line of lines) { - const match = line.match(/^(\s*)- \[/); - if (match && match[1].length > 0) { - minIndent = Math.min(minIndent, match[1].length); - } - } - - // Default to 2 if no indentation found - return minIndent === Infinity ? 2 : minIndent; -}; - -/** - * Pure function to parse markdown content into task objects - * This is easily testable as it has no side effects - */ -export const parseMarkdownTasks = (content: string): TaskParseResult => { - const lines = content.split('\n'); - const tasks: ParsedTask[] = []; - const errors: string[] = []; - const parentStack: Array<{ - indent: number; - id: string | null; - line: number; - taskIndex: number; - }> = []; - const seenIds = new Set(); - - // Detect the indentation size used in this file - const detectedIndentSize = detectIndentSize(lines); - console.log(`📏 Detected indent size: ${detectedIndentSize} spaces per level`); - - // First pass: identify all task lines and parse tasks - lines.forEach((line, index) => { - // Try new format first (no sp: prefix, no spaces) - let taskMatch = line.match(/^(\s*)- \[([ x])\]\s*(?:)?\s*(.*)$/); - - // If no match, try old format (with sp: prefix and spaces) - if (!taskMatch) { - taskMatch = line.match(/^(\s*)- \[([ x])\]\s*(?:)?\s*(.*)$/); - } - - if (taskMatch) { - const [, indent, completed, id, title] = taskMatch; - const indentLevel = indent.length; - - // Calculate depth using actual detected indent size - let depth: number; - if (detectedIndentSize === 1) { - // Special handling for 1-space indentation - // 0 spaces = depth 0, 1 space = depth 1, 2+ spaces = depth 2+ - depth = indentLevel; - } else { - // Normal calculation for 2+ space indentation - depth = detectedIndentSize > 0 ? Math.floor(indentLevel / detectedIndentSize) : 0; - } - - // Only process tasks at depth 0 or 1 (parent tasks and subtasks) - if (depth <= 1) { - const isSubtask = indentLevel > 0; - - let parentId: string | null = null; - if (isSubtask) { - // Find parent from stack - for (let i = parentStack.length - 1; i >= 0; i--) { - if (parentStack[i].indent < indentLevel) { - parentId = parentStack[i].id; - break; - } - } - - // Clean up stack - while ( - parentStack.length > 0 && - parentStack[parentStack.length - 1].indent >= indentLevel - ) { - parentStack.pop(); - } - } else { - parentStack.length = 0; - } - - // Check for duplicate IDs - let actualId: string | null = id || null; - if (id && seenIds.has(id)) { - errors.push(`Duplicate task ID "${id}" at line ${index + 1} - ID removed`); - actualId = null; - } else if (id) { - seenIds.add(id); - } - - // Skip tasks with empty titles - if (!title.trim()) { - errors.push(`Skipping task with empty title at line ${index + 1}`); - return; // Skip this iteration in forEach - } - - const taskIndex = tasks.length; - tasks.push({ - line: index, - indent: indentLevel, - completed: completed === 'x', - id: actualId || null, - title: title.trim(), - originalLine: line, - parentId, - isSubtask, - originalId: id || null, - depth, - }); - - // Add to parent stack - parentStack.push({ - indent: indentLevel, - id: actualId || null, - line: index, - taskIndex, - }); - } - } - }); - - // Second pass: collect notes for all tasks at depth 0 and 1 - // Notes are any lines between a task and the next task - for (let i = 0; i < tasks.length; i++) { - const task = tasks[i]; - - // Both parent tasks (depth 0) and subtasks (depth 1) can have notes - if (task.depth <= 1) { - const startLine = task.line + 1; - let endLine = lines.length; - - // Find the next task line - for (let j = i + 1; j < tasks.length; j++) { - if (tasks[j].line > task.line) { - endLine = tasks[j].line; - break; - } - } - - // Collect all lines between this task and the next as notes - const noteLines: string[] = []; - for (let lineNum = startLine; lineNum < endLine; lineNum++) { - const line = lines[lineNum]; - // Include all lines, preserving empty lines - noteLines.push(line); - } - - // Trim trailing empty lines - while (noteLines.length > 0 && !noteLines[noteLines.length - 1].trim()) { - noteLines.pop(); - } - - if (noteLines.length > 0) { - task.noteLines = noteLines; - } - } - } - - return { tasks, errors, detectedIndentSize }; -}; diff --git a/packages/plugin-dev/sync-md/src/background/old/message-handler.ts b/packages/plugin-dev/sync-md/src/background/old/message-handler.ts deleted file mode 100644 index 3d0f15447..000000000 --- a/packages/plugin-dev/sync-md/src/background/old/message-handler.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Handles messages between UI and background - */ - -import { SyncConfig } from '../shared/types'; - -import { LOG_PREFIX } from './config.const'; -import { MessageCallbacks } from './models/message.model'; -import { readFileContent } from './helper/node-file-helper'; - -export const setupMessageHandler = (callbacks: MessageCallbacks): void => { - if (!PluginAPI.onMessage) { - console.error(`${LOG_PREFIX.PLUGIN} No onMessage API`); - return; - } - - PluginAPI.onMessage(async (message: unknown) => { - console.log('Message received:', message); - - try { - if (typeof message !== 'object' || message === null || !('type' in message)) { - return { success: false, error: 'Invalid message' }; - } - - const msg = message as { type: string; config?: SyncConfig; filePath?: string }; - - switch (msg.type) { - case 'configUpdated': - if (msg.config) { - await callbacks.onConfigUpdated(msg.config); - } - return { success: true }; - - case 'syncNow': - await callbacks.onSyncNow(); - return { success: true }; - - case 'testFile': - return await handleTestFile(msg.filePath); - - // TODO can be called from ui directly - case 'getProjects': - return await handleGetProjects(); - - // TODO can be called from ui directly - case 'getTasks': - return await handleGetTasks(); - - case 'checkDesktopMode': - return handleCheckDesktopMode(); - - default: - return { success: false, error: `Unknown type: ${msg.type}` }; - } - } catch (error) { - console.error('Message error:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - } - }); -}; - -export const handleTestFile = async ( - filePath: string, -): Promise<{ success: boolean; preview?: string; error?: string }> => { - if (!filePath) { - return { success: false, error: 'No file path provided' }; - } - - try { - // Test file access - const content = await readFileContent(filePath); - const lines = content.split('\n').slice(0, 10); - return { - success: true, - preview: lines.join('\n') + (content.split('\n').length > 10 ? '\n...' : ''), - }; - } catch (error) { - return { success: false, error: (error as any)?.message }; - } -}; - -export const handleGetProjects = async (): Promise<{ - success: boolean; - projects: any[]; -}> => { - // Always get fresh data - const projects = await PluginAPI.getAllProjects(); - return { success: true, projects }; -}; - -export const handleGetTasks = async (): Promise<{ success: boolean; tasks: any[] }> => { - // Always get fresh data - const tasks = await PluginAPI.getTasks(); - return { success: true, tasks }; -}; - -export const handleCheckDesktopMode = (): { success: boolean; isDesktop: boolean } => { - // Check if we're in Electron environment with Node.js capabilities - return { - success: true, - isDesktop: typeof PluginAPI?.executeNodeScript === 'function', - }; -}; diff --git a/packages/plugin-dev/sync-md/src/background/old/models/file-operations.model.ts b/packages/plugin-dev/sync-md/src/background/old/models/file-operations.model.ts deleted file mode 100644 index 48b199d31..000000000 --- a/packages/plugin-dev/sync-md/src/background/old/models/file-operations.model.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Models for file operations and watching - */ - -export interface FileWatcherOptions { - filePath: string; - onFileChange: (modifiedTime: number) => void; - onError: (error: Error) => void; -} - -export interface NodeScriptOptions { - script: string; - args: unknown[]; - timeout: number; -} - -export interface NodeScriptResult { - success: boolean; - result?: unknown; - error?: string; -} - -export interface PluginAPILike { - executeNodeScript(options: NodeScriptOptions): Promise; -} diff --git a/packages/plugin-dev/sync-md/src/background/old/models/markdown.model.ts b/packages/plugin-dev/sync-md/src/background/old/models/markdown.model.ts deleted file mode 100644 index e83f16f85..000000000 --- a/packages/plugin-dev/sync-md/src/background/old/models/markdown.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Models for markdown parsing and task representation - */ - -export interface ParsedTask { - line: number; - indent: number; - completed: boolean; - id: string | null; - title: string; - originalLine: string; - parentId?: string | null; - isSubtask: boolean; - originalId?: string | null; // Track the original ID even if it's a duplicate - depth: number; // Track depth level (0 = root, 1 = subtask, 2+ = notes content) - noteLines?: string[]; // Third-level content to be synced as notes -} - -export interface TaskParseResult { - tasks: ParsedTask[]; - errors: string[]; - detectedIndentSize?: number; -} diff --git a/packages/plugin-dev/sync-md/src/background/old/models/message.model.ts b/packages/plugin-dev/sync-md/src/background/old/models/message.model.ts deleted file mode 100644 index 1bb4f7b93..000000000 --- a/packages/plugin-dev/sync-md/src/background/old/models/message.model.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Models for UI-background communication - */ - -import { SyncConfig } from '../../shared/types'; - -export interface MessageCallbacks { - onConfigUpdated: (config: SyncConfig) => Promise; - onSyncNow: () => Promise; -} diff --git a/packages/plugin-dev/sync-md/src/background/old/models/sync.model.ts b/packages/plugin-dev/sync-md/src/background/old/models/sync.model.ts deleted file mode 100644 index 2940935d7..000000000 --- a/packages/plugin-dev/sync-md/src/background/old/models/sync.model.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Models for sync operations - */ - -export interface SyncResult { - success: boolean; - taskCount?: number; - error?: string; - type: 'file-to-sp' | 'sp-to-file' | 'integrity-check'; -} - -export interface SpTask { - id: string; - title: string; - projectId?: string; - parentId?: string; - isDone: boolean; - notes?: string; - subTaskIds?: string[]; -} diff --git a/packages/plugin-dev/sync-md/src/background/old/simple-file-watcher.ts b/packages/plugin-dev/sync-md/src/background/old/simple-file-watcher.ts deleted file mode 100644 index 28dfaf312..000000000 --- a/packages/plugin-dev/sync-md/src/background/old/simple-file-watcher.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { PluginFileOperations } from './helper/file-operations'; -import { Debouncer } from './helper/debouncer'; -import { FileOperations } from './models/file-operations.model'; -import { FileWatcherOptions } from './models/file-operations.model'; - -export class SimpleFileWatcher { - private fileOps: FileOperations; - private watchHandle: unknown = null; - private isWatching: boolean = false; - private debouncer = new Debouncer(); - private lastSeenModifiedTime: number = 0; - - constructor(private options: FileWatcherOptions) { - this.fileOps = new PluginFileOperations(PluginAPI); - } - - /** - * Start watching the file for changes - */ - async start(): Promise { - if (this.isWatching) { - console.log('📁 File watcher already running'); - return; - } - - try { - // Get initial file state - const stats = await this.fileOps.getFileStats(this.options.filePath); - this.lastSeenModifiedTime = stats.modifiedTime; - - // Start watching - this.watchHandle = await this.fileOps.watchFile(this.options.filePath, async () => { - // Debounce file change events - this.debouncer.debounce( - 'file-change', - async () => { - try { - const fileStats = await this.fileOps.getFileStats(this.options.filePath); - - // Only trigger if modified time actually changed - if (fileStats.modifiedTime > this.lastSeenModifiedTime) { - console.log('📄 File change detected'); - this.lastSeenModifiedTime = fileStats.modifiedTime; - this.options.onFileChange(fileStats.modifiedTime); - } - } catch (error) { - console.error('Error checking file stats:', error); - this.options.onError(error as Error); - } - }, - 500, - ); // 500ms debounce - }); - - this.isWatching = true; - console.log('👀 Started watching file:', this.options.filePath); - } catch (error) { - console.error('Failed to start file watcher:', error); - this.options.onError(error as Error); - throw error; - } - } - - /** - * Stop watching the file - */ - stop(): void { - if (!this.isWatching) { - return; - } - - if (this.watchHandle && typeof this.watchHandle === 'function') { - (this.watchHandle as () => void)(); - } - - this.watchHandle = null; - this.isWatching = false; - this.debouncer.cancelAll(); - console.log('🛑 Stopped watching file'); - } -} diff --git a/packages/plugin-dev/sync-md/src/background/old/sync-coordinator.ts b/packages/plugin-dev/sync-md/src/background/old/sync-coordinator.ts deleted file mode 100644 index 9c35413f3..000000000 --- a/packages/plugin-dev/sync-md/src/background/old/sync-coordinator.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { SyncConfig } from '../shared/types'; -import { parseMarkdownTasks } from './markdown-parser'; -import { generateTaskOperations } from './sync-utils'; -import { PluginFileOperations } from './helper/file-operations'; -import { SyncResult } from './models/sync.model'; -import { FileOperations } from './models/file-operations.model'; - -export class SyncCoordinator { - private fileOps: FileOperations; - private config: SyncConfig; - private lastWriteTime: number = 0; - private readonly WRITE_SETTLE_TIME = 1000; // 1 second to consider our own writes - - constructor(config: SyncConfig) { - this.config = config; - this.fileOps = new PluginFileOperations(PluginAPI); - } - - /** - * Check if a file change was caused by our own write - */ - isOwnWrite(fileModifiedTime: number): boolean { - return fileModifiedTime - this.lastWriteTime < this.WRITE_SETTLE_TIME; - } - - /** - * Sync from markdown file to Super Productivity - */ - async syncFileToSP(): Promise { - try { - console.log('📥 Syncing: Markdown → SuperProductivity'); - - // Read and parse markdown file - const content = await this.fileOps.readFile(this.config.filePath); - const { tasks: mdTasks, errors } = parseMarkdownTasks(content); - - if (errors.length > 0) { - console.warn('⚠️ Markdown parsing errors:', errors); - } - - // Get current SP state - const [spTasks] = await Promise.all([ - PluginAPI.getTasks(), - PluginAPI.getAllProjects(), - ]); - - // Generate operations to sync MD -> SP - const operations = generateTaskOperations( - mdTasks, - spTasks.filter( - (t: unknown) => - (t as { projectId: string }).projectId === this.config.projectId, - ), - this.config.projectId, - ); - - if (operations.length === 0) { - console.log('✅ No changes needed - MD and SP are in sync'); - return { success: true, taskCount: mdTasks.length, type: 'file-to-sp' }; - } - - // Apply operations to SP - await PluginAPI.batchUpdateForProject({ - projectId: this.config.projectId, - operations, - }); - - console.log(`✅ Applied ${operations.length} operations to SP`); - return { - success: true, - taskCount: mdTasks.length, - type: 'file-to-sp', - }; - } catch (error) { - console.error('❌ Error syncing file to SP:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - type: 'file-to-sp', - }; - } - } - - /** - * Sync from Super Productivity to markdown file - */ - async syncSPToFile(): Promise { - try { - console.log('📤 Syncing: SuperProductivity → Markdown'); - - // Get current SP state - const [tasks, projects] = await Promise.all([ - PluginAPI.getTasks(), - PluginAPI.getAllProjects(), - ]); - - const project = projects.find( - (p: unknown) => (p as { id: string }).id === this.config.projectId, - ); - if (!project) { - throw new Error(`Project ${this.config.projectId} not found`); - } - - // Filter tasks for this project - const projectTasks = tasks.filter( - (t: unknown) => (t as { projectId: string }).projectId === this.config.projectId, - ); - - // Convert to markdown content - const content = this.generateMarkdownContent(projectTasks, project); - - // Write to file - await this.fileOps.writeFile(this.config.filePath, content); - this.lastWriteTime = Date.now(); - - console.log(`✅ Wrote ${projectTasks.length} tasks to markdown`); - return { - success: true, - taskCount: projectTasks.length, - type: 'sp-to-file', - }; - } catch (error) { - console.error('❌ Error syncing SP to file:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - type: 'sp-to-file', - }; - } - } - - /** - * Check integrity between SP and markdown file - */ - async checkIntegrity(): Promise { - try { - console.log('🔍 Checking integrity between SP and markdown'); - - // Read current file - const content = await this.fileOps.readFile(this.config.filePath); - const { tasks: mdTasks } = parseMarkdownTasks(content); - - // Get SP tasks - const spTasks = await PluginAPI.getTasks(); - const projectTasks = spTasks.filter((t: unknown) => { - const task = t as { projectId: string; parentId?: string }; - return task.projectId === this.config.projectId && !task.parentId; - }); - - // Compare counts first - const mdRootTasks = mdTasks.filter((t) => !t.isSubtask); - if (mdRootTasks.length !== projectTasks.length) { - const error = `Task count mismatch: MD has ${mdRootTasks.length}, SP has ${projectTasks.length}`; - console.error('❌ ' + error); - return { success: false, error, type: 'integrity-check' }; - } - - // Check each task - const mdTaskMap = new Map(mdRootTasks.map((t) => [t.title, t])); - const spTaskMap = new Map( - projectTasks.map((t: unknown) => { - const task = t as { title: string }; - return [task.title, task]; - }), - ); - - const differences: string[] = []; - - // Check for missing/extra tasks - for (const [title] of mdTaskMap) { - if (!spTaskMap.has(title)) { - differences.push(`Task "${title}" exists in MD but not in SP`); - } - } - - for (const [title] of spTaskMap) { - if (!mdTaskMap.has(title)) { - differences.push(`Task "${title}" exists in SP but not in MD`); - } - } - - if (differences.length > 0) { - const error = `Integrity check failed: ${differences.join('; ')}`; - console.error('❌ ' + error); - return { success: false, error, type: 'integrity-check' }; - } - - console.log('✅ Integrity check passed'); - return { success: true, type: 'integrity-check' }; - } catch (error) { - console.error('❌ Error checking integrity:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - type: 'integrity-check', - }; - } - } - - /** - * Generate markdown content from SP tasks - */ - private generateMarkdownContent(tasks: unknown[], project: unknown): string { - const lines: string[] = []; - const typedProject = project as { taskIds: string[] }; - - // Get root tasks in order - const rootTasks = typedProject.taskIds - .map((id: string) => tasks.find((t) => (t as { id: string }).id === id)) - .filter(Boolean); - - for (const task of rootTasks) { - const typedTask = task as { - id?: string; - isDone: boolean; - title: string; - notes?: string; - subTaskIds?: string[]; - }; - - // Add parent task - const parentLine = - `- [${typedTask.isDone ? 'x' : ' '}] ${typedTask.id ? `` : ''} ${typedTask.title}`.trim(); - lines.push(parentLine); - - // Add parent notes - if (typedTask.notes) { - lines.push(...typedTask.notes.split('\n')); - } - - // Add subtasks - if (typedTask.subTaskIds && typedTask.subTaskIds.length > 0) { - for (const subId of typedTask.subTaskIds) { - const subtask = tasks.find((t) => (t as { id: string }).id === subId); - if (subtask) { - const typedSubtask = subtask as { - id?: string; - isDone: boolean; - title: string; - notes?: string; - }; - const subLine = - ` - [${typedSubtask.isDone ? 'x' : ' '}] ${typedSubtask.id ? `` : ''} ${typedSubtask.title}`.trim(); - lines.push(subLine); - - // Add subtask notes - if (typedSubtask.notes) { - lines.push(...typedSubtask.notes.split('\n').map((line) => ' ' + line)); - } - } - } - } - } - - return lines.join('\n'); - } -} diff --git a/packages/plugin-dev/sync-md/src/background/old/sync-manager.ts b/packages/plugin-dev/sync-md/src/background/old/sync-manager.ts deleted file mode 100644 index f551a4a13..000000000 --- a/packages/plugin-dev/sync-md/src/background/old/sync-manager.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Simple sync manager that handles sync operations and state - */ - -import { SyncCoordinator } from './sync-coordinator'; -import { - SYNC_DEBOUNCE_TIME_FOCUSED, - SYNC_DEBOUNCE_TIME_UNFOCUSED, - MIN_SYNC_INTERVAL, - LOG_PREFIX, -} from './config.const'; - -export class SyncManager { - private syncCoordinator: SyncCoordinator = new SyncCoordinator(); - private syncDebounceTimer: unknown = null; - private lastSyncTime = 0; - private syncInProgress = false; - private pendingSyncReason: string | null = null; - private isWindowFocused = true; - - setWindowFocused(focused: boolean): void { - this.isWindowFocused = focused; - } - - async requestSync(reason: string): Promise { - console.log(`${LOG_PREFIX.SYNC} Request sync: ${reason}`); - this.pendingSyncReason = reason; - - if (this.syncInProgress) { - console.log(`${LOG_PREFIX.SYNC} Sync in progress, queuing`); - return; - } - - // Clear existing timer - if (this.syncDebounceTimer) { - clearTimeout(this.syncDebounceTimer as number); - } - - // Schedule sync - const debounceTime = this.isWindowFocused - ? SYNC_DEBOUNCE_TIME_FOCUSED - : SYNC_DEBOUNCE_TIME_UNFOCUSED; - - this.syncDebounceTimer = setTimeout(() => this.performSync(), debounceTime); - } - - handleFileChange(modifiedTime: number): void { - if (this.syncCoordinator?.isOwnWrite(modifiedTime)) { - console.log(`${LOG_PREFIX.SYNC} Ignoring own write`); - this.syncCoordinator.checkIntegrity().then((result) => { - if (!result.success) { - PluginAPI.showSnack({ - msg: `Integrity check failed: ${result.error}`, - type: 'ERROR', - }); - } - }); - return; - } - - this.requestSync('File changed'); - } - - private async performSync(): Promise { - if (!this.syncCoordinator) { - console.log(`${LOG_PREFIX.SYNC} No coordinator`); - return; - } - - const timeSinceLastSync = Date.now() - this.lastSyncTime; - if (timeSinceLastSync < MIN_SYNC_INTERVAL && this.lastSyncTime > 0) { - setTimeout(() => this.performSync(), MIN_SYNC_INTERVAL - timeSinceLastSync); - return; - } - - this.syncInProgress = true; - const reason = this.pendingSyncReason || 'Unknown'; - this.pendingSyncReason = null; - - try { - console.log(`${LOG_PREFIX.SYNC} Performing sync: ${reason}`); - - if (reason.includes('File changed')) { - const result = await this.syncCoordinator.syncFileToSP(); - if (!result.success) throw new Error(result.error); - } else { - const result = await this.syncCoordinator.syncSPToFile(); - if (!result.success) throw new Error(result.error); - - // Check integrity after write - const integrityResult = await this.syncCoordinator.checkIntegrity(); - if (!integrityResult.success) { - console.error( - `${LOG_PREFIX.SYNC} Integrity check failed:`, - integrityResult.error, - ); - PluginAPI.showSnack({ - msg: `Integrity check failed: ${integrityResult.error}`, - type: 'ERROR', - }); - } - } - - this.lastSyncTime = Date.now(); - console.log(`${LOG_PREFIX.SYNC} Sync completed`); - } catch (error) { - console.error(`${LOG_PREFIX.SYNC} Sync error:`, error); - PluginAPI.showSnack({ - msg: `Sync error: ${error instanceof Error ? error.message : 'Unknown error'}`, - type: 'ERROR', - }); - } finally { - this.syncInProgress = false; - - if (this.pendingSyncReason) { - setTimeout(() => this.performSync(), 1000); - } - } - } -} diff --git a/packages/plugin-dev/sync-md/src/shared/plugin-api.ts b/packages/plugin-dev/sync-md/src/shared/plugin-api.ts deleted file mode 100644 index 11a8d8b54..000000000 --- a/packages/plugin-dev/sync-md/src/shared/plugin-api.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Type declarations for Super Productivity Plugin API - -import { PluginAPI } from '@super-productivity/plugin-api'; - -declare global { - const PluginAPI: PluginAPI; - - interface Window { - PluginAPI: PluginAPI; - initPlugin?: () => void | Promise; - // @ts-ignore - parent: Window; - } -}