mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-14 03:20:49 +00:00
* CLI: include commit hash in --version output * fix(version): harden commit SHA resolution and keep output consistent * CLI: keep install checks compatible with commit-tagged version output * fix(cli): include commit hash in root version fast path * test(cli): allow null commit-hash mocks * Installer: share version parser across install scripts * Installer: avoid sourcing helpers from stdin cwd * CLI: note commit-tagged version output * CLI: anchor commit hash resolution to module root * CLI: harden commit hash resolution * CLI: fix commit hash lookup edge cases * CLI: prefer live git metadata in dev builds * CLI: keep git lookup inside package root * Infra: tolerate invalid moduleUrl hints * CLI: cache baked commit metadata fallbacks * CLI: align changelog attribution with prep gate * CLI: restore changelog contributor credit --------- Co-authored-by: echoVic <echovic@163.com> Co-authored-by: echoVic <echoVic@users.noreply.github.com>
166 lines
5.3 KiB
TypeScript
166 lines
5.3 KiB
TypeScript
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 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 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, 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;
|
|
}
|