From 3eb259aae5df8379b5a1a4f404a3cfef1846245c Mon Sep 17 00:00:00 2001 From: Michael Heap Date: Thu, 13 Aug 2026 10:07:40 +0100 Subject: [PATCH] Add card:activity command --- .../.changeset/card-activity-command.md | 5 + packages/trello-cli/README.md | 5 + .../trello-cli/src/commands/card/activity.ts | 54 +++++++ .../test/commands/card/activity.test.ts | 149 ++++++++++++++++++ 4 files changed, 213 insertions(+) create mode 100644 packages/trello-cli/.changeset/card-activity-command.md create mode 100644 packages/trello-cli/src/commands/card/activity.ts create mode 100644 packages/trello-cli/test/commands/card/activity.test.ts diff --git a/packages/trello-cli/.changeset/card-activity-command.md b/packages/trello-cli/.changeset/card-activity-command.md new file mode 100644 index 0000000..4c45def --- /dev/null +++ b/packages/trello-cli/.changeset/card-activity-command.md @@ -0,0 +1,5 @@ +--- +"@trello-cli/cli": minor +--- + +Add card:activity command to list recent activity on a card, with an optional --filter flag for comma-separated action types diff --git a/packages/trello-cli/README.md b/packages/trello-cli/README.md index 795c942..86dd3e1 100644 --- a/packages/trello-cli/README.md +++ b/packages/trello-cli/README.md @@ -25,6 +25,7 @@ Most commands identify boards, lists, and cards by name, e.g. `--board "My Board - `trello card:get-by-id` — Show card details by ID. Flag: `--id` - `trello card:assigned-to` — Show cards assigned to a user (default: `me`). Flag: `--user` - `trello card:comments` — List comments on a card. Flags: `--board`, `--list`, `--card` +- `trello card:activity` — List recent activity on a card. Flags: `--board`, `--list`, `--card`, `--filter` (comma-separated action types, e.g. `commentCard,updateCard`) - `trello card:attachments` — List attachments on a card. Flags: `--board`, `--list`, `--card` - `trello card:checklists` — List checklists on a card. Flags: `--board`, `--list`, `--card` @@ -162,6 +163,10 @@ $ trello card:create --board "Inbox" --list "Inbox" --name "Quick card added fro $ trello card:comments --board "Inbox" --list "Inbox" --card "Quick card added from command line" ``` +```bash +$ trello card:activity --board "Inbox" --list "Inbox" --card "Quick card added from command line" --filter "commentCard,updateCard" +``` + ```bash $ trello card:attachments --board "Inbox" --list "Inbox" --card "Quick card added from command line" ``` diff --git a/packages/trello-cli/src/commands/card/activity.ts b/packages/trello-cli/src/commands/card/activity.ts new file mode 100644 index 0000000..88f1fb2 --- /dev/null +++ b/packages/trello-cli/src/commands/card/activity.ts @@ -0,0 +1,54 @@ +import { BaseCommand } from "../../BaseCommand"; +import { Flags } from "@oclif/core"; + +export default class CardActivity extends BaseCommand { + static description = "Show recent activity on a card"; + + protected defaultOutput = "fancy" as const; + + static flags = { + board: Flags.string({ required: true }), + list: Flags.string({ required: true }), + card: Flags.string({ required: true }), + filter: Flags.string({ + char: "f", + description: + "Comma-separated list of action types to show (e.g. commentCard,updateCard). Defaults to all activity.", + }), + }; + + async run(): Promise { + const actions = await this.client.cards.getCardActions({ + id: this.lookups.card, + ...(this.flags.filter ? { filter: this.flags.filter } : {}), + }); + this.output(actions); + } + + protected toData(data: any) { + return data.map((action: any) => ({ + id: action.id, + type: action.type, + author: action.memberCreator?.fullName, + date: action.date, + text: action.data?.text, + card: action.data?.card?.name, + list: action.data?.list?.name, + board: action.data?.board?.name, + })); + } + + protected async format(data: any): Promise { + if (data.length === 0) { + return "No activity"; + } + return data + .map((a: any) => { + const context = [a.list, a.card, a.board].filter(Boolean).join(" / "); + const summary = context ? ` (${context})` : ""; + const text = a.text ? ` - ${a.text}` : ""; + return `${a.date} ${a.author}: ${a.type}${summary}${text}`; + }) + .join("\n"); + } +} diff --git a/packages/trello-cli/test/commands/card/activity.test.ts b/packages/trello-cli/test/commands/card/activity.test.ts new file mode 100644 index 0000000..4ef9969 --- /dev/null +++ b/packages/trello-cli/test/commands/card/activity.test.ts @@ -0,0 +1,149 @@ +import { runCommand } from "@oclif/test"; +import Config from "@trello-cli/config"; +import { ux } from "@oclif/core"; + +const mockActions = [ + { + id: "action1", + type: "commentCard", + data: { + text: "First comment", + card: { name: "TestCard" }, + list: { name: "ToDo" }, + board: { name: "MyBoard" }, + }, + memberCreator: { fullName: "John Doe" }, + date: "2026-01-15T10:00:00.000Z", + }, + { + id: "action2", + type: "updateCard", + data: { + card: { name: "TestCard" }, + list: { name: "Done" }, + board: { name: "MyBoard" }, + }, + memberCreator: { fullName: "Jane Smith" }, + date: "2026-01-16T12:00:00.000Z", + }, +]; + +const getCardActions = jest.fn().mockResolvedValue(mockActions); +const getListCards = jest.fn().mockResolvedValue([ + { id: "card123", name: "TestCard" }, +]); + +jest.mock("trello.js", () => ({ + TrelloClient: jest.fn().mockImplementation(() => ({ + cards: { getCardActions }, + lists: { getListCards }, + })), +})); + +const mockGetBoardIdByName = jest.fn().mockResolvedValue("board123"); +const mockGetListIdByBoardAndName = jest.fn().mockResolvedValue("list123"); + +jest.mock("@trello-cli/cache", () => { + return { + __esModule: true, + default: jest.fn().mockImplementation(() => ({ + getBoardIdByName: mockGetBoardIdByName, + getListIdByBoardAndName: mockGetListIdByBoardAndName, + })), + }; +}); + +let stdoutSpy: jest.SpyInstance; + +beforeEach(() => { + jest + .spyOn(Config.prototype, "getToken") + .mockImplementation(() => Promise.resolve("fake_token")); + jest + .spyOn(Config.prototype, "getApiKey") + .mockImplementation(() => Promise.resolve("fake_api_key")); + + stdoutSpy = jest.spyOn(ux, "stdout").mockImplementation(() => {}); + + getCardActions.mockClear(); + getListCards.mockClear(); + mockGetBoardIdByName.mockClear(); + mockGetListIdByBoardAndName.mockClear(); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe("card:activity", () => { + it("throws when required flags are missing", async () => { + const { error } = await runCommand(["card:activity"]); + expect(error?.message).toContain("Missing required flag"); + }); + + it("calls getCardActions without a filter by default", async () => { + const { error } = await runCommand([ + "card:activity", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--format", "json", + ]); + expect(error).toBeUndefined(); + expect(getCardActions).toHaveBeenCalledTimes(1); + expect(getCardActions).toHaveBeenCalledWith({ + id: "card123", + }); + }); + + it("passes the filter flag to getCardActions", async () => { + await runCommand([ + "card:activity", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--filter", "commentCard,updateCard", + "--format", "json", + ]); + expect(getCardActions).toHaveBeenCalledWith({ + id: "card123", + filter: "commentCard,updateCard", + }); + }); + + it("outputs correct JSON shape", async () => { + await runCommand([ + "card:activity", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--format", "json", + ]); + + const outputCall = stdoutSpy.mock.calls[0][0]; + const output = JSON.parse(outputCall); + expect(output).toHaveLength(2); + expect(output[0].id).toBe("action1"); + expect(output[0].type).toBe("commentCard"); + expect(output[0].text).toBe("First comment"); + expect(output[0].author).toBe("John Doe"); + expect(output[0].card).toBe("TestCard"); + expect(output[0].list).toBe("ToDo"); + expect(output[0].board).toBe("MyBoard"); + }); + + it("handles empty actions array", async () => { + getCardActions.mockResolvedValueOnce([]); + await runCommand([ + "card:activity", + "--board", "MyBoard", + "--list", "ToDo", + "--card", "TestCard", + "--format", "json", + ]); + + const outputCall = stdoutSpy.mock.calls[0][0]; + const output = JSON.parse(outputCall); + expect(output).toHaveLength(0); + }); +});