CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
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.
| c53cf00 | 1 | export 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 | ||
| 11 | export 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 | ||
| 26 | export 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 | ||
| 41 | export interface TreeEntry { | |
| 42 | name: string; | |
| 43 | path: string; | |
| 44 | type: 'blob' | 'tree'; | |
| 45 | sha: string; | |
| 46 | size?: number; | |
| 47 | mode: string; | |
| 48 | } | |
| 49 | ||
| 50 | export 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 | ||
| 60 | export 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 | ||
| 74 | export 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 | ||
| 83 | export interface Label { | |
| 84 | id: string; | |
| 85 | name: string; | |
| 86 | color: string; | |
| 87 | description: string | null; | |
| 88 | } | |
| 89 | ||
| 90 | export 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 | ||
| 105 | export 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 | ||
| 116 | export interface ActivityEvent { | |
| 117 | id: string; | |
| 118 | type: string; | |
| 119 | payload: Record<string, unknown>; | |
| 120 | createdAt: string; | |
| 121 | repositoryId: string; | |
| 122 | } | |
| 123 | ||
| 124 | export interface Branch { | |
| 125 | name: string; | |
| 126 | sha: string; | |
| 127 | isDefault: boolean; | |
| 128 | } | |
| 129 | ||
| 130 | export interface LoginResponse { | |
| 131 | user: { id: string; username: string; email: string }; | |
| 132 | token: string; | |
| 133 | } | |
| 134 | ||
| 135 | class 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 | ||
| 146 | let _baseUrl = 'https://gluecron.com'; | |
| 147 | ||
| 148 | export function setBaseUrl(url: string) { | |
| 149 | _baseUrl = url.replace(/\/$/, ''); | |
| 150 | } | |
| 151 | ||
| 152 | export function getBaseUrl() { | |
| 153 | return _baseUrl; | |
| 154 | } | |
| 155 | ||
| 156 | class 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 | ||
| 305 | export const api = new GluecronClient(); | |
| 306 | export { ApiError }; |