/** * Forgejo API client. * * Endpoints used: * GET /api/v1/user -> verify credentials / obtain login name * POST /api/v1/users/{login}/tokens -> create a personal access token * DELETE /api/v1/users/{login}/tokens/{id} -> revoke a personal access token */ class ForgejoClient { constructor(baseUrl, token = null) { this.baseUrl = baseUrl.replace(/\/$/, ''); this.token = token; } static fromConfig(config) { if (!config || !config.url || !config.token) { throw new Error('Not authenticated. Run: forgejo auth login'); } return new ForgejoClient(config.url, config.token); } withBasicAuth(username, password) { const clone = new ForgejoClient(this.baseUrl, this.token); clone.basicAuth = Buffer.from(`${username}:${password}`).toString('base64'); return clone; } headers(extra = {}) { const h = { Accept: 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'forgejo-cli/1.0.0', ...extra, }; if (this.token) { h.Authorization = `token ${this.token}`; } else if (this.basicAuth) { h.Authorization = `Basic ${this.basicAuth}`; } return h; } async request(method, endpoint, body = null) { const url = `${this.baseUrl}/api/v1${endpoint}`; const opts = { method, headers: this.headers(), }; if (body !== null) { opts.body = JSON.stringify(body); } let res; try { res = await fetch(url, opts); } catch (err) { throw new Error(`Network error reaching ${this.baseUrl}: ${err.message}`); } const text = await res.text(); let data = null; if (text) { try { data = JSON.parse(text); } catch { data = { raw: text }; } } if (!res.ok) { const msg = data?.message || data?.raw || `HTTP ${res.status}`; const err = new Error(msg); err.status = res.status; err.body = data; throw err; } return data; } get(endpoint) { return this.request('GET', endpoint); } post(endpoint, body) { return this.request('POST', endpoint, body); } del(endpoint) { return this.request('DELETE', endpoint); } async verifyBasicAuth(username, password) { const client = this.withBasicAuth(username, password); return client.get('/user'); } async createToken(username, password, name, scopes) { const client = this.withBasicAuth(username, password); return client.post(`/users/${encodeURIComponent(username)}/tokens`, { name, scopes }); } async deleteToken(login, id) { return this.del(`/users/${encodeURIComponent(login)}/tokens/${id}`); } async createRepo(payload) { return this.post('/user/repos', payload); } async migrateRepo(payload) { return this.post('/repos/migrate', payload); } async getAll(endpoint, params = {}) { const pageSize = 50; const all = []; for (let page = 1; page <= 1000; page += 1) { const query = new URLSearchParams({ ...params, limit: String(pageSize), page: String(page) }).toString(); const items = await this.get(`${endpoint}?${query}`); if (!Array.isArray(items) || items.length === 0) break; all.push(...items); if (items.length < pageSize) break; } return all; } async listRepos(opts = {}) { return this.getAll('/user/repos', opts); } async listIssues(owner, repo, opts = {}) { const params = {}; if (opts.state) params.state = opts.state; if (opts.type) params.type = opts.type; return this.getAll(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues`, params); } async listPullRequests(owner, repo, opts = {}) { return this.getAll(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls`, opts); } async listBranches(owner, repo, opts = {}) { return this.getAll(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/branches`, opts); } } module.exports = { ForgejoClient };