@taizo-pro/github-discussions-cli
Version:
A powerful command-line tool for interacting with GitHub Discussions without opening a browser
316 lines • 10.6 kB
JavaScript
import { graphql } from '@octokit/graphql';
import { ErrorType, } from './types.js';
export class GitHubClient {
graphqlWithAuth;
constructor(token) {
this.graphqlWithAuth = graphql.defaults({
headers: {
authorization: `token ${token}`,
},
});
}
async listDiscussions(repo, options = {}) {
try {
const [owner, name] = repo.split('/');
const { first = 20, after, orderBy } = options;
const query = `
query GetDiscussions($owner: String!, $name: String!, $first: Int!, $after: String, $orderBy: DiscussionOrder) {
repository(owner: $owner, name: $name) {
discussions(first: $first, after: $after, orderBy: $orderBy) {
nodes {
id
title
createdAt
updatedAt
url
locked
comments {
totalCount
}
author {
login
avatarUrl
}
category {
id
name
}
}
}
}
}
`;
const variables = {
owner,
name,
first,
after,
orderBy: orderBy || { field: 'UPDATED_AT', direction: 'DESC' },
};
const response = await this.graphqlWithAuth(query, variables);
return response.repository.discussions.nodes.map((node) => ({
id: node.id,
title: node.title,
author: {
login: node.author.login,
avatarUrl: node.author.avatarUrl,
},
createdAt: new Date(node.createdAt),
updatedAt: new Date(node.updatedAt),
commentCount: node.comments.totalCount,
category: node.category
? { id: node.category.id, name: node.category.name }
: undefined,
url: node.url,
locked: node.locked,
}));
}
catch (error) {
throw this.handleError(error);
}
}
async getDiscussion(repo, discussionId) {
try {
const [owner, name] = repo.split('/');
const query = `
query GetDiscussion($owner: String!, $name: String!, $number: Int!) {
repository(owner: $owner, name: $name) {
discussion(number: $number) {
id
title
body
createdAt
updatedAt
url
locked
author {
login
avatarUrl
}
category {
id
name
}
comments(first: 100) {
totalCount
nodes {
id
body
createdAt
url
author {
login
avatarUrl
}
}
}
}
}
}
`;
const discussionNumber = parseInt(discussionId, 10);
const response = await this.graphqlWithAuth(query, {
owner,
name,
number: discussionNumber,
});
const discussion = response.repository.discussion;
const comments = discussion.comments.nodes.map((comment) => ({
id: comment.id,
author: {
login: comment.author.login,
avatarUrl: comment.author.avatarUrl,
},
body: comment.body,
createdAt: new Date(comment.createdAt),
url: comment.url,
}));
return {
id: discussion.id,
title: discussion.title,
body: discussion.body,
author: {
login: discussion.author.login,
avatarUrl: discussion.author.avatarUrl,
},
createdAt: new Date(discussion.createdAt),
updatedAt: new Date(discussion.updatedAt),
commentCount: discussion.comments.totalCount,
category: discussion.category
? { id: discussion.category.id, name: discussion.category.name }
: undefined,
url: discussion.url,
locked: discussion.locked,
comments,
};
}
catch (error) {
throw this.handleError(error);
}
}
async createComment(repo, discussionId, body) {
try {
const mutation = `
mutation AddDiscussionComment($discussionId: ID!, $body: String!) {
addDiscussionComment(input: { discussionId: $discussionId, body: $body }) {
comment {
id
body
createdAt
url
author {
login
avatarUrl
}
}
}
}
`;
const response = await this.graphqlWithAuth(mutation, {
discussionId,
body,
});
const comment = response.addDiscussionComment.comment;
return {
id: comment.id,
author: {
login: comment.author.login,
avatarUrl: comment.author.avatarUrl,
},
body: comment.body,
createdAt: new Date(comment.createdAt),
url: comment.url,
};
}
catch (error) {
throw this.handleError(error);
}
}
async createDiscussion(repo, title, body, categoryId) {
try {
const [owner, name] = repo.split('/');
if (!categoryId) {
const categoriesQuery = `
query GetCategories($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
discussionCategories(first: 10) {
nodes {
id
name
}
}
}
}
`;
const categoriesResponse = await this.graphqlWithAuth(categoriesQuery, {
owner,
name,
});
const generalCategory = categoriesResponse.repository.discussionCategories.nodes.find((cat) => cat.name.toLowerCase() === 'general');
if (generalCategory) {
categoryId = generalCategory.id;
}
else {
categoryId = categoriesResponse.repository.discussionCategories.nodes[0]?.id;
}
}
const mutation = `
mutation CreateDiscussion($repositoryId: ID!, $categoryId: ID!, $title: String!, $body: String!) {
createDiscussion(input: { repositoryId: $repositoryId, categoryId: $categoryId, title: $title, body: $body }) {
discussion {
id
title
createdAt
updatedAt
url
locked
author {
login
avatarUrl
}
category {
id
name
}
comments {
totalCount
}
}
}
}
`;
const repositoryQuery = `
query GetRepositoryId($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
id
}
}
`;
const repoResponse = await this.graphqlWithAuth(repositoryQuery, {
owner,
name,
});
const response = await this.graphqlWithAuth(mutation, {
repositoryId: repoResponse.repository.id,
categoryId,
title,
body,
});
const discussion = response.createDiscussion.discussion;
return {
id: discussion.id,
title: discussion.title,
author: {
login: discussion.author.login,
avatarUrl: discussion.author.avatarUrl,
},
createdAt: new Date(discussion.createdAt),
updatedAt: new Date(discussion.updatedAt),
commentCount: discussion.comments.totalCount,
category: discussion.category
? { id: discussion.category.id, name: discussion.category.name }
: undefined,
url: discussion.url,
locked: discussion.locked,
};
}
catch (error) {
throw this.handleError(error);
}
}
handleError(error) {
if (error.errors) {
const graphqlError = error.errors[0];
if (graphqlError.type === 'UNAUTHORIZED') {
return {
type: ErrorType.AUTHENTICATION_ERROR,
message: 'Invalid or expired GitHub token',
suggestions: [
'Check your GitHub Personal Access Token',
'Ensure the token has discussions scope',
'Run gh-discussions config to update your token',
],
};
}
return {
type: ErrorType.API_ERROR,
message: graphqlError.message,
details: error.errors,
};
}
if ((error.code && (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED')) ||
(error.status && (error.status === 'ENOTFOUND' || error.status === 'ECONNREFUSED'))) {
return {
type: ErrorType.NETWORK_ERROR,
message: 'Network connection failed',
suggestions: ['Check your internet connection', 'Try again later'],
};
}
return {
type: ErrorType.API_ERROR,
message: error.message || 'Unknown error occurred',
details: error,
};
}
}
//# sourceMappingURL=github-client.js.map