diff --git a/plugins/fusion-plugin-even-cards/CHANGELOG.md b/plugins/fusion-plugin-even-cards/CHANGELOG.md deleted file mode 100644 index cde05d1e8b..0000000000 --- a/plugins/fusion-plugin-even-cards/CHANGELOG.md +++ /dev/null @@ -1,19 +0,0 @@ -# @fusion-plugin-examples/even-cards - -## 0.1.3 - -### Patch Changes - -- @fusion/plugin-sdk@0.26.0 - -## 0.1.2 - -### Patch Changes - -- @fusion/plugin-sdk@0.25.0 - -## 0.1.1 - -### Patch Changes - -- @fusion/plugin-sdk@0.24.0 diff --git a/plugins/fusion-plugin-even-cards/README.md b/plugins/fusion-plugin-even-cards/README.md deleted file mode 100644 index 29ac44316f..0000000000 --- a/plugins/fusion-plugin-even-cards/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Even Cards Plugin (Deprecated) - -`fusion-plugin-even-cards` is deprecated and superseded by the canonical plugin: - -- `fusion-plugin-even-realities-glasses` - -Board/task card APIs previously provided here (`/board/cards`, `/board`, `/tasks/:id/cards`) now live under the canonical plugin id and route namespace. - -## Migration - -Use: - -- `/api/plugins/fusion-plugin-even-realities-glasses/board/cards` -- `/api/plugins/fusion-plugin-even-realities-glasses/board` -- `/api/plugins/fusion-plugin-even-realities-glasses/tasks/:id/cards` - -This package is removed from the active workspace package list and retained only as a temporary compatibility artifact pending full cleanup. diff --git a/plugins/fusion-plugin-even-cards/package.json b/plugins/fusion-plugin-even-cards/package.json deleted file mode 100644 index 4f2325a55f..0000000000 --- a/plugins/fusion-plugin-even-cards/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@fusion-plugin-examples/even-cards", - "version": "0.1.3", - "type": "module", - "description": "Even Realities on-device cards flow for Fusion board/task status", - "private": true, - "exports": { - ".": { - "types": "./src/index.ts", - "source": "./src/index.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "tsc", - "test": "vitest run --silent=passed-only --reporter=dot" - }, - "dependencies": { - "@fusion/plugin-sdk": "workspace:*" - }, - "devDependencies": { - "@types/node": "^25.5.2", - "typescript": "^5.7.0", - "vitest": "^4.1.0" - } -} diff --git a/plugins/fusion-plugin-even-cards/src/__tests__/auth.test.ts b/plugins/fusion-plugin-even-cards/src/__tests__/auth.test.ts deleted file mode 100644 index 817e8a79bf..0000000000 --- a/plugins/fusion-plugin-even-cards/src/__tests__/auth.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { requireApiKey } from "../routes/auth.js"; - -const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }; - -describe("requireApiKey", () => { - it("returns 503 when api key setting is missing", () => { - const result = requireApiKey({ settings: {}, logger } as any, { headers: {} }); - expect(result).toEqual({ ok: false, response: { status: 503, body: { error: "plugin not configured" } } }); - }); - - it("returns 401 when header missing", () => { - const result = requireApiKey({ settings: { apiKey: "secret" }, logger } as any, { headers: {} }); - expect(result).toEqual({ ok: false, response: { status: 401, body: { error: "unauthorized" } } }); - }); - - it("returns 401 when key does not match", () => { - const result = requireApiKey({ settings: { apiKey: "secret" }, logger } as any, { headers: { authorization: "Bearer nope" } }); - expect(result).toEqual({ ok: false, response: { status: 401, body: { error: "unauthorized" } } }); - }); - - it("returns ok on matching key", () => { - const result = requireApiKey({ settings: { apiKey: "secret" }, logger } as any, { headers: { authorization: "Bearer secret" } }); - expect(result).toEqual({ ok: true }); - }); -}); diff --git a/plugins/fusion-plugin-even-cards/src/__tests__/board-routes.test.ts b/plugins/fusion-plugin-even-cards/src/__tests__/board-routes.test.ts deleted file mode 100644 index 6006103124..0000000000 --- a/plugins/fusion-plugin-even-cards/src/__tests__/board-routes.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import plugin from "../index.js"; -import type { FusionTask } from "../cards/types.js"; - -function makeTask(id: string, column: FusionTask["column"], updatedAt: string): FusionTask { - return { - id, - title: id, - description: id, - column, - status: "pending", - priority: "normal", - createdAt: "2026-05-08T10:00:00.000Z", - updatedAt, - currentStep: 0, - steps: [], - dependencies: [], - } as FusionTask; -} - -function createContext(tasks: FusionTask[], apiKey = "secret") { - return { - settings: { apiKey }, - logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, - taskStore: { - listTasks: vi.fn(async () => tasks), - getTask: vi.fn(async (id: string) => tasks.find((task) => task.id === id)), - }, - } as any; -} - -function route(path: string) { - return plugin.routes!.find((entry) => entry.path === path)!; -} - -describe("even cards routes", () => { - it("returns 401 when auth header missing", async () => { - const response = (await route("/board/cards").handler({ headers: {} }, createContext([]))) as any; - expect(response.status).toBe(401); - }); - - it("returns 503 when plugin is not configured", async () => { - const response = (await route("/board/cards").handler({ headers: { authorization: "Bearer secret" } }, createContext([], ""))) as any; - expect(response.status).toBe(503); - }); - - it("returns deck on happy path", async () => { - const tasks = [makeTask("FN-1", "todo", "2026-05-08T11:00:00.000Z"), makeTask("FN-2", "in-progress", "2026-05-08T12:00:00.000Z")]; - const response = (await route("/board/cards").handler({ headers: { authorization: "Bearer secret" } }, createContext(tasks))) as any; - expect(response.status).toBe(200); - expect(response.body.deck.cards[0].id).toBe("summary"); - expect(response.body.deck.cards).toHaveLength(3); - }); - - it("filters columns in memory", async () => { - const tasks = [makeTask("FN-1", "todo", "2026-05-08T11:00:00.000Z"), makeTask("FN-2", "triage", "2026-05-08T12:00:00.000Z")]; - const response = (await route("/board/cards").handler( - { headers: { authorization: "Bearer secret" }, query: { columns: "triage" } }, - createContext(tasks), - )) as any; - expect(response.body.deck.summary.counts.triage).toBe(1); - expect(response.body.deck.summary.counts.todo).toBe(0); - }); - - it("clamps max bounds", async () => { - const tasks = Array.from({ length: 30 }, (_, idx) => makeTask(`FN-${idx + 1}`, "todo", `2026-05-08T12:${String(idx).padStart(2, "0")}:00.000Z`)); - const low = (await route("/board/cards").handler( - { headers: { authorization: "Bearer secret" }, query: { max: "0" } }, - createContext(tasks), - )) as any; - const high = (await route("/board/cards").handler( - { headers: { authorization: "Bearer secret" }, query: { max: "99" } }, - createContext(tasks), - )) as any; - - expect(low.body.deck.cards).toHaveLength(1); - expect(high.body.deck.cards).toHaveLength(20); - }); - - it("returns board summary", async () => { - const tasks = [makeTask("FN-1", "todo", "2026-05-08T11:00:00.000Z")]; - const response = (await route("/board").handler({ headers: { authorization: "Bearer secret" } }, createContext(tasks))) as any; - expect(response.status).toBe(200); - expect(response.body.summary.counts.todo).toBe(1); - }); - - it("returns task deck for known id", async () => { - const tasks = [makeTask("FN-1", "todo", "2026-05-08T11:00:00.000Z")]; - const response = (await route("/tasks/:id/cards").handler( - { headers: { authorization: "Bearer secret" }, params: { id: "FN-1" } }, - createContext(tasks), - )) as any; - expect(response.status).toBe(200); - expect(response.body.deck.cards[0].id).toBe("FN-1"); - }); - - it("returns 404 for unknown task id", async () => { - const response = (await route("/tasks/:id/cards").handler( - { headers: { authorization: "Bearer secret" }, params: { id: "FN-404" } }, - createContext([]), - )) as any; - expect(response.status).toBe(404); - }); - - it("excludes archived tasks from deck cards", async () => { - const tasks = [makeTask("FN-1", "archived", "2026-05-08T12:00:00.000Z"), makeTask("FN-2", "todo", "2026-05-08T11:00:00.000Z")]; - const response = (await route("/board/cards").handler({ headers: { authorization: "Bearer secret" } }, createContext(tasks))) as any; - const ids = response.body.deck.cards.map((card: any) => card.id); - expect(ids).toEqual(["summary", "FN-2"]); - }); -}); diff --git a/plugins/fusion-plugin-even-cards/src/__tests__/cards.test.ts b/plugins/fusion-plugin-even-cards/src/__tests__/cards.test.ts deleted file mode 100644 index f875290e50..0000000000 --- a/plugins/fusion-plugin-even-cards/src/__tests__/cards.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { boardToDeck } from "../cards/board-cards.js"; -import { formatRelativeAge, formatTaskId, statusBadge, truncateLine, wrapLines } from "../cards/format.js"; -import { taskToCard } from "../cards/task-cards.js"; -import type { FusionTask } from "../cards/types.js"; - -function makeTask(id: string, column: FusionTask["column"], updatedAt: string): FusionTask { - return { - id, - description: `Task ${id}`, - title: `Title ${id}`, - column, - status: "pending", - priority: "normal", - currentStep: 0, - steps: [], - dependencies: [], - createdAt: "2026-05-08T10:00:00.000Z", - updatedAt, - } as FusionTask; -} - -describe("cards format", () => { - it("truncates at boundary", () => { - expect(truncateLine("12345", 5)).toBe("12345"); - expect(truncateLine("123456", 5)).toBe("1234…"); - }); - - it("wraps across lines", () => { - expect(wrapLines("one two three four", 7, 3)).toEqual(["one two", "three", "four"]); - }); - - it("formats task id and relative age", () => { - expect(formatTaskId("fn-42")).toBe("FN-42"); - expect(formatRelativeAge("2026-05-08T11:58:00.000Z", "2026-05-08T12:00:00.000Z")).toBe("2m"); - }); - - it("maps all task columns to badges", () => { - expect(statusBadge("triage").tone).toBe("triage"); - expect(statusBadge("todo").tone).toBe("todo"); - expect(statusBadge("in-progress").tone).toBe("in-progress"); - expect(statusBadge("in-review").tone).toBe("in-review"); - expect(statusBadge("done").tone).toBe("done"); - expect(statusBadge("archived").tone).toBe("neutral"); - }); -}); - -describe("board deck", () => { - it("summarizes counts and handles empty board", () => { - const deck = boardToDeck([], { now: "2026-05-08T12:00:00.000Z" }); - expect(deck.cards).toHaveLength(1); - expect(deck.summary.counts.todo).toBe(0); - }); - - it("creates a task card", () => { - const card = taskToCard(makeTask("FN-7", "in-review", "2026-05-08T12:00:00.000Z"), { now: "2026-05-08T12:10:00.000Z" }); - expect(card.id).toBe("FN-7"); - expect(card.badge.tone).toBe("in-review"); - expect(card.lines.join(" ")).toContain("Assignee unassigned"); - }); - - it("caps deck and sorts deterministically", () => { - const tasks: FusionTask[] = [ - makeTask("FN-1", "todo", "2026-05-08T11:00:00.000Z"), - makeTask("FN-9", "in-progress", "2026-05-08T11:00:00.000Z"), - makeTask("FN-2", "done", "2026-05-08T12:00:00.000Z"), - makeTask("FN-3", "archived", "2026-05-08T12:00:00.000Z"), - ]; - const deck = boardToDeck(tasks, { maxCards: 3, now: "2026-05-08T12:00:00.000Z" }); - - expect(deck.cards).toHaveLength(3); - expect(deck.cards[1].id).toBe("FN-9"); - expect(deck.cards[2].id).toBe("FN-1"); - }); -}); diff --git a/plugins/fusion-plugin-even-cards/src/cards/board-cards.ts b/plugins/fusion-plugin-even-cards/src/cards/board-cards.ts deleted file mode 100644 index e8aa579577..0000000000 --- a/plugins/fusion-plugin-even-cards/src/cards/board-cards.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { DEFAULT_MAX_CARDS_PER_DECK, DEFAULT_MAX_CHARS_PER_LINE, DEFAULT_MAX_LINES_PER_CARD, statusBadge, truncateLine, wrapLines } from "./format.js"; -import { taskToCard } from "./task-cards.js"; -import type { BoardSummary, CardDeck, FusionColumn, FusionTask, GlassesCard } from "./types.js"; - -/* -FNXC:WorkflowResolvedColumns 2026-07-30-21:10 DELIBERATE-LITERAL: no resolution source reaches this package. - -This plugin depends on `@fusion/plugin-sdk` ONLY (see its package.json) — not `@fusion/core` — and the -SDK does not re-export the lifecycle role helpers. There is no IR, no store, and no trait flags on -`FusionTask` here, so there is nothing to resolve FROM: converting is possible only at the caller or by -giving the plugin a new dependency, both structural changes and out of scope for the conversion. - -`COLUMN_ORDER` is a DISPLAY ordering for the glasses deck and degrades honestly on a renamed board — an -unknown column is simply absent from the summary line. The `!== "archived" && !== "done"` filter in -`boardToDeck` is the one that misreads such a board: a finished card is neither, so it shows as active. -That is a cosmetic over-report on a secondary surface, not a lifecycle decision. The real fix is the SDK -exposing role flags on the task shape it hands plugins — recorded here as the correct home rather than -guessed at. -*/ -const COLUMN_ORDER: FusionColumn[] = ["triage", "todo", "in-progress", "in-review", "done", "archived"]; - -function boardSummary(tasks: FusionTask[]): BoardSummary { - const counts = Object.fromEntries(COLUMN_ORDER.map((column) => [column, 0])); - let updatedAt: string | null = null; - for (const task of tasks) { - counts[task.column] = (counts[task.column] ?? 0) + 1; - if (!updatedAt || task.updatedAt > updatedAt) updatedAt = task.updatedAt; - } - return { counts, updatedAt }; -} - -function summaryCard(summary: BoardSummary, now: string, maxCharsPerLine: number, maxLines: number): GlassesCard { - const summaryText = `Triage ${summary.counts.triage} Todo ${summary.counts.todo} Doing ${summary.counts["in-progress"]} Review ${summary.counts["in-review"]} Done ${summary.counts.done}`; - return { - id: "summary", - kind: "summary", - title: truncateLine("Fusion board", maxCharsPerLine), - lines: wrapLines(summaryText, maxCharsPerLine, maxLines), - badge: statusBadge("todo"), - updatedAt: summary.updatedAt ?? now, - }; -} - -/* FNXC:WorkflowResolvedColumns 2026-07-30-21:10 DELIBERATE-LITERAL: no resolution source in this package — see the note above `COLUMN_ORDER`. */ -export function boardToDeck(tasks: FusionTask[], opts?: { maxCharsPerLine?: number; maxLines?: number; maxCards?: number; now?: string }): CardDeck { - const maxCharsPerLine = opts?.maxCharsPerLine ?? DEFAULT_MAX_CHARS_PER_LINE; - const maxLines = opts?.maxLines ?? DEFAULT_MAX_LINES_PER_CARD; - const maxCards = opts?.maxCards ?? DEFAULT_MAX_CARDS_PER_DECK; - const now = opts?.now ?? new Date().toISOString(); - - const summary = boardSummary(tasks); - const active = tasks - .filter((task) => task.column !== "archived" && task.column !== "done") - .sort((a, b) => (b.updatedAt === a.updatedAt ? b.id.localeCompare(a.id) : b.updatedAt.localeCompare(a.updatedAt))) - .slice(0, Math.max(0, maxCards - 1)); - - const cards: GlassesCard[] = [summaryCard(summary, now, maxCharsPerLine, maxLines), ...active.map((task) => taskToCard(task, { maxCharsPerLine, maxLines, now }))]; - return { cards, summary }; -} diff --git a/plugins/fusion-plugin-even-cards/src/cards/format.ts b/plugins/fusion-plugin-even-cards/src/cards/format.ts deleted file mode 100644 index 44f9311906..0000000000 --- a/plugins/fusion-plugin-even-cards/src/cards/format.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { CardStatusBadge, FusionColumn } from "./types.js"; - -export const DEFAULT_MAX_CHARS_PER_LINE = 24; -export const DEFAULT_MAX_LINES_PER_CARD = 4; -export const DEFAULT_MAX_CARDS_PER_DECK = 8; - -export function truncateLine(input: string, max: number): string { - const value = input.trim(); - if (value.length <= max) return value; - if (max <= 1) return "…"; - return `${value.slice(0, max - 1)}…`; -} - -export function wrapLines(text: string, max: number, maxLines: number): string[] { - const words = text.trim().split(/\s+/).filter(Boolean); - if (words.length === 0) return []; - const lines: string[] = []; - let current = ""; - - for (const word of words) { - const candidate = current ? `${current} ${word}` : word; - if (candidate.length <= max) { - current = candidate; - continue; - } - if (current) { - lines.push(current); - if (lines.length === maxLines) return lines.map((line, idx) => (idx === maxLines - 1 ? truncateLine(line, max) : line)); - } - current = word.length <= max ? word : truncateLine(word, max); - } - - if (current && lines.length < maxLines) { - lines.push(current); - } - - return lines.slice(0, maxLines).map((line, idx, arr) => (idx === arr.length - 1 ? truncateLine(line, max) : line)); -} - -export function statusBadge(column: FusionColumn): CardStatusBadge { - switch (column) { - case "triage": - case "todo": - case "in-progress": - case "in-review": - case "done": - return { label: column, tone: column }; - case "archived": - default: - return { label: column, tone: "neutral" }; - } -} - -export function formatTaskId(id: string): string { - return id.trim().toUpperCase(); -} - -export function formatRelativeAge(createdAtIso: string, nowIso: string): string { - const created = Date.parse(createdAtIso); - const now = Date.parse(nowIso); - if (!Number.isFinite(created) || !Number.isFinite(now) || now <= created) return "0m"; - - const diffMs = now - created; - const minutes = Math.floor(diffMs / 60000); - if (minutes < 60) return `${minutes}m`; - const hours = Math.floor(minutes / 60); - if (hours < 24) return `${hours}h`; - const days = Math.floor(hours / 24); - return `${days}d`; -} diff --git a/plugins/fusion-plugin-even-cards/src/cards/task-cards.ts b/plugins/fusion-plugin-even-cards/src/cards/task-cards.ts deleted file mode 100644 index b51dfb0dbf..0000000000 --- a/plugins/fusion-plugin-even-cards/src/cards/task-cards.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { DEFAULT_MAX_CHARS_PER_LINE, DEFAULT_MAX_LINES_PER_CARD, formatRelativeAge, formatTaskId, statusBadge, truncateLine, wrapLines } from "./format.js"; -import type { FusionTask, GlassesCard } from "./types.js"; - -export function taskToCard(task: FusionTask, opts?: { maxCharsPerLine?: number; maxLines?: number; now?: string }): GlassesCard { - const maxCharsPerLine = opts?.maxCharsPerLine ?? DEFAULT_MAX_CHARS_PER_LINE; - const maxLines = opts?.maxLines ?? DEFAULT_MAX_LINES_PER_CARD; - const now = opts?.now ?? new Date().toISOString(); - const assignee = task.assignedAgentId ?? task.assigneeUserId ?? "unassigned"; - const body = [`Priority ${task.priority ?? "normal"}`, `Assignee ${assignee}`, `Age ${formatRelativeAge(task.createdAt, now)}`].join(" "); - - return { - id: task.id, - taskId: task.id, - kind: "task", - title: truncateLine(formatTaskId(task.id) + " " + (task.title?.trim() || task.description), maxCharsPerLine), - lines: wrapLines(body, maxCharsPerLine, maxLines), - badge: statusBadge(task.column), - updatedAt: task.updatedAt, - }; -} diff --git a/plugins/fusion-plugin-even-cards/src/cards/types.ts b/plugins/fusion-plugin-even-cards/src/cards/types.ts deleted file mode 100644 index f60b124d0d..0000000000 --- a/plugins/fusion-plugin-even-cards/src/cards/types.ts +++ /dev/null @@ -1,40 +0,0 @@ -export type FusionColumn = "triage" | "todo" | "in-progress" | "in-review" | "done" | "archived"; - -export interface FusionTask { - id: string; - title?: string; - description: string; - column: FusionColumn; - priority?: string; - assignedAgentId?: string; - assigneeUserId?: string; - createdAt: string; - updatedAt: string; -} - -export type CardTone = "triage" | "todo" | "in-progress" | "in-review" | "done" | "neutral"; - -export interface CardStatusBadge { - label: string; - tone: CardTone; -} - -export interface GlassesCard { - id: string; - kind: "summary" | "task"; - title: string; - lines: string[]; - badge: CardStatusBadge; - taskId?: string; - updatedAt: string; -} - -export interface BoardSummary { - counts: Record; - updatedAt: string | null; -} - -export interface CardDeck { - cards: GlassesCard[]; - summary: BoardSummary; -} diff --git a/plugins/fusion-plugin-even-cards/src/index.ts b/plugins/fusion-plugin-even-cards/src/index.ts deleted file mode 100644 index 28c1dc260f..0000000000 --- a/plugins/fusion-plugin-even-cards/src/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { definePlugin } from "@fusion/plugin-sdk"; -import type { FusionPlugin, PluginSettingSchema } from "@fusion/plugin-sdk"; -import { boardRoutes } from "./routes/board-routes.js"; - -const settingsSchema: Record = { - apiKey: { - type: "password", - label: "Companion API Key", - description: "Bearer token used by the glasses companion app.", - required: true, - group: "Authentication", - }, - boardPollingMs: { - type: "number", - label: "Board Polling Interval (ms)", - defaultValue: 10000, - group: "On-device Cards", - }, -}; - -const plugin: FusionPlugin = definePlugin({ - manifest: { - id: "fusion-plugin-even-cards", - name: "Even Cards", - version: "0.1.0", - description: "On-device card payloads for board/task status in Even Realities flows", - author: "Fusion Team", - settingsSchema, - }, - state: "installed", - routes: [...boardRoutes], - hooks: { - onLoad: async (ctx) => { - ctx.logger.info("Even Cards plugin loaded"); - }, - }, -}); - -export default plugin; diff --git a/plugins/fusion-plugin-even-cards/src/routes/auth.ts b/plugins/fusion-plugin-even-cards/src/routes/auth.ts deleted file mode 100644 index ab7b7b32f3..0000000000 --- a/plugins/fusion-plugin-even-cards/src/routes/auth.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { createHash, timingSafeEqual } from "node:crypto"; -import type { PluginContext, PluginRouteResponse } from "@fusion/plugin-sdk"; - -function toDigest(value: string): Buffer { - return createHash("sha256").update(value).digest(); -} - -function readBearer(headers: Record): string | undefined { - const auth = headers.authorization ?? headers.Authorization; - const header = Array.isArray(auth) ? auth[0] : auth; - if (!header) return undefined; - const match = header.match(/^Bearer\s+(.+)$/i); - return match?.[1]?.trim() || undefined; -} - -export function requireApiKey( - ctx: PluginContext, - req: { headers: Record }, -): { ok: true } | { ok: false; response: PluginRouteResponse } { - const expected = typeof ctx.settings.apiKey === "string" ? ctx.settings.apiKey.trim() : ""; - if (!expected) { - return { ok: false, response: { status: 503, body: { error: "plugin not configured" } } }; - } - - const provided = readBearer(req.headers ?? {}); - if (!provided) { - return { ok: false, response: { status: 401, body: { error: "unauthorized" } } }; - } - - const expectedDigest = toDigest(expected); - const providedDigest = toDigest(provided); - const valid = timingSafeEqual(expectedDigest, providedDigest); - - if (!valid) { - return { ok: false, response: { status: 401, body: { error: "unauthorized" } } }; - } - - return { ok: true }; -} diff --git a/plugins/fusion-plugin-even-cards/src/routes/board-routes.ts b/plugins/fusion-plugin-even-cards/src/routes/board-routes.ts deleted file mode 100644 index e9516ae099..0000000000 --- a/plugins/fusion-plugin-even-cards/src/routes/board-routes.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { PluginContext, PluginRouteDefinition, PluginRouteResponse } from "@fusion/plugin-sdk"; -import { boardToDeck } from "../cards/board-cards.js"; -import { DEFAULT_MAX_CARDS_PER_DECK, DEFAULT_MAX_CHARS_PER_LINE, DEFAULT_MAX_LINES_PER_CARD } from "../cards/format.js"; -import { taskToCard } from "../cards/task-cards.js"; -import type { CardDeck, FusionColumn, FusionTask } from "../cards/types.js"; -import { requireApiKey } from "./auth.js"; - -const ALLOWED_COLUMNS: FusionColumn[] = ["triage", "todo", "in-progress", "in-review", "done", "archived"]; - -function parseColumns(raw: unknown): Set | null { - if (typeof raw !== "string" || !raw.trim()) return null; - const parsed = raw - .split(",") - .map((value) => value.trim()) - .filter((value): value is FusionColumn => ALLOWED_COLUMNS.includes(value as FusionColumn)); - return parsed.length ? new Set(parsed) : null; -} - -function parseMax(raw: unknown): number { - const value = typeof raw === "string" ? Number.parseInt(raw, 10) : Number.NaN; - if (!Number.isFinite(value)) return DEFAULT_MAX_CARDS_PER_DECK; - return Math.max(1, Math.min(20, Math.floor(value))); -} - -function requestData(req: unknown): { headers: Record; query: Record; params: Record } { - const candidate = (req ?? {}) as { headers?: Record; query?: Record; params?: Record }; - return { headers: candidate.headers ?? {}, query: candidate.query ?? {}, params: candidate.params ?? {} }; -} - -async function getBoardCards(req: unknown, ctx: PluginContext): Promise { - const request = requestData(req); - const auth = requireApiKey(ctx, { headers: request.headers }); - if (!auth.ok) return auth.response; - - const all = ((await ctx.taskStore.listTasks({ includeArchived: false })) as FusionTask[]) ?? []; - const columns = parseColumns(request.query.columns); - const filtered = columns ? all.filter((task) => columns.has(task.column)) : all; - const maxCards = parseMax(request.query.max); - const deck = boardToDeck(filtered, { maxCharsPerLine: DEFAULT_MAX_CHARS_PER_LINE, maxLines: DEFAULT_MAX_LINES_PER_CARD, maxCards }); - return { status: 200, body: { deck, generatedAt: new Date().toISOString() } }; -} - -async function getBoardSummary(req: unknown, ctx: PluginContext): Promise { - const request = requestData(req); - const auth = requireApiKey(ctx, { headers: request.headers }); - if (!auth.ok) return auth.response; - - const all = ((await ctx.taskStore.listTasks({ includeArchived: false })) as FusionTask[]) ?? []; - const columns = parseColumns(request.query.columns); - const filtered = columns ? all.filter((task) => columns.has(task.column)) : all; - const deck = boardToDeck(filtered, { maxCharsPerLine: DEFAULT_MAX_CHARS_PER_LINE, maxLines: DEFAULT_MAX_LINES_PER_CARD, maxCards: 1 }); - - return { status: 200, body: { summary: deck.summary, updatedAt: deck.summary.updatedAt } }; -} - -async function getTaskCards(req: unknown, ctx: PluginContext): Promise { - const request = requestData(req); - const auth = requireApiKey(ctx, { headers: request.headers }); - if (!auth.ok) return auth.response; - - const taskId = typeof request.params.id === "string" ? request.params.id.trim() : ""; - if (!taskId) return { status: 400, body: { error: "task id is required" } }; - - const task = (await ctx.taskStore.getTask(taskId)) as FusionTask | undefined; - if (!task) return { status: 404, body: { error: "task not found" } }; - - const deck: CardDeck = { - cards: [taskToCard(task, { maxCharsPerLine: DEFAULT_MAX_CHARS_PER_LINE, maxLines: DEFAULT_MAX_LINES_PER_CARD })], - summary: boardToDeck([task], { maxCards: 1 }).summary, - }; - - return { status: 200, body: { deck, generatedAt: new Date().toISOString() } }; -} - -export const boardRoutes: PluginRouteDefinition[] = [ - { method: "GET", path: "/board/cards", handler: getBoardCards, description: "Read-only board card deck" }, - { method: "GET", path: "/board", handler: getBoardSummary, description: "Read-only board summary" }, - { method: "GET", path: "/tasks/:id/cards", handler: getTaskCards, description: "Read-only single task card deck" }, -]; diff --git a/plugins/fusion-plugin-even-cards/tsconfig.json b/plugins/fusion-plugin-even-cards/tsconfig.json deleted file mode 100644 index fdc529a99c..0000000000 --- a/plugins/fusion-plugin-even-cards/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src" - }, - "include": ["src/**/*"] -} diff --git a/plugins/fusion-plugin-even-cards/vitest.config.ts b/plugins/fusion-plugin-even-cards/vitest.config.ts deleted file mode 100644 index 0ce6bd3a96..0000000000 --- a/plugins/fusion-plugin-even-cards/vitest.config.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { fileURLToPath } from "node:url"; -import { defineConfig } from "vitest/config"; -import { computeMaxWorkers } from "../../packages/core/src/__test-utils__/vitest-workers"; - -const maxWorkers = computeMaxWorkers(); - -export default defineConfig({ - resolve: { - alias: { - "@fusion/core": fileURLToPath(new URL("../../packages/core/src/index.ts", import.meta.url)), - "@fusion/plugin-sdk": fileURLToPath(new URL("../../packages/plugin-sdk/src/index.ts", import.meta.url)), - }, - }, - test: { - environment: "node", - include: ["src/__tests__/**/*.test.ts"], - setupFiles: [fileURLToPath(new URL("../../packages/core/src/__test-utils__/vitest-setup.ts", import.meta.url))], - globalSetup: [fileURLToPath(new URL("../../packages/core/src/__test-utils__/vitest-teardown.ts", import.meta.url))], - pool: "threads", - maxWorkers, - minWorkers: 1, - }, -}); diff --git a/scripts/lib/lane-wiring-baseline.json b/scripts/lib/lane-wiring-baseline.json index 98984ea903..3793880375 100644 --- a/scripts/lib/lane-wiring-baseline.json +++ b/scripts/lib/lane-wiring-baseline.json @@ -19,7 +19,6 @@ "packages/cli/src/commands/dashboard-tui/bucket-mapping.ts": 1, "packages/cli/src/extension.ts": 1, "plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.tsx": 1, - "plugins/fusion-plugin-even-cards/src/routes/board-routes.ts": 3, "plugins/fusion-plugin-even-realities-glasses/src/routes/board-routes.ts": 1 } } diff --git a/scripts/lib/lifecycle-column-census-baseline.json b/scripts/lib/lifecycle-column-census-baseline.json index 9e6a449014..732b680241 100644 --- a/scripts/lib/lifecycle-column-census-baseline.json +++ b/scripts/lib/lifecycle-column-census-baseline.json @@ -127,8 +127,6 @@ "packages/engine/src/scheduler.ts\u0000todo": 1, "packages/engine/src/self-healing.ts\u0000archived": 1, "packages/engine/src/triage.ts\u0000triage": 1, - "plugins/fusion-plugin-even-cards/src/cards/board-cards.ts\u0000archived": 1, - "plugins/fusion-plugin-even-cards/src/cards/board-cards.ts\u0000done": 1, "plugins/fusion-plugin-even-realities-glasses/src/notifications/diff.ts\u0000done": 1, "plugins/fusion-plugin-reports/src/store/report-types.ts\u0000archived": 1 },