trelly 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +191 -0
- package/bin/run-ts +31 -0
- package/bin/trelly +2 -0
- package/bin/trelly-mcp +2 -0
- package/package.json +64 -0
- package/src/api/client.ts +332 -0
- package/src/api/http.ts +86 -0
- package/src/auth/browser-flow.ts +217 -0
- package/src/auth/profiles.ts +163 -0
- package/src/cli/commands/actions.ts +17 -0
- package/src/cli/commands/api.ts +46 -0
- package/src/cli/commands/auth.ts +248 -0
- package/src/cli/commands/boards.ts +152 -0
- package/src/cli/commands/cards.ts +194 -0
- package/src/cli/commands/checklists.ts +79 -0
- package/src/cli/commands/custom-fields.ts +75 -0
- package/src/cli/commands/labels.ts +47 -0
- package/src/cli/commands/lists.ts +54 -0
- package/src/cli/commands/members.ts +14 -0
- package/src/cli/commands/orgs.ts +23 -0
- package/src/cli/commands/run.ts +32 -0
- package/src/cli/commands/search.ts +23 -0
- package/src/cli/commands/ui.ts +48 -0
- package/src/cli/commands/webhooks.ts +44 -0
- package/src/cli/context.ts +70 -0
- package/src/cli/index.ts +47 -0
- package/src/cli/ui/app.tsx +753 -0
- package/src/cli/ui/custom-fields.ts +75 -0
- package/src/cli/ui/palette.ts +69 -0
- package/src/cli/ui/shapes.ts +68 -0
- package/src/cli/ui/static.tsx +382 -0
- package/src/index.test.ts +117 -0
- package/src/mcp/handlers.ts +61 -0
- package/src/mcp/server.ts +17 -0
- package/src/mcp/tools/api.ts +27 -0
- package/src/mcp/tools/boards.ts +116 -0
- package/src/mcp/tools/cards.ts +138 -0
- package/src/mcp/tools/checklists.ts +40 -0
- package/src/mcp/tools/index.ts +22 -0
- package/src/mcp/tools/labels.ts +40 -0
- package/src/mcp/tools/lists.ts +37 -0
- package/src/mcp/tools/profiles.ts +40 -0
- package/src/mcp/tools/search.ts +31 -0
- package/src/mcp/tools/webhooks.ts +52 -0
- package/src/util/runtime.ts +39 -0
- package/src/version.ts +15 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { formatDue } from "./palette.ts";
|
|
2
|
+
|
|
3
|
+
export type UiCustomFieldOption = { id: string; text: string; color: string | null };
|
|
4
|
+
|
|
5
|
+
export type UiCustomFieldDef = {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
type: string;
|
|
9
|
+
cardFront: boolean;
|
|
10
|
+
options: UiCustomFieldOption[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type UiCustomFieldItem = {
|
|
14
|
+
idCustomField: string;
|
|
15
|
+
idValue?: string | null;
|
|
16
|
+
value?: { text?: string; number?: string; date?: string; checked?: string } | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type CardChip = { id: string; label: string; color: string | null };
|
|
20
|
+
|
|
21
|
+
type RawCustomFieldDef = {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
type: string;
|
|
25
|
+
display?: { cardFront?: boolean };
|
|
26
|
+
options?: Array<{ id: string; color?: string | null; value?: { text?: string } }>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function toCustomFieldDefs(raw: unknown): UiCustomFieldDef[] {
|
|
30
|
+
if (!Array.isArray(raw)) return [];
|
|
31
|
+
return (raw as RawCustomFieldDef[]).map((def) => ({
|
|
32
|
+
id: def.id,
|
|
33
|
+
name: def.name,
|
|
34
|
+
type: def.type,
|
|
35
|
+
cardFront: def.display?.cardFront === true,
|
|
36
|
+
options: (def.options ?? []).map((opt) => ({
|
|
37
|
+
id: opt.id,
|
|
38
|
+
text: opt.value?.text ?? "",
|
|
39
|
+
color: opt.color ?? null,
|
|
40
|
+
})),
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Resolve a card's custom field items to displayable chips. */
|
|
45
|
+
export function customFieldChips(
|
|
46
|
+
defs: UiCustomFieldDef[],
|
|
47
|
+
items: UiCustomFieldItem[] | undefined,
|
|
48
|
+
): CardChip[] {
|
|
49
|
+
const chips: CardChip[] = [];
|
|
50
|
+
for (const item of items ?? []) {
|
|
51
|
+
const def = defs.find((d) => d.id === item.idCustomField);
|
|
52
|
+
if (!def) continue;
|
|
53
|
+
if (item.idValue) {
|
|
54
|
+
const opt = def.options.find((o) => o.id === item.idValue);
|
|
55
|
+
if (opt) {
|
|
56
|
+
chips.push({ id: def.id, label: `${def.name}: ${opt.text}`, color: opt.color });
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const value = item.value;
|
|
61
|
+
if (!value) continue;
|
|
62
|
+
if (value.checked !== undefined) {
|
|
63
|
+
if (value.checked === "true") {
|
|
64
|
+
chips.push({ id: def.id, label: `${def.name} ✓`, color: null });
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const text =
|
|
69
|
+
value.text ?? value.number ?? (value.date ? formatDue(value.date) : undefined);
|
|
70
|
+
if (text !== undefined) {
|
|
71
|
+
chips.push({ id: def.id, label: `${def.name}: ${text}`, color: null });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return chips;
|
|
75
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const TRELLO_LABEL_HEX: Record<string, string> = {
|
|
2
|
+
green: "#61bd4f",
|
|
3
|
+
yellow: "#f2d600",
|
|
4
|
+
orange: "#ff9f1a",
|
|
5
|
+
red: "#eb5a46",
|
|
6
|
+
purple: "#0079bf",
|
|
7
|
+
blue: "#0079bf",
|
|
8
|
+
sky: "#00c2e0",
|
|
9
|
+
lime: "#51e898",
|
|
10
|
+
pink: "#ff78cb",
|
|
11
|
+
black: "#344563",
|
|
12
|
+
// List-color values (Trello list colors) beyond the classic label set
|
|
13
|
+
teal: "#6cc3e0",
|
|
14
|
+
magenta: "#e774bb",
|
|
15
|
+
gray: "#8590a2",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const TRELLO_BLUE = "#0079bf";
|
|
19
|
+
|
|
20
|
+
const FALLBACK_HEX = "#6b778c";
|
|
21
|
+
|
|
22
|
+
/** Trello sends shades like "green_dark" — map them to the base color. */
|
|
23
|
+
export function labelHex(color: string | null | undefined): string {
|
|
24
|
+
const base = (color ?? "").split("_")[0] ?? "";
|
|
25
|
+
return TRELLO_LABEL_HEX[base] ?? FALLBACK_HEX;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Accent for a Trello list: its set color, else Trello blue. */
|
|
29
|
+
export function listAccentHex(color: string | null | undefined): string {
|
|
30
|
+
return color ? labelHex(color) : TRELLO_BLUE;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type DueStatus = "none" | "complete" | "overdue" | "soon" | "later";
|
|
34
|
+
|
|
35
|
+
const SOON_MS = 24 * 60 * 60 * 1000;
|
|
36
|
+
|
|
37
|
+
export function dueStatus(
|
|
38
|
+
due: string | null | undefined,
|
|
39
|
+
dueComplete: boolean | undefined,
|
|
40
|
+
now: Date = new Date(),
|
|
41
|
+
): DueStatus {
|
|
42
|
+
if (!due) return "none";
|
|
43
|
+
if (dueComplete) return "complete";
|
|
44
|
+
const t = Date.parse(due);
|
|
45
|
+
if (Number.isNaN(t)) return "none";
|
|
46
|
+
if (t < now.getTime()) return "overdue";
|
|
47
|
+
if (t - now.getTime() <= SOON_MS) return "soon";
|
|
48
|
+
return "later";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function dueHex(status: DueStatus): string | undefined {
|
|
52
|
+
switch (status) {
|
|
53
|
+
case "complete":
|
|
54
|
+
return TRELLO_LABEL_HEX.green;
|
|
55
|
+
case "overdue":
|
|
56
|
+
return TRELLO_LABEL_HEX.red;
|
|
57
|
+
case "soon":
|
|
58
|
+
return TRELLO_LABEL_HEX.yellow;
|
|
59
|
+
default:
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function formatDue(due: string): string {
|
|
65
|
+
return new Date(due).toLocaleDateString("en-US", {
|
|
66
|
+
month: "short",
|
|
67
|
+
day: "numeric",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export type Rec = Record<string, unknown>;
|
|
2
|
+
|
|
3
|
+
export function isRecord(v: unknown): v is Rec {
|
|
4
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isCard(v: unknown): v is Rec {
|
|
8
|
+
return isRecord(v) && typeof v.name === "string" && "idList" in v;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isBoard(v: unknown): v is Rec {
|
|
12
|
+
return (
|
|
13
|
+
isRecord(v) &&
|
|
14
|
+
typeof v.name === "string" &&
|
|
15
|
+
!("idBoard" in v) &&
|
|
16
|
+
!("idList" in v) &&
|
|
17
|
+
("prefs" in v || "idOrganization" in v || "shortUrl" in v)
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isList(v: unknown): v is Rec {
|
|
22
|
+
return (
|
|
23
|
+
isRecord(v) &&
|
|
24
|
+
typeof v.name === "string" &&
|
|
25
|
+
"idBoard" in v &&
|
|
26
|
+
"pos" in v &&
|
|
27
|
+
!("idList" in v) &&
|
|
28
|
+
!("color" in v)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isLabel(v: unknown): v is Rec {
|
|
33
|
+
return isRecord(v) && "color" in v && "idBoard" in v && !("pos" in v);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isMember(v: unknown): v is Rec {
|
|
37
|
+
return isRecord(v) && typeof v.username === "string";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isWebhook(v: unknown): v is Rec {
|
|
41
|
+
return isRecord(v) && "callbackURL" in v;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isAction(v: unknown): v is Rec {
|
|
45
|
+
return (
|
|
46
|
+
isRecord(v) && typeof v.type === "string" && "date" in v && "idMemberCreator" in v
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isChecklist(v: unknown): v is Rec {
|
|
51
|
+
return isRecord(v) && Array.isArray(v.checkItems) && "idCard" in v;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isSearchResult(v: unknown): v is Rec {
|
|
55
|
+
return (
|
|
56
|
+
isRecord(v) && "options" in v && (Array.isArray(v.cards) || Array.isArray(v.boards))
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isProfilesPayload(v: unknown): v is Rec {
|
|
61
|
+
return (
|
|
62
|
+
isRecord(v) && Array.isArray(v.profiles) && typeof v.defaultProfile === "string"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isMessagePayload(v: unknown): v is Rec {
|
|
67
|
+
return isRecord(v) && typeof v.message === "string";
|
|
68
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { Box, render, Text } from "ink";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { CliError, CliResult } from "../context.ts";
|
|
4
|
+
import { dueHex, dueStatus, formatDue, labelHex, listAccentHex } from "./palette.ts";
|
|
5
|
+
import {
|
|
6
|
+
isAction,
|
|
7
|
+
isBoard,
|
|
8
|
+
isCard,
|
|
9
|
+
isChecklist,
|
|
10
|
+
isLabel,
|
|
11
|
+
isList,
|
|
12
|
+
isMember,
|
|
13
|
+
isMessagePayload,
|
|
14
|
+
isProfilesPayload,
|
|
15
|
+
isRecord,
|
|
16
|
+
isSearchResult,
|
|
17
|
+
isWebhook,
|
|
18
|
+
type Rec,
|
|
19
|
+
} from "./shapes.ts";
|
|
20
|
+
|
|
21
|
+
const TRELLO_BLUE = "#0079bf";
|
|
22
|
+
const GREEN = "#61bd4f";
|
|
23
|
+
const RED = "#eb5a46";
|
|
24
|
+
const YELLOW = "#f2d600";
|
|
25
|
+
|
|
26
|
+
function clip(text: string, max: number): string {
|
|
27
|
+
return text.length <= max ? text : `${text.slice(0, Math.max(0, max - 1))}…`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Render a result as a one-shot ink frame (colors auto-disable when piped). */
|
|
31
|
+
export async function renderResult(result: CliResult | CliError): Promise<void> {
|
|
32
|
+
const instance = render(<ResultView result={result} />, {
|
|
33
|
+
exitOnCtrlC: false,
|
|
34
|
+
patchConsole: false,
|
|
35
|
+
});
|
|
36
|
+
instance.unmount();
|
|
37
|
+
await instance.waitUntilExit();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ResultView({ result }: { result: CliResult | CliError }) {
|
|
41
|
+
if (!result.ok) {
|
|
42
|
+
return (
|
|
43
|
+
<Box flexDirection="column">
|
|
44
|
+
<Text color={RED}>
|
|
45
|
+
✗ {result.error}
|
|
46
|
+
{result.status ? <Text dimColor> (HTTP {result.status})</Text> : null}
|
|
47
|
+
</Text>
|
|
48
|
+
{result.details && isRecord(result.details) ? (
|
|
49
|
+
<Text dimColor>{clip(JSON.stringify(result.details), 120)}</Text>
|
|
50
|
+
) : null}
|
|
51
|
+
</Box>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return (
|
|
55
|
+
<Box flexDirection="column">
|
|
56
|
+
<ResultData data={result.data} />
|
|
57
|
+
{result.profile !== "default" ? (
|
|
58
|
+
<Text dimColor>profile: {result.profile}</Text>
|
|
59
|
+
) : null}
|
|
60
|
+
</Box>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function ResultData({ data }: { data: unknown }) {
|
|
65
|
+
if (Array.isArray(data)) return <ArrayView items={data} />;
|
|
66
|
+
if (isSearchResult(data)) return <SearchView data={data} />;
|
|
67
|
+
if (isProfilesPayload(data)) return <ProfilesView data={data} />;
|
|
68
|
+
if (isMessagePayload(data)) return <MessageView data={data} />;
|
|
69
|
+
if (isCard(data)) return <CardDetail card={data} />;
|
|
70
|
+
if (isChecklist(data)) return <ChecklistView checklist={data} />;
|
|
71
|
+
if (isMember(data)) return <MemberRow member={data} />;
|
|
72
|
+
if (isBoard(data)) return <BoardRow board={data} />;
|
|
73
|
+
if (isRecord(data)) return <KeyValue obj={data} />;
|
|
74
|
+
if (data === null || data === undefined) return <Text dimColor>(no data)</Text>;
|
|
75
|
+
return <Text>{String(data)}</Text>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function ArrayView({ items }: { items: unknown[] }) {
|
|
79
|
+
if (items.length === 0) return <Text dimColor>(none)</Text>;
|
|
80
|
+
const first = items[0];
|
|
81
|
+
let rows: ReactNode;
|
|
82
|
+
if (isCard(first)) {
|
|
83
|
+
rows = items.filter(isCard).map((c) => <CardRow key={String(c.id)} card={c} />);
|
|
84
|
+
} else if (isList(first)) {
|
|
85
|
+
rows = items.filter(isList).map((l) => <ListRow key={String(l.id)} list={l} />);
|
|
86
|
+
} else if (isLabel(first)) {
|
|
87
|
+
rows = items.filter(isLabel).map((l) => <LabelRow key={String(l.id)} label={l} />);
|
|
88
|
+
} else if (isBoard(first)) {
|
|
89
|
+
rows = items.filter(isBoard).map((b) => <BoardRow key={String(b.id)} board={b} />);
|
|
90
|
+
} else if (isWebhook(first)) {
|
|
91
|
+
rows = items
|
|
92
|
+
.filter(isWebhook)
|
|
93
|
+
.map((w) => <WebhookRow key={String(w.id)} webhook={w} />);
|
|
94
|
+
} else if (isAction(first)) {
|
|
95
|
+
rows = items
|
|
96
|
+
.filter(isAction)
|
|
97
|
+
.map((a) => <ActionRow key={String(a.id)} action={a} />);
|
|
98
|
+
} else {
|
|
99
|
+
rows = items.map((item, i) => (
|
|
100
|
+
<GenericRow key={isRecord(item) ? String(item.id ?? i) : i} item={item} />
|
|
101
|
+
));
|
|
102
|
+
}
|
|
103
|
+
return (
|
|
104
|
+
<Box flexDirection="column">
|
|
105
|
+
{rows}
|
|
106
|
+
<Text dimColor>{items.length} total</Text>
|
|
107
|
+
</Box>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function LabelDots({ labels }: { labels: unknown }) {
|
|
112
|
+
if (!Array.isArray(labels) || labels.length === 0) return null;
|
|
113
|
+
return (
|
|
114
|
+
<>
|
|
115
|
+
{labels.filter(isRecord).map((label) => (
|
|
116
|
+
<Text key={String(label.id)} color={labelHex(label.color as string | null)}>
|
|
117
|
+
●{" "}
|
|
118
|
+
</Text>
|
|
119
|
+
))}
|
|
120
|
+
</>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function DueBadge({ card }: { card: Rec }) {
|
|
125
|
+
const due = card.due as string | null | undefined;
|
|
126
|
+
if (!due) return null;
|
|
127
|
+
const status = dueStatus(due, card.dueComplete as boolean | undefined);
|
|
128
|
+
return (
|
|
129
|
+
<Text color={dueHex(status)} dimColor={status === "later"}>
|
|
130
|
+
{formatDue(due)}
|
|
131
|
+
{status === "complete" ? " ✓" : ""}{" "}
|
|
132
|
+
</Text>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function CardRow({ card }: { card: Rec }) {
|
|
137
|
+
const badges = isRecord(card.badges) ? card.badges : undefined;
|
|
138
|
+
const checkItems = (badges?.checkItems as number | undefined) ?? 0;
|
|
139
|
+
const checked = (badges?.checkItemsChecked as number | undefined) ?? 0;
|
|
140
|
+
const comments = (badges?.comments as number | undefined) ?? 0;
|
|
141
|
+
const attachments = (badges?.attachments as number | undefined) ?? 0;
|
|
142
|
+
return (
|
|
143
|
+
<Text wrap="truncate">
|
|
144
|
+
<LabelDots labels={card.labels} />
|
|
145
|
+
{card.dueComplete === true ? <Text color={GREEN}>✓ </Text> : null}
|
|
146
|
+
{String(card.name)} <DueBadge card={card} />
|
|
147
|
+
{checkItems > 0 ? (
|
|
148
|
+
<Text dimColor>
|
|
149
|
+
✓{checked}/{checkItems}{" "}
|
|
150
|
+
</Text>
|
|
151
|
+
) : null}
|
|
152
|
+
{card.desc ? <Text dimColor>≡ </Text> : null}
|
|
153
|
+
{comments > 0 ? <Text dimColor>💬{comments} </Text> : null}
|
|
154
|
+
{attachments > 0 ? <Text dimColor>📎{attachments} </Text> : null}
|
|
155
|
+
<Text dimColor>{String(card.shortLink ?? card.id ?? "")}</Text>
|
|
156
|
+
</Text>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function CardDetail({ card }: { card: Rec }) {
|
|
161
|
+
const labels = Array.isArray(card.labels) ? card.labels.filter(isRecord) : [];
|
|
162
|
+
const desc = typeof card.desc === "string" ? card.desc : "";
|
|
163
|
+
return (
|
|
164
|
+
<Box flexDirection="column">
|
|
165
|
+
<Text bold wrap="truncate">
|
|
166
|
+
{String(card.name)}
|
|
167
|
+
</Text>
|
|
168
|
+
{labels.length > 0 ? (
|
|
169
|
+
<Box gap={1}>
|
|
170
|
+
{labels.map((label) => (
|
|
171
|
+
<Text
|
|
172
|
+
key={String(label.id)}
|
|
173
|
+
backgroundColor={labelHex(label.color as string | null)}
|
|
174
|
+
color="#1d2125"
|
|
175
|
+
>
|
|
176
|
+
{` ${String(label.name || label.color || "label")} `}
|
|
177
|
+
</Text>
|
|
178
|
+
))}
|
|
179
|
+
</Box>
|
|
180
|
+
) : null}
|
|
181
|
+
{card.due ? (
|
|
182
|
+
<Text>
|
|
183
|
+
Due: <DueBadge card={card} />
|
|
184
|
+
</Text>
|
|
185
|
+
) : null}
|
|
186
|
+
{desc ? <Text>{clip(desc, 600)}</Text> : null}
|
|
187
|
+
<Text dimColor>{String(card.shortUrl ?? card.url ?? card.id ?? "")}</Text>
|
|
188
|
+
</Box>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function BoardRow({ board }: { board: Rec }) {
|
|
193
|
+
return (
|
|
194
|
+
<Text wrap="truncate">
|
|
195
|
+
<Text color={TRELLO_BLUE}>● </Text>
|
|
196
|
+
{String(board.name)}
|
|
197
|
+
{board.closed === true ? <Text color={RED}> (closed)</Text> : null}{" "}
|
|
198
|
+
<Text dimColor>{String(board.shortUrl ?? board.id ?? "")}</Text>
|
|
199
|
+
</Text>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function ListRow({ list }: { list: Rec }) {
|
|
204
|
+
return (
|
|
205
|
+
<Text wrap="truncate">
|
|
206
|
+
<Text color={listAccentHex(list.color as string | null | undefined)}>▍</Text>{" "}
|
|
207
|
+
{String(list.name)}
|
|
208
|
+
{list.closed === true ? <Text color={RED}> (archived)</Text> : null}{" "}
|
|
209
|
+
<Text dimColor>{String(list.id ?? "")}</Text>
|
|
210
|
+
</Text>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function LabelRow({ label }: { label: Rec }) {
|
|
215
|
+
return (
|
|
216
|
+
<Text wrap="truncate">
|
|
217
|
+
<Text backgroundColor={labelHex(label.color as string | null)} color="#1d2125">
|
|
218
|
+
{` ${String(label.name || label.color || "label")} `}
|
|
219
|
+
</Text>{" "}
|
|
220
|
+
<Text dimColor>{String(label.id ?? "")}</Text>
|
|
221
|
+
</Text>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function WebhookRow({ webhook }: { webhook: Rec }) {
|
|
226
|
+
return (
|
|
227
|
+
<Text wrap="truncate">
|
|
228
|
+
<Text color={webhook.active === false ? RED : GREEN}>● </Text>
|
|
229
|
+
{String(webhook.description || webhook.callbackURL)}{" "}
|
|
230
|
+
<Text dimColor>
|
|
231
|
+
model {String(webhook.idModel ?? "")} · {String(webhook.id ?? "")}
|
|
232
|
+
</Text>
|
|
233
|
+
</Text>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function ActionRow({ action }: { action: Rec }) {
|
|
238
|
+
const date = String(action.date ?? "")
|
|
239
|
+
.slice(0, 16)
|
|
240
|
+
.replace("T", " ");
|
|
241
|
+
return (
|
|
242
|
+
<Text wrap="truncate">
|
|
243
|
+
<Text dimColor>{date} </Text>
|
|
244
|
+
{String(action.type)}
|
|
245
|
+
</Text>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function ChecklistView({ checklist }: { checklist: Rec }) {
|
|
250
|
+
const items = (checklist.checkItems as unknown[]).filter(isRecord);
|
|
251
|
+
return (
|
|
252
|
+
<Box flexDirection="column">
|
|
253
|
+
<Text bold>{String(checklist.name)}</Text>
|
|
254
|
+
{items.map((item) => {
|
|
255
|
+
const done = item.state === "complete";
|
|
256
|
+
return (
|
|
257
|
+
<Text key={String(item.id)}>
|
|
258
|
+
{done ? <Text color={GREEN}>☑ </Text> : "☐ "}
|
|
259
|
+
<Text dimColor={done} strikethrough={done}>
|
|
260
|
+
{String(item.name)}
|
|
261
|
+
</Text>
|
|
262
|
+
</Text>
|
|
263
|
+
);
|
|
264
|
+
})}
|
|
265
|
+
</Box>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function MemberRow({ member }: { member: Rec }) {
|
|
270
|
+
return (
|
|
271
|
+
<Box flexDirection="column">
|
|
272
|
+
<Text>
|
|
273
|
+
<Text bold color={TRELLO_BLUE}>
|
|
274
|
+
@{String(member.username)}
|
|
275
|
+
</Text>
|
|
276
|
+
{member.fullName ? <Text> {String(member.fullName)}</Text> : null}
|
|
277
|
+
</Text>
|
|
278
|
+
{member.url ? <Text dimColor>{String(member.url)}</Text> : null}
|
|
279
|
+
</Box>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function ProfilesView({ data }: { data: Rec }) {
|
|
284
|
+
const profiles = (data.profiles as unknown[]).filter(isRecord);
|
|
285
|
+
return (
|
|
286
|
+
<Box flexDirection="column">
|
|
287
|
+
{profiles.length === 0 ? (
|
|
288
|
+
<Text dimColor>No profiles. Run: trelly auth login</Text>
|
|
289
|
+
) : (
|
|
290
|
+
profiles.map((p) => (
|
|
291
|
+
<Text key={String(p.name)}>
|
|
292
|
+
{p.isDefault === true ? <Text color={YELLOW}>★ </Text> : " "}
|
|
293
|
+
<Text bold={p.isDefault === true}>{String(p.name)}</Text>
|
|
294
|
+
{p.label ? <Text dimColor> {String(p.label)}</Text> : null}
|
|
295
|
+
</Text>
|
|
296
|
+
))
|
|
297
|
+
)}
|
|
298
|
+
<Text dimColor>app key: {data.hasAppApiKey === true ? "✓" : "not set"}</Text>
|
|
299
|
+
</Box>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function MessageView({ data }: { data: Rec }) {
|
|
304
|
+
const { message, ...rest } = data;
|
|
305
|
+
return (
|
|
306
|
+
<Box flexDirection="column">
|
|
307
|
+
<Text color={GREEN}>✓ {String(message)}</Text>
|
|
308
|
+
{Object.keys(rest).length > 0 ? <KeyValue obj={rest} dim /> : null}
|
|
309
|
+
</Box>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function SearchView({ data }: { data: Rec }) {
|
|
314
|
+
const boards = Array.isArray(data.boards) ? data.boards : [];
|
|
315
|
+
const cards = Array.isArray(data.cards) ? data.cards : [];
|
|
316
|
+
return (
|
|
317
|
+
<Box flexDirection="column">
|
|
318
|
+
{boards.length > 0 ? (
|
|
319
|
+
<Box flexDirection="column" marginBottom={cards.length > 0 ? 1 : 0}>
|
|
320
|
+
<Text bold dimColor>
|
|
321
|
+
Boards
|
|
322
|
+
</Text>
|
|
323
|
+
{boards.filter(isBoard).map((b) => (
|
|
324
|
+
<BoardRow key={String(b.id)} board={b} />
|
|
325
|
+
))}
|
|
326
|
+
</Box>
|
|
327
|
+
) : null}
|
|
328
|
+
{cards.length > 0 ? (
|
|
329
|
+
<Box flexDirection="column">
|
|
330
|
+
<Text bold dimColor>
|
|
331
|
+
Cards
|
|
332
|
+
</Text>
|
|
333
|
+
{cards.filter(isCard).map((c) => (
|
|
334
|
+
<CardRow key={String(c.id)} card={c} />
|
|
335
|
+
))}
|
|
336
|
+
</Box>
|
|
337
|
+
) : null}
|
|
338
|
+
{boards.length === 0 && cards.length === 0 ? (
|
|
339
|
+
<Text dimColor>(no results)</Text>
|
|
340
|
+
) : null}
|
|
341
|
+
</Box>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function GenericRow({ item }: { item: unknown }) {
|
|
346
|
+
if (isRecord(item) && typeof item.name === "string") {
|
|
347
|
+
return (
|
|
348
|
+
<Text wrap="truncate">
|
|
349
|
+
{item.name} <Text dimColor>{String(item.id ?? "")}</Text>
|
|
350
|
+
</Text>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
return <Text wrap="truncate">{clip(JSON.stringify(item) ?? "", 100)}</Text>;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function KeyValue({ obj, dim = false }: { obj: Rec; dim?: boolean }) {
|
|
357
|
+
return (
|
|
358
|
+
<Box flexDirection="column">
|
|
359
|
+
{Object.entries(obj).map(([key, value]) => (
|
|
360
|
+
<Box key={key}>
|
|
361
|
+
<Text dimColor>{key}: </Text>
|
|
362
|
+
<ValueText value={value} dim={dim} />
|
|
363
|
+
</Box>
|
|
364
|
+
))}
|
|
365
|
+
</Box>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function ValueText({ value, dim }: { value: unknown; dim: boolean }) {
|
|
370
|
+
if (value === null || value === undefined) return <Text dimColor>—</Text>;
|
|
371
|
+
if (typeof value === "boolean") {
|
|
372
|
+
return <Text color={value ? GREEN : RED}>{String(value)}</Text>;
|
|
373
|
+
}
|
|
374
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
375
|
+
return (
|
|
376
|
+
<Text dimColor={dim} wrap="truncate">
|
|
377
|
+
{String(value)}
|
|
378
|
+
</Text>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
return <Text dimColor>{clip(JSON.stringify(value) ?? "", 80)}</Text>;
|
|
382
|
+
}
|