UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

194 lines • 8.45 kB
// ========= 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