md-linear-sync
Version:
Sync Linear tickets to local markdown files with status-based folder organization
208 lines • 8.94 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TimezoneUtils = exports.TicketFileParser = void 0;
const gray_matter_1 = __importDefault(require("gray-matter"));
const js_yaml_1 = __importDefault(require("js-yaml"));
const contentProcessor_1 = require("../utils/contentProcessor");
class TicketFileParser {
static parseFile(content) {
try {
// Parse frontmatter and content using gray-matter
const parsed = (0, gray_matter_1.default)(content);
// Extract frontmatter as metadata
const frontmatter = parsed.data;
// Split content to separate main content from comments
const parts = parsed.content.split(`\n${this.COMMENTS_SEPARATOR}\n`);
let mainContent = parts[0].trim();
// Remove duplicate H1 title if it matches the frontmatter title
mainContent = (0, contentProcessor_1.removeDuplicateTitle)(mainContent, frontmatter.title);
// Parse comments from backmatter if it exists
let comments = [];
if (parts.length > 1 && parts[1].trim()) {
try {
const commentsData = js_yaml_1.default.load(parts[1].trim());
comments = Array.isArray(commentsData) ? commentsData : [];
}
catch (error) {
console.warn('Failed to parse comments section:', error);
comments = [];
}
}
return {
frontmatter,
content: mainContent,
comments
};
}
catch (error) {
throw new Error(`Failed to parse ticket file: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
static generateFile(ticket) {
try {
// Validate required frontmatter fields
this.validateFrontmatter(ticket.frontmatter);
// Generate frontmatter section
const frontmatterYaml = js_yaml_1.default.dump(ticket.frontmatter, {
sortKeys: true,
lineWidth: -1 // Prevent line wrapping
});
// Start with frontmatter
let content = '---\n' + frontmatterYaml + '---\n\n';
// Add main content
content += ticket.content;
// Add comments section if comments exist
if (ticket.comments && ticket.comments.length > 0) {
content += '\n\n' + this.COMMENTS_SEPARATOR + '\n';
try {
const commentsYaml = js_yaml_1.default.dump(ticket.comments, {
sortKeys: false,
lineWidth: -1,
quotingType: '"',
forceQuotes: true
});
content += commentsYaml;
}
catch (yamlError) {
// Fallback: skip comments section but don't fail the entire file generation
}
}
return content;
}
catch (error) {
throw new Error(`Failed to generate ticket file: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
static generateFilename(linearId, title, parentId) {
// Sanitize title for filename
const sanitizedTitle = title
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '') // Remove special chars
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-') // Collapse multiple hyphens
.replace(/^-|-$/g, '') // Remove leading/trailing hyphens
.substring(0, 50); // Limit length
// Handle parent-child relationships properly
if (parentId) {
// Child ticket: PAP-434.447-child-task.md
const childNumber = linearId.split('-')[1];
return `${parentId}.${childNumber}-${sanitizedTitle}.md`;
}
else {
// Regular ticket: PAP-447-implement-feature.md
return `${linearId}-${sanitizedTitle}.md`;
}
}
static extractLinearIdFromFilename(filename) {
// Extract Linear ID from filename:
// PAP-431-implement-feature.md -> PAP-431 (regular)
// PAP-434.447-child-task.md -> PAP-447 (child ticket, return actual ticket ID)
const match = filename.match(/^(?:([A-Z]+-\d+)\.(\d+)|([A-Z]+-\d+))/);
if (match) {
// If it's a parent.child format, return the child ID (PAP-447)
if (match[1] && match[2]) {
const parentPrefix = match[1].split('-')[0]; // PAP
return `${parentPrefix}-${match[2]}`;
}
// Otherwise return the regular ID
return match[3];
}
return null;
}
static validateFile(content) {
const errors = [];
try {
const ticket = this.parseFile(content);
// Validate frontmatter
const frontmatterErrors = this.validateFrontmatter(ticket.frontmatter);
errors.push(...frontmatterErrors);
// Validate content exists
if (!ticket.content || ticket.content.trim().length === 0) {
errors.push('Content section is empty');
}
// Validate comments structure if present
if (ticket.comments) {
for (let i = 0; i < ticket.comments.length; i++) {
const comment = ticket.comments[i];
if (!comment.id)
errors.push(`Comment ${i + 1} missing id`);
if (!comment.author)
errors.push(`Comment ${i + 1} missing author`);
if (!comment.content)
errors.push(`Comment ${i + 1} missing content`);
if (!comment.created_at)
errors.push(`Comment ${i + 1} missing created_at`);
}
}
}
catch (error) {
errors.push(`Parse error: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
return {
valid: errors.length === 0,
errors
};
}
static validateFrontmatter(frontmatter) {
const errors = [];
if (!frontmatter.linear_id)
errors.push('Missing linear_id');
if (!frontmatter.status)
errors.push('Missing status');
if (!frontmatter.url)
errors.push('Missing url');
if (!frontmatter.created_at)
errors.push('Missing created_at');
if (!frontmatter.updated_at)
errors.push('Missing updated_at');
// Validate priority is valid number
if (frontmatter.priority && ![1, 2, 3, 4].includes(frontmatter.priority)) {
errors.push('Priority must be 1, 2, 3, or 4');
}
// Validate labels is array
if (frontmatter.labels && !Array.isArray(frontmatter.labels)) {
errors.push('Labels must be an array');
}
// Validate dates are valid ISO strings
if (frontmatter.created_at && !this.isValidISODate(frontmatter.created_at)) {
errors.push('created_at must be valid ISO date string');
}
if (frontmatter.updated_at && !this.isValidISODate(frontmatter.updated_at)) {
errors.push('updated_at must be valid ISO date string');
}
if (frontmatter.due_date && !this.isValidISODate(frontmatter.due_date)) {
errors.push('due_date must be valid ISO date string');
}
return errors;
}
static isValidISODate(dateString) {
const date = new Date(dateString);
return date instanceof Date && !isNaN(date.getTime()) && dateString.includes('T');
}
}
exports.TicketFileParser = TicketFileParser;
TicketFileParser.COMMENTS_SEPARATOR = '---comments---';
// Utility functions for timezone conversion
class TimezoneUtils {
static utcToSGT(utcDateString) {
const utcDate = new Date(utcDateString);
// Convert to SGT (UTC+8)
const sgtDate = new Date(utcDate.getTime() + (8 * 60 * 60 * 1000));
return sgtDate.toISOString().replace('Z', '+08:00');
}
static sgtToUTC(sgtDateString) {
// Remove SGT timezone and parse as if it were UTC, then subtract 8 hours
const cleanDate = sgtDateString.replace('+08:00', 'Z');
const date = new Date(cleanDate);
const utcDate = new Date(date.getTime() - (8 * 60 * 60 * 1000));
return utcDate.toISOString();
}
static now() {
return this.utcToSGT(new Date().toISOString());
}
}
exports.TimezoneUtils = TimezoneUtils;
//# sourceMappingURL=index.js.map