// GET /api/themes (stream S4, PLAN C5): the installed, valid colour themes, found by listing // packages/plugins/theme/ on each request, so removing a module's directory takes its theme // out of the answer at once and putting it back returns it. The answer is data the Settings // picker and the page's theme code use: { themes: [{ id, displayName, tokens }] } with tokens // { light, dim, dark } of #rrggbb values, checked by plugins/src/theme.mjs. A module that is // refused is not in the answer; the refusal goes to the Console's log, once per distinct reason // so a poll does not repeat it. Nothing here reads a request body, a header or a query. import { installedThemes } from '../../plugins/src/theme.mjs'; const json = (res, status, value) => { res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(value)); }; export async function themesHTTP(req, res, { log = () => {}, list = installedThemes, seen = themesHTTP.seen } = {}) { if (req.method !== 'GET') return json(res, 405, { error: 'method not allowed' }); try { const { themes } = await list({ log: entry => { const key = `${entry.name}\0${entry.code}\0${entry.message}`; if (seen.has(key)) return; seen.add(key); log(`${new Date().toISOString()} theme refused name=${JSON.stringify(entry.name)} code=${JSON.stringify(entry.code)} message=${JSON.stringify(entry.message)}`); }, }); return json(res, 200, { themes: themes.map(({ id, displayName, tokens }) => ({ id, displayName, tokens })) }); } catch (err) { log(`${new Date().toISOString()} theme list failed error=${JSON.stringify(String(err?.message ?? err).slice(0, 200))}`); return json(res, 500, { error: 'themes-unavailable', message: 'the installed themes could not be read' }); } } // What this process has already logged; a test passes its own set. themesHTTP.seen = new Set();