const fs = require("node:fs").promises;
const {
handleError,
validateRequired,
ErrorTypes,
} = require("../utils/error-handler.cjs");
const path = require("node:path");
class GitHubAPIService {
constructor(token) {
this.token = token;
if (!this.token) {
console.error(
"Error: GH_TOKEN environment variable is not set or empty after trim"
);
process.exit(1);
}
}
async makeGraphQLRequest(query, variables = {}) {
const url = `https://api.github.com/graphql`;
const headers = {
Authorization: `bearer ${this.token}`, // GraphQL uses 'bearer'
"Content-Type": "application/json",
Accept: "application/json",
"User-Agent": "node-fetch",
};
try {
const response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify({ query, variables }),
});
if (!response.ok) {
const errorData = await response.text();
throw new Error(
`GitHub GraphQL API error: ${response.status} ${response.statusText}\n${errorData}`
);
}
const responseJson = await response.json();
if (responseJson.errors) {
throw new Error(
`GitHub GraphQL API errors: ${JSON.stringify(responseJson.errors)}`
);
}
return responseJson.data;
} catch (error) {
console.error(`GraphQL request failed: ${error.message}`);
throw error;
}
}
async makeGitHubRequest(endpoint, options = {}) {
// Debug logging disabled to avoid MCP protocol interference
// console.error("makeGitHubRequest method entered.");
const url = `https://api.github.com${endpoint}`;
const headers = {
Authorization: `token ${this.token}`,
Accept: "application/vnd.github.v3+json",
"User-Agent": "node-fetch",
...options.headers,
};
try {
const response = await fetch(url, {
...options,
headers,
});
if (response.status === 204) {
return {
success: true,
message: `Operation successful (${response.status})`,
};
}
if (!response.ok) {
const errorData = await response.text();
throw new Error(
`GitHub API error: ${response.status} ${response.statusText}\n${errorData}`
);
}
// For other successful responses, try to parse JSON
const responseText = await response.text();
if (responseText) {
return JSON.parse(responseText);
}
return {
success: true,
message: `Operation successful (${response.status})`,
};
} catch (error) {
console.error(`API request failed: ${error.message}`);
throw error;
}
}
async createOrUpdateFile(
owner,
repo,
filePathInRepo,
commitMessage,
base64Content,
sha, // Optional: Required for updates
branch // Optional: Specify a branch
) {
let endpoint = `/repos/${owner}/${repo}/contents/${filePathInRepo}`;
// Debug logging disabled to avoid MCP protocol interference
// console.error(
// `Attempting to create/update file at: ${endpoint} on branch: ${branch}`
// );
const payload = {
message: commitMessage,
content: base64Content,
};
if (sha) {
payload.sha = sha; // Required for updating an existing file
}
if (branch) {
payload.branch = branch;
}
try {
const result = await this.makeGitHubRequest(endpoint, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
return result;
} catch (error) {
console.error(
`Failed to create/update file '${filePathInRepo}': ${error.message}`
);
throw error;
}
}
async deleteFileFromRepo(
owner,
repo,
filePathInRepo,
commitMessage,
sha, // Required for deleting a file
branch // Optional: Specify a branch
) {
let endpoint = `/repos/${owner}/${repo}/contents/${filePathInRepo}`;
// Debug logging disabled to avoid MCP protocol interference
// console.error(
// `Attempting to delete file at: ${endpoint} on branch: ${branch}`
// );
const payload = {
message: commitMessage,
sha: sha, // Required
};
if (branch) {
payload.branch = branch;
}
try {
const result = await this.makeGitHubRequest(endpoint, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
return result; // GitHub API returns 200 with commit details or 204 if no content
} catch (error) {
console.error(
`Failed to delete file '${filePathInRepo}': ${error.message}`
);
throw error;
}
}
async getRepoContents(owner, repo, filePath, ref) {
let endpoint = `/repos/${owner}/${repo}/contents/${filePath}`;
if (ref) {
endpoint += `?ref=${encodeURIComponent(ref)}`;
}
// Debug logging disabled to avoid MCP protocol interference
// console.error(`Attempting to get contents for: ${endpoint}`);
try {
const result = await this.makeGitHubRequest(endpoint);
return result;
} catch (error) {
// If it's a 404, it means the file/path doesn't exist, which can be normal.
// The makeGitHubRequest method throws for non-ok statuses, so we catch here.
if (error.message && error.message.includes("404")) {
// console.error(`File or path not found (404): ${endpoint}`);
// Re-throw as a more specific error or return a specific value if preferred by consumers
} else {
console.error(
`Failed to get repo contents for '${filePath}': ${error.message}`
);
}
throw error; // Re-throw to be handled by the caller
}
}
async testAuthentication() {
try {
const user = await this.makeGitHubRequest("/user");
return user;
} catch (error) {
console.error("Authentication failed:", error.message);
throw error;
}
}
// --- Pull Request Methods ---
async createPullRequest(owner, repo, title, head, base, body) {
const endpoint = `/repos/${owner}/${repo}/pulls`;
const payload = { title, head, base };
if (body) {
payload.body = body;
}
return this.makeGitHubRequest(endpoint, {
method: "POST",
body: JSON.stringify(payload),
headers: { "Content-Type": "application/json" },
});
}
async updatePullRequest(owner, repo, pullNumber, data) {
const endpoint = `/repos/${owner}/${repo}/pulls/${pullNumber}`;
// Filter out undefined values from data to avoid sending them in the payload
const payload = Object.fromEntries(
Object.entries(data).filter(([_, v]) => v !== undefined)
);
return this.makeGitHubRequest(endpoint, {
method: "PATCH",
body: JSON.stringify(payload),
headers: { "Content-Type": "application/json" },
});
}
async getPullRequest(owner, repo, pullNumber) {
const endpoint = `/repos/${owner}/${repo}/pulls/${pullNumber}`;
return this.makeGitHubRequest(endpoint);
}
async listPullRequestReviews(
owner,
repo,
pullNumber,
per_page = 30,
page = 1
) {
const endpoint = `/repos/${owner}/${repo}/pulls/${pullNumber}/reviews?per_page=${per_page}&page=${page}`;
return this.makeGitHubRequest(endpoint);
}
async createPullRequestReview(owner, repo, pullNumber, reviewData) {
const endpoint = `/repos/${owner}/${repo}/pulls/${pullNumber}/reviews`;
// Filter out undefined values
const payload = Object.fromEntries(
Object.entries(reviewData).filter(([_, v]) => v !== undefined)
);
return this.makeGitHubRequest(endpoint, {
method: "POST",
body: JSON.stringify(payload),
headers: { "Content-Type": "application/json" },
});
}
async listPullRequestFiles(owner, repo, pullNumber, per_page = 30, page = 1) {
const endpoint = `/repos/${owner}/${repo}/pulls/${pullNumber}/files?per_page=${per_page}&page=${page}`;
return this.makeGitHubRequest(endpoint);
}
// --- Security & Access Management Methods ---
async listDeployKeys(owner, repo, per_page = 30) {
const endpoint = `/repos/${owner}/${repo}/keys?per_page=${per_page}`;
return this.makeGitHubRequest(endpoint);
}
async createDeployKey(owner, repo, title, key, read_only = false) {
const endpoint = `/repos/${owner}/${repo}/keys`;
const payload = { title, key, read_only };
return this.makeGitHubRequest(endpoint, {
method: "POST",
body: JSON.stringify(payload),
headers: { "Content-Type": "application/json" },
});
}
async deleteDeployKey(owner, repo, keyId) {
const endpoint = `/repos/${owner}/${repo}/keys/${keyId}`;
return this.makeGitHubRequest(endpoint, {
method: "DELETE",
});
}
async listWebhooks(owner, repo, per_page = 30) {
const endpoint = `/repos/${owner}/${repo}/hooks?per_page=${per_page}`;
return this.makeGitHubRequest(endpoint);
}
async createWebhook(owner, repo, config, events = ["push"], active = true) {
const endpoint = `/repos/${owner}/${repo}/hooks`;
const payload = { name: "web", active, events, config };
return this.makeGitHubRequest(endpoint, {
method: "POST",
body: JSON.stringify(payload),
headers: { "Content-Type": "application/json" },
});
}
async updateWebhook(owner, repo, hookId, data) {
const endpoint = `/repos/${owner}/${repo}/hooks/${hookId}`;
// Filter out undefined values from data to avoid sending them in the payload
const payload = Object.fromEntries(
Object.entries(data).filter(([_, v]) => v !== undefined)
);
return this.makeGitHubRequest(endpoint, {
method: "PATCH",
body: JSON.stringify(payload),
headers: { "Content-Type": "application/json" },
});
}
async deleteWebhook(owner, repo, hookId) {
const endpoint = `/repos/${owner}/${repo}/hooks/${hookId}`;
return this.makeGitHubRequest(endpoint, {
method: "DELETE",
});
}
async listSecrets(owner, repo, per_page = 30) {
const endpoint = `/repos/${owner}/${repo}/actions/secrets?per_page=${per_page}`;
return this.makeGitHubRequest(endpoint);
}
async createOrUpdateSecret(owner, repo, secretName, encryptedValue, keyId) {
const endpoint = `/repos/${owner}/${repo}/actions/secrets/${secretName}`;
const payload = {
encrypted_value: encryptedValue,
key_id: keyId,
};
return this.makeGitHubRequest(endpoint, {
method: "PUT",
body: JSON.stringify(payload),
headers: { "Content-Type": "application/json" },
});
}
async getRepoPublicKey(owner, repo) {
const endpoint = `/repos/${owner}/${repo}/actions/secrets/public-key`;
return this.makeGitHubRequest(endpoint);
}
// --- GitHub Actions & Workflows Methods ---
async listWorkflows(owner, repo, per_page = 30, page = 1) {
const endpoint = `/repos/${owner}/${repo}/actions/workflows?per_page=${per_page}&page=${page}`;
return this.makeGitHubRequest(endpoint);
}
async listWorkflowRuns(owner, repo, workflow_id, params = {}) {
// params can include: actor, branch, event, status, per_page, page, created, exclude_pull_requests, check_suite_id, head_sha
const queryParams = new URLSearchParams();
if (params.actor) queryParams.append("actor", params.actor);
if (params.branch) queryParams.append("branch", params.branch);
if (params.event) queryParams.append("event", params.event);
if (params.status) queryParams.append("status", params.status);
queryParams.append("per_page", params.per_page || 30);
queryParams.append("page", params.page || 1);
if (params.created) queryParams.append("created", params.created);
if (params.exclude_pull_requests !== undefined)
queryParams.append("exclude_pull_requests", params.exclude_pull_requests);
if (params.check_suite_id)
queryParams.append("check_suite_id", params.check_suite_id);
if (params.head_sha) queryParams.append("head_sha", params.head_sha);
const endpoint = `/repos/${owner}/${repo}/actions/workflows/${workflow_id}/runs?${queryParams.toString()}`;
return this.makeGitHubRequest(endpoint);
}
async getWorkflowRunDetails(
owner,
repo,
run_id,
exclude_pull_requests = false
) {
const queryParams = new URLSearchParams();
if (exclude_pull_requests) {
queryParams.append("exclude_pull_requests", "true");
}
const queryString = queryParams.toString();
const endpoint = `/repos/${owner}/${repo}/actions/runs/${run_id}${
queryString ? `?${queryString}` : ""
}`;
return this.makeGitHubRequest(endpoint);
}
async triggerWorkflow(owner, repo, workflow_id, ref, inputs = {}) {
const endpoint = `/repos/${owner}/${repo}/actions/workflows/${workflow_id}/dispatches`;
const payload = { ref, inputs };
return this.makeGitHubRequest(endpoint, {
method: "POST",
body: JSON.stringify(payload),
headers: { "Content-Type": "application/json" },
});
}
async downloadWorkflowArtifact(
owner,
repo,
artifact_id,
archive_format = "zip"
) {
// This endpoint returns a 302 redirect to the artifact download URL.
// makeGitHubRequest is not designed to return the Location header directly.
// We need a slightly different approach here.
const endpoint = `/repos/${owner}/${repo}/actions/artifacts/${artifact_id}/${archive_format}`;
const url = `https://api.github.com${endpoint}`;
const headers = {
Authorization: `token ${this.token}`,
Accept: "application/vnd.github.v3+json", // Though for 302, this might not be strictly necessary
"User-Agent": "node-fetch",
};
try {
const response = await fetch(url, {
method: "GET", // GitHub docs specify GET, but it's a redirect
headers,
redirect: "manual", // Important: we want to handle the redirect ourselves to get the URL
});
if (response.status === 302) {
const location = response.headers.get("Location");
if (location) {
return { download_url: location, success: true };
}
throw new Error(
"Artifact download redirect (302) received, but no Location header found."
);
}
// Handle other non-successful statuses if not a 302
if (!response.ok) {
const errorData = await response.text();
throw new Error(
`GitHub API error for artifact download: ${response.status} ${response.statusText}\n${errorData}`
);
}
// Should not happen if redirect is manual and status is 302
return {
success: false,
message: `Unexpected response status: ${response.status}`,
};
} catch (error) {
console.error(
`Failed to get workflow artifact download URL for artifact ID '${artifact_id}': ${error.message}`
);
throw error;
}
}
async cancelWorkflowRun(owner, repo, run_id) {
const endpoint = `/repos/${owner}/${repo}/actions/runs/${run_id}/cancel`;
// This API returns a 202 Accepted on success
return this.makeGitHubRequest(endpoint, {
method: "POST",
// No body required for this POST request
});
}
// --- Repository Analytics & Insights Methods ---
async getRepositoryTrafficViews(owner, repo, per = "day") {
// per can be 'day' or 'week'
const endpoint = `/repos/${owner}/${repo}/traffic/views?per=${per}`;
return this.makeGitHubRequest(endpoint);
}
async getRepositoryTrafficClones(owner, repo, per = "day") {
// per can be 'day' or 'week'
const endpoint = `/repos/${owner}/${repo}/traffic/clones?per=${per}`;
return this.makeGitHubRequest(endpoint);
}
async getRepositoryPopularReferrers(owner, repo) {
const endpoint = `/repos/${owner}/${repo}/traffic/popular/referrers`;
return this.makeGitHubRequest(endpoint);
}
async getRepositoryPopularPaths(owner, repo) {
const endpoint = `/repos/${owner}/${repo}/traffic/popular/paths`;
return this.makeGitHubRequest(endpoint);
}
async listRepositoryTopics(owner, repo) {
const endpoint = `/repos/${owner}/${repo}/topics`;
return this.makeGitHubRequest(endpoint, {
headers: { Accept: "application/vnd.github.mercy-preview+json" }, // Required for topics API
});
}
async updateRepositoryTopics(owner, repo, names) {
const endpoint = `/repos/${owner}/${repo}/topics`;
const payload = { names };
return this.makeGitHubRequest(endpoint, {
method: "PUT",
body: JSON.stringify(payload),
headers: {
"Content-Type": "application/json",
Accept: "application/vnd.github.mercy-preview+json", // Required for topics API
},
});
}
async getRepositoryLanguages(owner, repo) {
const endpoint = `/repos/${owner}/${repo}/languages`;
return this.makeGitHubRequest(endpoint);
}
async listStargazers(owner, repo, per_page = 30, page = 1) {
const endpoint = `/repos/${owner}/${repo}/stargazers?per_page=${per_page}&page=${page}`;
// To get stargazer timestamps, a different media type is needed:
// Accept: application/vnd.github.v3.star+json
// For now, sticking to default to keep it simple, can be enhanced later.
return this.makeGitHubRequest(endpoint);
}
async listWatchers(owner, repo, per_page = 30, page = 1) {
// Watchers are subscribers in the API
const endpoint = `/repos/${owner}/${repo}/subscribers?per_page=${per_page}&page=${page}`;
return this.makeGitHubRequest(endpoint);
}
async listForks(owner, repo, sort = "newest", per_page = 30, page = 1) {
const endpoint = `/repos/${owner}/${repo}/forks?sort=${sort}&per_page=${per_page}&page=${page}`;
return this.makeGitHubRequest(endpoint);
}
// --- Search Methods ---
async searchIssues(octokit, { query, sort, order, per_page = 30, page = 1 }) {
const q = query; // query is already the full search string
const params = { q, sort, order, per_page, page };
// Filter out undefined params
Object.keys(params).forEach(
(key) => params[key] === undefined && delete params[key]
);
const endpoint = `/search/issues?${new URLSearchParams(params).toString()}`;
return this.makeGitHubRequest(endpoint);
}
async searchCommits(
octokit,
{ query, sort, order, per_page = 30, page = 1 }
) {
const q = query;
const params = { q, sort, order, per_page, page };
Object.keys(params).forEach(
(key) => params[key] === undefined && delete params[key]
);
// The 'Accept' header is crucial for commit search
const endpoint = `/search/commits?${new URLSearchParams(
params
).toString()}`;
return this.makeGitHubRequest(endpoint, {
headers: { Accept: "application/vnd.github.cloak-preview+json" },
});
}
async searchCode(octokit, { query, sort, order, per_page = 30, page = 1 }) {
const q = query;
const params = { q, sort, order, per_page, page };
Object.keys(params).forEach(
(key) => params[key] === undefined && delete params[key]
);
const endpoint = `/search/code?${new URLSearchParams(params).toString()}`;
return this.makeGitHubRequest(endpoint);
}
async searchUsers(octokit, { query, sort, order, per_page = 30, page = 1 }) {
const q = query;
const params = { q, sort, order, per_page, page };
Object.keys(params).forEach(
(key) => params[key] === undefined && delete params[key]
);
const endpoint = `/search/users?${new URLSearchParams(params).toString()}`;
return this.makeGitHubRequest(endpoint);
}
async searchTopics(octokit, { query, sort, order, per_page = 30, page = 1 }) {
// GitHub's topic search is actually a repository search filtered by topic.
// The 'q' parameter should include 'topic:your-topic' or just 'your-topic'
// and the API will infer.
const q = query; // Assuming query is already formatted or just the topic names
const params = { q, sort, order, per_page, page };
Object.keys(params).forEach(
(key) => params[key] === undefined && delete params[key]
);
const endpoint = `/search/repositories?${new URLSearchParams(
params
).toString()}`;
// The 'Accept' header might be needed for topic search, using mercy-preview as it's common for topics.
return this.makeGitHubRequest(endpoint, {
headers: { Accept: "application/vnd.github.mercy-preview+json" },
});
}
// --- Organization Management Methods ---
async listOrgRepositories(octokit, params) {
// octokit is not used here as we are using the internal makeGitHubRequest
const {
org,
type = "all",
sort = "created",
direction,
per_page = 30,
page = 1,
} = params;
const queryParams = new URLSearchParams({ type, sort, per_page, page });
if (direction) {
queryParams.append("direction", direction);
}
const endpoint = `/orgs/${org}/repos?${queryParams.toString()}`;
return this.makeGitHubRequest(endpoint);
}
async listOrgMembers(octokit, params) {
const {
org,
filter = "all",
role = "all",
per_page = 30,
page = 1,
} = params;
const queryParams = new URLSearchParams({ filter, role, per_page, page });
const endpoint = `/orgs/${org}/members?${queryParams.toString()}`;
return this.makeGitHubRequest(endpoint);
}
async getOrgDetails(octokit, org) {
const endpoint = `/orgs/${org}`;
return this.makeGitHubRequest(endpoint);
}
async listOrgTeams(octokit, params) {
const { org, per_page = 30, page = 1 } = params;
const queryParams = new URLSearchParams({ per_page, page });
const endpoint = `/orgs/${org}/teams?${queryParams.toString()}`;
return this.makeGitHubRequest(endpoint);
}
async listTeamMembers(octokit, params) {
const { org, team_slug, role = "all", per_page = 30, page = 1 } = params;
const queryParams = new URLSearchParams({ role, per_page, page });
const endpoint = `/orgs/${org}/teams/${team_slug}/members?${queryParams.toString()}`;
return this.makeGitHubRequest(endpoint);
}
async addOrUpdateTeamRepoPermissions(octokit, params) {
const { org, team_slug, owner, repo, permission } = params;
// Note: The 'owner' in the URL is the org, the 'owner' in the payload is the repo owner (can be different for forks)
// However, for typical org repos, org and owner (of repo) are the same.
// The API endpoint is /orgs/{org}/teams/{team_slug}/repos/{owner}/{repo}
// but the Octokit documentation and common usage imply owner/repo for the repository itself.
// Let's assume 'owner' param refers to the repository's owner, which is often the 'org'.
const endpoint = `/orgs/${org}/teams/${team_slug}/repos/${owner}/${repo}`;
const payload = { permission }; // e.g., 'push', 'pull', 'admin'
return this.makeGitHubRequest(endpoint, {
method: "PUT",
body: JSON.stringify(payload),
headers: { "Content-Type": "application/json" },
});
}
async removeTeamRepo(octokit, params) {
const { org, team_slug, owner, repo } = params;
const endpoint = `/orgs/${org}/teams/${team_slug}/repos/${owner}/${repo}`;
return this.makeGitHubRequest(endpoint, {
method: "DELETE",
});
}
// --- Projects V2 (GraphQL) Methods ---
async listProjectsV2({
owner,
repo,
per_page = 10,
state = "open",
afterCursor = null,
}) {
// If 'repo' is provided, we assume listing projects for that specific repository.
// If only 'owner' is provided, it could be for an organization or a user.
// The query needs to be adapted based on whether 'owner' is a user or org,
// and whether 'repo' is specified.
// This is a simplified placeholder.
let ownerIdQuery = `user(login: "${owner}")`;
if (repo) {
// Assuming owner is a user if repo is specified for simplicity here
ownerIdQuery = `repository(owner: "${owner}", name: "${repo}")`;
} else {
// Could also be organization(login: "${owner}") - needs logic to determine
}
const query = `
query ListProjects($ownerLogin: String!, $repoName: String, $first: Int!, $states: [ProjectV2State!], $after: String) {
owner: user(login: $ownerLogin) { # Or organization(login: $ownerLogin)
... on ProjectV2Owner {
projectsV2(first: $first, states: $states, after: $after, orderBy: {field: UPDATED_AT, direction: DESC}) {
nodes {
id
title
number
url
state
closedAt
createdAt
updatedAt
readme
public
creator { login }
owner { __typename id }
}
pageInfo {
endCursor
hasNextPage
}
totalCount
}
}
}
# If repo specific:
# repository(owner: $ownerLogin, name: $repoName) {
# projectsV2(first: $first, states: $states, after: $after, orderBy: {field: UPDATED_AT, direction: DESC}) {
# # ... same fields as above
# }
# }
}
`;
// This is a very basic placeholder. Actual query depends on whether it's user/org projects
// or repository-specific projects.
console.warn(
"listProjectsV2 is a placeholder and needs a more robust query based on owner type and repo presence."
);
// return this.makeGraphQLRequest(query, { ownerLogin: owner, repoName: repo, first: per_page, states: [state.toUpperCase()], after: afterCursor });
return {
status: "pending",
message: "listProjectsV2 GraphQL not fully implemented.",
};
}
async createProjectV2({ owner, title, body, repoName }) {
// owner here is the node ID of the user or organization that will own the project.
// First, need to get the owner's Node ID (user or organization)
// This is a simplified placeholder.
const getOwnerIdQuery = `
query GetOwnerId($login: String!) {
user(login: $login) { id }
organization(login: $login) { id }
}
`;
// const ownerIdData = await this.makeGraphQLRequest(getOwnerIdQuery, { login: owner });
// const ownerNodeId = ownerIdData.user ? ownerIdData.user.id : ownerIdData.organization.id;
// if (!ownerNodeId) throw new Error(`Could not find user or organization with login: ${owner}`);
const mutation = `
mutation CreateProject($ownerId: ID!, $title: String!, $repositoryId: ID) {
createProjectV2(input: {ownerId: $ownerId, title: $title, repositoryId: $repositoryId}) {
projectV2 {
id
title
number
url
}
}
}
`;
// If repoName is provided, need to get repository's Node ID first.
// This is a placeholder and needs more logic.
console.warn(
"createProjectV2 is a placeholder and needs owner/repo Node ID fetching."
);
// return this.makeGraphQLRequest(mutation, { ownerId: "USER_OR_ORG_NODE_ID", title, repositoryId: "REPO_NODE_ID_IF_ANY" });
return {
status: "pending",
message: "createProjectV2 GraphQL not fully implemented.",
};
}
async listProjectFieldsV2({ project_id, per_page = 30, afterCursor = null }) {
// Corresponds to list_project_columns
const query = `
query ListProjectFields($projectId: ID!, $first: Int!, $after: String) {
node(id: $projectId) {
... on ProjectV2 {
fields(first: $first, after: $after) {
nodes {
id
name
dataType # TEXT, NUMBER, DATE, SINGLE_SELECT, ITERATION
... on ProjectV2SingleSelectField {
options { id name }
}
# ... other field types as needed
}
pageInfo {
endCursor
hasNextPage
}
totalCount
}
}
}
}
`;
// return this.makeGraphQLRequest(query, { projectId: project_id, first: per_page, after: afterCursor });
return {
status: "pending",
message: "listProjectFieldsV2 GraphQL not fully implemented.",
};
}
async listProjectItemsV2({ project_id, per_page = 30, afterCursor = null }) {
// Corresponds to list_project_cards
const query = `
query ListProjectItems($projectId: ID!, $first: Int!, $after: String) {
node(id: $projectId) {
... on ProjectV2 {
items(first: $first, after: $after) {
nodes {
id
type # DRAFT_ISSUE, ISSUE, PULL_REQUEST
createdAt
updatedAt
content {
... on DraftIssue { title body }
... on Issue { title number url repository { nameWithOwner } assignees(first:5){nodes{login}} labels(first:5){nodes{name color}} }
... on PullRequest { title number url repository {ครื่องWithOwner } assignees(first:5){nodes{login}} labels(first:5){nodes{name color}} }
}
# To get field values (like status):
# fieldValues(first: 10) {
# nodes {
# ... on ProjectV2ItemFieldSingleSelectValue {
# name # Status name
# optionId # Status option ID
# field { ... on ProjectV2SingleSelectField { name id } }
# }
# }
# }
}
pageInfo {
endCursor
hasNextPage
}
totalCount
}
}
}
}
`;
// return this.makeGraphQLRequest(query, { projectId: project_id, first: per_page, after: afterCursor });
return {
status: "pending",
message: "listProjectItemsV2 GraphQL not fully implemented.",
};
}
async createProjectItemV2({ project_id, content_id, note }) {
// content_id is the Node ID of an existing Issue or PullRequest
// note is for creating a new DraftIssue
let mutation;
let variables;
if (content_id) {
mutation = `
mutation AddItemToProject($projectId: ID!, $contentId: ID!) {
addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
item {
id
type
}
}
}
`;
variables = { projectId: project_id, contentId: content_id };
} else if (note) {
mutation = `
mutation AddDraftIssueToProject($projectId: ID!, $title: String!) { # Assuming note is the title for simplicity
addProjectV2DraftIssue(input: {projectId: $projectId, title: $title}) {
projectItem {
id
type
content { ... on DraftIssue { title }}
}
}
}
`;
variables = { projectId: project_id, title: note }; // Simplified, might need body too
} else {
throw new Error(
"Either content_id (for Issue/PR) or note (for DraftIssue) must be provided."
);
}
// return this.makeGraphQLRequest(mutation, variables);
return {
status: "pending",
message: "createProjectItemV2 GraphQL not fully implemented.",
};
}
async updateProjectItemFieldV2({ project_id, item_id, field_id, option_id }) {
// Corresponds to move_project_card. Updates a single select field (like Status).
// project_id: Project's Node ID
// item_id: Project Item's Node ID
// field_id: Node ID of the Single Select Field (e.g., "Status" field)
// option_id: Node ID of the new Option for that field (e.g., "Done" option for "Status" field)
const mutation = `
mutation UpdateProjectItemField($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
updateProjectV2ItemFieldValue(
input: {
projectId: $projectId
itemId: $itemId
fieldId: $fieldId
value: {
singleSelectOptionId: $optionId
}
}
) {
projectV2Item {
id
# You can query updated field values here if needed
}
}
}
`;
// return this.makeGraphQLRequest(mutation, { projectId: project_id, itemId: item_id, fieldId: field_id, optionId: option_id });
return {
status: "pending",
message: "updateProjectItemFieldV2 GraphQL not fully implemented.",
};
}
// --- Placeholder Methods for Advanced Features ---
async runCodeQualityChecks(owner, repo, tool_name, config_path) {
// Debug logging disabled to avoid MCP protocol interference
// console.log(
// "Placeholder: GitHubAPIService.runCodeQualityChecks called with:",
// owner,
// repo,
// tool_name,
// config_path
// );
// In a real implementation, this might involve:
// - Triggering a GitHub Action that runs linters/scanners.
// - Fetching results from a third-party code quality service API.
// - Analyzing repository contents directly if the tool can be run locally/on-the-fly.
return Promise.resolve({
status: "pending_implementation",
message: "Code quality check feature not yet implemented.",
details: { tool_name, config_path },
});
}
async manageCustomDashboard(owner, repo, dashboard_id, action, settings) {
// Debug logging disabled to avoid MCP protocol interference
// console.log(
// "Placeholder: GitHubAPIService.manageCustomDashboard called with:",
// owner,
// repo,
// dashboard_id,
// action,
// settings
// );
// This would likely involve interacting with a custom datastore or a
// visualization service API, rather than directly with GitHub's primary API.
// Or, it could involve fetching various GitHub stats and composing them.
return Promise.resolve({
status: "pending_implementation",
message: "Custom dashboard feature not yet implemented.",
details: { dashboard_id, action },
});
}
async generateAutomatedReport(owner, repo, report_type, output_format) {
// Debug logging disabled to avoid MCP protocol interference
// console.log(
// "Placeholder: GitHubAPIService.generateAutomatedReport called with:",
// owner,
// repo,
// report_type,
// output_format
// );
// This would involve:
// - Fetching various data points from GitHub (issues, PRs, commits, traffic).
// - Aggregating and formatting this data into the specified report_type and output_format.
return Promise.resolve({
status: "pending_implementation",
message: "Automated reporting feature not yet implemented.",
details: { report_type, output_format },
});
}
async manageNotifications(action, thread_id, options = {}) {
// Debug logging disabled to avoid MCP protocol interference
// console.log(
// "Placeholder: GitHubAPIService.manageNotifications called with:",
// action,
// thread_id,
// options
// );
// This would use GitHub's Notifications API.
// Example for listing notifications:
// const endpoint = `/notifications?all=${options.all}&participating=${options.participating}`;
// Example for marking a thread as read:
// const endpoint = `/notifications/threads/${thread_id}`; method: 'PATCH'
return Promise.resolve({
status: "pending_implementation",
message: "Notification management feature not yet implemented.",
details: { action, thread_id },
});
}
async manageRelease(owner, repo, action, releaseData = {}) {
// Debug logging disabled to avoid MCP protocol interference
// console.log(
// "Placeholder: GitHubAPIService.manageRelease called with:",
// owner,
// repo,
// action,
// releaseData
// );
// This would use GitHub's Releases API.
// Examples:
// List: GET /repos/{owner}/{repo}/releases
// Create: POST /repos/{owner}/{repo}/releases with body: { tag_name, target_commitish, name, body, draft, prerelease }
// Get: GET /repos/{owner}/{repo}/releases/{release_id} or /tags/{tag_name}
// Update: PATCH /repos/{owner}/{repo}/releases/{release_id}
// Delete: DELETE /repos/{owner}/{repo}/releases/{release_id}
// Upload Asset: POST to release.upload_url
return Promise.resolve({
status: "pending_implementation",
message: "Release management feature not yet implemented.",
details: {
action,
release_id: releaseData.release_id,
tag_name: releaseData.tag_name,
},
});
}
async analyzeDependencies(owner, repo, report_type, depth) {
// Debug logging disabled to avoid MCP protocol interference
// console.log(
// "Placeholder: GitHubAPIService.analyzeDependencies called with:",
// owner,
// repo,
// report_type,
// depth
// );
// This could involve:
// - Using GitHub's Dependency Graph API (GraphQL).
// - Fetching dependency files (e.g., package.json, pom.xml) and analyzing them.
// - Integrating with GitHub Advanced Security features like Dependabot alerts API.
return Promise.resolve({
status: "pending_implementation",
message: "Dependency analysis feature not yet implemented.",
details: { report_type, depth },
});
}
}
module.exports = GitHubAPIService;