Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 36 additions & 20 deletions src/cli/ascii-banner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
//
// COLORS is the paint map. Each character corresponds 1:1 to the character
// at the same position in SHAPE:
// B = blue (#1a4fd0) — the sphere
// S = silver (#e5e7eb) — the rings and the "Bitsocial" text
// B = blue accent — the sphere
// S = default foreground — the rings and the "Bitsocial" text
// . = no color (pass the glyph through as-is; use this for spaces)
//
// To retouch the art, find a glyph in SHAPE, then flip the character at the
Expand All @@ -15,7 +15,8 @@
//
// Both grids MUST have the same number of rows. Each row in COLORS must be at
// least as wide as the corresponding SHAPE row (extra chars are ignored).
// Palette sourced from bitsocialnet/bitsocial-web/about/tailwind.config.ts.
// Use the terminal's default foreground for the wordmark/rings so the banner
// stays readable on both light and dark terminal themes.

const SHAPE = [
" ⢀⣴⣿⣿⣦⡀ ",
Expand Down Expand Up @@ -59,36 +60,51 @@ const COLORS = [
"................SSSSSS......................................................................................."
];

const BLUE = "\x1b[38;2;26;79;208m";
const SILVER = "\x1b[38;2;229;231;235m";
const RESET = "\x1b[0m";
const BLUE = "\x1b[94m";
const DEFAULT_FOREGROUND = "\x1b[39m";

function paint(shape: string, colors: string): string {
let out = "";
let current = ".";
let blueActive = false;
for (let i = 0; i < shape.length; i++) {
const glyph = shape[i]!;
const want = colors[i] ?? ".";
if (want !== current) {
if (current !== ".") out += RESET;
if (want === "B") out += BLUE;
else if (want === "S") out += SILVER;
current = want;
const wantBlue = want === "B";
if (wantBlue !== blueActive) {
out += wantBlue ? BLUE : DEFAULT_FOREGROUND;
blueActive = wantBlue;
}
out += glyph;
}
if (current !== ".") out += RESET;
if (blueActive) out += DEFAULT_FOREGROUND;
return out;
}

function supportsColor(): boolean {
if (process.env["NO_COLOR"]) return false;
if (process.env["FORCE_COLOR"]) return true;
return Boolean(process.stdout.isTTY);
interface RenderBannerOptions {
env?: Record<string, string | undefined>;
forceColor?: boolean;
stdoutIsTTY?: boolean;
}

export function printBanner(): void {
const useColor = supportsColor();
function envForcesColor(value: string | undefined): boolean {
if (value === undefined) return false;
return value !== "0" && value.toLowerCase() !== "false";
}

function supportsColor(options: RenderBannerOptions = {}): boolean {
const env = options.env ?? process.env;
if (env["NO_COLOR"] !== undefined) return false;
if (options.forceColor) return true;
if (env["FORCE_COLOR"] !== undefined) return envForcesColor(env["FORCE_COLOR"]);
return Boolean(options.stdoutIsTTY ?? process.stdout.isTTY);
}

export function renderBanner(options: RenderBannerOptions = {}): string {
const useColor = supportsColor(options);
const lines = SHAPE.map((row, i) => (useColor ? paint(row, COLORS[i] ?? "") : row));
process.stdout.write(lines.join("\n") + "\n\n");
return lines.join("\n") + "\n\n";
}

export function printBanner(options: RenderBannerOptions = {}): void {
process.stdout.write(renderBanner(options));
}
11 changes: 6 additions & 5 deletions src/cli/commands/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@ export default class Daemon extends Command {
}

async run() {
printBanner();
// Daemon output is often viewed through Docker/systemd logs where stdout is not a TTY.
printBanner({ forceColor: true });
// Non-blocking update check — fire-and-forget, won't delay startup
import("../../update/npm-registry.js")
.then(({ fetchLatestVersion }) =>
Expand Down Expand Up @@ -471,10 +472,6 @@ export default class Daemon extends Command {
}
};

// RPC port was already verified free above (fail-fast); only the kuboRpcClientsOptions branch skips local kubo.
if (!pkcOptionsFromFlag?.kuboRpcClientsOptions) await keepKuboUp();
await createOrConnectRpc();

let keepKuboUpInterval: NodeJS.Timeout | undefined;
const { asyncExitHook } = await import("exit-hook");
const killKuboProcessGroup = (pid: number, signal: NodeJS.Signals) => {
Expand Down Expand Up @@ -570,6 +567,10 @@ export default class Daemon extends Command {
}
});

// RPC port was already verified free above (fail-fast); only the kuboRpcClientsOptions branch skips local kubo.
if (!pkcOptionsFromFlag?.kuboRpcClientsOptions) await keepKuboUp();
await createOrConnectRpc();

keepKuboUpInterval = setInterval(async () => {
if (mainProcessExited) return;
await runKeepKuboUpTick({
Expand Down
46 changes: 46 additions & 0 deletions test/cli/ascii-banner.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";
import { renderBanner } from "../../dist/cli/ascii-banner.js";

const ANSI_PATTERN = /\x1b\[[0-9;]*m/;

describe("ASCII banner", () => {
it("renders plain readable output when color is disabled", () => {
const banner = renderBanner({ env: { NO_COLOR: "1" }, stdoutIsTTY: true });

expect(banner).not.toMatch(ANSI_PATTERN);
expect(banner).toContain("888888b.");
expect(banner).toContain("⣿");
});

it("uses only a blue accent and terminal default foreground in color mode", () => {
const banner = renderBanner({ env: {}, stdoutIsTTY: true });

expect(banner).toContain("\x1b[94m");
expect(banner).toContain("\x1b[39m");
expect(banner).not.toContain("\x1b[38;2;229;231;235m");
});

it("keeps non-TTY output plain unless color is forced", () => {
const banner = renderBanner({ env: {}, stdoutIsTTY: false });

expect(banner).not.toMatch(ANSI_PATTERN);
});

it("supports forced color for captured terminal logs", () => {
const banner = renderBanner({ env: {}, forceColor: true, stdoutIsTTY: false });

expect(banner).toContain("\x1b[94m");
});

it("supports FORCE_COLOR for standard CLI color control", () => {
const banner = renderBanner({ env: { FORCE_COLOR: "1" }, stdoutIsTTY: false });

expect(banner).toContain("\x1b[94m");
});

it("lets FORCE_COLOR=0 disable color", () => {
const banner = renderBanner({ env: { FORCE_COLOR: "0" }, stdoutIsTTY: true });

expect(banner).not.toMatch(ANSI_PATTERN);
});
});
Loading