/home/techb158/cosmic-risk.abdallabala.com/src/integrations/pm-clients
NameSizeModeActions
asana-client.js32490644editdlrm
http-client.js24180644editdlrm
index.js22710644editdlrm
jira-client.js43610644editdlrm
planner-client.js41170644editdlrm
trello-client.js87900644editdlrm
Edit: /home/techb158/cosmic-risk.abdallabala.com/src/integrations/pm-clients/trello-client.js (8790B)
const { fetchJson, encodeQuery, buildRiskDescription } = require("./http-client"); const DIMENSION_COLORS = { Organizational: "yellow", Technical: "purple", Human: "blue", Governance: "red", Legal: "green", Ethical: "orange", Operational: "sky" }; const PHASE_COLORS = { Design: "pink", Development: "lime", Testing: "black", Deployment: "sky" }; const ALLOWED_COLORS = new Set(["yellow","purple","blue","red","green","orange","black","sky","pink","lime"]); function unusedColor(used) { for (const c of ALLOWED_COLORS) { if (!used.has(c)) return c; } return "green"; } class TrelloClient { constructor(options = {}) { this.apiKey = options.apiKey; this.token = options.token; this.listId = options.listId; this.fetchImpl = options.fetchImpl; this.statusListMap = options.statusListMap || {}; this._labelCache = null; if (!this.apiKey || !this.token) { throw new Error("Trello live sync requires apiKey and token credentials."); } } authQuery(extra = {}) { return encodeQuery(Object.assign({}, extra, { key: this.apiKey, token: this.token })); } async testConnection() { const member = await fetchJson(`https://api.trello.com/1/members/me?${this.authQuery({ fields: "id,username,fullName" })}`, {}, this.fetchImpl); return { ok: true, provider: "TRELLO", account: member.fullName || member.username || member.id }; } async _boardIdForList(listId) { if (!listId || !/^[a-f0-9]{24}$/i.test(listId)) { return null; } try { const list = await fetchJson(`https://api.trello.com/1/lists/${listId}?${this.authQuery({ fields: "idBoard" })}`, {}, this.fetchImpl); return list.idBoard; } catch (err) { return null; } } async _existingLabels(boardId) { return fetchJson(`https://api.trello.com/1/boards/${boardId}/labels?${this.authQuery({ fields: "id,name,color" })}`, {}, this.fetchImpl); } async _createLabel(boardId, name, color) { return fetchJson(`https://api.trello.com/1/boards/${boardId}/labels?${this.authQuery()}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, color }) }, this.fetchImpl); } async ensureLabels(boardId) { if (this._labelCache && this._labelCache.boardId === boardId) return this._labelCache; const existing = await this._existingLabels(boardId); const byName = {}; for (const label of existing) byName[label.name] = label; const desired = [ ...Object.keys(DIMENSION_COLORS).map(d => ({ name: `COSMIC:${d}`, color: DIMENSION_COLORS[d] })), ...Object.keys(PHASE_COLORS).map(p => ({ name: `COSMIC:Phase:${p}`, color: PHASE_COLORS[p] })) ]; const labelMap = {}; const usedColors = new Set(existing.map(l => l.color).filter(Boolean)); for (const item of desired) { if (byName[item.name]) { labelMap[item.name] = byName[item.name].id; } else { const color = ALLOWED_COLORS.has(item.color) && !usedColors.has(item.color) ? item.color : unusedColor(usedColors); const created = await this._createLabel(boardId, item.name, color).catch(() => null); if (created) { usedColors.add(created.color); labelMap[item.name] = created.id; } } } this._labelCache = { boardId, labelMap }; return this._labelCache; } async _resolveListId(risk, integration) { const override = integration.liveConfig?.statusListMap; const map = override || this.statusListMap; const mapped = map[risk.status]; if (mapped) return mapped; const fallback = integration.externalProjectKey || this.listId; if (!fallback) return null; return fallback; } async _resolveLabelIds(risk, integration) { try { const listId = await this._resolveListId(risk, integration); if (!listId) return []; const boardId = await this._boardIdForList(listId); if (!boardId) return []; const cache = await this.ensureLabels(boardId); const ids = []; const dimName = `COSMIC:${risk.dimension}`; const phaseName = `COSMIC:Phase:${risk.lifecyclePhase}`; if (cache.labelMap[dimName]) ids.push(cache.labelMap[dimName]); if (cache.labelMap[phaseName]) ids.push(cache.labelMap[phaseName]); return ids; } catch (err) { return []; } } async createRiskWorkItem(risk, mitigations = [], integration = {}) { const listId = await this._resolveListId(risk, integration); if (!listId) { throw new Error("Trello live sync requires a list ID. Configure status→list mapping or set externalProjectKey."); } const idLabels = await this._resolveLabelIds(risk, integration); if (!/^[a-f0-9]{24}$/i.test(listId)) { throw new Error( `Invalid Trello list ID "${listId}" (status="${risk.status}"). Expected a 24-character hex ID. ` + `Use "Discover Boards" to find valid list IDs, then set externalProjectKey or configure status→list mapping.` ); } const card = await fetchJson(`https://api.trello.com/1/cards?${this.authQuery()}`, { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify({ idList: listId, name: `[${risk.status}] ${risk.title}`, desc: buildRiskDescription(risk, mitigations), idLabels: idLabels.length ? idLabels : undefined }) }, this.fetchImpl); return { externalId: card.id, externalKey: card.shortLink || card.id, externalUrl: card.shortUrl || card.url || `https://trello.com/c/${card.shortLink || card.id}`, externalStatus: risk.status }; } async updateRiskWorkItem(mapping, risk, mitigations = []) { const listId = await this._resolveListId(risk, { liveConfig: { statusListMap: this.statusListMap } }); const body = { name: `[${risk.status}] ${risk.title}`, desc: buildRiskDescription(risk, mitigations) }; if (listId) { if (!/^[a-f0-9]{24}$/i.test(listId)) { throw new Error( `Invalid Trello list ID "${listId}" (status="${risk.status}"). Expected a 24-character hex ID. ` + `Use "Discover Boards" to find valid list IDs.` ); } body.idList = listId; } const card = await fetchJson(`https://api.trello.com/1/cards/${encodeURIComponent(mapping.externalItemId)}?${this.authQuery()}`, { method: "PUT", headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify(body) }, this.fetchImpl); return { externalId: card.id || mapping.externalItemId, externalKey: card.shortLink || mapping.externalItemKey || mapping.externalItemId, externalUrl: card.shortUrl || card.url || mapping.externalUrl, externalStatus: risk.status }; } async fetchWorkItems(options = {}) { const listId = options.listId || this.listId; if (!listId) { throw new Error("Trello fetchWorkItems requires a listId."); } if (!/^[a-f0-9]{24}$/i.test(listId)) { throw new Error(`Invalid Trello list ID "${listId}". Expected a 24-character hex ID.`); } const cards = await fetchJson(`https://api.trello.com/1/lists/${listId}/cards?${this.authQuery({ fields: "id,name,desc,idList,due,idMembers,labels,shortLink,shortUrl" })}`, {}, this.fetchImpl); return (cards || []).map(card => ({ id: card.id, title: card.name, description: card.desc || "", listId: card.idList, dueDate: card.due || null, labels: (card.labels || []).map(l => ({ id: l.id, name: l.name, color: l.color })), memberIds: card.idMembers || [], externalUrl: card.shortUrl || `https://trello.com/c/${card.shortLink || card.id}`, externalKey: card.shortLink || card.id, raw: card })); } async discoverResources() { const member = await fetchJson(`https://api.trello.com/1/members/me?${this.authQuery({ fields: "id,username,fullName" })}`, {}, this.fetchImpl); const boards = await fetchJson(`https://api.trello.com/1/members/${member.id}/boards?${this.authQuery({ fields: "id,name,closed", filter: "open" })}`, {}, this.fetchImpl); const result = []; for (const board of boards) { if (board.closed) continue; const lists = await fetchJson(`https://api.trello.com/1/boards/${board.id}/lists?${this.authQuery({ fields: "id,name,closed" })}`, {}, this.fetchImpl); result.push({ type: "board", id: board.id, name: board.name, lists: lists.filter(l => !l.closed).map(l => ({ type: "list", id: l.id, name: l.name, parentId: board.id, parentName: board.name })) }); } return result; } } module.exports = { TrelloClient };