2026-07-22 15:03:32 +00:00
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|
2026-07-22 15:06:35 +00:00
|
|
|
|
|
|
|
|
async addCollaborator(owner, repo, username, permission) {
|
|
|
|
|
return this.request('PUT', `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(username)}`, {
|
|
|
|
|
permission,
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-07-22 15:16:42 +00:00
|
|
|
|
|
|
|
|
async renameRepo(owner, repo, newName) {
|
|
|
|
|
return this.request('PATCH', `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`, {
|
|
|
|
|
name: newName,
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-07-22 17:47:48 +00:00
|
|
|
|
|
|
|
|
async transferRepo(owner, repo, newOwner) {
|
|
|
|
|
return this.post(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/transfer`, {
|
|
|
|
|
new_owner: newOwner,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async createOrg(payload) {
|
|
|
|
|
return this.post('/orgs', payload);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async listOrgRepos(org, opts = {}) {
|
|
|
|
|
return this.getAll(`/orgs/${encodeURIComponent(org)}/repos`, opts);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateOrgAvatar(org, base64Image) {
|
|
|
|
|
return this.post(`/orgs/${encodeURIComponent(org)}/avatar`, {
|
|
|
|
|
image: base64Image,
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-07-22 15:03:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = { ForgejoClient };
|