import { resolveCommitHash } from "../infra/git-commit.js" ;
import { visibleWidth } from "../terminal/ansi.js" ;
import { isRich, theme } from "../terminal/theme.js" ;
import { hasRootVersionAlias } from "./argv.js" ;
import { parseTaglineMode, readCliBannerTaglineMode } from "./banner-config-lite.js" ;
import { pickTagline, type TaglineMode, type TaglineOptions } from "./tagline.js" ;
type BannerOptions = TaglineOptions & {
argv?: string[];
commit?: string | null ;
columns?: number;
richTty?: boolean ;
};
let bannerEmitted = false ;
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);
}
try {
return Array.from(graphemeSegmenter.segment(value), (seg) => seg.segment);
} catch {
return Array.from(value);
}
}
const hasJsonFlag = (argv: string[]) =>
argv.some((arg) => arg === "--json" || arg.startsWith("--json=" ));
const hasVersionFlag = (argv: string[]) =>
argv.some((arg) => arg === "--version" || arg === "-V" ) || hasRootVersionAlias(argv);
function resolveTaglineMode(options: BannerOptions): TaglineMode | undefined {
const explicit = parseTaglineMode(options.mode);
if (explicit) {
return explicit;
}
return readCliBannerTaglineMode(options.env);
}
export function formatCliBannerLine(version: string, options: BannerOptions = {}): string {
const commit =
options.commit ?? resolveCommitHash({ env: options.env, moduleUrl: import .meta.url });
const commitLabel = commit ?? "unknown" ;
const tagline = pickTagline({ ...options, mode: resolveTaglineMode(options) });
const rich = options.richTty ?? isRich();
const title = " OpenClaw" ;
const prefix = " " ;
const columns = options.columns ?? process.stdout.columns ?? 120 ;
const plainBaseLine = `${title} ${version} (${commitLabel})`;
const plainFullLine = tagline ? `${plainBaseLine} — ${tagline}` : plainBaseLine;
const fitsOnOneLine = visibleWidth(plainFullLine) <= columns;
if (rich) {
if (fitsOnOneLine) {
if (!tagline) {
return `${theme.heading(title)} ${theme.info(version)} ${theme.muted(`(${commitLabel})`)}`;
}
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(
`(${commitLabel})`,
)}`;
if (!tagline) {
return line1;
}
const line2 = `${" " .repeat(prefix.length)}${theme.accentDim(tagline)}`;
return `${line1}\n${line2}`;
}
if (fitsOnOneLine) {
return plainFullLine;
}
const line1 = plainBaseLine;
if (!tagline) {
return line1;
}
const line2 = `${" " .repeat(prefix.length)}${tagline}`;
return `${line1}\n${line2}`;
}
const LOBSTER_ASCII = [
"▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄" ,
"██░▄▄▄░██░▄▄░██░▄▄▄██░▀██░██░▄▄▀██░████░▄▄▀██░███░██" ,
"██░███░██░▀▀░██░▄▄▄██░█░█░██░█████░████░▀▀░██░█░█░██" ,
"██░▀▀▀░██░█████░▀▀▀██░██▄░██░▀▀▄██░▀▀░█░██░██▄▀▄▀▄██" ,
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀" ,
" OPENCLAW " ,
" " ,
];
export function formatCliBannerArt(options: BannerOptions = {}): string {
const rich = options.richTty ?? isRich();
if (!rich) {
return LOBSTER_ASCII.join("\n" );
}
const colorChar = (ch: string) => {
if (ch === "█" ) {
return theme.accentBright(ch);
}
if (ch === "░" ) {
return theme.accentDim(ch);
}
if (ch === "▀" ) {
return theme.accent(ch);
}
return theme.muted(ch);
};
const colored = LOBSTER_ASCII.map((line) => {
if (line.includes("OPENCLAW" )) {
return (
theme.muted(" " ) +
theme.accent("" ) +
theme.info(" OPENCLAW " ) +
theme.accent("" )
);
}
return splitGraphemes(line).map(colorChar).join("" );
});
return colored.join("\n" );
}
export function emitCliBanner(version: string, options: BannerOptions = {}) {
if (bannerEmitted) {
return ;
}
const argv = options.argv ?? process.argv;
if (!process.stdout.isTTY) {
return ;
}
if (hasJsonFlag(argv)) {
return ;
}
if (hasVersionFlag(argv)) {
return ;
}
const line = formatCliBannerLine(version, options);
process.stdout.write(`\n${line}\n\n`);
bannerEmitted = true ;
}
export function hasEmittedCliBanner(): boolean {
return bannerEmitted;
}
Messung V0.5 in Prozent C=100 H=100 G=100
¤ Dauer der Verarbeitung: 0.9 Sekunden
(vorverarbeitet am 2026-06-09)
¤
*© Formatika GbR, Deutschland