@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
194 lines • 8.45 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { GRAPHQL_PATH } from "./constants.js";
import { githubPages } from "./paginate.js";
import { graphqlData } from "./repo.js";
function path(ref, tail = '') {
return `/repos/${ref.owner}/${ref.repo}/issues${tail}`;
}
function onlyIssue(value, number) {
if (value !== null && typeof value === 'object' && 'pull_request' in value) {
throw new Error(`#${String(number)} is a pull request, not an issue`);
}
return value;
}
export async function listIssues(transport, ref, params, limit) {
return githubPages(transport, path(ref), {
params,
limit,
include: (row) => !('pull_request' in row),
});
}
export async function getIssue(transport, ref, number) {
return onlyIssue(await transport.get(path(ref, `/${String(number)}`)), number);
}
export function createIssue(transport, ref, body) {
return transport.request('POST', path(ref), body);
}
export async function editIssue(transport, ref, number, body) {
await getIssue(transport, ref, number);
return transport.request('PATCH', path(ref, `/${String(number)}`), body);
}
export async function commentIssue(transport, ref, number, body) {
await getIssue(transport, ref, number);
return transport.request('POST', path(ref, `/${String(number)}/comments`), { body });
}
const COMMENTS_QUERY = `query($owner: String!, $repo: String!, $number: Int!, $cursor: String) {
repository(owner: $owner, name: $repo) {
issueOrPullRequest(number: $number) {
... on Issue {
comments(first: 100, after: $cursor) {
nodes { ...CommentFields }
pageInfo { hasNextPage endCursor }
}
}
... on PullRequest {
comments(first: 100, after: $cursor) {
nodes { ...CommentFields }
pageInfo { hasNextPage endCursor }
}
}
}
}
}
fragment CommentFields on IssueComment {
id
author { login }
authorAssociation
body
createdAt
includesCreatedEdit
isMinimized
minimizedReason
reactionGroups { content users { totalCount } }
url
viewerDidAuthor
}`;
export async function issueComments(transport, ref, number) {
const rows = [];
let cursor = null;
for (;;) {
const response = (await transport.request('POST', GRAPHQL_PATH, {
query: COMMENTS_QUERY,
variables: { owner: ref.owner, repo: ref.repo, number, cursor },
}));
if (response.errors?.length)
throw new Error(response.errors.map((e) => e.message).join('; '));
const comments = response.data?.repository?.issueOrPullRequest?.comments;
if (!comments)
throw new Error('Could not resolve comments for this issue or pull request');
for (const item of comments.nodes)
rows.push(item);
if (!comments.pageInfo.hasNextPage)
return rows;
const next = comments.pageInfo.endCursor;
if (!next || next === cursor)
throw new Error('GitHub returned a non-advancing comments cursor');
cursor = next;
}
}
/**
* The selected fields of one issue, over GraphQL, as gh's IssueByNumber asks
* for them for `issue view --json`: the number read as an issue or as a pull
* request, each half with its own selection. A selection that reads a later
* page of a connection is given that cursor as `endCursor`.
*
* Args:
* transport (GitHubTransport): the API client.
* ref (RepoRef): the repository.
* number (number): the issue or pull request.
* selections (IssueSelections): what to read of each.
* endCursor (string | undefined): the cursor `$endCursor` carries.
*/
export async function issueFields(transport, ref, number, selections, endCursor) {
const variables = { owner: ref.owner, repo: ref.repo, number };
if (endCursor !== undefined)
variables.endCursor = endCursor;
const cursor = endCursor === undefined ? '' : ', $endCursor: String';
const halves = (selections.issue === '' ? '' : `\n ...on Issue{${selections.issue}}`) +
(selections.pull === '' ? '' : `\n ...on PullRequest{${selections.pull}}`);
const data = await graphqlData(transport, `query IssueByNumber($owner: String!, $repo: String!, $number: Int!${cursor}) {\n` +
' repository(owner: $owner, name: $repo) {\n hasIssuesEnabled\n' +
` issue: issueOrPullRequest(number: $number) {\n __typename${halves}\n` +
' }\n }\n }', variables);
const repository = data.repository;
const issue = repository?.issue;
if (issue !== null && typeof issue === 'object')
return issue;
if (repository?.hasIssuesEnabled === false) {
throw new Error(`the '${ref.owner}/${ref.repo}' repository has disabled issues`);
}
throw new Error('issue was not found but GraphQL reported no error');
}
const ISSUE_LIST = '\tquery IssueList($owner: String!, $repo: String!, $limit: Int, $endCursor: String, ' +
'$states: [IssueState!] = OPEN, $assignee: String, $author: String, $mention: String, ' +
'$labels: [String!]) {\n\t\trepository(owner: $owner, name: $repo) {\n' +
'\t\t\thasIssuesEnabled\n\t\t\tissues(first: $limit, after: $endCursor, orderBy: ' +
'{field: CREATED_AT, direction: DESC}, states: $states, filterBy: {assignee: $assignee, ' +
'createdBy: $author, mentioned: $mention, labels: $labels}) {\n\t\t\t\ttotalCount\n' +
'\t\t\t\tnodes {\n\t\t\t\t\t...issue\n\t\t\t\t}\n\t\t\t\tpageInfo {\n' +
'\t\t\t\t\thasNextPage\n\t\t\t\t\tendCursor\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t';
/**
* The selected fields of a repository's issues, over GraphQL, as gh's
* IssueList asks for them for `issue list --json`: newest first, a page of
* up to 100 at a time until `limit`. gh reaches for search to narrow by
* label; the connection's own `labels` filter narrows to the same issues.
*
* Args:
* transport (GitHubTransport): the API client.
* ref (RepoRef): the repository.
* filter (IssueListFilter): the states, assignee, author and labels.
* limit (number): how many issues at most.
* selection (string): the GraphQL selection for each issue.
*/
export async function listIssueFields(transport, ref, filter, limit, selection) {
const query = `fragment issue on Issue {${selection}}\n${ISSUE_LIST}`;
const rows = [];
let cursor = null;
let pageLimit = Math.min(limit, 100);
while (rows.length < limit) {
const variables = {
owner: ref.owner,
repo: ref.repo,
states: filter.states,
limit: pageLimit,
};
if (filter.assignee !== undefined && filter.assignee !== '')
variables.assignee = filter.assignee;
if (filter.author !== undefined && filter.author !== '')
variables.author = filter.author;
if (filter.labels !== undefined && filter.labels.length > 0)
variables.labels = filter.labels;
if (cursor !== null)
variables.endCursor = cursor;
const data = await graphqlData(transport, query, variables);
const repository = data.repository;
if (repository?.hasIssuesEnabled === false) {
throw new Error(`the '${ref.owner}/${ref.repo}' repository has disabled issues`);
}
const page = repository?.issues;
for (const node of page?.nodes ?? []) {
rows.push(node);
if (rows.length === limit)
break;
}
const next = page?.pageInfo?.endCursor ?? null;
if (page?.pageInfo?.hasNextPage !== true || next === null || next === cursor)
break;
cursor = next;
pageLimit = Math.min(pageLimit, limit - rows.length);
}
return rows;
}
//# sourceMappingURL=issue.js.map