md-linear-sync
Version:
Sync Linear tickets to local markdown files with status-based folder organization
287 lines • 10.9 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.SlackNotificationServiceImpl = void 0;
const Diff = __importStar(require("diff"));
class SlackNotificationServiceImpl {
constructor() {
this.botToken = process.env.SLACK_BOT_TOKEN || '';
this.channel = process.env.SLACK_CHANNEL || 'md-linear-sync-notifications';
if (!this.botToken) {
console.warn('⚠️ SLACK_BOT_TOKEN not configured - Slack notifications disabled');
}
}
async sendWebhookNotification(event) {
if (!this.botToken) {
return; // Silently skip if not configured
}
try {
const message = this.formatWebhookMessage(event);
await this.sendToSlack(message);
}
catch (error) {
console.error('Failed to send Slack notification:', error instanceof Error ? error.message : 'Unknown error');
}
}
formatWebhookMessage(event) {
const { ticketId, ticketTitle, ticketUrl, type, changes, comment } = event;
if (type === 'comment_added' && comment) {
return {
text: `New comment on ${ticketId}`,
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: `New comment on ${ticketId}`
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*<${ticketUrl}|${ticketTitle}>*`
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*${comment.author}* commented:\n${this.truncateText(comment.content, 200)}`
}
},
]
};
}
if (type === 'issue_created') {
return {
text: `${ticketId} created`,
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: `${ticketId} created`
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*<${ticketUrl}|${ticketTitle}>*`
}
}
]
};
}
if (type === 'issue_deleted') {
return {
text: `${ticketId} deleted`,
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: `${ticketId} deleted`
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*${ticketTitle}*`
}
}
]
};
}
if (type === 'issue_updated' && changes) {
const changeTexts = this.formatChanges(changes);
return {
text: `${ticketId} updated`,
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: `${ticketId} updated`
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*<${ticketUrl}|${ticketTitle}>*`
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: changeTexts.join('\n')
}
},
]
};
}
// Fallback simple message
return {
text: `${ticketId} synced from Linear`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*<${ticketUrl}|${ticketId}>* synced from Linear`
}
}
]
};
}
formatChanges(changes) {
const changeTexts = [];
if (changes.status) {
if (changes.status.from) {
changeTexts.push(`Status: \`${changes.status.from}\` → \`${changes.status.to}\``);
}
else {
changeTexts.push(`Status: \`${changes.status.to}\``);
}
}
if (changes.assignee && changes.assignee.from !== changes.assignee.to) {
const fromText = changes.assignee.from || 'Unassigned';
const toText = changes.assignee.to || 'Unassigned';
changeTexts.push(`Assignee: \`${fromText}\` → \`${toText}\``);
}
if (changes.title && changes.title.from !== changes.title.to) {
changeTexts.push(`Title: \`${this.truncateText(changes.title.from, 30)}\` → \`${this.truncateText(changes.title.to, 30)}\``);
}
if (changes.description && changes.description.from !== changes.description.to) {
const diff = this.generateDiff(changes.description.from, changes.description.to);
changeTexts.push(`Description:\n\`\`\`\n${diff}\n\`\`\``);
}
if (changes.labels) {
if (changes.labels.added.length > 0) {
changeTexts.push(`Labels added: ${changes.labels.added.map(l => `\`${l}\``).join(', ')}`);
}
if (changes.labels.removed.length > 0) {
changeTexts.push(`Labels removed: ${changes.labels.removed.map(l => `\`${l}\``).join(', ')}`);
}
}
if (changes.priority && changes.priority.from !== changes.priority.to) {
const priorityNames = ['No Priority', 'Urgent', 'High', 'Normal', 'Low'];
const fromPriority = priorityNames[changes.priority.from] || `Priority ${changes.priority.from}`;
const toPriority = priorityNames[changes.priority.to] || `Priority ${changes.priority.to}`;
changeTexts.push(`Priority: \`${fromPriority}\` → \`${toPriority}\``);
}
return changeTexts;
}
getStatusEmoji(status) {
const statusMap = {
'Todo': '📋',
'Backlog': '📚',
'In Progress': '🔄',
'In Development': '💻',
'In Review': '👀',
'Code Review': '🔍',
'Done': '✅',
'Completed': '✅',
'Cancelled': '❌'
};
return statusMap[status] || '📝';
}
truncateText(text, maxLength) {
if (text.length <= maxLength) {
return text;
}
return text.substring(0, maxLength - 3) + '...';
}
generateDiff(oldText, newText) {
// Use proper diff library for better results
const diff = Diff.diffLines(oldText, newText);
const diffLines = [];
let changeCount = 0;
const maxChanges = 15; // Limit diff size for Slack
for (const part of diff) {
if (changeCount >= maxChanges)
break;
if (part.added) {
const lines = part.value.trim().split('\n');
for (const line of lines) {
if (changeCount < maxChanges && line.trim()) {
diffLines.push(`+ ${line}`);
changeCount++;
}
}
}
else if (part.removed) {
const lines = part.value.trim().split('\n');
for (const line of lines) {
if (changeCount < maxChanges && line.trim()) {
diffLines.push(`- ${line}`);
changeCount++;
}
}
}
// Skip unchanged parts for brevity
}
if (changeCount >= maxChanges) {
diffLines.push('... (truncated for display)');
}
return diffLines.length > 0 ? diffLines.join('\n') : 'No significant changes detected';
}
async sendToSlack(message) {
const response = await fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.botToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
channel: this.channel.replace('#', ''),
...message
})
});
const result = await response.json();
if (!result.ok) {
throw new Error(`Slack API error: ${result.error}`);
}
}
static getInstance() {
return new SlackNotificationServiceImpl();
}
}
exports.SlackNotificationServiceImpl = SlackNotificationServiceImpl;
//# sourceMappingURL=SlackNotificationService.js.map