import { definePlugin } from "@fusion/plugin-sdk"; import type { FusionPlugin, PluginContext, PluginSettingSchema, PluginRouteDefinition, PluginUiSlotDefinition, } from "@fusion/plugin-sdk"; // ── Types ────────────────────────────────────────────────────────────────────── interface BranchStatus { branch: string; status: string; lastChecked: string; url?: string; } // ── Settings Schema ───────────────────────────────────────────────────────────── const settingsSchema: Record = { ciUrl: { type: "string", label: "CI API URL", description: "Base URL for CI API", required: true, }, pollIntervalMs: { type: "number", label: "Poll Interval (ms)", description: "How often to poll CI status", defaultValue: 30000, }, branchPrefix: { type: "string", label: "Branch Prefix", description: "Only poll branches with this prefix", defaultValue: "fusion/", }, }; // ── Module-Level State ───────────────────────────────────────────────────────── const branchStatuses = new Map(); let pollInterval: ReturnType | null = null; let pluginLogger: PluginContext["logger"] | null = null; // ── CI Polling Logic ─────────────────────────────────────────────────────────── async function pollCIStatus( ciUrl: string, logger: PluginContext["logger"], ): Promise { const branchesToPoll = Array.from(branchStatuses.keys()); if (branchesToPoll.length === 0) { return; } logger.info(`Polling CI status for ${branchesToPoll.length} branches`); try { // Try to fetch from the configured CI URL const response = await fetch(`${ciUrl}/status`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ branches: branchesToPoll }), }); if (response.ok) { const data = (await response.json()) as { statuses?: Array<{ branch: string; status: string; url?: string }>; }; if (data.statuses) { for (const s of data.statuses) { branchStatuses.set(s.branch, { branch: s.branch, status: s.status, lastChecked: new Date().toISOString(), url: s.url, }); } } } } catch (err) { // CI polling is best-effort; log and continue logger.warn( `CI polling failed: ${err instanceof Error ? err.message : String(err)}`, ); } } // ── Route Handlers ───────────────────────────────────────────────────────────── // Route handlers use `unknown` for request type to match PluginRouteDefinition signature. // Cast to local MockRequest type where property access is needed. interface MockRequest { params: Record; method: string; url: string; } function getStatusAllHandler( _req: unknown, _ctx: PluginContext, ): { branches: BranchStatus[] } { const branches = Array.from(branchStatuses.values()); return { branches }; } function getStatusBranchHandler( req: unknown, _ctx: PluginContext, ): { branch: string; status: string; lastChecked: string; url?: string } { const request = req as MockRequest; const branch = request.params.branch; const status = branchStatuses.get(branch); if (!status) { const error = new Error("Branch not found") as Error & { statusCode?: number }; error.statusCode = 404; throw error; } return status; } function postRefreshHandler( _req: unknown, ctx: PluginContext, ): { branches: BranchStatus[]; refreshed: boolean } { const ciUrl = ctx.settings.ciUrl as string; if (ciUrl) { // Trigger immediate poll pollCIStatus(ciUrl, ctx.logger).catch(() => { // Best-effort polling }); } const branches = Array.from(branchStatuses.values()); return { branches, refreshed: true }; } // ── Plugin Routes ───────────────────────────────────────────────────────────── const routes: PluginRouteDefinition[] = [ { method: "GET", path: "/status", handler: getStatusAllHandler as unknown as (req: unknown, ctx: PluginContext) => Promise, description: "Get status of all tracked branches", }, { method: "GET", path: "/status/:branch", handler: getStatusBranchHandler as unknown as (req: unknown, ctx: PluginContext) => Promise, description: "Get status of a specific branch", }, { method: "POST", path: "/refresh", handler: postRefreshHandler as unknown as (req: unknown, ctx: PluginContext) => Promise, description: "Trigger an immediate CI status refresh", }, ]; // ── UI Slots ───────────────────────────────────────────────────────────────── // UI slots declare where the plugin provides dashboard UI components. // The dashboard renders placeholder elements for each registered slot. // Full dynamic component loading will be added in a future iteration. const uiSlots: PluginUiSlotDefinition[] = [ { slotId: "task-card-badge", label: "CI Status", icon: "circle-check", componentPath: "./components/ci-badge.js", }, { slotId: "task-detail-tab", label: "CI History", icon: "history", componentPath: "./components/ci-history-tab.js", }, ]; // ── Plugin Definition ─────────────────────────────────────────────────────────── const plugin: FusionPlugin = definePlugin({ manifest: { id: "fusion-plugin-ci-status", name: "CI Status Plugin", version: "0.1.0", description: "Polls CI status for branches and provides a custom API to query results", settingsSchema, }, state: "installed", routes, uiSlots, hooks: { onLoad: (ctx) => { pluginLogger = ctx.logger; ctx.logger.info("CI Status plugin loaded"); const pollIntervalMs = (ctx.settings.pollIntervalMs as number) || 30000; // Start polling pollInterval = setInterval(() => { const ciUrl = ctx.settings.ciUrl as string | undefined; if (ciUrl) { pollCIStatus(ciUrl, ctx.logger).catch(() => { // Best-effort polling }); } }, pollIntervalMs); ctx.logger.info( `CI polling started with interval ${pollIntervalMs}ms`, ); }, onUnload: () => { if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } // Clear branch statuses on unload branchStatuses.clear(); pluginLogger?.info("CI Status plugin unloaded"); pluginLogger = null; }, onTaskMoved: (task, fromColumn, toColumn, ctx) => { // Track branches for tasks that move to in-progress if (toColumn === "in-progress") { const branchPrefix = (ctx.settings.branchPrefix as string) || "fusion/"; const branchName = `${branchPrefix}${task.id.toLowerCase()}`; if (!branchStatuses.has(branchName)) { branchStatuses.set(branchName, { branch: branchName, status: "pending", lastChecked: new Date().toISOString(), }); ctx.logger.info(`Tracking branch for task ${task.id}: ${branchName}`); } } // Remove from tracking when task is done if (toColumn === "done" || toColumn === "archived") { const branchPrefix = (ctx.settings.branchPrefix as string) || "fusion/"; const branchName = `${branchPrefix}${task.id.toLowerCase()}`; if (branchStatuses.has(branchName)) { branchStatuses.delete(branchName); ctx.logger.info(`Stopped tracking branch for task ${task.id}`); } } }, }, }); export default plugin;