Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

67 lines · 2.9 KBRaw History
1import type { Io } from "./io.js";
2
3export interface Colors {
4 enabled: boolean;
5 bold(s: string): string;
6 dim(s: string): string;
7 red(s: string): string;
8 green(s: string): string;
9 yellow(s: string): string;
10 cyan(s: string): string;
11 magenta(s: string): string;
12}
13
14export function colors(io: Io): Colors {
15 const on = !!io.stdout.isTTY && io.env.NO_COLOR === undefined && io.env.TERM !== "dumb";
16 const wrap = (open: number, close: number) => (s: string) => (on ? `\x1b[${open}m${s}\x1b[${close}m` : s);
17 return { enabled: on, bold: wrap(1, 22), dim: wrap(2, 22), red: wrap(31, 39), green: wrap(32, 39), yellow: wrap(33, 39), cyan: wrap(36, 39), magenta: wrap(35, 39) };
18}
19
20const ANSI = /\x1b\[[0-9;]*m/g;
21const width = (s: string) => [...s.replace(ANSI, "")].length;
22
23/** Left-aligned columns separated by two spaces; the last column is not padded. */
24export function table(rows: string[][]): string {
25 if (!rows.length) return "";
26 const cols = Math.max(...rows.map((r) => r.length));
27 const widths = Array.from({ length: cols }, (_, i) => Math.max(...rows.map((r) => width(r[i] ?? ""))));
28 return rows.map((r) => r.map((cell, i) => (i === r.length - 1 ? cell : cell + " ".repeat(widths[i]! - width(cell)))).join(" ").trimEnd()).join("\n") + "\n";
29}
30
31export function json(io: Io, v: unknown): void {
32 io.stdout.write(JSON.stringify(v, null, 2) + "\n");
33}
34
35export function truncate(s: string | null | undefined, n: number): string {
36 const t = (s ?? "").replace(/\s+/g, " ").trim();
37 return t.length > n ? `${t.slice(0, n - 1)}…` : t;
38}
39
40export function ago(iso: string | null | undefined, now = Date.now()): string {
41 if (!iso) return "";
42 const t = Date.parse(iso);
43 if (Number.isNaN(t)) return iso;
44 const s = Math.max(0, Math.round((now - t) / 1000));
45 if (s < 60) return `${s}s ago`;
46 if (s < 3600) return `${Math.floor(s / 60)}m ago`;
47 if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
48 if (s < 86400 * 30) return `${Math.floor(s / 86400)}d ago`;
49 return iso.slice(0, 10);
50}
51
52/** Colors a status/conclusion word. */
53export function statusText(c: Colors, status: string | null | undefined, conclusion?: string | null): string {
54 if (status && status !== "completed") return c.yellow(status.replace(/_/g, " "));
55 const v = conclusion ?? status ?? "unknown";
56 if (v === "success" || v === "merged" || v === "open" || v === "clean") return c.green(v);
57 if (v === "failure" || v === "timed_out" || v === "cancelled" || v === "action_required") return c.red(v.replace(/_/g, " "));
58 if (v === "closed" || v === "skipped" || v === "neutral") return c.dim(v);
59 return c.yellow(v.replace(/_/g, " "));
60}
61
62export function symbol(c: Colors, status: string | null | undefined, conclusion?: string | null): string {
63 if (status && status !== "completed") return c.yellow("*");
64 if (conclusion === "success") return c.green("✓");
65 if (conclusion === "skipped" || conclusion === "neutral") return c.dim("-");
66 return c.red("X");
67}