md-linear-sync
Version:
Sync Linear tickets to local markdown files with status-based folder organization
471 lines • 15.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.LinearClient = exports.LinearSyncClient = exports.LinearDiscoveryClient = void 0;
const { LinearClient } = require('@linear/sdk');
exports.LinearClient = LinearClient;
const RetryManager_1 = require("../utils/RetryManager");
class LinearDiscoveryClient {
constructor(apiKey) {
this.client = new LinearClient({ apiKey });
}
async getTeams() {
try {
const response = await this.client.teams();
return response.nodes.map((team) => ({
id: team.id,
name: team.name,
key: team.key
}));
}
catch (error) {
throw new Error(`Failed to fetch teams: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async getProjects(teamId) {
try {
// Get the team first, then get its projects directly
const team = await this.client.team(teamId);
const projectsConnection = await team.projects();
return projectsConnection.nodes.map((project) => ({
id: project.id,
name: project.name,
description: project.description || undefined
}));
}
catch (error) {
throw new Error(`Failed to fetch projects: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async getWorkflowStates(teamId) {
try {
const response = await this.client.workflowStates({
filter: { team: { id: { eq: teamId } } }
});
return response.nodes.map((state) => ({
id: state.id,
name: state.name,
type: state.type,
position: state.position
})).sort((a, b) => a.position - b.position);
}
catch (error) {
throw new Error(`Failed to fetch workflow states: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async getTeamLabels(teamId) {
try {
// Use direct GraphQL query through team object as the issueLabels filter doesn't work correctly
const query = `
query GetTeamLabels($teamId: String!) {
team(id: $teamId) {
labels {
nodes {
id
name
color
description
}
}
}
}
`;
const rawResponse = await this.client.client.rawRequest(query, { teamId });
const labels = rawResponse.data?.team?.labels?.nodes || [];
return labels.map((label) => ({
id: label.id,
name: label.name,
color: label.color,
description: label.description
}));
}
catch (error) {
throw new Error(`Failed to fetch team labels: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async validateApiKey() {
try {
const viewer = await this.client.viewer;
return {
valid: true,
user: {
name: viewer.name,
email: viewer.email
}
};
}
catch (error) {
return { valid: false };
}
}
}
exports.LinearDiscoveryClient = LinearDiscoveryClient;
class LinearSyncClient {
constructor(apiKey) {
this.client = new LinearClient({ apiKey });
}
async getIssue(issueId) {
return RetryManager_1.RetryManager.withRetry(async () => {
const query = `
query GetIssue($issueId: String!) {
issue(id: $issueId) {
id
identifier
title
description
url
priority
createdAt
updatedAt
dueDate
branchName
number
labels {
nodes {
id
name
}
}
assignee {
id
name
email
}
creator {
id
name
email
}
parent {
id
identifier
}
state {
id
name
type
}
comments(first: 50) {
nodes {
id
body
createdAt
user {
id
name
email
}
}
}
}
}
`;
const variables = { issueId };
const rawResponse = await this.client.client.rawRequest(query, variables);
const issue = rawResponse.data?.issue;
if (!issue) {
throw new Error(`Issue ${issueId} not found`);
}
return issue;
}, {}, `fetch issue ${issueId}`);
}
async getIssues(teamId, projectId, limit = 100) {
try {
const filter = { team: { id: { eq: teamId } } };
if (projectId) {
filter.project = { id: { eq: projectId } };
}
// Use rawRequest to get headers and fetch issues with comments in one call
const query = `
query IssuesWithCommentsQuery($filter: IssueFilter, $first: Int, $includeArchived: Boolean) {
issues(filter: $filter, first: $first, includeArchived: $includeArchived) {
nodes {
id
identifier
title
description
url
priority
createdAt
updatedAt
dueDate
branchName
number
labels {
nodes {
id
name
}
}
assignee {
id
name
email
}
creator {
id
name
email
}
parent {
id
identifier
}
state {
id
name
type
}
comments(first: 50) {
nodes {
id
body
createdAt
user {
id
name
email
}
}
}
}
}
}
`;
const variables = {
filter,
first: limit,
includeArchived: false
};
const rawResponse = await this.client.client.rawRequest(query, variables);
// Extract rate limit info from headers
let apiUsage = undefined;
if (rawResponse.headers) {
const headers = rawResponse.headers;
apiUsage = {
requestsLimit: this.parseNumber(headers.get?.('x-ratelimit-requests-limit')),
requestsRemaining: this.parseNumber(headers.get?.('x-ratelimit-requests-remaining')),
requestsResetAt: this.parseNumber(headers.get?.('x-ratelimit-requests-reset')),
note: "Rate limit info extracted from response headers"
};
}
return {
issues: rawResponse.data?.issues?.nodes || [],
apiUsage
};
}
catch (error) {
// If it's a rate limit error, extract the rate limit info
if (error instanceof Error && 'requestsRemaining' in error) {
const rateLimitError = error;
throw new Error(`Rate limit exceeded: ${rateLimitError.requestsRemaining}/${rateLimitError.requestsLimit} requests remaining`);
}
throw new Error(`Failed to fetch issues: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
parseNumber(value) {
if (value === undefined || value === null || value === "") {
return undefined;
}
return Number(value) ?? undefined;
}
async createIssue(teamId, title, description, stateId, projectId, labelIds, parentId, priority) {
try {
const issueInput = {
teamId,
title,
description: description || ''
};
if (stateId)
issueInput.stateId = stateId;
if (projectId)
issueInput.projectId = projectId;
if (labelIds && labelIds.length > 0)
issueInput.labelIds = labelIds;
if (parentId)
issueInput.parentId = parentId;
if (priority !== undefined)
issueInput.priority = priority;
const response = await this.client.createIssue(issueInput);
return response.issue;
}
catch (error) {
throw new Error(`Failed to create issue: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async findIssueByIdentifier(identifier) {
try {
const query = `
query FindIssueByIdentifier($identifier: String!) {
issue(id: $identifier) {
id
identifier
title
}
}
`;
const rawResponse = await this.client.client.rawRequest(query, { identifier });
return rawResponse.data?.issue || null;
}
catch (error) {
// Issue not found is not an error - return null
return null;
}
}
async updateIssue(issueId, updates) {
return RetryManager_1.RetryManager.withRetry(async () => {
const response = await this.client.updateIssue(issueId, updates);
return response.issue;
}, {}, `update issue ${issueId}`);
}
async getComments(issueId) {
try {
const response = await this.client.comments({
filter: { issue: { id: { eq: issueId } } }
});
return response.nodes;
}
catch (error) {
throw new Error(`Failed to fetch comments for issue ${issueId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async createComment(issueId, body) {
try {
const response = await this.client.createComment({
issueId,
body
});
return response.comment;
}
catch (error) {
throw new Error(`Failed to create comment: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Webhook management methods
async getWebhooks() {
try {
const query = `
query GetWebhooks {
webhooks {
nodes {
id
url
enabled
secret
team {
id
name
}
}
}
}
`;
const rawResponse = await this.client.client.rawRequest(query);
return rawResponse.data?.webhooks?.nodes || [];
}
catch (error) {
throw new Error(`Failed to fetch webhooks: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async createWebhook(input) {
try {
const mutation = `
mutation WebhookCreate($input: WebhookCreateInput!) {
webhookCreate(input: $input) {
success
webhook {
id
url
enabled
}
}
}
`;
const variables = {
input: {
url: input.url,
teamId: input.teamId,
secret: input.secret || this.generateWebhookSecret(),
resourceTypes: ["Issue", "Comment"]
}
};
const rawResponse = await this.client.client.rawRequest(mutation, variables);
const result = rawResponse.data?.webhookCreate;
if (!result?.success) {
throw new Error('Failed to create webhook');
}
return result.webhook.id;
}
catch (error) {
throw new Error(`Failed to create webhook: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async updateWebhook(id, input) {
try {
const mutation = `
mutation WebhookUpdate($id: String!, $input: WebhookUpdateInput!) {
webhookUpdate(id: $id, input: $input) {
success
webhook {
id
url
enabled
}
}
}
`;
const variables = { id, input };
const rawResponse = await this.client.client.rawRequest(mutation, variables);
const result = rawResponse.data?.webhookUpdate;
if (!result?.success) {
throw new Error('Failed to update webhook');
}
}
catch (error) {
throw new Error(`Failed to update webhook: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async deleteWebhook(id) {
try {
const mutation = `
mutation WebhookDelete($id: String!) {
webhookDelete(id: $id) {
success
}
}
`;
const variables = { id };
const rawResponse = await this.client.client.rawRequest(mutation, variables);
const result = rawResponse.data?.webhookDelete;
if (!result?.success) {
throw new Error('Failed to delete webhook');
}
}
catch (error) {
throw new Error(`Failed to delete webhook: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async upsertWebhook(input) {
try {
// Check if webhook exists for this team
const existingWebhooks = await this.getWebhooks();
const ourWebhook = existingWebhooks.find(w => w.team?.id === input.teamId && w.url.includes('ngrok'));
if (ourWebhook) {
// Update existing
await this.updateWebhook(ourWebhook.id, { url: input.url });
return ourWebhook.id;
}
else {
// Create new
return await this.createWebhook(input);
}
}
catch (error) {
throw new Error(`Failed to upsert webhook: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
generateWebhookSecret() {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}
}
exports.LinearSyncClient = LinearSyncClient;
//# sourceMappingURL=index.js.map