@forge-glance/sdk 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,359 @@
1
+ // src/logger.ts
2
+ var noopLogger = {
3
+ debug() {},
4
+ info() {},
5
+ warn() {},
6
+ error() {}
7
+ };
8
+
9
+ // src/MRDetailFetcher.ts
10
+ class MRDetailFetcher {
11
+ baseURL;
12
+ token;
13
+ log;
14
+ constructor(baseURL, token, options = {}) {
15
+ this.baseURL = baseURL.replace(/\/$/, "");
16
+ this.token = token;
17
+ this.log = options.logger ?? noopLogger;
18
+ }
19
+ async fetchDetail(projectId, mrIid) {
20
+ const url = `${this.baseURL}/api/v4/projects/${projectId}/merge_requests/${mrIid}/discussions?per_page=100`;
21
+ const res = await fetch(url, {
22
+ headers: { "PRIVATE-TOKEN": this.token }
23
+ });
24
+ if (!res.ok) {
25
+ throw new Error(`MR discussions fetch failed: ${res.status} ${res.statusText}`);
26
+ }
27
+ const raw = await res.json();
28
+ const discussions = raw.map((d) => ({
29
+ id: d.id,
30
+ resolvable: null,
31
+ resolved: null,
32
+ notes: d.notes.map(toNote)
33
+ }));
34
+ this.log.debug("MRDetailFetcher.fetchDetail", {
35
+ projectId,
36
+ mrIid,
37
+ discussionCount: discussions.length
38
+ });
39
+ return { mrIid, repositoryId: `gitlab:${projectId}`, discussions };
40
+ }
41
+ }
42
+ function toNote(n) {
43
+ return {
44
+ id: n.id,
45
+ body: n.body,
46
+ author: toAuthor(n.author),
47
+ createdAt: n.created_at,
48
+ system: n.system,
49
+ type: n.type,
50
+ resolvable: n.resolvable ?? null,
51
+ resolved: n.resolved ?? null,
52
+ position: n.position ? toPosition(n.position) : null
53
+ };
54
+ }
55
+ function toAuthor(a) {
56
+ return {
57
+ id: `gitlab:user:${a.id}`,
58
+ username: a.username,
59
+ name: a.name,
60
+ avatarUrl: a.avatar_url
61
+ };
62
+ }
63
+ function toPosition(p) {
64
+ return {
65
+ newPath: p.new_path ?? null,
66
+ oldPath: p.old_path ?? null,
67
+ newLine: p.new_line ?? null,
68
+ oldLine: p.old_line ?? null,
69
+ positionType: p.position_type ?? null
70
+ };
71
+ }
72
+
73
+ // src/GitLabProvider.ts
74
+ function parseGitLabRepoId(repositoryId) {
75
+ const parts = repositoryId.split(":");
76
+ return parseInt(parts.at(-1) ?? "0", 10);
77
+ }
78
+ var MR_DASHBOARD_FRAGMENT = `
79
+ fragment MRDashboardFields on MergeRequest {
80
+ id iid projectId title description state draft
81
+ sourceBranch targetBranch webUrl
82
+ diffHeadSha
83
+ updatedAt createdAt
84
+ conflicts
85
+ detailedMergeStatus
86
+ approved
87
+ diffStatsSummary { additions deletions fileCount }
88
+ author { id username name avatarUrl }
89
+ assignees(first: 20) { nodes { id username name avatarUrl } }
90
+ reviewers(first: 20) { nodes { id username name avatarUrl } }
91
+ approvedBy(first: 20) { nodes { id username name avatarUrl } }
92
+ approvalsLeft
93
+ resolvableDiscussionsCount
94
+ resolvedDiscussionsCount
95
+ headPipeline {
96
+ id iid status
97
+ createdAt
98
+ path
99
+ stages(first: 20) { nodes {
100
+ name
101
+ jobs(first: 50) { nodes {
102
+ id name status
103
+ allowFailure
104
+ webPath
105
+ stage { name }
106
+ }}
107
+ }}
108
+ }
109
+ }
110
+ `;
111
+ var AUTHORED_QUERY = `
112
+ query GlanceDashboardAuthored {
113
+ currentUser {
114
+ authoredMergeRequests(state: opened, first: 100) {
115
+ nodes { ...MRDashboardFields }
116
+ }
117
+ }
118
+ }
119
+ ${MR_DASHBOARD_FRAGMENT}
120
+ `;
121
+ var REVIEWING_QUERY = `
122
+ query GlanceDashboardReviewing {
123
+ currentUser {
124
+ reviewRequestedMergeRequests(state: opened, first: 100) {
125
+ nodes { ...MRDashboardFields }
126
+ }
127
+ }
128
+ }
129
+ ${MR_DASHBOARD_FRAGMENT}
130
+ `;
131
+ var ASSIGNED_QUERY = `
132
+ query GlanceDashboardAssigned {
133
+ currentUser {
134
+ assignedMergeRequests(state: opened, first: 100) {
135
+ nodes { ...MRDashboardFields }
136
+ }
137
+ }
138
+ }
139
+ ${MR_DASHBOARD_FRAGMENT}
140
+ `;
141
+ function numericId(gid) {
142
+ const parts = gid.split("/");
143
+ return parseInt(parts[parts.length - 1] ?? "0", 10);
144
+ }
145
+ function domainId(type, id) {
146
+ return `gitlab:${type}:${id}`;
147
+ }
148
+ function toUserRef(u) {
149
+ return {
150
+ id: `gitlab:user:${numericId(u.id)}`,
151
+ username: u.username,
152
+ name: u.name,
153
+ avatarUrl: u.avatarUrl
154
+ };
155
+ }
156
+ function toPipeline(p, baseURL) {
157
+ const allJobs = p.stages.nodes.flatMap((stage) => stage.jobs.nodes.map((job) => ({
158
+ id: `gitlab:job:${numericId(job.id)}`,
159
+ name: job.name,
160
+ stage: job.stage.name,
161
+ status: job.status,
162
+ allowFailure: job.allowFailure,
163
+ webUrl: job.webPath ? `${baseURL}${job.webPath}` : null
164
+ })));
165
+ return {
166
+ id: domainId("pipeline", numericId(p.id)),
167
+ status: normalizePipelineStatus(p),
168
+ createdAt: p.createdAt,
169
+ webUrl: p.path ? `${baseURL}${p.path}` : null,
170
+ jobs: allJobs
171
+ };
172
+ }
173
+ function normalizePipelineStatus(p) {
174
+ const allJobs = p.stages.nodes.flatMap((s) => s.jobs.nodes);
175
+ const hasAllowFailFailed = allJobs.some((j) => j.allowFailure && j.status === "failed");
176
+ if (p.status === "success" && hasAllowFailFailed) {
177
+ return "success_with_warnings";
178
+ }
179
+ return p.status;
180
+ }
181
+ function toMR(gql, role, baseURL) {
182
+ const resolvable = gql.resolvableDiscussionsCount ?? 0;
183
+ const resolved = gql.resolvedDiscussionsCount ?? 0;
184
+ const unresolvedThreadCount = Math.max(0, resolvable - resolved);
185
+ const diffStats = gql.diffStatsSummary ? {
186
+ additions: gql.diffStatsSummary.additions,
187
+ deletions: gql.diffStatsSummary.deletions,
188
+ filesChanged: gql.diffStatsSummary.fileCount
189
+ } : null;
190
+ return {
191
+ id: `gitlab:mr:${numericId(gql.id)}`,
192
+ iid: parseInt(gql.iid, 10),
193
+ repositoryId: `gitlab:${gql.projectId}`,
194
+ title: gql.title,
195
+ description: gql.description ?? null,
196
+ state: gql.state,
197
+ draft: gql.draft,
198
+ conflicts: gql.conflicts || gql.detailedMergeStatus === "conflict",
199
+ webUrl: gql.webUrl,
200
+ sourceBranch: gql.sourceBranch,
201
+ targetBranch: gql.targetBranch,
202
+ createdAt: gql.createdAt,
203
+ updatedAt: gql.updatedAt,
204
+ sha: gql.diffHeadSha,
205
+ author: toUserRef(gql.author),
206
+ assignees: gql.assignees.nodes.map(toUserRef),
207
+ reviewers: gql.reviewers.nodes.map(toUserRef),
208
+ roles: [role],
209
+ pipeline: gql.headPipeline ? toPipeline(gql.headPipeline, baseURL) : null,
210
+ unresolvedThreadCount,
211
+ approvalsLeft: gql.approvalsLeft ?? 0,
212
+ approved: gql.approved ?? false,
213
+ approvedBy: gql.approvedBy.nodes.map(toUserRef),
214
+ diffStats,
215
+ detailedMergeStatus: gql.detailedMergeStatus ?? null
216
+ };
217
+ }
218
+ var MR_DETAIL_QUERY = `
219
+ query GlanceMRDetail($projectPath: ID!, $iid: String!) {
220
+ project(fullPath: $projectPath) {
221
+ mergeRequest(iid: $iid) {
222
+ ...MRDashboardFields
223
+ }
224
+ }
225
+ }
226
+ ${MR_DASHBOARD_FRAGMENT}
227
+ `;
228
+
229
+ class GitLabProvider {
230
+ providerName = "gitlab";
231
+ baseURL;
232
+ token;
233
+ log;
234
+ mrDetailFetcher;
235
+ constructor(baseURL, token, options = {}) {
236
+ this.baseURL = baseURL.replace(/\/$/, "");
237
+ this.token = token;
238
+ this.log = options.logger ?? noopLogger;
239
+ this.mrDetailFetcher = new MRDetailFetcher(this.baseURL, token, { logger: this.log });
240
+ }
241
+ async validateToken() {
242
+ const url = `${this.baseURL}/api/v4/user`;
243
+ const res = await fetch(url, {
244
+ headers: { "PRIVATE-TOKEN": this.token }
245
+ });
246
+ if (!res.ok) {
247
+ throw new Error(`Token validation failed: ${res.status} ${res.statusText}`);
248
+ }
249
+ const user = await res.json();
250
+ return {
251
+ id: `gitlab:user:${user.id}`,
252
+ username: user.username,
253
+ name: user.name,
254
+ avatarUrl: user.avatar_url
255
+ };
256
+ }
257
+ async fetchSingleMR(projectPath, mrIid, currentUserNumericId) {
258
+ let resp;
259
+ try {
260
+ resp = await this.runQuery(MR_DETAIL_QUERY, {
261
+ projectPath,
262
+ iid: String(mrIid)
263
+ });
264
+ } catch (err) {
265
+ const message = err instanceof Error ? err.message : String(err);
266
+ this.log.warn("fetchSingleMR failed", { projectPath, mrIid, message });
267
+ return null;
268
+ }
269
+ const gql = resp.project?.mergeRequest;
270
+ if (!gql)
271
+ return null;
272
+ const roles = [];
273
+ if (currentUserNumericId !== null) {
274
+ const userGqlId = `gid://gitlab/User/${currentUserNumericId}`;
275
+ if (gql.author.id === userGqlId)
276
+ roles.push("author");
277
+ if (gql.assignees.nodes.some((u) => u.id === userGqlId))
278
+ roles.push("assignee");
279
+ if (gql.reviewers.nodes.some((u) => u.id === userGqlId))
280
+ roles.push("reviewer");
281
+ }
282
+ const pr = toMR(gql, roles[0] ?? "author", this.baseURL);
283
+ pr.roles = roles.length > 0 ? roles : pr.roles;
284
+ return pr;
285
+ }
286
+ async fetchPullRequests() {
287
+ const [authored, reviewing, assigned] = await Promise.all([
288
+ this.runQuery(AUTHORED_QUERY),
289
+ this.runQuery(REVIEWING_QUERY),
290
+ this.runQuery(ASSIGNED_QUERY)
291
+ ]);
292
+ const byId = new Map;
293
+ const addAll = (mrs, role) => {
294
+ for (const gql of mrs) {
295
+ const existing = byId.get(gql.id);
296
+ if (existing) {
297
+ if (!existing.roles.includes(role)) {
298
+ existing.roles.push(role);
299
+ }
300
+ } else {
301
+ byId.set(gql.id, toMR(gql, role, this.baseURL));
302
+ }
303
+ }
304
+ };
305
+ addAll(authored.currentUser.authoredMergeRequests.nodes, "author");
306
+ addAll(reviewing.currentUser.reviewRequestedMergeRequests.nodes, "reviewer");
307
+ addAll(assigned.currentUser.assignedMergeRequests.nodes, "assignee");
308
+ const prs = [...byId.values()];
309
+ this.log.debug("fetchPullRequests", { count: prs.length });
310
+ return prs;
311
+ }
312
+ async fetchMRDiscussions(repositoryId, mrIid) {
313
+ const projectId = parseGitLabRepoId(repositoryId);
314
+ return this.mrDetailFetcher.fetchDetail(projectId, mrIid);
315
+ }
316
+ async restRequest(method, path, body) {
317
+ const url = `${this.baseURL}${path}`;
318
+ const headers = {
319
+ "PRIVATE-TOKEN": this.token
320
+ };
321
+ if (body !== undefined) {
322
+ headers["Content-Type"] = "application/json";
323
+ }
324
+ return fetch(url, {
325
+ method,
326
+ headers,
327
+ body: body !== undefined ? JSON.stringify(body) : undefined
328
+ });
329
+ }
330
+ async runQuery(query, variables) {
331
+ const url = `${this.baseURL}/api/graphql`;
332
+ const body = JSON.stringify({ query, variables: variables ?? {} });
333
+ const res = await fetch(url, {
334
+ method: "POST",
335
+ headers: {
336
+ "Content-Type": "application/json",
337
+ Authorization: `Bearer ${this.token}`
338
+ },
339
+ body
340
+ });
341
+ if (!res.ok) {
342
+ throw new Error(`GraphQL request failed: ${res.status} ${res.statusText}`);
343
+ }
344
+ const envelope = await res.json();
345
+ if (envelope.errors?.length) {
346
+ const msg = envelope.errors.map((e) => e.message).join("; ");
347
+ throw new Error(`GraphQL errors: ${msg}`);
348
+ }
349
+ if (!envelope.data) {
350
+ throw new Error("GraphQL response missing data");
351
+ }
352
+ return envelope.data;
353
+ }
354
+ }
355
+ export {
356
+ parseGitLabRepoId,
357
+ MR_DASHBOARD_FRAGMENT,
358
+ GitLabProvider
359
+ };
@@ -0,0 +1,50 @@
1
+ import type { MRDetail, PullRequest, UserRef } from "./types.ts";
2
+ /**
3
+ * Provider-agnostic interface for a Git hosting service.
4
+ *
5
+ * `GitLabProvider` implements this today; `GitHubProvider` will follow.
6
+ * `Connection` stores a `GitProvider` and uses only this interface — it never
7
+ * reaches into provider-specific internals.
8
+ */
9
+ export interface GitProvider {
10
+ /** The provider slug stored in `connected_accounts.provider`. */
11
+ readonly providerName: string;
12
+ /** The base URL for this provider instance, e.g. "https://gitlab.com". */
13
+ readonly baseURL: string;
14
+ /** Validate the stored credentials and return the authenticated user. */
15
+ validateToken(): Promise<UserRef>;
16
+ /**
17
+ * Fetch all pull/merge requests the current user is involved in
18
+ * (authored, assigned, reviewing).
19
+ */
20
+ fetchPullRequests(): Promise<PullRequest[]>;
21
+ /**
22
+ * Fetch a single MR/PR by project path and IID.
23
+ * Returns null if the project or MR doesn't exist.
24
+ * Used by SubscriptionManager for real-time update handling.
25
+ */
26
+ fetchSingleMR(projectPath: string, mrIid: number, currentUserNumericId: number | null): Promise<PullRequest | null>;
27
+ /**
28
+ * Fetch discussions (comments, threads) for a specific MR/PR.
29
+ * Returns the MRDetail with discussions populated.
30
+ */
31
+ fetchMRDiscussions(repositoryId: string, mrIid: number): Promise<MRDetail>;
32
+ /**
33
+ * Make an authenticated REST API request to the provider.
34
+ * Used for operations that don't have a typed method yet (job traces,
35
+ * pipeline retries, etc.).
36
+ *
37
+ * Implementations translate the path to the provider's API URL format.
38
+ */
39
+ restRequest(method: string, path: string, body?: unknown): Promise<Response>;
40
+ }
41
+ /**
42
+ * Parse the numeric project/repo ID from a scoped repositoryId string.
43
+ * e.g. "gitlab:42" → 42, "github:12345" → 12345
44
+ */
45
+ export declare function parseRepoId(repositoryId: string): number;
46
+ /**
47
+ * Extract the provider prefix from a scoped repositoryId.
48
+ * e.g. "gitlab:42" → "gitlab", "github:12345" → "github"
49
+ */
50
+ export declare function repoIdProvider(repositoryId: string): string;
@@ -0,0 +1,12 @@
1
+ // src/GitProvider.ts
2
+ function parseRepoId(repositoryId) {
3
+ const parts = repositoryId.split(":");
4
+ return parseInt(parts.at(-1) ?? "0", 10);
5
+ }
6
+ function repoIdProvider(repositoryId) {
7
+ return repositoryId.split(":")[0] ?? "unknown";
8
+ }
9
+ export {
10
+ repoIdProvider,
11
+ parseRepoId
12
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Fetches MR detail (discussions) from GitLab via the REST API.
3
+ *
4
+ * Accepts numeric projectId for REST URL construction; returns DomainMRDetail
5
+ * with a scoped repositoryId string (e.g. "gitlab:42").
6
+ * Endpoint: GET /api/v4/projects/:id/merge_requests/:iid/discussions?per_page=100
7
+ */
8
+ import type { MRDetail } from "./types.ts";
9
+ import { type ForgeLogger } from "./logger.ts";
10
+ export declare class MRDetailFetcher {
11
+ private readonly baseURL;
12
+ private readonly token;
13
+ private readonly log;
14
+ constructor(baseURL: string, token: string, options?: {
15
+ logger?: ForgeLogger;
16
+ });
17
+ fetchDetail(projectId: number, mrIid: number): Promise<MRDetail>;
18
+ }
@@ -0,0 +1,74 @@
1
+ // src/logger.ts
2
+ var noopLogger = {
3
+ debug() {},
4
+ info() {},
5
+ warn() {},
6
+ error() {}
7
+ };
8
+
9
+ // src/MRDetailFetcher.ts
10
+ class MRDetailFetcher {
11
+ baseURL;
12
+ token;
13
+ log;
14
+ constructor(baseURL, token, options = {}) {
15
+ this.baseURL = baseURL.replace(/\/$/, "");
16
+ this.token = token;
17
+ this.log = options.logger ?? noopLogger;
18
+ }
19
+ async fetchDetail(projectId, mrIid) {
20
+ const url = `${this.baseURL}/api/v4/projects/${projectId}/merge_requests/${mrIid}/discussions?per_page=100`;
21
+ const res = await fetch(url, {
22
+ headers: { "PRIVATE-TOKEN": this.token }
23
+ });
24
+ if (!res.ok) {
25
+ throw new Error(`MR discussions fetch failed: ${res.status} ${res.statusText}`);
26
+ }
27
+ const raw = await res.json();
28
+ const discussions = raw.map((d) => ({
29
+ id: d.id,
30
+ resolvable: null,
31
+ resolved: null,
32
+ notes: d.notes.map(toNote)
33
+ }));
34
+ this.log.debug("MRDetailFetcher.fetchDetail", {
35
+ projectId,
36
+ mrIid,
37
+ discussionCount: discussions.length
38
+ });
39
+ return { mrIid, repositoryId: `gitlab:${projectId}`, discussions };
40
+ }
41
+ }
42
+ function toNote(n) {
43
+ return {
44
+ id: n.id,
45
+ body: n.body,
46
+ author: toAuthor(n.author),
47
+ createdAt: n.created_at,
48
+ system: n.system,
49
+ type: n.type,
50
+ resolvable: n.resolvable ?? null,
51
+ resolved: n.resolved ?? null,
52
+ position: n.position ? toPosition(n.position) : null
53
+ };
54
+ }
55
+ function toAuthor(a) {
56
+ return {
57
+ id: `gitlab:user:${a.id}`,
58
+ username: a.username,
59
+ name: a.name,
60
+ avatarUrl: a.avatar_url
61
+ };
62
+ }
63
+ function toPosition(p) {
64
+ return {
65
+ newPath: p.new_path ?? null,
66
+ oldPath: p.old_path ?? null,
67
+ newLine: p.new_line ?? null,
68
+ oldLine: p.old_line ?? null,
69
+ positionType: p.position_type ?? null
70
+ };
71
+ }
72
+ export {
73
+ MRDetailFetcher
74
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * REST API helpers for GitLab note mutations.
3
+ *
4
+ * Uses numeric projectId + mrIid so callers don't need the project full path.
5
+ * Mirrors the GitLab REST API:
6
+ * POST /api/v4/projects/:id/merge_requests/:mrIid/notes
7
+ * POST /api/v4/projects/:id/merge_requests/:mrIid/discussions/:discussionId/notes
8
+ * PUT /api/v4/projects/:id/merge_requests/:mrIid/notes/:noteId
9
+ * DELETE /api/v4/projects/:id/merge_requests/:mrIid/notes/:noteId
10
+ */
11
+ export interface CreatedNote {
12
+ id: number;
13
+ body: string;
14
+ author: {
15
+ id: number;
16
+ username: string;
17
+ name: string;
18
+ avatar_url: string | null;
19
+ };
20
+ created_at: string;
21
+ resolvable: boolean | null;
22
+ resolved: boolean | null;
23
+ }
24
+ export declare class NoteMutator {
25
+ private readonly baseURL;
26
+ private readonly token;
27
+ constructor(baseURL: string, token: string);
28
+ /**
29
+ * Create a note on an MR, optionally within an existing discussion thread.
30
+ * If `discussionId` is provided the note is posted as a reply to that thread.
31
+ */
32
+ createNote(projectId: number, mrIid: number, body: string, discussionId?: string): Promise<CreatedNote>;
33
+ /** Edit the body of an existing note. */
34
+ updateNote(projectId: number, mrIid: number, noteId: number, body: string): Promise<void>;
35
+ /** Permanently delete a note. */
36
+ deleteNote(projectId: number, mrIid: number, noteId: number): Promise<void>;
37
+ }
@@ -0,0 +1,54 @@
1
+ // src/NoteMutator.ts
2
+ class NoteMutator {
3
+ baseURL;
4
+ token;
5
+ constructor(baseURL, token) {
6
+ this.baseURL = baseURL.replace(/\/$/, "");
7
+ this.token = token;
8
+ }
9
+ async createNote(projectId, mrIid, body, discussionId) {
10
+ const url = discussionId ? `${this.baseURL}/api/v4/projects/${projectId}/merge_requests/${mrIid}/discussions/${discussionId}/notes` : `${this.baseURL}/api/v4/projects/${projectId}/merge_requests/${mrIid}/notes`;
11
+ const res = await fetch(url, {
12
+ method: "POST",
13
+ headers: {
14
+ "Content-Type": "application/json",
15
+ "PRIVATE-TOKEN": this.token
16
+ },
17
+ body: JSON.stringify({ body })
18
+ });
19
+ if (!res.ok) {
20
+ const text = await res.text().catch(() => "");
21
+ throw new Error(`createNote failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
22
+ }
23
+ return await res.json();
24
+ }
25
+ async updateNote(projectId, mrIid, noteId, body) {
26
+ const url = `${this.baseURL}/api/v4/projects/${projectId}/merge_requests/${mrIid}/notes/${noteId}`;
27
+ const res = await fetch(url, {
28
+ method: "PUT",
29
+ headers: {
30
+ "Content-Type": "application/json",
31
+ "PRIVATE-TOKEN": this.token
32
+ },
33
+ body: JSON.stringify({ body })
34
+ });
35
+ if (!res.ok) {
36
+ const text = await res.text().catch(() => "");
37
+ throw new Error(`updateNote failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
38
+ }
39
+ }
40
+ async deleteNote(projectId, mrIid, noteId) {
41
+ const url = `${this.baseURL}/api/v4/projects/${projectId}/merge_requests/${mrIid}/notes/${noteId}`;
42
+ const res = await fetch(url, {
43
+ method: "DELETE",
44
+ headers: { "PRIVATE-TOKEN": this.token }
45
+ });
46
+ if (!res.ok) {
47
+ const text = await res.text().catch(() => "");
48
+ throw new Error(`deleteNote failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`);
49
+ }
50
+ }
51
+ }
52
+ export {
53
+ NoteMutator
54
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @forge-glance/sdk — GitHub & GitLab API client.
3
+ *
4
+ * Provides provider-agnostic types, REST/GraphQL clients, and real-time
5
+ * ActionCable subscriptions for GitLab. Designed for use in any Node/Bun
6
+ * runtime.
7
+ *
8
+ * @example
9
+ * import { GitLabProvider, ActionCableClient, type PullRequest } from '@forge-glance/sdk';
10
+ *
11
+ * const provider = new GitLabProvider('https://gitlab.com', token, { logger: console });
12
+ * const prs = await provider.fetchPullRequests();
13
+ */
14
+ export type { PullRequest, PullRequestsSnapshot, Pipeline, PipelineJob, UserRef, DiffStats, Discussion, Note, NoteAuthor, NotePosition, MRDetail, FeedEvent, FeedSnapshot, ServerNotification, } from "./types.ts";
15
+ export type { GitProvider } from "./GitProvider.ts";
16
+ export { parseRepoId, repoIdProvider } from "./GitProvider.ts";
17
+ export type { ForgeLogger } from "./logger.ts";
18
+ export { noopLogger } from "./logger.ts";
19
+ export { GitLabProvider, parseGitLabRepoId, MR_DASHBOARD_FRAGMENT } from "./GitLabProvider.ts";
20
+ export { GitHubProvider } from "./GitHubProvider.ts";
21
+ export { createProvider, SUPPORTED_PROVIDERS } from "./providers.ts";
22
+ export type { ProviderSlug } from "./providers.ts";
23
+ export { ActionCableClient } from "./ActionCableClient.ts";
24
+ export type { ActionCableCallbacks } from "./ActionCableClient.ts";
25
+ export { MRDetailFetcher } from "./MRDetailFetcher.ts";
26
+ export { NoteMutator } from "./NoteMutator.ts";
27
+ export type { CreatedNote } from "./NoteMutator.ts";