openclaw/src/cli/banner.ts

133 lines
4.3 KiB
TypeScript
Raw Normal View History

2026-01-08 05:32:49 +01:00
import { resolveCommitHash } from "../infra/git-commit.js";
2026-01-22 08:49:51 +00:00
import { visibleWidth } from "../terminal/ansi.js";
2026-01-08 05:32:49 +01:00
import { isRich, theme } from "../terminal/theme.js";
import { pickTagline, type TaglineOptions } from "./tagline.js";
type BannerOptions = TaglineOptions & {
argv?: string[];
commit?: string | null;
2026-01-22 08:49:51 +00:00
columns?: number;
2026-01-08 05:32:49 +01:00
richTty?: boolean;
};
let bannerEmitted = false;
2026-01-09 08:51:22 +01:00
const graphemeSegmenter =
typeof Intl !== "undefined" && "Segmenter" in Intl
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
: null;
function splitGraphemes(value: string): string[] {
if (!graphemeSegmenter) {
return Array.from(value);
}
2026-01-09 08:51:22 +01:00
try {
return Array.from(graphemeSegmenter.segment(value), (seg) => seg.segment);
} catch {
return Array.from(value);
}
}
2026-01-08 05:32:49 +01:00
const hasJsonFlag = (argv: string[]) =>
argv.some((arg) => arg === "--json" || arg.startsWith("--json="));
const hasVersionFlag = (argv: string[]) =>
argv.some((arg) => arg === "--version" || arg === "-V" || arg === "-v");
export function formatCliBannerLine(version: string, options: BannerOptions = {}): string {
2026-01-08 05:32:49 +01:00
const commit = options.commit ?? resolveCommitHash({ env: options.env });
const commitLabel = commit ?? "unknown";
const tagline = pickTagline(options);
const rich = options.richTty ?? isRich();
2026-01-30 03:15:10 +01:00
const title = "🦞 OpenClaw";
2026-01-22 08:49:51 +00:00
const prefix = "🦞 ";
const columns = options.columns ?? process.stdout.columns ?? 120;
const plainFullLine = `${title} ${version} (${commitLabel}) — ${tagline}`;
const fitsOnOneLine = visibleWidth(plainFullLine) <= columns;
2026-01-08 05:32:49 +01:00
if (rich) {
2026-01-22 08:49:51 +00:00
if (fitsOnOneLine) {
return `${theme.heading(title)} ${theme.info(version)} ${theme.muted(
`(${commitLabel})`,
)} ${theme.muted("—")} ${theme.accentDim(tagline)}`;
}
const line1 = `${theme.heading(title)} ${theme.info(version)} ${theme.muted(
2026-01-08 05:32:49 +01:00
`(${commitLabel})`,
2026-01-22 08:49:51 +00:00
)}`;
2026-01-24 01:26:17 +00:00
const line2 = `${" ".repeat(prefix.length)}${theme.accentDim(tagline)}`;
2026-01-22 08:49:51 +00:00
return `${line1}\n${line2}`;
2026-01-08 05:32:49 +01:00
}
2026-01-22 08:49:51 +00:00
if (fitsOnOneLine) {
return plainFullLine;
}
const line1 = `${title} ${version} (${commitLabel})`;
2026-01-24 01:26:17 +00:00
const line2 = `${" ".repeat(prefix.length)}${tagline}`;
2026-01-22 08:49:51 +00:00
return `${line1}\n${line2}`;
2026-01-08 05:32:49 +01:00
}
2026-01-09 07:51:32 +01:00
const LOBSTER_ASCII = [
"▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄",
"██░▄▄▄░██░▄▄░██░▄▄▄██░▀██░██░▄▄▀██░████░▄▄▀██░███░██",
"██░███░██░▀▀░██░▄▄▄██░█░█░██░█████░████░▀▀░██░█░█░██",
"██░▀▀▀░██░█████░▀▀▀██░██▄░██░▀▀▄██░▀▀░█░██░██▄▀▄▀▄██",
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀",
" 🦞 OPENCLAW 🦞 ",
2026-01-28 11:39:35 -05:00
" ",
2026-01-09 07:51:32 +01:00
];
export function formatCliBannerArt(options: BannerOptions = {}): string {
const rich = options.richTty ?? isRich();
if (!rich) {
return LOBSTER_ASCII.join("\n");
}
2026-01-09 07:51:32 +01:00
const colorChar = (ch: string) => {
if (ch === "█") {
return theme.accentBright(ch);
}
if (ch === "░") {
return theme.accentDim(ch);
}
if (ch === "▀") {
return theme.accent(ch);
}
2026-01-09 07:51:32 +01:00
return theme.muted(ch);
};
const colored = LOBSTER_ASCII.map((line) => {
2026-01-30 03:15:10 +01:00
if (line.includes("OPENCLAW")) {
2026-01-09 07:51:32 +01:00
return (
theme.muted(" ") +
theme.accent("🦞") +
2026-01-30 03:15:10 +01:00
theme.info(" OPENCLAW ") +
2026-01-09 07:51:32 +01:00
theme.accent("🦞")
);
}
2026-01-09 08:51:22 +01:00
return splitGraphemes(line).map(colorChar).join("");
2026-01-09 07:51:32 +01:00
});
return colored.join("\n");
}
2026-01-08 04:44:11 +00:00
export function emitCliBanner(version: string, options: BannerOptions = {}) {
if (bannerEmitted) {
return;
}
2026-01-08 05:32:49 +01:00
const argv = options.argv ?? process.argv;
if (!process.stdout.isTTY) {
return;
}
if (hasJsonFlag(argv)) {
return;
}
if (hasVersionFlag(argv)) {
return;
}
2026-01-08 05:32:49 +01:00
const line = formatCliBannerLine(version, options);
process.stdout.write(`\n${line}\n\n`);
bannerEmitted = true;
}
2026-01-10 19:57:24 +01:00
export function hasEmittedCliBanner(): boolean {
return bannerEmitted;
}