@forgeportal/plugin-github-insights 1.3.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,4 @@
1
+
2
+ > @forgeportal/plugin-github-insights@1.3.0 build /home/runner/work/forgeportal/forgeportal/packages/plugin-github-insights
3
+ > tsc
4
+
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bendaamerahmed
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import type { Entity } from '@forgeportal/plugin-sdk';
3
+ interface GitHubInsightsTabProps {
4
+ entity: Entity;
5
+ }
6
+ export declare function GitHubInsightsTab({ entity }: GitHubInsightsTabProps): React.ReactElement;
7
+ export {};
8
+ //# sourceMappingURL=GitHubInsightsTab.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GitHubInsightsTab.d.ts","sourceRoot":"","sources":["../src/GitHubInsightsTab.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAExC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AA4GtD,UAAU,sBAAsB;IAAG,MAAM,EAAE,MAAM,CAAA;CAAE;AAEnD,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,EAAE,sBAAsB,GAAG,KAAK,CAAC,YAAY,CAoTxF"}
@@ -0,0 +1,111 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { useApi } from '@forgeportal/plugin-sdk/react';
4
+ import { parseGitHubUrl } from './api-client.js';
5
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
6
+ /**
7
+ * Extracts GitHub owner/repo from:
8
+ * 1. entity.annotations['forgeportal.dev/github-repo'] (explicit override)
9
+ * 2. entity.links — first link pointing to github.com
10
+ * 3. entity.spec.scmUrl — if it's a github.com URL
11
+ */
12
+ function extractGitHubRef(entity) {
13
+ // 1. Explicit annotation: forgeportal.dev/github-repo = owner/repo
14
+ const annotationRepo = entity.annotations?.['forgeportal.dev/github-repo'];
15
+ if (annotationRepo) {
16
+ const parts = annotationRepo.replace(/^https?:\/\/github\.com\//, '').split('/');
17
+ if (parts.length >= 2 && parts[0] && parts[1]) {
18
+ return { owner: parts[0], repo: parts[1] };
19
+ }
20
+ }
21
+ // 2. Links — first github.com URL
22
+ for (const link of entity.links ?? []) {
23
+ const ref = parseGitHubUrl(link.url);
24
+ if (ref)
25
+ return ref;
26
+ }
27
+ // 3. spec.scmUrl
28
+ const scmUrl = entity.spec?.['scmUrl'];
29
+ if (typeof scmUrl === 'string') {
30
+ const ref = parseGitHubUrl(scmUrl);
31
+ if (ref)
32
+ return ref;
33
+ }
34
+ return null;
35
+ }
36
+ function relativeTime(isoDate) {
37
+ const diff = Date.now() - new Date(isoDate).getTime();
38
+ const minutes = Math.floor(diff / 60_000);
39
+ if (minutes < 60)
40
+ return `${minutes}m ago`;
41
+ const hours = Math.floor(minutes / 60);
42
+ if (hours < 24)
43
+ return `${hours}h ago`;
44
+ const days = Math.floor(hours / 24);
45
+ if (days < 30)
46
+ return `${days}d ago`;
47
+ return new Date(isoDate).toLocaleDateString();
48
+ }
49
+ function truncateSha(sha) {
50
+ return sha.slice(0, 7);
51
+ }
52
+ function truncateMessage(msg, max = 72) {
53
+ const first = msg.split('\n')[0] ?? msg;
54
+ return first.length > max ? `${first.slice(0, max)}…` : first;
55
+ }
56
+ // ─── Sub-components ──────────────────────────────────────────────────────────
57
+ function SectionTitle({ children, count }) {
58
+ return (_jsxs("div", { className: "flex items-center gap-2 mb-3", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-700", children: children }), count !== undefined && (_jsx("span", { className: "rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-500", children: count }))] }));
59
+ }
60
+ function PRStatusBadge({ draft, labels }) {
61
+ if (draft) {
62
+ return _jsx("span", { className: "rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-500", children: "Draft" });
63
+ }
64
+ return (_jsxs("div", { className: "flex flex-wrap gap-1", children: [labels.map((l) => (_jsx("span", { className: "rounded-full px-2 py-0.5 text-xs font-medium", style: { backgroundColor: `#${l.color}22`, color: `#${l.color}` }, children: l.name }, l.name))), labels.length === 0 && (_jsx("span", { className: "rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-700", children: "Open" }))] }));
65
+ }
66
+ export function GitHubInsightsTab({ entity }) {
67
+ const [activeView, setActiveView] = useState('overview');
68
+ const ghRef = extractGitHubRef(entity);
69
+ // Not configured state
70
+ if (!ghRef) {
71
+ return (_jsxs("div", { className: "rounded-lg border border-dashed border-gray-200 bg-gray-50 p-8 text-center", children: [_jsx("p", { className: "text-sm font-medium text-gray-700 mb-1", children: "No GitHub repository linked" }), _jsxs("p", { className: "text-xs text-gray-500 mb-4", children: ["Add a GitHub link to your", ' ', _jsx("code", { className: "rounded bg-gray-100 px-1 py-0.5", children: "entity.yaml" }), ' ', "or set the annotation", ' ', _jsx("code", { className: "rounded bg-gray-100 px-1 py-0.5", children: "forgeportal.dev/github-repo" }), "."] }), _jsx("pre", { className: "mx-auto max-w-md rounded bg-gray-800 p-3 text-left text-xs text-green-300", children: `metadata:\n links:\n - title: GitHub\n url: https://github.com/owner/repo` })] }));
72
+ }
73
+ const { owner, repo } = ghRef;
74
+ const baseUrl = `/api/v1/plugins/github-insights/entities/${entity.id}`;
75
+ const q = `owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}`;
76
+ const { data: overviewData, isPending: overviewLoading, error: overviewError } = useApi(`${baseUrl}/overview?${q}`, { staleTime: 60_000 });
77
+ const { data: prsData, isPending: prsLoading } = useApi(`${baseUrl}/prs?${q}`, {
78
+ enabled: activeView === 'prs',
79
+ staleTime: 60_000,
80
+ });
81
+ const { data: commitsData, isPending: commitsLoading } = useApi(`${baseUrl}/commits?${q}`, {
82
+ enabled: activeView === 'commits',
83
+ staleTime: 60_000,
84
+ });
85
+ const { data: contributorsData, isPending: contributorsLoading } = useApi(`${baseUrl}/contributors?${q}`, {
86
+ enabled: activeView === 'contributors',
87
+ staleTime: 300_000,
88
+ });
89
+ const overview = overviewData?.data;
90
+ const prs = prsData?.data ?? [];
91
+ const commits = commitsData?.data ?? [];
92
+ const contributors = contributorsData?.data ?? [];
93
+ const views = [
94
+ { id: 'overview', label: 'Overview' },
95
+ { id: 'prs', label: `PRs${overview ? ` (${overview.openPRCount})` : ''}` },
96
+ { id: 'commits', label: 'Commits' },
97
+ { id: 'contributors', label: 'Contributors' },
98
+ ];
99
+ return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("a", { href: `https://github.com/${owner}/${repo}`, target: "_blank", rel: "noreferrer", className: "flex items-center gap-2 text-sm font-medium text-indigo-600 hover:underline", children: [_jsx("svg", { className: "h-4 w-4", fill: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { d: "M12 0C5.37 0 0 5.373 0 12c0 5.303 3.438 9.8 8.207 11.387.6.113.82-.258.82-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.09-.745.083-.729.083-.729 1.205.084 1.84 1.237 1.84 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.76-1.605-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.3 1.23A11.51 11.51 0 0 1 12 5.803c1.02.005 2.046.138 3.006.404 2.291-1.553 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.91 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .322.218.694.825.576C20.565 21.796 24 17.3 24 12c0-6.627-5.373-12-12-12z" }) }), owner, "/", repo] }), overview && (_jsxs("div", { className: "flex items-center gap-4 text-xs text-gray-500", children: [_jsxs("span", { children: ["\u2B50 ", overview.repo.stargazers_count.toLocaleString()] }), _jsxs("span", { children: ["\uD83C\uDF74 ", overview.repo.forks_count.toLocaleString()] }), overview.repo.language && _jsxs("span", { children: ["\uD83D\uDD35 ", overview.repo.language] })] }))] }), _jsx("div", { className: "flex gap-1 border-b border-gray-200", children: views.map((v) => (_jsx("button", { onClick: () => setActiveView(v.id), className: `px-3 py-1.5 text-xs font-medium transition-colors ${activeView === v.id
100
+ ? 'border-b-2 border-indigo-500 text-indigo-600 -mb-px'
101
+ : 'text-gray-500 hover:text-gray-700'}`, children: v.label }, v.id))) }), overviewError && (_jsxs("div", { className: "rounded-md bg-amber-50 border border-amber-200 p-3 text-xs text-amber-800", children: [_jsx("span", { className: "font-medium", children: "Limited access: " }), overviewError instanceof Error ? overviewError.message : 'GitHub API error'] })), activeView === 'overview' && (_jsxs("div", { className: "space-y-4", children: [overviewLoading && (_jsx("p", { className: "text-xs text-gray-400 animate-pulse", children: "Loading repository overview\u2026" })), overview && (_jsxs(_Fragment, { children: [_jsx("div", { className: "grid grid-cols-2 gap-3 sm:grid-cols-4", children: [
102
+ { label: 'Open PRs', value: overview.openPRCount, icon: '🔀' },
103
+ { label: 'Open Issues', value: overview.repo.open_issues_count, icon: '🐛' },
104
+ { label: 'Stars', value: overview.repo.stargazers_count, icon: '⭐' },
105
+ { label: 'Forks', value: overview.repo.forks_count, icon: '🍴' },
106
+ ].map((stat) => (_jsxs("div", { className: "rounded-lg border border-gray-200 bg-white p-3", children: [_jsxs("p", { className: "text-xs text-gray-500", children: [stat.icon, " ", stat.label] }), _jsx("p", { className: "mt-1 text-lg font-semibold text-gray-800", children: stat.value.toLocaleString() })] }, stat.label))) }), overview.repo.description && (_jsx("p", { className: "text-xs text-gray-600 italic", children: overview.repo.description })), overview.latestCommit && (_jsxs("div", { className: "rounded-lg border border-gray-200 bg-white p-4", children: [_jsx("p", { className: "text-xs font-medium text-gray-500 mb-2", children: "Latest Commit" }), _jsxs("div", { className: "flex items-start gap-3", children: [overview.latestCommit.author && (_jsx("img", { src: overview.latestCommit.author.avatar_url, alt: overview.latestCommit.author.login, className: "h-6 w-6 rounded-full" })), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "text-xs text-gray-800 font-medium truncate", children: truncateMessage(overview.latestCommit.commit.message) }), _jsxs("p", { className: "text-xs text-gray-500 mt-0.5", children: [_jsx("a", { href: overview.latestCommit.html_url, target: "_blank", rel: "noreferrer", className: "font-mono text-indigo-600 hover:underline", children: truncateSha(overview.latestCommit.sha) }), ' · ', overview.latestCommit.commit.author.name, ' · ', relativeTime(overview.latestCommit.commit.author.date)] })] })] })] }))] }))] })), activeView === 'prs' && (_jsxs("div", { children: [_jsx(SectionTitle, { count: prs.length, children: "Open Pull Requests" }), prsLoading && _jsx("p", { className: "text-xs text-gray-400 animate-pulse", children: "Loading PRs\u2026" }), !prsLoading && prs.length === 0 && (_jsx("p", { className: "text-xs text-gray-400", children: "No open pull requests. \uD83C\uDF89" })), _jsx("div", { className: "space-y-2", children: prs.map((pr) => (_jsx("div", { className: "rounded-lg border border-gray-200 bg-white p-3 hover:bg-gray-50", children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("img", { src: pr.user.avatar_url, alt: pr.user.login, className: "h-5 w-5 rounded-full mt-0.5 flex-shrink-0" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("a", { href: pr.html_url, target: "_blank", rel: "noreferrer", className: "text-xs font-medium text-gray-800 hover:text-indigo-600 hover:underline line-clamp-1", children: pr.title }), _jsx("div", { className: "flex items-center gap-2 mt-1", children: _jsxs("span", { className: "text-xs text-gray-400", children: ["#", pr.number, " \u00B7 ", pr.user.login, " \u00B7 ", relativeTime(pr.created_at)] }) })] }), _jsx(PRStatusBadge, { draft: pr.draft, labels: pr.labels })] }) }, pr.number))) })] })), activeView === 'commits' && (_jsxs("div", { children: [_jsx(SectionTitle, { count: commits.length, children: "Recent Commits" }), commitsLoading && _jsx("p", { className: "text-xs text-gray-400 animate-pulse", children: "Loading commits\u2026" }), _jsx("div", { className: "overflow-x-auto rounded-lg border border-gray-200", children: _jsxs("table", { className: "min-w-full text-xs", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-gray-200 bg-gray-50", children: [_jsx("th", { className: "px-3 py-2 text-left font-medium text-gray-600", children: "SHA" }), _jsx("th", { className: "px-3 py-2 text-left font-medium text-gray-600", children: "Message" }), _jsx("th", { className: "px-3 py-2 text-left font-medium text-gray-600", children: "Author" }), _jsx("th", { className: "px-3 py-2 text-left font-medium text-gray-600", children: "Date" })] }) }), _jsxs("tbody", { className: "divide-y divide-gray-100 bg-white", children: [commits.map((c) => (_jsxs("tr", { className: "hover:bg-gray-50", children: [_jsx("td", { className: "px-3 py-2", children: _jsx("a", { href: c.html_url, target: "_blank", rel: "noreferrer", className: "font-mono text-indigo-600 hover:underline", children: truncateSha(c.sha) }) }), _jsx("td", { className: "px-3 py-2 max-w-xs", children: _jsx("p", { className: "truncate text-gray-800", children: truncateMessage(c.commit.message, 60) }) }), _jsx("td", { className: "px-3 py-2", children: _jsxs("div", { className: "flex items-center gap-1", children: [c.author && (_jsx("img", { src: c.author.avatar_url, alt: c.author.login, className: "h-4 w-4 rounded-full" })), _jsx("span", { className: "text-gray-600", children: c.commit.author.name })] }) }), _jsx("td", { className: "px-3 py-2 text-gray-500 whitespace-nowrap", children: relativeTime(c.commit.author.date) })] }, c.sha))), !commitsLoading && commits.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: 4, className: "py-6 text-center text-gray-400", children: "No commits found." }) }))] })] }) })] })), activeView === 'contributors' && (_jsxs("div", { children: [_jsx(SectionTitle, { count: contributors.length, children: "Top Contributors" }), contributorsLoading && _jsx("p", { className: "text-xs text-gray-400 animate-pulse", children: "Loading contributors\u2026" }), contributors.length > 0 && (() => {
107
+ const max = contributors[0]?.contributions ?? 1;
108
+ return (_jsx("div", { className: "space-y-3", children: contributors.map((c) => (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx("img", { src: c.avatar_url, alt: c.login, className: "h-7 w-7 rounded-full flex-shrink-0" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center justify-between mb-1", children: [_jsx("a", { href: c.html_url, target: "_blank", rel: "noreferrer", className: "text-xs font-medium text-gray-800 hover:text-indigo-600 hover:underline", children: c.login }), _jsxs("span", { className: "text-xs text-gray-500", children: [c.contributions, " commits"] })] }), _jsx("div", { className: "h-1.5 w-full rounded-full bg-gray-100", children: _jsx("div", { className: "h-1.5 rounded-full bg-indigo-500", style: { width: `${Math.round((c.contributions / max) * 100)}%` } }) })] })] }, c.login))) }));
109
+ })(), !contributorsLoading && contributors.length === 0 && (_jsx("p", { className: "text-xs text-gray-400", children: "No contributors data available." }))] }))] }));
110
+ }
111
+ //# sourceMappingURL=GitHubInsightsTab.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GitHubInsightsTab.js","sourceRoot":"","sources":["../src/GitHubInsightsTab.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=api-client.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/api-client.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,159 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { GitHubInsightsClient, parseGitHubUrl } from '../api-client.js';
3
+ // ── Fixtures ──────────────────────────────────────────────────────────────────
4
+ const CONFIG = {
5
+ token: 'ghp_test_token',
6
+ cacheTTLSeconds: 0, // disable cache for tests
7
+ };
8
+ const REPO_FIXTURE = {
9
+ full_name: 'acme/payments-api',
10
+ description: 'The payments API',
11
+ default_branch: 'main',
12
+ stargazers_count: 42,
13
+ forks_count: 7,
14
+ open_issues_count: 3,
15
+ language: 'TypeScript',
16
+ html_url: 'https://github.com/acme/payments-api',
17
+ pushed_at: '2026-02-20T10:00:00Z',
18
+ };
19
+ const PR_FIXTURE = [{
20
+ number: 99,
21
+ title: 'feat: add webhook support',
22
+ html_url: 'https://github.com/acme/payments-api/pull/99',
23
+ state: 'open',
24
+ user: { login: 'alice', avatar_url: 'https://avatars.githubusercontent.com/alice' },
25
+ created_at: '2026-02-18T09:00:00Z',
26
+ updated_at: '2026-02-19T14:00:00Z',
27
+ labels: [],
28
+ }];
29
+ // ── Helpers ───────────────────────────────────────────────────────────────────
30
+ function mockFetch(body, status = 200) {
31
+ const mock = vi.fn().mockResolvedValue({
32
+ ok: status >= 200 && status < 300,
33
+ status,
34
+ statusText: status === 200 ? 'OK' : 'Error',
35
+ headers: { get: () => null },
36
+ json: () => Promise.resolve(body),
37
+ text: () => Promise.resolve(JSON.stringify(body)),
38
+ });
39
+ vi.stubGlobal('fetch', mock);
40
+ return mock;
41
+ }
42
+ beforeEach(() => vi.restoreAllMocks());
43
+ afterEach(() => vi.unstubAllGlobals());
44
+ // ── parseGitHubUrl ────────────────────────────────────────────────────────────
45
+ describe('parseGitHubUrl', () => {
46
+ it('parses a standard HTTPS GitHub URL', () => {
47
+ expect(parseGitHubUrl('https://github.com/acme/payments-api')).toEqual({
48
+ owner: 'acme',
49
+ repo: 'payments-api',
50
+ });
51
+ });
52
+ it('strips .git suffix', () => {
53
+ expect(parseGitHubUrl('https://github.com/acme/payments-api.git')).toEqual({
54
+ owner: 'acme',
55
+ repo: 'payments-api',
56
+ });
57
+ });
58
+ it('parses URL without protocol prefix', () => {
59
+ expect(parseGitHubUrl('github.com/acme/my-repo')).toEqual({
60
+ owner: 'acme',
61
+ repo: 'my-repo',
62
+ });
63
+ });
64
+ it('returns null for non-GitHub URLs', () => {
65
+ expect(parseGitHubUrl('https://gitlab.com/acme/repo')).toBeNull();
66
+ expect(parseGitHubUrl('https://bitbucket.org/acme/repo')).toBeNull();
67
+ });
68
+ it('returns null for invalid URLs', () => {
69
+ expect(parseGitHubUrl('not-a-url')).toBeNull();
70
+ expect(parseGitHubUrl('')).toBeNull();
71
+ });
72
+ it('returns null when owner or repo is missing', () => {
73
+ expect(parseGitHubUrl('https://github.com/acme')).toBeNull();
74
+ expect(parseGitHubUrl('https://github.com/')).toBeNull();
75
+ });
76
+ });
77
+ // ── GitHubInsightsClient.getRepo ──────────────────────────────────────────────
78
+ describe('GitHubInsightsClient.getRepo', () => {
79
+ const client = new GitHubInsightsClient(CONFIG);
80
+ it('fetches repository metadata', async () => {
81
+ mockFetch(REPO_FIXTURE);
82
+ const repo = await client.getRepo('acme', 'payments-api');
83
+ expect(repo.full_name).toBe('acme/payments-api');
84
+ expect(repo.stargazers_count).toBe(42);
85
+ });
86
+ it('sends the Authorization header', async () => {
87
+ const fetchMock = mockFetch(REPO_FIXTURE);
88
+ await client.getRepo('acme', 'payments-api');
89
+ const [, init] = fetchMock.mock.calls[0];
90
+ expect(init.headers['Authorization']).toBe('Bearer ghp_test_token');
91
+ });
92
+ it('sends Accept: application/vnd.github+json', async () => {
93
+ const fetchMock = mockFetch(REPO_FIXTURE);
94
+ await client.getRepo('acme', 'payments-api');
95
+ const [, init] = fetchMock.mock.calls[0];
96
+ expect(init.headers['Accept']).toBe('application/vnd.github+json');
97
+ });
98
+ it('throws "not found" on 404', async () => {
99
+ mockFetch({ message: 'Not Found' }, 404);
100
+ await expect(client.getRepo('acme', 'ghost-repo')).rejects.toThrow('not found');
101
+ });
102
+ it('throws rate limit error on 403', async () => {
103
+ const mock = vi.fn().mockResolvedValue({
104
+ ok: false, status: 403,
105
+ headers: { get: (h) => h === 'Retry-After' ? '60' : null },
106
+ json: () => Promise.resolve({}),
107
+ text: () => Promise.resolve('rate limit'),
108
+ });
109
+ vi.stubGlobal('fetch', mock);
110
+ await expect(client.getRepo('acme', 'payments-api')).rejects.toThrow(/rate limit/i);
111
+ });
112
+ it('throws rate limit error on 429', async () => {
113
+ const mock = vi.fn().mockResolvedValue({
114
+ ok: false, status: 429,
115
+ headers: { get: () => null },
116
+ json: () => Promise.resolve({}),
117
+ text: () => Promise.resolve('too many'),
118
+ });
119
+ vi.stubGlobal('fetch', mock);
120
+ await expect(client.getRepo('acme', 'payments-api')).rejects.toThrow(/rate limit/i);
121
+ });
122
+ });
123
+ // ── GitHubInsightsClient.getOpenPRs ───────────────────────────────────────────
124
+ describe('GitHubInsightsClient.getOpenPRs', () => {
125
+ const client = new GitHubInsightsClient(CONFIG);
126
+ it('returns open pull requests', async () => {
127
+ mockFetch(PR_FIXTURE);
128
+ const prs = await client.getOpenPRs('acme', 'payments-api');
129
+ expect(prs).toHaveLength(1);
130
+ expect(prs[0]?.number).toBe(99);
131
+ expect(prs[0]?.title).toBe('feat: add webhook support');
132
+ });
133
+ it('requests the correct query parameters', async () => {
134
+ const fetchMock = mockFetch(PR_FIXTURE);
135
+ await client.getOpenPRs('acme', 'payments-api');
136
+ const [url] = fetchMock.mock.calls[0];
137
+ expect(url).toContain('state=open');
138
+ expect(url).toContain('per_page=25');
139
+ });
140
+ });
141
+ // ── TTL cache ─────────────────────────────────────────────────────────────────
142
+ describe('GitHubInsightsClient TTL cache', () => {
143
+ it('serves cached data on second request when TTL > 0', async () => {
144
+ const cachedClient = new GitHubInsightsClient({ ...CONFIG, cacheTTLSeconds: 300 });
145
+ const fetchMock = mockFetch(REPO_FIXTURE);
146
+ await cachedClient.getRepo('acme', 'payments-api');
147
+ await cachedClient.getRepo('acme', 'payments-api');
148
+ // fetch should only be called once — second call is from cache
149
+ expect(fetchMock).toHaveBeenCalledTimes(1);
150
+ });
151
+ it('bypasses cache when TTL is 0', async () => {
152
+ const noCache = new GitHubInsightsClient({ ...CONFIG, cacheTTLSeconds: 0 });
153
+ const fetchMock = mockFetch(REPO_FIXTURE);
154
+ await noCache.getRepo('acme', 'payments-api');
155
+ await noCache.getRepo('acme', 'payments-api');
156
+ expect(fetchMock).toHaveBeenCalledTimes(2);
157
+ });
158
+ });
159
+ //# sourceMappingURL=api-client.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.test.js","sourceRoot":"","sources":["../../src/__tests__/api-client.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import type { GHRepo, GHPR, GHCommit, GHContributor, GHWorkflowRun, GitHubInsightsConfig } from './types.js';
2
+ /**
3
+ * Minimal GitHub REST API client with in-process TTL cache.
4
+ * Uses the standard GitHub token from config (falls back to SCM_GITHUB_TOKEN).
5
+ * Respects Retry-After headers to avoid secondary rate limit penalties.
6
+ */
7
+ export declare class GitHubInsightsClient {
8
+ private readonly token;
9
+ private readonly ttlMs;
10
+ private readonly cache;
11
+ constructor(config: GitHubInsightsConfig);
12
+ private get;
13
+ /** GET /repos/{owner}/{repo} */
14
+ getRepo(owner: string, repo: string): Promise<GHRepo>;
15
+ /** GET /repos/{owner}/{repo}/pulls?state=open&per_page=25 */
16
+ getOpenPRs(owner: string, repo: string): Promise<GHPR[]>;
17
+ /** GET /repos/{owner}/{repo}/commits?per_page=20 */
18
+ getRecentCommits(owner: string, repo: string): Promise<GHCommit[]>;
19
+ /** GET /repos/{owner}/{repo}/contributors?per_page=10&anon=false */
20
+ getContributors(owner: string, repo: string): Promise<GHContributor[]>;
21
+ /** GET /repos/{owner}/{repo}/actions/runs?per_page=10 */
22
+ getWorkflowRuns(owner: string, repo: string): Promise<{
23
+ workflow_runs: GHWorkflowRun[];
24
+ }>;
25
+ }
26
+ /**
27
+ * Parses a GitHub repo URL and returns { owner, repo } or null.
28
+ * Handles: https://github.com/owner/repo and github.com/owner/repo
29
+ */
30
+ export declare function parseGitHubUrl(url: string): {
31
+ owner: string;
32
+ repo: string;
33
+ } | null;
34
+ //# sourceMappingURL=api-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EACN,IAAI,EACJ,QAAQ,EACR,aAAa,EACb,aAAa,EACb,oBAAoB,EAErB,MAAM,YAAY,CAAC;AAIpB;;;;GAIG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAW;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAW;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6C;gBAEvD,MAAM,EAAE,oBAAoB;YAK1B,GAAG;IA4CjB,gCAAgC;IAChC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIrD,6DAA6D;IAC7D,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;IAMxD,oDAAoD;IACpD,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAMlE,oEAAoE;IACpE,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAMtE,yDAAyD;IACzD,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,aAAa,EAAE,aAAa,EAAE,CAAA;KAAE,CAAC;CAK1F;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAUlF"}
@@ -0,0 +1,85 @@
1
+ const GITHUB_API = 'https://api.github.com';
2
+ /**
3
+ * Minimal GitHub REST API client with in-process TTL cache.
4
+ * Uses the standard GitHub token from config (falls back to SCM_GITHUB_TOKEN).
5
+ * Respects Retry-After headers to avoid secondary rate limit penalties.
6
+ */
7
+ export class GitHubInsightsClient {
8
+ token;
9
+ ttlMs;
10
+ cache = new Map();
11
+ constructor(config) {
12
+ this.token = config.token;
13
+ this.ttlMs = config.cacheTTLSeconds * 1000;
14
+ }
15
+ async get(path) {
16
+ const cacheKey = path;
17
+ if (this.ttlMs > 0) {
18
+ const hit = this.cache.get(cacheKey);
19
+ if (hit && Date.now() < hit.expiresAt)
20
+ return hit.data;
21
+ }
22
+ const res = await fetch(`${GITHUB_API}${path}`, {
23
+ headers: {
24
+ Authorization: `Bearer ${this.token}`,
25
+ Accept: 'application/vnd.github+json',
26
+ 'X-GitHub-Api-Version': '2022-11-28',
27
+ },
28
+ });
29
+ if (res.status === 403 || res.status === 429) {
30
+ const retryAfter = res.headers.get('Retry-After');
31
+ throw new Error(`GitHub rate limit hit (${res.status}).${retryAfter ? ` Retry after ${retryAfter}s.` : ''}`);
32
+ }
33
+ if (res.status === 404) {
34
+ throw new Error(`GitHub resource not found: ${path}`);
35
+ }
36
+ if (!res.ok) {
37
+ const body = await res.text().catch(() => '');
38
+ throw new Error(`GitHub API ${res.status}: ${body || res.statusText}`);
39
+ }
40
+ const data = await res.json();
41
+ if (this.ttlMs > 0) {
42
+ this.cache.set(cacheKey, { data, expiresAt: Date.now() + this.ttlMs });
43
+ }
44
+ return data;
45
+ }
46
+ /** GET /repos/{owner}/{repo} */
47
+ getRepo(owner, repo) {
48
+ return this.get(`/repos/${owner}/${repo}`);
49
+ }
50
+ /** GET /repos/{owner}/{repo}/pulls?state=open&per_page=25 */
51
+ getOpenPRs(owner, repo) {
52
+ return this.get(`/repos/${owner}/${repo}/pulls?state=open&per_page=25&sort=updated&direction=desc`);
53
+ }
54
+ /** GET /repos/{owner}/{repo}/commits?per_page=20 */
55
+ getRecentCommits(owner, repo) {
56
+ return this.get(`/repos/${owner}/${repo}/commits?per_page=20`);
57
+ }
58
+ /** GET /repos/{owner}/{repo}/contributors?per_page=10&anon=false */
59
+ getContributors(owner, repo) {
60
+ return this.get(`/repos/${owner}/${repo}/contributors?per_page=10&anon=false`);
61
+ }
62
+ /** GET /repos/{owner}/{repo}/actions/runs?per_page=10 */
63
+ getWorkflowRuns(owner, repo) {
64
+ return this.get(`/repos/${owner}/${repo}/actions/runs?per_page=10`);
65
+ }
66
+ }
67
+ /**
68
+ * Parses a GitHub repo URL and returns { owner, repo } or null.
69
+ * Handles: https://github.com/owner/repo and github.com/owner/repo
70
+ */
71
+ export function parseGitHubUrl(url) {
72
+ try {
73
+ const parsed = new URL(url.startsWith('http') ? url : `https://${url}`);
74
+ if (!parsed.hostname.endsWith('github.com'))
75
+ return null;
76
+ const parts = parsed.pathname.replace(/^\//, '').replace(/\.git$/, '').split('/');
77
+ if (parts.length < 2 || !parts[0] || !parts[1])
78
+ return null;
79
+ return { owner: parts[0], repo: parts[1] };
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
85
+ //# sourceMappingURL=api-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { ForgeBackendPluginSDK } from '@forgeportal/plugin-sdk';
2
+ /**
3
+ * Backend entry point for the GitHub Insights plugin.
4
+ * Called by the ForgePortal plugin loader at startup.
5
+ *
6
+ * Configuration (forgeportal.yaml -> plugins.github-insights.config):
7
+ * cacheTTLSeconds: number (default: 300)
8
+ *
9
+ * Token resolution (first match wins):
10
+ * 1. FORGEPORTAL_PLUGIN_GITHUB_INSIGHTS_TOKEN env var (dedicated token)
11
+ * 2. SCM_GITHUB_TOKEN env var (shared SCM token)
12
+ */
13
+ export declare function registerBackendPlugin(sdk: ForgeBackendPluginSDK): void;
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAIrE;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,qBAAqB,GAAG,IAAI,CA0BtE"}
package/dist/index.js ADDED
@@ -0,0 +1,32 @@
1
+ import { createRoutes } from './routes.js';
2
+ /**
3
+ * Backend entry point for the GitHub Insights plugin.
4
+ * Called by the ForgePortal plugin loader at startup.
5
+ *
6
+ * Configuration (forgeportal.yaml -> plugins.github-insights.config):
7
+ * cacheTTLSeconds: number (default: 300)
8
+ *
9
+ * Token resolution (first match wins):
10
+ * 1. FORGEPORTAL_PLUGIN_GITHUB_INSIGHTS_TOKEN env var (dedicated token)
11
+ * 2. SCM_GITHUB_TOKEN env var (shared SCM token)
12
+ */
13
+ export function registerBackendPlugin(sdk) {
14
+ const token = process.env['FORGEPORTAL_PLUGIN_GITHUB_INSIGHTS_TOKEN'] ??
15
+ process.env['SCM_GITHUB_TOKEN'] ??
16
+ '';
17
+ const cacheTTLSeconds = sdk.config.get('cacheTTLSeconds') ?? 300;
18
+ if (!token) {
19
+ sdk.logger.warn('github-insights plugin: no GitHub token found. ' +
20
+ 'Set FORGEPORTAL_PLUGIN_GITHUB_INSIGHTS_TOKEN or SCM_GITHUB_TOKEN. ' +
21
+ 'API calls will fail for private repos.');
22
+ }
23
+ else {
24
+ sdk.logger.info(`github-insights plugin: ready (cache TTL: ${cacheTTLSeconds}s)`);
25
+ }
26
+ const config = { token, cacheTTLSeconds };
27
+ sdk.registerBackendRoute({
28
+ path: '',
29
+ handler: createRoutes(config),
30
+ });
31
+ }
32
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAA0B;IAC9D,MAAM,KAAK,GACT,OAAO,CAAC,GAAG,CAAC,0CAA0C,CAAC;QACvD,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;QAC/B,EAAE,CAAC;IAEL,MAAM,eAAe,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,CAAS,iBAAiB,CAAC,IAAI,GAAG,CAAC;IAEzE,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,GAAG,CAAC,MAAM,CAAC,IAAI,CACb,iDAAiD;YACjD,oEAAoE;YACpE,wCAAwC,CACzC,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,GAAG,CAAC,MAAM,CAAC,IAAI,CACb,6CAA6C,eAAe,IAAI,CACjE,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAyB,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC;IAEhE,GAAG,CAAC,oBAAoB,CAAC;QACvB,IAAI,EAAK,EAAE;QACX,OAAO,EAAE,YAAY,CAAC,MAAM,CAAC;KAC9B,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { GitHubInsightsConfig } from './types.js';
3
+ /**
4
+ * Creates Fastify route handlers for the GitHub Insights plugin.
5
+ * All routes mounted under /api/v1/plugins/github-insights/ by the plugin loader.
6
+ *
7
+ * Routes:
8
+ * GET entities/:entityId/overview — repo info + open PR count + latest commit
9
+ * GET entities/:entityId/prs — paginated open PRs
10
+ * GET entities/:entityId/commits — last 20 commits
11
+ * GET entities/:entityId/contributors — top contributors
12
+ */
13
+ export declare function createRoutes(config: GitHubInsightsConfig): (fastify: FastifyInstance) => Promise<void>;
14
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAE7E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAKvD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,oBAAoB,IAGzB,SAAS,eAAe,KAAG,OAAO,CAAC,IAAI,CAAC,CAkIvE"}