@jgengine/github 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ export const WEEKS = 53;
2
+ export const DAYS = 7;
3
+ export const CELL_COUNT = WEEKS * DAYS;
4
+ export const WEEKDAY_NAMES = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
5
+ export const MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
6
+ const ANCHOR = Date.UTC(2024, 0, 7);
7
+ function mulberry32(seed) {
8
+ let a = seed >>> 0;
9
+ return () => {
10
+ a |= 0;
11
+ a = (a + 0x6d2b79f5) | 0;
12
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
13
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
14
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
15
+ };
16
+ }
17
+ /** GitHub's quartile bucketing of a day's contribution count (0 = none … 4 = most).
18
+ * @internal
19
+ */
20
+ export function levelForCount(count) {
21
+ if (count <= 0)
22
+ return 0;
23
+ if (count <= 2)
24
+ return 1;
25
+ if (count <= 5)
26
+ return 2;
27
+ if (count <= 9)
28
+ return 3;
29
+ return 4;
30
+ }
31
+ /** @internal */
32
+ export function dateOf(index) {
33
+ return new Date(ANCHOR + index * 86400000);
34
+ }
35
+ /** @internal */
36
+ export function dateLabel(index) {
37
+ return labelFromISO(dateOf(index).toISOString().slice(0, 10));
38
+ }
39
+ /** @internal */
40
+ export function labelFromISO(iso) {
41
+ const d = new Date(`${iso}T00:00:00Z`);
42
+ return `${WEEKDAY_NAMES[d.getUTCDay()]}, ${MONTH_NAMES[d.getUTCMonth()]} ${String(d.getUTCDate()).padStart(2, "0")}`;
43
+ }
44
+ /** Deterministic plausible contribution year for demos / offline fallback.
45
+ * @internal
46
+ */
47
+ export function generateYear(seed) {
48
+ const rng = mulberry32(seed);
49
+ const seasonPhase = rng() * Math.PI * 2;
50
+ const cells = [];
51
+ let streakHeat = 0;
52
+ for (let week = 0; week < WEEKS; week += 1) {
53
+ const season = 0.5 + 0.5 * Math.sin(seasonPhase + (week / WEEKS) * Math.PI * 2);
54
+ streakHeat = rng() < 0.16 ? 1.6 + rng() * 2.2 : streakHeat * 0.72;
55
+ for (let weekday = 0; weekday < DAYS; weekday += 1) {
56
+ const index = week * DAYS + weekday;
57
+ const weekendDrag = weekday === 0 || weekday === 6 ? 0.35 : 1;
58
+ const base = season * 5 * weekendDrag + streakHeat * weekendDrag;
59
+ const jitter = rng() * rng() * 6;
60
+ const idle = rng() < 0.28 - season * 0.14 ? 0 : 1;
61
+ const count = Math.max(0, Math.round((base + jitter) * idle));
62
+ cells.push({ index, week, weekday, count, level: levelForCount(count), label: dateLabel(index) });
63
+ }
64
+ }
65
+ return cells;
66
+ }
@@ -0,0 +1,34 @@
1
+ export interface GitHubClientOptions {
2
+ /**
3
+ * Proxy base (e.g. "/api/github") that holds a server-side token and forwards
4
+ * allowlisted GETs to api.github.com. Required in the browser for private /
5
+ * authed data and for GraphQL; omit for direct public REST.
6
+ */
7
+ endpoint?: string;
8
+ /** Personal access token for DIRECT calls — server/Node only, never shipped to a browser. */
9
+ token?: string;
10
+ /** Injectable fetch (tests, non-browser runtimes). Defaults to global fetch. */
11
+ fetchImpl?: typeof fetch;
12
+ }
13
+ /** @internal */
14
+ export declare class GitHubError extends Error {
15
+ readonly status: number;
16
+ constructor(status: number, message: string);
17
+ }
18
+ /**
19
+ * Minimal transport over the GitHub API. `rest`/`graphql` are the two primitives;
20
+ * resource helpers (repos, pullRequests, …) are thin functions built on `rest`.
21
+ * Public reads can go direct; anything private/authed/GraphQL routes through a
22
+ * proxy endpoint that keeps the token server-side.
23
+ */
24
+ export interface GitHubClient {
25
+ rest<T>(path: string): Promise<T>;
26
+ /**
27
+ * Direct mode (no `endpoint`): `query` is raw GraphQL text sent with the token.
28
+ * Proxy mode: `query` is the allowlisted operation name — the server owns the
29
+ * actual query text, so the browser never transmits free-form GraphQL.
30
+ */
31
+ graphql<T>(query: string, variables?: Record<string, unknown>): Promise<T>;
32
+ }
33
+ /** @internal */
34
+ export declare function createGitHub(options?: GitHubClientOptions): GitHubClient;
package/dist/client.js ADDED
@@ -0,0 +1,48 @@
1
+ /** @internal */
2
+ export class GitHubError extends Error {
3
+ status;
4
+ constructor(status, message) {
5
+ super(message);
6
+ this.status = status;
7
+ this.name = "GitHubError";
8
+ }
9
+ }
10
+ const API = "https://api.github.com";
11
+ const REST_HEADERS = { Accept: "application/vnd.github+json", "User-Agent": "jgengine" };
12
+ async function unwrapGraphQL(response) {
13
+ if (!response.ok)
14
+ throw new GitHubError(response.status, `GitHub GraphQL failed (${response.status})`);
15
+ const payload = (await response.json());
16
+ if (payload.errors?.length)
17
+ throw new GitHubError(502, payload.errors.map((e) => e.message).join("; "));
18
+ return payload.data;
19
+ }
20
+ /** @internal */
21
+ export function createGitHub(options = {}) {
22
+ const doFetch = options.fetchImpl ?? fetch;
23
+ const { endpoint, token } = options;
24
+ async function rest(path) {
25
+ const url = endpoint === undefined ? `${API}${path}` : `${endpoint}?path=${encodeURIComponent(path)}`;
26
+ const headers = endpoint === undefined && token !== undefined ? { ...REST_HEADERS, Authorization: `Bearer ${token}` } : REST_HEADERS;
27
+ const response = await doFetch(url, { headers });
28
+ if (!response.ok)
29
+ throw new GitHubError(response.status, `GitHub GET ${path} failed (${response.status})`);
30
+ return (await response.json());
31
+ }
32
+ async function graphql(query, variables = {}) {
33
+ if (endpoint === undefined) {
34
+ if (token === undefined)
35
+ throw new GitHubError(401, "GraphQL requires a token (or a proxy endpoint that holds one).");
36
+ const headers = { ...REST_HEADERS, "Content-Type": "application/json", Authorization: `Bearer ${token}` };
37
+ const response = await doFetch(`${API}/graphql`, { method: "POST", headers, body: JSON.stringify({ query, variables }) });
38
+ return unwrapGraphQL(response);
39
+ }
40
+ const response = await doFetch(`${endpoint}?op=${encodeURIComponent(query)}`, {
41
+ method: "POST",
42
+ headers: { ...REST_HEADERS, "Content-Type": "application/json" },
43
+ body: JSON.stringify({ variables }),
44
+ });
45
+ return unwrapGraphQL(response);
46
+ }
47
+ return { rest, graphql };
48
+ }
@@ -0,0 +1,11 @@
1
+ export { CELL_COUNT, DAYS, WEEKS, WEEKDAY_NAMES, MONTH_NAMES, dateOf, dateLabel, labelFromISO, generateYear, levelForCount, type DayCell, } from "./calendar.js";
2
+ export { summarize, type ContributionStats } from "./analytics.js";
3
+ export { DEFAULT_ENDPOINT, contributions, wireToCells, type ContributionData, type ContributionsWire, type GitHubProfile, } from "./source.js";
4
+ export { createGitHub, GitHubError, type GitHubClient, type GitHubClientOptions } from "./client.js";
5
+ export * from "./resources/repos.js";
6
+ export * from "./resources/issues.js";
7
+ export * from "./resources/pulls.js";
8
+ export * from "./resources/actions.js";
9
+ export * from "./resources/activity.js";
10
+ export * from "./resources/search.js";
11
+ export * from "./resources/users.js";
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { CELL_COUNT, DAYS, WEEKS, WEEKDAY_NAMES, MONTH_NAMES, dateOf, dateLabel, labelFromISO, generateYear, levelForCount, } from "./calendar.js";
2
+ export { summarize } from "./analytics.js";
3
+ export { DEFAULT_ENDPOINT, contributions, wireToCells, } from "./source.js";
4
+ export { createGitHub, GitHubError } from "./client.js";
5
+ export * from "./resources/repos.js";
6
+ export * from "./resources/issues.js";
7
+ export * from "./resources/pulls.js";
8
+ export * from "./resources/actions.js";
9
+ export * from "./resources/activity.js";
10
+ export * from "./resources/search.js";
11
+ export * from "./resources/users.js";
@@ -0,0 +1,26 @@
1
+ import type { GitHubClient } from "../client.js";
2
+ export interface Workflow {
3
+ id: number;
4
+ name: string;
5
+ state: string;
6
+ path: string;
7
+ }
8
+ export interface WorkflowRun {
9
+ id: number;
10
+ name: string | null;
11
+ status: string;
12
+ conclusion: string | null;
13
+ event: string;
14
+ headBranch: string | null;
15
+ runNumber: number;
16
+ createdAt: string;
17
+ url: string;
18
+ }
19
+ /** List the workflow definitions configured on a repository. */
20
+ export declare function workflows(gh: GitHubClient, owner: string, name: string): Promise<Workflow[]>;
21
+ export interface WorkflowRunsOptions {
22
+ perPage?: number;
23
+ branch?: string;
24
+ }
25
+ /** List recent workflow runs (CI history) for a repository. */
26
+ export declare function workflowRuns(gh: GitHubClient, owner: string, name: string, opts?: WorkflowRunsOptions): Promise<WorkflowRun[]>;
@@ -0,0 +1,26 @@
1
+ function clampPerPage(perPage) {
2
+ return Math.min(perPage ?? 30, 100);
3
+ }
4
+ /** List the workflow definitions configured on a repository. */
5
+ export async function workflows(gh, owner, name) {
6
+ const raw = await gh.rest(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/actions/workflows`);
7
+ return raw.workflows.map((w) => ({ id: w.id, name: w.name, state: w.state, path: w.path }));
8
+ }
9
+ /** List recent workflow runs (CI history) for a repository. */
10
+ export async function workflowRuns(gh, owner, name, opts = {}) {
11
+ const params = new URLSearchParams({ per_page: String(clampPerPage(opts.perPage)) });
12
+ if (opts.branch !== undefined)
13
+ params.set("branch", opts.branch);
14
+ const raw = await gh.rest(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/actions/runs?${params.toString()}`);
15
+ return raw.workflow_runs.map((r) => ({
16
+ id: r.id,
17
+ name: r.name,
18
+ status: r.status,
19
+ conclusion: r.conclusion,
20
+ event: r.event,
21
+ headBranch: r.head_branch,
22
+ runNumber: r.run_number,
23
+ createdAt: r.created_at,
24
+ url: r.html_url,
25
+ }));
26
+ }
@@ -0,0 +1,24 @@
1
+ import { type GitHubClient } from "../client.js";
2
+ export interface ActivityEvent {
3
+ type: string;
4
+ repo: string;
5
+ createdAt: string;
6
+ action?: string;
7
+ }
8
+ export interface EventsOptions {
9
+ perPage?: number;
10
+ }
11
+ /** A user's public activity feed (pushes, issue/PR actions, stars, forks, …). */
12
+ export declare function events(gh: GitHubClient, user: string, opts?: EventsOptions): Promise<ActivityEvent[]>;
13
+ export interface CommitActivityWeek {
14
+ weekStart: number;
15
+ total: number;
16
+ days: number[];
17
+ }
18
+ /**
19
+ * Weekly commit counts for the last year. GitHub computes these stats
20
+ * asynchronously and may respond 202 with an empty body while the cache
21
+ * warms up; when the parsed payload isn't an array, this returns `[]`
22
+ * rather than throwing.
23
+ */
24
+ export declare function commitActivity(gh: GitHubClient, owner: string, name: string): Promise<CommitActivityWeek[]>;
@@ -0,0 +1,36 @@
1
+ import { GitHubError } from "../client.js";
2
+ function clampPerPage(perPage) {
3
+ return Math.min(perPage ?? 30, 100);
4
+ }
5
+ /** A user's public activity feed (pushes, issue/PR actions, stars, forks, …). */
6
+ export async function events(gh, user, opts = {}) {
7
+ const params = new URLSearchParams({ per_page: String(clampPerPage(opts.perPage)) });
8
+ const raw = await gh.rest(`/users/${encodeURIComponent(user)}/events?${params.toString()}`);
9
+ return raw.map((e) => {
10
+ const summary = { type: e.type ?? "unknown", repo: e.repo.name, createdAt: e.created_at ?? "" };
11
+ const action = e.payload?.action;
12
+ if (action !== undefined)
13
+ summary.action = action;
14
+ return summary;
15
+ });
16
+ }
17
+ /**
18
+ * Weekly commit counts for the last year. GitHub computes these stats
19
+ * asynchronously and may respond 202 with an empty body while the cache
20
+ * warms up; when the parsed payload isn't an array, this returns `[]`
21
+ * rather than throwing.
22
+ */
23
+ export async function commitActivity(gh, owner, name) {
24
+ let raw;
25
+ try {
26
+ raw = await gh.rest(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/stats/commit_activity`);
27
+ }
28
+ catch (err) {
29
+ if (err instanceof GitHubError)
30
+ throw err;
31
+ return [];
32
+ }
33
+ if (!Array.isArray(raw))
34
+ return [];
35
+ return raw.map((w) => ({ weekStart: w.week, total: w.total, days: w.days }));
36
+ }
@@ -0,0 +1,42 @@
1
+ import type { GitHubClient } from "../client.js";
2
+ export interface IssueSummary {
3
+ number: number;
4
+ title: string;
5
+ state: string;
6
+ comments: number;
7
+ isPullRequest: boolean;
8
+ createdAt: string;
9
+ closedAt: string | null;
10
+ author: string | null;
11
+ labels: string[];
12
+ url: string;
13
+ }
14
+ interface RawLabel {
15
+ name?: string;
16
+ }
17
+ export interface RawIssue {
18
+ number: number;
19
+ title: string;
20
+ state: string;
21
+ comments: number;
22
+ pull_request?: unknown;
23
+ created_at: string;
24
+ closed_at: string | null;
25
+ user: {
26
+ login: string;
27
+ } | null;
28
+ labels?: (string | RawLabel)[];
29
+ html_url: string;
30
+ }
31
+ export declare function toIssueSummary(raw: RawIssue): IssueSummary;
32
+ export interface IssuesOptions {
33
+ state?: "open" | "closed" | "all";
34
+ perPage?: number;
35
+ }
36
+ /**
37
+ * List issues on a repository. GitHub's `/issues` endpoint also returns pull
38
+ * requests (an item is a PR when it carries a `pull_request` key) — that's
39
+ * reflected in `isPullRequest`, and `comments` is the item's comment count.
40
+ */
41
+ export declare function issues(gh: GitHubClient, owner: string, name: string, opts?: IssuesOptions): Promise<IssueSummary[]>;
42
+ export {};
@@ -0,0 +1,30 @@
1
+ function clampPerPage(perPage) {
2
+ return Math.min(perPage ?? 30, 100);
3
+ }
4
+ function labelName(label) {
5
+ return typeof label === "string" ? label : label.name;
6
+ }
7
+ export function toIssueSummary(raw) {
8
+ return {
9
+ number: raw.number,
10
+ title: raw.title,
11
+ state: raw.state,
12
+ comments: raw.comments,
13
+ isPullRequest: raw.pull_request !== undefined,
14
+ createdAt: raw.created_at,
15
+ closedAt: raw.closed_at,
16
+ author: raw.user?.login ?? null,
17
+ labels: (raw.labels ?? []).map(labelName).filter((n) => n !== undefined),
18
+ url: raw.html_url,
19
+ };
20
+ }
21
+ /**
22
+ * List issues on a repository. GitHub's `/issues` endpoint also returns pull
23
+ * requests (an item is a PR when it carries a `pull_request` key) — that's
24
+ * reflected in `isPullRequest`, and `comments` is the item's comment count.
25
+ */
26
+ export async function issues(gh, owner, name, opts = {}) {
27
+ const params = new URLSearchParams({ per_page: String(clampPerPage(opts.perPage)), state: opts.state ?? "open" });
28
+ const raw = await gh.rest(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/issues?${params.toString()}`);
29
+ return raw.map(toIssueSummary);
30
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * NOTE: GitHub's pull-request LIST endpoint (`/repos/{owner}/{repo}/pulls`) does
3
+ * NOT include a comment count on each item. Callers that want comments-per-PR
4
+ * should use `issues()` from ./issues instead — the issues endpoint includes
5
+ * pull requests and reports `comments` on every item.
6
+ */
7
+ import type { GitHubClient } from "../client.js";
8
+ export interface PullRequestSummary {
9
+ number: number;
10
+ title: string;
11
+ state: string;
12
+ draft: boolean;
13
+ merged: boolean;
14
+ createdAt: string;
15
+ mergedAt: string | null;
16
+ author: string | null;
17
+ headRef: string;
18
+ baseRef: string;
19
+ url: string;
20
+ }
21
+ interface RawPullRequest {
22
+ number: number;
23
+ title: string;
24
+ state: string;
25
+ draft: boolean;
26
+ merged_at: string | null;
27
+ created_at: string;
28
+ user: {
29
+ login: string;
30
+ } | null;
31
+ head: {
32
+ ref: string;
33
+ };
34
+ base: {
35
+ ref: string;
36
+ };
37
+ html_url: string;
38
+ }
39
+ export declare function toPullRequestSummary(raw: RawPullRequest): PullRequestSummary;
40
+ export interface PullRequestsOptions {
41
+ state?: "open" | "closed" | "all";
42
+ perPage?: number;
43
+ }
44
+ /** List pull requests on a repository. See the module note re: comment counts. */
45
+ export declare function pullRequests(gh: GitHubClient, owner: string, name: string, opts?: PullRequestsOptions): Promise<PullRequestSummary[]>;
46
+ export {};
@@ -0,0 +1,24 @@
1
+ function clampPerPage(perPage) {
2
+ return Math.min(perPage ?? 30, 100);
3
+ }
4
+ export function toPullRequestSummary(raw) {
5
+ return {
6
+ number: raw.number,
7
+ title: raw.title,
8
+ state: raw.state,
9
+ draft: raw.draft,
10
+ merged: raw.merged_at !== null,
11
+ createdAt: raw.created_at,
12
+ mergedAt: raw.merged_at,
13
+ author: raw.user?.login ?? null,
14
+ headRef: raw.head.ref,
15
+ baseRef: raw.base.ref,
16
+ url: raw.html_url,
17
+ };
18
+ }
19
+ /** List pull requests on a repository. See the module note re: comment counts. */
20
+ export async function pullRequests(gh, owner, name, opts = {}) {
21
+ const params = new URLSearchParams({ per_page: String(clampPerPage(opts.perPage)), state: opts.state ?? "open" });
22
+ const raw = await gh.rest(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/pulls?${params.toString()}`);
23
+ return raw.map(toPullRequestSummary);
24
+ }
@@ -0,0 +1,55 @@
1
+ import type { GitHubClient } from "../client.js";
2
+ export interface RepoSummary {
3
+ name: string;
4
+ fullName: string;
5
+ description: string | null;
6
+ stars: number;
7
+ forks: number;
8
+ watchers: number;
9
+ openIssues: number;
10
+ language: string | null;
11
+ topics: string[];
12
+ license: string | null;
13
+ archived: boolean;
14
+ isFork: boolean;
15
+ createdAt: string;
16
+ pushedAt: string | null;
17
+ url: string;
18
+ }
19
+ export interface RawRepo {
20
+ name: string;
21
+ full_name: string;
22
+ description: string | null;
23
+ stargazers_count: number;
24
+ forks_count: number;
25
+ watchers_count: number;
26
+ open_issues_count: number;
27
+ language: string | null;
28
+ topics?: string[];
29
+ license: {
30
+ name: string;
31
+ } | null;
32
+ archived: boolean;
33
+ fork: boolean;
34
+ created_at: string;
35
+ pushed_at: string | null;
36
+ html_url: string;
37
+ }
38
+ export declare function toRepoSummary(raw: RawRepo): RepoSummary;
39
+ export interface ReposOptions {
40
+ sort?: "updated" | "pushed" | "stars";
41
+ perPage?: number;
42
+ }
43
+ /** List a user's repositories, mapped down to a lean summary shape. */
44
+ export declare function repos(gh: GitHubClient, user: string, opts?: ReposOptions): Promise<RepoSummary[]>;
45
+ /** Fetch a single repository by owner/name. */
46
+ export declare function repo(gh: GitHubClient, owner: string, name: string): Promise<RepoSummary>;
47
+ /** Bytes of code per language, as reported by GitHub's linguist pass. */
48
+ export declare function languages(gh: GitHubClient, owner: string, name: string): Promise<Record<string, number>>;
49
+ export interface LanguageShare {
50
+ language: string;
51
+ bytes: number;
52
+ share: number;
53
+ }
54
+ /** Rank a language→bytes map, attaching each entry's share of the total. */
55
+ export declare function topLanguages(bytes: Record<string, number>, n?: number): LanguageShare[];
@@ -0,0 +1,47 @@
1
+ function clampPerPage(perPage) {
2
+ return Math.min(perPage ?? 30, 100);
3
+ }
4
+ export function toRepoSummary(raw) {
5
+ return {
6
+ name: raw.name,
7
+ fullName: raw.full_name,
8
+ description: raw.description,
9
+ stars: raw.stargazers_count,
10
+ forks: raw.forks_count,
11
+ watchers: raw.watchers_count,
12
+ openIssues: raw.open_issues_count,
13
+ language: raw.language,
14
+ topics: raw.topics ?? [],
15
+ license: raw.license?.name ?? null,
16
+ archived: raw.archived,
17
+ isFork: raw.fork,
18
+ createdAt: raw.created_at,
19
+ pushedAt: raw.pushed_at,
20
+ url: raw.html_url,
21
+ };
22
+ }
23
+ /** List a user's repositories, mapped down to a lean summary shape. */
24
+ export async function repos(gh, user, opts = {}) {
25
+ const params = new URLSearchParams({ per_page: String(clampPerPage(opts.perPage)) });
26
+ if (opts.sort !== undefined)
27
+ params.set("sort", opts.sort);
28
+ const raw = await gh.rest(`/users/${encodeURIComponent(user)}/repos?${params.toString()}`);
29
+ return raw.map(toRepoSummary);
30
+ }
31
+ /** Fetch a single repository by owner/name. */
32
+ export async function repo(gh, owner, name) {
33
+ const raw = await gh.rest(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`);
34
+ return toRepoSummary(raw);
35
+ }
36
+ /** Bytes of code per language, as reported by GitHub's linguist pass. */
37
+ export async function languages(gh, owner, name) {
38
+ return gh.rest(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/languages`);
39
+ }
40
+ /** Rank a language→bytes map, attaching each entry's share of the total. */
41
+ export function topLanguages(bytes, n = 5) {
42
+ const total = Object.values(bytes).reduce((sum, value) => sum + value, 0);
43
+ return Object.entries(bytes)
44
+ .sort(([, a], [, b]) => b - a)
45
+ .slice(0, n)
46
+ .map(([language, value]) => ({ language, bytes: value, share: total === 0 ? 0 : value / total }));
47
+ }
@@ -0,0 +1,14 @@
1
+ import type { GitHubClient } from "../client.js";
2
+ import { type IssueSummary } from "./issues.js";
3
+ import { type RepoSummary } from "./repos.js";
4
+ export interface SearchResult<T> {
5
+ totalCount: number;
6
+ incomplete: boolean;
7
+ items: T[];
8
+ }
9
+ /** Join qualifier parts (skipping undefined values) into a GitHub search query string. */
10
+ export declare function buildQuery(parts: Record<string, string | number | undefined>): string;
11
+ /** Search issues and pull requests. Reuses the issues() mapping since search returns the same item shape. */
12
+ export declare function searchIssues(gh: GitHubClient, query: string): Promise<SearchResult<IssueSummary>>;
13
+ /** Search repositories. */
14
+ export declare function searchRepos(gh: GitHubClient, query: string): Promise<SearchResult<RepoSummary>>;
@@ -0,0 +1,19 @@
1
+ import { toIssueSummary } from "./issues.js";
2
+ import { toRepoSummary } from "./repos.js";
3
+ /** Join qualifier parts (skipping undefined values) into a GitHub search query string. */
4
+ export function buildQuery(parts) {
5
+ return Object.entries(parts)
6
+ .filter((entry) => entry[1] !== undefined)
7
+ .map(([key, value]) => `${key}:${value}`)
8
+ .join(" ");
9
+ }
10
+ /** Search issues and pull requests. Reuses the issues() mapping since search returns the same item shape. */
11
+ export async function searchIssues(gh, query) {
12
+ const raw = await gh.rest(`/search/issues?q=${encodeURIComponent(query)}`);
13
+ return { totalCount: raw.total_count, incomplete: raw.incomplete_results, items: raw.items.map(toIssueSummary) };
14
+ }
15
+ /** Search repositories. */
16
+ export async function searchRepos(gh, query) {
17
+ const raw = await gh.rest(`/search/repositories?q=${encodeURIComponent(query)}`);
18
+ return { totalCount: raw.total_count, incomplete: raw.incomplete_results, items: raw.items.map(toRepoSummary) };
19
+ }
@@ -0,0 +1,17 @@
1
+ import type { GitHubClient } from "../client.js";
2
+ export interface UserProfile {
3
+ login: string;
4
+ name: string | null;
5
+ bio: string | null;
6
+ company: string | null;
7
+ location: string | null;
8
+ blog: string | null;
9
+ followers: number;
10
+ following: number;
11
+ publicRepos: number;
12
+ avatarUrl: string;
13
+ createdAt: string;
14
+ hireable: boolean | null;
15
+ }
16
+ /** Fetch a public user profile. */
17
+ export declare function user(gh: GitHubClient, login: string): Promise<UserProfile>;
@@ -0,0 +1,18 @@
1
+ /** Fetch a public user profile. */
2
+ export async function user(gh, login) {
3
+ const raw = await gh.rest(`/users/${encodeURIComponent(login)}`);
4
+ return {
5
+ login: raw.login,
6
+ name: raw.name,
7
+ bio: raw.bio,
8
+ company: raw.company,
9
+ location: raw.location,
10
+ blog: raw.blog,
11
+ followers: raw.followers,
12
+ following: raw.following,
13
+ publicRepos: raw.public_repos,
14
+ avatarUrl: raw.avatar_url,
15
+ createdAt: raw.created_at,
16
+ hireable: raw.hireable,
17
+ };
18
+ }
@@ -0,0 +1,47 @@
1
+ import type { ContributionsWire } from "./wire.js";
2
+ export type { ContributionsWire } from "./wire.js";
3
+ /** Thrown by resolveContributions when the user does not exist (→ 404). */
4
+ export declare class GitHubUserNotFoundError extends Error {
5
+ }
6
+ export declare function parseContributionsHtml(html: string): {
7
+ date: string;
8
+ count: number;
9
+ weekday: number;
10
+ }[];
11
+ /** Server-side: fetch a user's contribution calendar via GraphQL (with token) or HTML scrape. */
12
+ export declare function resolveContributions(user: string, options?: {
13
+ token?: string;
14
+ }): Promise<ContributionsWire>;
15
+ /**
16
+ * Turnkey proxy handler — mount at any route in any Web-`Response` server (Nitro,
17
+ * workers, Deno). Reads `?user=`, validates, proxies GitHub, returns JSON + CORS.
18
+ */
19
+ export declare function githubContributionsHandler(options?: {
20
+ token?: string;
21
+ }): (request: Request) => Promise<Response>;
22
+ export interface GitHubProxyOptions {
23
+ /** Server-side token; forwarded as `Authorization: Bearer` so private/authed reads work. */
24
+ token?: string;
25
+ /**
26
+ * REST path allowlist — return true to forward a `?path=` GET. Every path is
27
+ * rejected (403) unless this says otherwise; omitting it denies everything.
28
+ * There is no "allow all" default, so a bare mount can never become an open proxy.
29
+ */
30
+ allowPath?: (path: string) => boolean;
31
+ /**
32
+ * GraphQL allowlist, keyed by operation name, holding the exact query text the
33
+ * server sends upstream for that name. Callers pick an `?op=` name and supply
34
+ * only `variables`; they never send query text, so an unlisted or forged query
35
+ * can't reach GitHub. Omitting this disables GraphQL entirely.
36
+ */
37
+ graphqlOperations?: Record<string, string>;
38
+ /** Injectable fetch (tests, non-browser runtimes). Defaults to global fetch. */
39
+ fetchImpl?: typeof fetch;
40
+ }
41
+ /**
42
+ * Named-operation GitHub proxy: forwards only the REST paths and GraphQL
43
+ * operations the caller explicitly allowlists, using the server-side token.
44
+ * Clients never supply raw GraphQL text or an unconstrained path — a bare
45
+ * mount with no `allowPath`/`graphqlOperations` forwards nothing at all.
46
+ */
47
+ export declare function githubProxyHandler(options?: GitHubProxyOptions): (request: Request) => Promise<Response>;