mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 21:20:22 +00:00
feat(cli): add configurable banner tagline mode
This commit is contained in:
60
src/cli/banner.test.ts
Normal file
60
src/cli/banner.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const loadConfigMock = vi.fn();
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
loadConfig: loadConfigMock,
|
||||
}));
|
||||
|
||||
let formatCliBannerLine: typeof import("./banner.js").formatCliBannerLine;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ formatCliBannerLine } = await import("./banner.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
loadConfigMock.mockReset();
|
||||
loadConfigMock.mockReturnValue({});
|
||||
});
|
||||
|
||||
describe("formatCliBannerLine", () => {
|
||||
it("hides tagline text when cli.banner.taglineMode is off", () => {
|
||||
loadConfigMock.mockReturnValue({
|
||||
cli: { banner: { taglineMode: "off" } },
|
||||
});
|
||||
|
||||
const line = formatCliBannerLine("2026.3.3", {
|
||||
commit: "abc1234",
|
||||
richTty: false,
|
||||
});
|
||||
|
||||
expect(line).toBe("🦞 OpenClaw 2026.3.3 (abc1234)");
|
||||
});
|
||||
|
||||
it("uses default tagline when cli.banner.taglineMode is default", () => {
|
||||
loadConfigMock.mockReturnValue({
|
||||
cli: { banner: { taglineMode: "default" } },
|
||||
});
|
||||
|
||||
const line = formatCliBannerLine("2026.3.3", {
|
||||
commit: "abc1234",
|
||||
richTty: false,
|
||||
});
|
||||
|
||||
expect(line).toBe("🦞 OpenClaw 2026.3.3 (abc1234) — All your chats, one OpenClaw.");
|
||||
});
|
||||
|
||||
it("prefers explicit tagline mode over config", () => {
|
||||
loadConfigMock.mockReturnValue({
|
||||
cli: { banner: { taglineMode: "off" } },
|
||||
});
|
||||
|
||||
const line = formatCliBannerLine("2026.3.3", {
|
||||
commit: "abc1234",
|
||||
richTty: false,
|
||||
mode: "default",
|
||||
});
|
||||
|
||||
expect(line).toBe("🦞 OpenClaw 2026.3.3 (abc1234) — All your chats, one OpenClaw.");
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,9 @@
|
||||
import { loadConfig } from "../config/config.js";
|
||||
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 { pickTagline, type TaglineOptions } from "./tagline.js";
|
||||
import { pickTagline, type TaglineMode, type TaglineOptions } from "./tagline.js";
|
||||
|
||||
type BannerOptions = TaglineOptions & {
|
||||
argv?: string[];
|
||||
@@ -35,18 +36,42 @@ const hasJsonFlag = (argv: string[]) =>
|
||||
const hasVersionFlag = (argv: string[]) =>
|
||||
argv.some((arg) => arg === "--version" || arg === "-V") || hasRootVersionAlias(argv);
|
||||
|
||||
function parseTaglineMode(value: unknown): TaglineMode | undefined {
|
||||
if (value === "random" || value === "default" || value === "off") {
|
||||
return value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveTaglineMode(options: BannerOptions): TaglineMode | undefined {
|
||||
const explicit = parseTaglineMode(options.mode);
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
try {
|
||||
return parseTaglineMode(loadConfig().cli?.banner?.taglineMode);
|
||||
} catch {
|
||||
// Fall back to default random behavior when config is missing/invalid.
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatCliBannerLine(version: string, options: BannerOptions = {}): string {
|
||||
const commit = options.commit ?? resolveCommitHash({ env: options.env });
|
||||
const commitLabel = commit ?? "unknown";
|
||||
const tagline = pickTagline(options);
|
||||
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 plainFullLine = `${title} ${version} (${commitLabel}) — ${tagline}`;
|
||||
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)}`;
|
||||
@@ -54,13 +79,19 @@ export function formatCliBannerLine(version: string, options: BannerOptions = {}
|
||||
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 = `${title} ${version} (${commitLabel})`;
|
||||
const line1 = plainBaseLine;
|
||||
if (!tagline) {
|
||||
return line1;
|
||||
}
|
||||
const line2 = `${" ".repeat(prefix.length)}${tagline}`;
|
||||
return `${line1}\n${line2}`;
|
||||
}
|
||||
|
||||
21
src/cli/tagline.test.ts
Normal file
21
src/cli/tagline.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DEFAULT_TAGLINE, pickTagline } from "./tagline.js";
|
||||
|
||||
describe("pickTagline", () => {
|
||||
it("returns empty string when mode is off", () => {
|
||||
expect(pickTagline({ mode: "off" })).toBe("");
|
||||
});
|
||||
|
||||
it("returns default tagline when mode is default", () => {
|
||||
expect(pickTagline({ mode: "default" })).toBe(DEFAULT_TAGLINE);
|
||||
});
|
||||
|
||||
it("keeps OPENCLAW_TAGLINE_INDEX behavior in random mode", () => {
|
||||
const value = pickTagline({
|
||||
mode: "random",
|
||||
env: { OPENCLAW_TAGLINE_INDEX: "0" } as NodeJS.ProcessEnv,
|
||||
});
|
||||
expect(value.length).toBeGreaterThan(0);
|
||||
expect(value).not.toBe(DEFAULT_TAGLINE);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
const DEFAULT_TAGLINE = "All your chats, one OpenClaw.";
|
||||
export type TaglineMode = "random" | "default" | "off";
|
||||
|
||||
const HOLIDAY_TAGLINES = {
|
||||
newYear:
|
||||
@@ -248,6 +249,7 @@ export interface TaglineOptions {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
random?: () => number;
|
||||
now?: () => Date;
|
||||
mode?: TaglineMode;
|
||||
}
|
||||
|
||||
export function activeTaglines(options: TaglineOptions = {}): string[] {
|
||||
@@ -260,6 +262,12 @@ export function activeTaglines(options: TaglineOptions = {}): string[] {
|
||||
}
|
||||
|
||||
export function pickTagline(options: TaglineOptions = {}): string {
|
||||
if (options.mode === "off") {
|
||||
return "";
|
||||
}
|
||||
if (options.mode === "default") {
|
||||
return DEFAULT_TAGLINE;
|
||||
}
|
||||
const env = options.env ?? process.env;
|
||||
const override = env?.OPENCLAW_TAGLINE_INDEX;
|
||||
if (override !== undefined) {
|
||||
|
||||
Reference in New Issue
Block a user