UNPKG

md-linear-sync

Version:

Sync Linear tickets to local markdown files with status-based folder organization

246 lines • 10.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.importCommand = importCommand; exports.getImportStats = getImportStats; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const client_1 = require("../client"); const config_1 = require("../config"); const parsers_1 = require("../parsers"); // Create debug log file const debugLogPath = path_1.default.join(process.cwd(), 'linear-import-debug.log'); function debugLog(message, data) { const timestamp = new Date().toISOString(); const logEntry = data ? `[${timestamp}] ${message}\n${JSON.stringify(data, null, 2)}\n\n` : `[${timestamp}] ${message}\n\n`; fs_1.default.appendFileSync(debugLogPath, logEntry); } async function importCommand() { console.log('šŸ“„ Importing tickets from Linear...\n'); try { // Clear previous debug log if (fs_1.default.existsSync(debugLogPath)) { fs_1.default.unlinkSync(debugLogPath); } debugLog('Starting import command'); // Load configuration const config = await config_1.ConfigManager.loadConfig(); debugLog('Loaded config', config); const envConfig = config_1.ConfigManager.loadEnvironmentConfig(); debugLog('Loaded env config', envConfig); // Initialize Linear client const client = new client_1.LinearSyncClient(envConfig.linear.apiKey); // Fetch issues from the configured team and project (limit to 2 for testing) console.log('šŸ” Fetching issues from Linear...'); debugLog(`Fetching issues for team: ${config.teamId}, project: ${config.projectId}`); const result = await client.getIssues(config.teamId, config.projectId); // Fetch all issues const issues = result.issues; debugLog(`Fetched ${issues.length} issues from Linear`, { apiUsage: result.apiUsage, issueCount: issues.length }); console.log(`šŸ“‹ Found ${issues.length} issues to import`); if (issues.length === 0) { console.log('ā„¹ļø No issues found. Your Linear project might be empty.'); return; } // Process each issue let imported = 0; let skipped = 0; let errors = 0; for (const issue of issues) { try { // Log all issues in detail for debugging (only 2 issues) debugLog(`Issue ${issue.identifier} full structure:`, issue); debugLog(`Processing issue ${issue.identifier}`, { id: issue.id, title: issue.title, state: issue.state, hasState: !!issue.state, stateName: issue.state?.name, stateId: issue.state?.id, // Show available properties availableProperties: Object.keys(issue).filter(key => !key.startsWith('_')) }); const result = await processIssue(issue, client, config); if (result.imported) { imported++; console.log(`āœ… ${issue.identifier}: ${issue.title}`); } else { skipped++; console.log(`ā­ļø ${issue.identifier}: ${result.reason}`); } } catch (error) { errors++; const errorMsg = error instanceof Error ? error.message : 'Unknown error'; console.error(`āŒ ${issue.identifier}: ${errorMsg}`); debugLog(`Error processing ${issue.identifier}`, { error: errorMsg, stack: error instanceof Error ? error.stack : undefined }); } } console.log(`\nšŸŽ‰ Import complete!`); console.log(`šŸ“Š Results: ${imported} imported, ${skipped} skipped, ${errors} errors`); // Show API usage if available if (result.apiUsage) { const usage = result.apiUsage; console.log(`🚦 API Usage:`); if (usage.requestsLimit && usage.requestsRemaining) { console.log(` Requests: ${usage.requestsRemaining}/${usage.requestsLimit} remaining`); } if (usage.complexityLimit && usage.complexityRemaining) { console.log(` Complexity: ${usage.complexityRemaining}/${usage.complexityLimit} remaining`); } if (usage.requestsResetAt) { const resetTime = new Date(usage.requestsResetAt * 1000); console.log(` Resets at: ${resetTime.toLocaleString()}`); } } if (imported > 0) { console.log('\nNext steps:'); console.log('- Edit markdown files in the md-linear-sync/linear-tickets/ directory'); console.log('- Move files between status folders to change ticket status'); console.log('- Run "md-linear-sync push" to sync changes back to Linear'); } } catch (error) { console.error('\nāŒ Import failed:', error instanceof Error ? error.message : 'Unknown error'); process.exit(1); } } async function processIssue(issue, client, config) { // Find the status folder for this issue const stateId = issue.state?.id; if (!stateId) { return { imported: false, reason: 'Issue has no state ID' }; } // Find the status name from our config mapping by state ID const statusName = findStatusNameByStateId(stateId, config); if (!statusName) { return { imported: false, reason: `Unknown state ID "${stateId}" not found in config` }; } const statusFolder = config.statusMapping[statusName].folder; // Generate filename const filename = parsers_1.TicketFileParser.generateFilename(issue.identifier, issue.title, issue.parent?.identifier); const filePath = path_1.default.join(process.cwd(), 'linear-tickets', statusFolder, filename); // Skip if file already exists if (fs_1.default.existsSync(filePath)) { return { imported: false, reason: 'File already exists' }; } // Comments are now included in the issue data from the GraphQL query const comments = issue.comments?.nodes || []; // Convert Linear issue to our ticket format const ticket = await convertLinearIssueToTicket(issue, comments, config); // Generate markdown content debugLog(`Generating file for ${issue.identifier} with ${ticket.comments?.length || 0} comments`, { hasComments: !!ticket.comments && ticket.comments.length > 0, commentCount: ticket.comments?.length || 0 }); const content = parsers_1.TicketFileParser.generateFile(ticket); debugLog(`Generated file content for ${issue.identifier}`, { contentLength: content.length, hasCommentsSection: content.includes('---comments---'), filePath: filePath }); // Ensure directory exists const dir = path_1.default.dirname(filePath); if (!fs_1.default.existsSync(dir)) { fs_1.default.mkdirSync(dir, { recursive: true }); } // Write file fs_1.default.writeFileSync(filePath, content, 'utf-8'); return { imported: true }; } function findStatusFolder(stateName, config) { for (const [configStateName, mapping] of Object.entries(config.statusMapping)) { if (configStateName === stateName) { return mapping.folder; } } return null; } function findStatusNameByStateId(stateId, config) { for (const [statusName, mapping] of Object.entries(config.statusMapping)) { if (mapping.id === stateId) { return statusName; } } return null; } async function convertLinearIssueToTicket(issue, linearComments, config) { // Convert timestamps to SGT const createdAt = parsers_1.TimezoneUtils.utcToSGT(issue.createdAt); const updatedAt = parsers_1.TimezoneUtils.utcToSGT(issue.updatedAt); const dueDate = issue.dueDate ? parsers_1.TimezoneUtils.utcToSGT(issue.dueDate) : undefined; // Get status name from state ID using config mapping const stateId = issue.state?.id; const statusName = stateId ? findStatusNameByStateId(stateId, config) : null; if (!statusName) { throw new Error(`Unable to determine status for issue ${issue.identifier} with state ID ${stateId}`); } // Build frontmatter metadata const frontmatter = { linear_id: issue.identifier, title: issue.title, status: statusName, // This is safe since statusName comes from our config keys assignee: issue.assignee?.email, labels: issue.labels?.nodes?.map((label) => label.name) || [], priority: issue.priority || 0, due_date: dueDate, url: issue.url, created_at: createdAt, updated_at: updatedAt }; // Convert description (handle null/undefined) const content = issue.description || `# ${issue.title}\n\n*No description provided*`; // Convert comments const comments = linearComments.map(comment => ({ id: comment.id, author: comment.user?.email || comment.user?.name || 'Unknown', content: comment.body || '', created_at: parsers_1.TimezoneUtils.utcToSGT(comment.createdAt), // Linear doesn't have nested replies in comments, so we don't include replies })); debugLog(`Converting ${linearComments.length} comments for ${issue.identifier}`, { rawComments: linearComments, convertedComments: comments }); return { frontmatter, content, comments }; } function getImportStats(directory = 'md-linear-sync/linear-tickets') { const linearDir = path_1.default.join(process.cwd(), directory); if (!fs_1.default.existsSync(linearDir)) { return { totalFiles: 0, byStatus: {} }; } const stats = { totalFiles: 0, byStatus: {} }; // Get all subdirectories (status folders) const statusFolders = fs_1.default.readdirSync(linearDir, { withFileTypes: true }) .filter(dirent => dirent.isDirectory()) .map(dirent => dirent.name); for (const folder of statusFolders) { const folderPath = path_1.default.join(linearDir, folder); const files = fs_1.default.readdirSync(folderPath) .filter(file => file.endsWith('.md') && file !== 'README.md'); stats.byStatus[folder] = files.length; stats.totalFiles += files.length; } return stats; } //# sourceMappingURL=import.js.map