/** * ───────────────────────────────────────────── * SPB Git — Personal Git Platform * ───────────────────────────────────────────── * Author : Simon-Pierre Boucher * Contact : contact@spboucher.ai * File : cli/lib/api.mjs * Purpose : SPB Git API client (native fetch, friendly errors) * License : MIT © Simon-Pierre Boucher * ───────────────────────────────────────────── */ import pc from 'picocolors'; /** Exit codes per CLAUDE.md §6.3. */ export const EXIT = Object.freeze({ OK: 0, USER: 1, NETWORK: 2, PARTIAL: 3 }); /** * Perform an API request against the configured server. * @param {object} config CLI config * @param {string} method * @param {string} path e.g. `/api/v1/repos` * @param {object} [body] * @param {{auth?: boolean}} [opts] * @returns {Promise} parsed JSON */ export async function api(config, method, path, body, opts = {}) { const headers = { Accept: 'application/json' }; if (opts.auth !== false && config.token) headers.Authorization = `Bearer ${config.token}`; if (body !== undefined) headers['Content-Type'] = 'application/json'; let response; try { response = await fetch(`${config.server.replace(/\/+$/, '')}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), }); } catch (err) { console.error(pc.red(`✗ Cannot reach ${config.server} — ${err.cause?.code ?? err.message}`)); console.error(' Check the server URL in ~/.spbgit/config.json or run: spbgit doctor'); process.exit(EXIT.NETWORK); } const text = await response.text(); let json = null; try { json = text ? JSON.parse(text) : null; } catch { /* non-JSON error body */ } if (!response.ok) { if (response.status === 401) { console.error(pc.red('✗ Token invalid or revoked.')); console.error(' Run `spbgit token new` on a machine with a valid token, then `spbgit init`.'); process.exit(EXIT.NETWORK); } const message = json?.error?.message ?? `HTTP ${response.status}`; const error = new Error(message); error.status = response.status; error.code = json?.error?.code; throw error; } return json; }