@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,108 @@
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
+
12
+ export interface CreatedNote {
13
+ id: number;
14
+ body: string;
15
+ author: {
16
+ id: number;
17
+ username: string;
18
+ name: string;
19
+ avatar_url: string | null;
20
+ };
21
+ created_at: string;
22
+ resolvable: boolean | null;
23
+ resolved: boolean | null;
24
+ }
25
+
26
+ export class NoteMutator {
27
+ private readonly baseURL: string;
28
+ private readonly token: string;
29
+
30
+ constructor(baseURL: string, token: string) {
31
+ this.baseURL = baseURL.replace(/\/$/, "");
32
+ this.token = token;
33
+ }
34
+
35
+ /**
36
+ * Create a note on an MR, optionally within an existing discussion thread.
37
+ * If `discussionId` is provided the note is posted as a reply to that thread.
38
+ */
39
+ async createNote(
40
+ projectId: number,
41
+ mrIid: number,
42
+ body: string,
43
+ discussionId?: string,
44
+ ): Promise<CreatedNote> {
45
+ const url = discussionId
46
+ ? `${this.baseURL}/api/v4/projects/${projectId}/merge_requests/${mrIid}/discussions/${discussionId}/notes`
47
+ : `${this.baseURL}/api/v4/projects/${projectId}/merge_requests/${mrIid}/notes`;
48
+
49
+ const res = await fetch(url, {
50
+ method: "POST",
51
+ headers: {
52
+ "Content-Type": "application/json",
53
+ "PRIVATE-TOKEN": this.token,
54
+ },
55
+ body: JSON.stringify({ body }),
56
+ });
57
+
58
+ if (!res.ok) {
59
+ const text = await res.text().catch(() => "");
60
+ throw new Error(
61
+ `createNote failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`,
62
+ );
63
+ }
64
+
65
+ return (await res.json()) as CreatedNote;
66
+ }
67
+
68
+ /** Edit the body of an existing note. */
69
+ async updateNote(
70
+ projectId: number,
71
+ mrIid: number,
72
+ noteId: number,
73
+ body: string,
74
+ ): Promise<void> {
75
+ const url = `${this.baseURL}/api/v4/projects/${projectId}/merge_requests/${mrIid}/notes/${noteId}`;
76
+ const res = await fetch(url, {
77
+ method: "PUT",
78
+ headers: {
79
+ "Content-Type": "application/json",
80
+ "PRIVATE-TOKEN": this.token,
81
+ },
82
+ body: JSON.stringify({ body }),
83
+ });
84
+
85
+ if (!res.ok) {
86
+ const text = await res.text().catch(() => "");
87
+ throw new Error(
88
+ `updateNote failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`,
89
+ );
90
+ }
91
+ }
92
+
93
+ /** Permanently delete a note. */
94
+ async deleteNote(projectId: number, mrIid: number, noteId: number): Promise<void> {
95
+ const url = `${this.baseURL}/api/v4/projects/${projectId}/merge_requests/${mrIid}/notes/${noteId}`;
96
+ const res = await fetch(url, {
97
+ method: "DELETE",
98
+ headers: { "PRIVATE-TOKEN": this.token },
99
+ });
100
+
101
+ if (!res.ok) {
102
+ const text = await res.text().catch(() => "");
103
+ throw new Error(
104
+ `deleteNote failed: ${res.status} ${res.statusText}${text ? ` — ${text}` : ""}`,
105
+ );
106
+ }
107
+ }
108
+ }
package/src/index.ts ADDED
@@ -0,0 +1,54 @@
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
+
15
+ // ── Domain types ──────────────────────────────────────────────────────────────
16
+ export type {
17
+ PullRequest,
18
+ PullRequestsSnapshot,
19
+ Pipeline,
20
+ PipelineJob,
21
+ UserRef,
22
+ DiffStats,
23
+ Discussion,
24
+ Note,
25
+ NoteAuthor,
26
+ NotePosition,
27
+ MRDetail,
28
+ FeedEvent,
29
+ FeedSnapshot,
30
+ ServerNotification,
31
+ } from "./types.ts";
32
+
33
+ // ── Provider interface ────────────────────────────────────────────────────────
34
+ export type { GitProvider } from "./GitProvider.ts";
35
+ export { parseRepoId, repoIdProvider } from "./GitProvider.ts";
36
+
37
+ // ── Logger ────────────────────────────────────────────────────────────────────
38
+ export type { ForgeLogger } from "./logger.ts";
39
+ export { noopLogger } from "./logger.ts";
40
+
41
+ // ── Providers ─────────────────────────────────────────────────────────────────
42
+ export { GitLabProvider, parseGitLabRepoId, MR_DASHBOARD_FRAGMENT } from "./GitLabProvider.ts";
43
+ export { GitHubProvider } from "./GitHubProvider.ts";
44
+ export { createProvider, SUPPORTED_PROVIDERS } from "./providers.ts";
45
+ export type { ProviderSlug } from "./providers.ts";
46
+
47
+ // ── GitLab real-time ──────────────────────────────────────────────────────────
48
+ export { ActionCableClient } from "./ActionCableClient.ts";
49
+ export type { ActionCableCallbacks } from "./ActionCableClient.ts";
50
+
51
+ // ── GitLab detail + mutations ─────────────────────────────────────────────────
52
+ export { MRDetailFetcher } from "./MRDetailFetcher.ts";
53
+ export { NoteMutator } from "./NoteMutator.ts";
54
+ export type { CreatedNote } from "./NoteMutator.ts";
package/src/logger.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Logger interface for @forge-glance/sdk.
3
+ *
4
+ * All providers and clients accept an optional `logger` in their constructor
5
+ * that must satisfy this interface. Defaults to `noopLogger` when omitted,
6
+ * so the SDK is silent out of the box.
7
+ *
8
+ * Consumers can pass any compatible logger — pino, winston, console, etc.
9
+ *
10
+ * @example
11
+ * import pino from 'pino';
12
+ * const provider = new GitLabProvider(url, token, { logger: pino() });
13
+ */
14
+ export interface ForgeLogger {
15
+ debug(msg: string, meta?: Record<string, unknown>): void;
16
+ info(msg: string, meta?: Record<string, unknown>): void;
17
+ warn(msg: string, meta?: Record<string, unknown>): void;
18
+ error(msg: string, meta?: Record<string, unknown>): void;
19
+ }
20
+
21
+ export const noopLogger: ForgeLogger = {
22
+ debug() {},
23
+ info() {},
24
+ warn() {},
25
+ error() {},
26
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Provider factory (Phase C1).
3
+ *
4
+ * Creates the correct GitProvider implementation based on the provider slug
5
+ * from `connected_accounts.provider`.
6
+ */
7
+
8
+ import type { GitProvider } from "./GitProvider.ts";
9
+ import { GitLabProvider } from "./GitLabProvider.ts";
10
+ import { GitHubProvider } from "./GitHubProvider.ts";
11
+ import type { ForgeLogger } from "./logger.ts";
12
+
13
+ /**
14
+ * Create a GitProvider for the given provider slug.
15
+ *
16
+ * @param provider - The provider slug from the DB, e.g. "gitlab" or "github".
17
+ * @param baseURL - The base URL for the provider instance.
18
+ * @param token - The decrypted PAT for the provider.
19
+ * @returns A GitProvider implementation.
20
+ * @throws If the provider slug is unknown.
21
+ */
22
+ export function createProvider(
23
+ provider: string,
24
+ baseURL: string,
25
+ token: string,
26
+ options: { logger?: ForgeLogger } = {},
27
+ ): GitProvider {
28
+ switch (provider) {
29
+ case "gitlab":
30
+ return new GitLabProvider(baseURL, token, options);
31
+ case "github":
32
+ return new GitHubProvider(baseURL, token, options);
33
+ default:
34
+ throw new Error(`Unknown provider: ${provider}`);
35
+ }
36
+ }
37
+
38
+ /** List of all supported provider slugs. */
39
+ export const SUPPORTED_PROVIDERS = ["gitlab", "github"] as const;
40
+ export type ProviderSlug = (typeof SUPPORTED_PROVIDERS)[number];
package/src/types.ts ADDED
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Provider-agnostic domain types.
3
+ *
4
+ * These are the canonical types sent to Swift clients via the WebSocket protocol.
5
+ * No raw provider payloads leave the provider layer — only these types.
6
+ *
7
+ * Field names match the Swift DomainXxx structs in Sources/GlanceLib/Models/Domain/.
8
+ */
9
+
10
+ export interface UserRef {
11
+ /** Scoped provider ID, e.g. "gitlab:12345". */
12
+ id: string;
13
+ username: string;
14
+ name: string;
15
+ avatarUrl: string | null;
16
+ }
17
+
18
+ export interface PipelineJob {
19
+ /** Scoped provider ID, e.g. "gitlab:12345". */
20
+ id: string;
21
+ name: string;
22
+ stage: string;
23
+ /** Normalized status: "success" | "failed" | "running" | "pending" | "canceled" | "skipped" | "manual" | etc. */
24
+ status: string;
25
+ allowFailure: boolean;
26
+ webUrl: string | null;
27
+ }
28
+
29
+ export interface Pipeline {
30
+ /** Scoped provider ID, e.g. "gitlab:pipeline:12345". */
31
+ id: string;
32
+ /** Normalized status. */
33
+ status: string;
34
+ createdAt: string | null;
35
+ webUrl: string | null;
36
+ jobs: PipelineJob[];
37
+ }
38
+
39
+ export interface DiffStats {
40
+ additions: number;
41
+ deletions: number;
42
+ filesChanged: number;
43
+ }
44
+
45
+ export interface PullRequest {
46
+ /** Scoped provider ID, e.g. "gitlab:12345". */
47
+ id: string;
48
+ /** Numeric MR/PR number within the project. */
49
+ iid: number;
50
+ /** Scoped repository ID, e.g. "gitlab:42". */
51
+ repositoryId: string;
52
+ title: string;
53
+ /** Full MR description body. Used by AI summarization and the MR header. */
54
+ description: string | null;
55
+ /** "opened" | "merged" | "closed" */
56
+ state: string;
57
+ draft: boolean;
58
+ conflicts: boolean;
59
+ webUrl: string | null;
60
+ sourceBranch: string;
61
+ targetBranch: string;
62
+ createdAt: string | null;
63
+ updatedAt: string | null;
64
+ /** Head commit SHA. */
65
+ sha: string | null;
66
+
67
+ author: UserRef;
68
+ assignees: UserRef[];
69
+ reviewers: UserRef[];
70
+ /** Roles the current authenticated user has on this MR. Values: "author" | "reviewer" | "assignee". */
71
+ roles: string[];
72
+
73
+ pipeline: Pipeline | null;
74
+ unresolvedThreadCount: number;
75
+ approvalsLeft: number;
76
+ /** True when the MR has met its approval requirements. */
77
+ approved: boolean;
78
+ approvedBy: UserRef[];
79
+ diffStats: DiffStats | null;
80
+ /**
81
+ * Raw GitLab detailedMergeStatus value, e.g. "mergeable", "conflict",
82
+ * "not_approved", "discussions_not_resolved". Null for non-GitLab providers.
83
+ */
84
+ detailedMergeStatus: string | null;
85
+ }
86
+
87
+ /** Snapshot payload sent when a client first connects. */
88
+ export interface PullRequestsSnapshot {
89
+ items: PullRequest[];
90
+ }
91
+
92
+ /** Feed event emitted as `feed_event` (incremental) or inside `feed_snapshot` (initial batch). */
93
+ export interface FeedEvent {
94
+ /** Stable event ID, e.g. "note-1234" or "projEvent-5678". */
95
+ id: string;
96
+ /** MR IID within the project. */
97
+ mrIid: number;
98
+ /** Scoped repository ID, e.g. "gitlab:42". */
99
+ repositoryId: string;
100
+ /** Actor's GitLab username. */
101
+ actor: string;
102
+ actorAvatarUrl: string | null;
103
+ body: string | null;
104
+ /** ISO 8601 timestamp. */
105
+ createdAt: string;
106
+ /**
107
+ * View scope the event was fetched under.
108
+ * "authored_by_me" | "reviewing" | "assigned_to_me" | "any"
109
+ */
110
+ scope: string;
111
+ /** Normalized action: "commented" | "approved" | "merged" | "closed" | etc. */
112
+ action: string;
113
+ /** Normalized target type, e.g. "merge_request". */
114
+ targetType: string;
115
+ /** Note ID when the event originates from a note; null for action-only events. */
116
+ noteId: number | null;
117
+ /** MR title — the primary title shown in the Swift feed row. */
118
+ title: string;
119
+ /** Short human-readable line, e.g. "username commented". */
120
+ subtitle: string;
121
+ /** MR web URL for deep-linking. */
122
+ webUrl: string | null;
123
+ /**
124
+ * True for events within the initial history window (not new since last poll).
125
+ * Swift uses this to set event state to `.read` on insertion.
126
+ */
127
+ isHistorical: boolean;
128
+ }
129
+
130
+ /** Payload for a `feed_snapshot` event sent on first connect. */
131
+ export interface FeedSnapshot {
132
+ items: FeedEvent[];
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // D3: MR Detail types (discussions, notes, positions)
137
+ // ---------------------------------------------------------------------------
138
+
139
+ export interface NoteAuthor {
140
+ /** Scoped provider ID, e.g. "gitlab:user:12345". */
141
+ id: string;
142
+ username: string;
143
+ name: string;
144
+ avatarUrl: string | null;
145
+ }
146
+
147
+ export interface NotePosition {
148
+ newPath: string | null;
149
+ oldPath: string | null;
150
+ newLine: number | null;
151
+ oldLine: number | null;
152
+ positionType: string | null;
153
+ }
154
+
155
+ export interface Note {
156
+ id: number;
157
+ body: string;
158
+ author: NoteAuthor;
159
+ createdAt: string;
160
+ system: boolean;
161
+ /** "DiffNote" | "DiscussionNote" | null */
162
+ type: string | null;
163
+ resolvable: boolean | null;
164
+ resolved: boolean | null;
165
+ position: NotePosition | null;
166
+ }
167
+
168
+ export interface Discussion {
169
+ id: string;
170
+ resolvable: boolean | null;
171
+ resolved: boolean | null;
172
+ notes: Note[];
173
+ }
174
+
175
+ export interface MRDetail {
176
+ mrIid: number;
177
+ /** Scoped repository ID, e.g. "gitlab:42". */
178
+ repositoryId: string;
179
+ discussions: Discussion[];
180
+ }
181
+
182
+ /**
183
+ * Payload for a `notification` event emitted by the server to connected clients.
184
+ *
185
+ * Rules are intentionally simple for Phase A6 (prefs stubbed).
186
+ * When per-user preferences land (Phase B), the server will evaluate a proper
187
+ * rules engine before deciding whether to emit this event.
188
+ */
189
+ export interface ServerNotification {
190
+ title: string;
191
+ body: string;
192
+ /** Scoped PR id, e.g. "gitlab:mr:12345". Omitted for non-MR notifications. */
193
+ mrId?: string;
194
+ /** Deep-link URL to open when the notification is tapped. */
195
+ webUrl?: string | null;
196
+ }