Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

client.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

client.tsBlame306 lines · 1 contributor
c53cf00Claude1export interface User {
2 id: string;
3 username: string;
4 email: string;
5 displayName: string | null;
6 bio: string | null;
7 avatarUrl: string | null;
8 createdAt: string;
9}
10
11export interface Repository {
12 id: string;
13 name: string;
14 description: string | null;
15 isPrivate: boolean;
16 starCount: number;
17 forkCount: number;
18 issueCount: number;
19 defaultBranch: string;
20 language: string | null;
21 updatedAt: string;
22 createdAt: string;
23 ownerId: string;
24}
25
26export interface Commit {
27 sha: string;
28 message: string;
29 author: {
30 name: string;
31 email: string;
32 date: string;
33 };
34 committer: {
35 name: string;
36 email: string;
37 date: string;
38 };
39}
40
41export interface TreeEntry {
42 name: string;
43 path: string;
44 type: 'blob' | 'tree';
45 sha: string;
46 size?: number;
47 mode: string;
48}
49
50export interface FileContent {
51 name: string;
52 path: string;
53 sha: string;
54 size: number;
55 content: string;
56 encoding: string;
57 type: string;
58}
59
60export interface Issue {
61 id: string;
62 number: number;
63 title: string;
64 body: string | null;
65 state: 'open' | 'closed';
66 authorId: string;
67 createdAt: string;
68 updatedAt: string;
69 closedAt: string | null;
70 commentCount?: number;
71 labels?: Label[];
72}
73
74export interface IssueComment {
75 id: string;
76 body: string;
77 authorId: string;
78 createdAt: string;
79 updatedAt: string;
80 author?: { username: string; displayName: string | null };
81}
82
83export interface Label {
84 id: string;
85 name: string;
86 color: string;
87 description: string | null;
88}
89
90export interface PullRequest {
91 id: string;
92 number: number;
93 title: string;
94 body: string | null;
95 state: 'open' | 'closed' | 'merged';
96 baseBranch: string;
97 headBranch: string;
98 authorId: string;
99 createdAt: string;
100 updatedAt: string;
101 mergedAt: string | null;
102 closedAt: string | null;
103}
104
105export interface PullComment {
106 id: string;
107 body: string;
108 authorId: string;
109 filePath: string | null;
110 line: number | null;
111 createdAt: string;
112 isAiReview: boolean;
113 author?: { username: string; displayName: string | null };
114}
115
116export interface ActivityEvent {
117 id: string;
118 type: string;
119 payload: Record<string, unknown>;
120 createdAt: string;
121 repositoryId: string;
122}
123
124export interface Branch {
125 name: string;
126 sha: string;
127 isDefault: boolean;
128}
129
130export interface LoginResponse {
131 user: { id: string; username: string; email: string };
132 token: string;
133}
134
135class ApiError extends Error {
136 constructor(
137 public status: number,
138 public statusText: string,
139 public body?: unknown,
140 ) {
141 super(`${status} ${statusText}`);
142 this.name = 'ApiError';
143 }
144}
145
146let _baseUrl = 'https://gluecron.com';
147
148export function setBaseUrl(url: string) {
149 _baseUrl = url.replace(/\/$/, '');
150}
151
152export function getBaseUrl() {
153 return _baseUrl;
154}
155
156class GluecronClient {
157 private token: string | null = null;
158
159 setToken(token: string) {
160 this.token = token;
161 }
162
163 clearToken() {
164 this.token = null;
165 }
166
167 getToken() {
168 return this.token;
169 }
170
171 private async request<T>(path: string, options?: RequestInit): Promise<T> {
172 const url = `${_baseUrl}${path}`;
173 const headers: Record<string, string> = {
174 'Content-Type': 'application/json',
175 };
176 if (this.token) {
177 headers['Authorization'] = `Bearer ${this.token}`;
178 }
179 if (options?.headers) {
180 Object.assign(headers, options.headers);
181 }
182
183 const res = await fetch(url, {
184 ...options,
185 headers,
186 });
187
188 if (!res.ok) {
189 let body: unknown;
190 try {
191 body = await res.json();
192 } catch {
193 body = undefined;
194 }
195 throw new ApiError(res.status, res.statusText, body);
196 }
197
198 return res.json() as Promise<T>;
199 }
200
201 // ── Auth ────────────────────────────────────────────────────────────────────
202
203 async login(username: string, password: string): Promise<LoginResponse> {
204 return this.request<LoginResponse>('/api/auth/login', {
205 method: 'POST',
206 body: JSON.stringify({ username, password }),
207 });
208 }
209
210 async getMe(): Promise<User> {
211 return this.request<User>('/api/v2/user');
212 }
213
214 // ── Repos ───────────────────────────────────────────────────────────────────
215
216 async listUserRepos(username: string, sort = 'updated'): Promise<Repository[]> {
217 return this.request<Repository[]>(`/api/v2/users/${encodeURIComponent(username)}/repos?sort=${sort}`);
218 }
219
220 async getRepo(owner: string, repo: string): Promise<Repository> {
221 return this.request<Repository>(`/api/v2/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`);
222 }
223
224 async listBranches(owner: string, repo: string): Promise<Branch[]> {
225 return this.request<Branch[]>(`/api/v2/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branches`);
226 }
227
228 async listCommits(owner: string, repo: string, branch?: string, page = 1, limit = 30): Promise<Commit[]> {
229 const params = new URLSearchParams({ page: String(page), limit: String(limit) });
230 if (branch) params.set('branch', branch);
231 return this.request<Commit[]>(`/api/v2/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits?${params}`);
232 }
233
234 async getFileTree(owner: string, repo: string, ref = 'HEAD', path?: string): Promise<TreeEntry[]> {
235 const params = new URLSearchParams();
236 if (path) params.set('path', path);
237 return this.request<TreeEntry[]>(`/api/v2/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/tree/${encodeURIComponent(ref)}?${params}`);
238 }
239
240 async getFileContent(owner: string, repo: string, filePath: string, ref = 'HEAD'): Promise<FileContent> {
241 return this.request<FileContent>(`/api/v2/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${filePath}?ref=${encodeURIComponent(ref)}`);
242 }
243
244 async getRepoActivity(owner: string, repo: string, limit = 30): Promise<ActivityEvent[]> {
245 return this.request<ActivityEvent[]>(`/api/v2/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/activity?limit=${limit}`);
246 }
247
248 // ── Issues ──────────────────────────────────────────────────────────────────
249
250 async listIssues(owner: string, repo: string, state: 'open' | 'closed' | 'all' = 'open', page = 1, limit = 30): Promise<Issue[]> {
251 const params = new URLSearchParams({ state, page: String(page), limit: String(limit) });
252 return this.request<Issue[]>(`/api/v2/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues?${params}`);
253 }
254
255 async getIssue(owner: string, repo: string, number: number): Promise<{ issue: Issue; comments: IssueComment[] }> {
256 return this.request<{ issue: Issue; comments: IssueComment[] }>(`/api/v2/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${number}`);
257 }
258
259 async createIssue(owner: string, repo: string, title: string, body: string): Promise<Issue> {
260 return this.request<Issue>(`/api/v2/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues`, {
261 method: 'POST',
262 body: JSON.stringify({ title, body }),
263 });
264 }
265
266 async createIssueComment(owner: string, repo: string, number: number, body: string): Promise<IssueComment> {
267 return this.request<IssueComment>(`/api/v2/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${number}/comments`, {
268 method: 'POST',
269 body: JSON.stringify({ body }),
270 });
271 }
272
273 async updateIssueState(owner: string, repo: string, number: number, state: 'open' | 'closed'): Promise<Issue> {
274 return this.request<Issue>(`/api/v2/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues/${number}`, {
275 method: 'PATCH',
276 body: JSON.stringify({ state }),
277 });
278 }
279
280 // ── Pull Requests ────────────────────────────────────────────────────────────
281
282 async listPulls(owner: string, repo: string, state: 'open' | 'closed' | 'merged' | 'all' = 'open', page = 1, limit = 30): Promise<PullRequest[]> {
283 const params = new URLSearchParams({ state, page: String(page), limit: String(limit) });
284 return this.request<PullRequest[]>(`/api/v2/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls?${params}`);
285 }
286
287 async getPull(owner: string, repo: string, number: number): Promise<{ pull: PullRequest; comments: PullComment[] }> {
288 return this.request<{ pull: PullRequest; comments: PullComment[] }>(`/api/v2/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${number}`);
289 }
290
291 async createPull(owner: string, repo: string, title: string, body: string, head: string, base: string): Promise<PullRequest> {
292 return this.request<PullRequest>(`/api/v2/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls`, {
293 method: 'POST',
294 body: JSON.stringify({ title, body, head, base }),
295 });
296 }
297
298 // ── User repos shorthand ────────────────────────────────────────────────────
299
300 async listMyRepos(username: string): Promise<Repository[]> {
301 return this.listUserRepos(username, 'updated');
302 }
303}
304
305export const api = new GluecronClient();
306export { ApiError };