@bunli/utils
Shared utilities for CLI development
Installation
bash bun add @bunli/utils
bash npm install @bunli/utils
bash pnpm add @bunli/utils
Colors
Terminal colors with automatic detection and fallback.
Basic Colors
import { colors } from "@bunli/utils";
// Basic colors
console.log(colors.red("Error!"));
console.log(colors.green("Success!"));
console.log(colors.yellow("Warning!"));
console.log(colors.blue("Info"));
console.log(colors.magenta("Debug"));
console.log(colors.cyan("Note"));
console.log(colors.gray("Disabled"));
// Bright colors
console.log(colors.brightRed("Critical error!"));
console.log(colors.brightGreen("Great success!"));
console.log(colors.brightYellow("Important warning!"));Text Styles
// Modifiers
console.log(colors.bold("Bold text"));
console.log(colors.dim("Dimmed text"));
console.log(colors.italic("Italic text"));
console.log(colors.underline("Underlined"));
console.log(colors.strikethrough("Strikethrough"));
// Strip ANSI codes
const colored = colors.red("Error!");
const plain = colors.strip(colored); // 'Error!'Background Colors
console.log(colors.bgRed("Error background"));
console.log(colors.bgGreen("Success background"));
console.log(colors.bgYellow.black("Warning"));Color Detection
// Colors automatically disabled when:
// - Not in TTY environment (process.stdout.isTTY === false)
// - NO_COLOR environment variable is set
// - CI environment detected
const message = colors.red("Error!"); // Returns plain 'Error!' if colors disabledXDG Directory Paths
Platform-standard directories following the XDG base directory specification.
import { configDir, dataDir, stateDir, cacheDir } from "@bunli/utils";configDir
Returns the platform-standard config directory:
- Linux/macOS:
$XDG_CONFIG_HOME/<appName>or~/.config/<appName> - Windows:
%APPDATA%/<appName>or~/AppData/Roaming/<appName>
const dir = configDir("my-cli");
// Linux: /home/user/.config/my-cli
// macOS: /Users/user/.config/my-cli
// Windows: C:\Users\user\AppData\Roaming\my-clidataDir
Returns the platform-standard data directory:
- Linux/macOS:
$XDG_DATA_HOME/<appName>or~/.local/share/<appName> - Windows:
%LOCALAPPDATA%/<appName>/Data
const dir = dataDir("my-cli");
// Linux: /home/user/.local/share/my-clistateDir
Returns the platform-standard state directory:
- Linux/macOS:
$XDG_STATE_HOME/<appName>or~/.local/state/<appName> - Windows:
%LOCALAPPDATA%/<appName>/State
const dir = stateDir("my-cli");
// Linux: /home/user/.local/state/my-clicacheDir
Returns the platform-standard cache directory:
- Linux/macOS:
$XDG_CACHE_HOME/<appName>or~/.cache/<appName> - Windows:
%LOCALAPPDATA%/<appName>/Cache
const dir = cacheDir("my-cli");
// Linux: /home/user/.cache/my-cliTesting with PlatformEnv
All functions accept an optional PlatformEnv parameter for deterministic testing:
import { configDir, type PlatformEnv } from "@bunli/utils";
const testEnv: PlatformEnv = {
platform: "linux",
env: { XDG_CONFIG_HOME: "/test/config" },
homedir: "/test/home",
};
const dir = configDir("my-cli", testEnv);
// Returns: /test/config/my-cliLog Utilities
Structured logging to stderr with level, timestamp, and field support.
import { formatLog, log, type LogLevel, type LogOptions } from "@bunli/utils";formatLog
Format a log message and return an ANSI-styled string:
const msg = formatLog("Server started", { level: "info" });
// Returns: 'INFO Server started'
const msg = formatLog("Connection failed", {
level: "error",
timestamp: true,
prefix: "db",
fields: { host: "localhost", port: 5432 },
});
// Returns: '2024-01-15 10:30:00 ERROR db Connection failed host=localhost port=5432'LogOptions:
| Option | Type | Default | Description |
|---|---|---|---|
level | LogLevel | 'info' | Log level: 'debug' | 'info' | 'warn' | 'error' | 'fatal' |
timestamp | boolean | false | Include timestamp |
timestampFormat | 'iso' | 'short' | 'time' | 'short' | Timestamp format |
prefix | string | — | Prefix shown after level |
separator | string | '=' | Separator between field key and value |
fields | Record<string, string | number | boolean> | — | Structured fields to append |
log
Write a formatted log message directly to stderr:
log("Build complete", { level: "info", timestamp: true });
// Outputs to stderr: '2024-01-15 10:30:00 INFO Build complete\n'Shell Integration
Utilities for shell command composition and stdin/stdout handling.
import { EXIT_CODES, readStdinLines, stripAnsi, writeStdout, writeStdoutLines } from "@bunli/utils";readStdinLines
Read lines from stdin when piped (non-TTY). Returns empty array if stdin is a TTY.
// Piped input: echo -e "line1\nline2" | bun run script.ts
const lines = await readStdinLines();
// Returns: ['line1', 'line2']
// With custom delimiter
const paragraphs = await readStdinLines("\n\n");writeStdout
Write to stdout, automatically stripping ANSI codes when piping to non-TTY:
writeStdout(colors.green("Success!"));
// TTY: outputs green text
// Pipe: outputs plain 'Success!'writeStdoutLines
Write multiple values to stdout, one per line:
writeStdoutLines(["item1", "item2", "item3"]);stripAnsi
Remove ANSI escape codes from a string:
const plain = stripAnsi("\x1b[32mSuccess!\x1b[0m");
// Returns: 'Success!'EXIT_CODES
Conventional exit codes matching gum conventions:
import { EXIT_CODES } from "@bunli/utils";
process.exit(EXIT_CODES.SUCCESS); // 0
process.exit(EXIT_CODES.CANCEL); // 1
process.exit(EXIT_CODES.TIMEOUT); // 124
process.exit(EXIT_CODES.SIGINT); // 130Validation
Schema validation helpers that work with Standard Schema v1 libraries (Zod, Valibot, etc.):
import { validate, validateFields, SchemaError } from "@bunli/utils";
import { z } from "zod";validate
Validate a single value against a schema, returning a Result:
const schema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().positive(),
});
const result = await validate(schema, { name: "Alice", email: "alice@example.com", age: 30 });
if (result.ok) {
console.log(result.value); // Fully typed and validated
} else {
console.error(result.error); // SchemaError with issues
}validateFields
Validate multiple fields at once with aggregated errors:
const schemas = {
name: z.string().min(1),
email: z.string().email(),
port: z.number().int().positive(),
};
const result = await validateFields(schemas, {
name: "John",
email: "invalid-email",
port: -1,
});
if (result.ok) {
console.log(result.value.name); // 'John'
console.log(result.value.port); // typed as number
} else {
// result.error is Record<string, string[]>
for (const [field, errors] of Object.entries(result.error)) {
console.error(`${field}: ${errors.join(", ")}`);
}
}SchemaError
Error class for validation failures:
import { SchemaError } from "@bunli/utils";
// Also re-exported from @standard-schema/utils
const result = await validate(schema, invalidValue);
if (result.error instanceof SchemaError) {
console.log(result.error.issues);
// [{ path: ['email'], message: 'Invalid email' }]
}getDotPath
Re-exported from @standard-schema/utils. Extracts the dot-notation path string from a Standard Schema validation issue:
import { getDotPath } from "@bunli/utils";
// Given a validation issue with path segments
const issue = { message: "Invalid email", path: ["user", "profile", "email"] };
getDotPath(issue); // 'user.profile.email'
// Issue without a path
const rootIssue = { message: "Invalid input" };
getDotPath(rootIssue); // undefined
// Useful for formatting validation error messages
const result = await validate(schema, input);
if (!result.ok) {
for (const issue of result.error.issues) {
const path = getDotPath(issue);
console.error(`${path ?? "(root)"}: ${issue.message}`);
}
}Also exported from @standard-schema/utils directly:
import { getDotPath } from "@standard-schema/utils";Best Practices
-
Use appropriate log levels:
log("Debug info", { level: "debug" }); log("Server started", { level: "info" }); log("Connection failed", { level: "error" }); -
Use XDG directories for user data:
const configPath = configDir("my-cli"); const dataPath = dataDir("my-cli"); -
Handle shell stdin/stdout correctly:
// Read piped input const lines = await readStdinLines(); if (lines.length === 0) { // No piped input - read from interactive prompt } -
Use structured logging in production:
log("Request processed", { level: "info", timestamp: true, fields: { method: "GET", status: 200, duration: 45 }, }); -
Use colors for terminal output, not logging:
// Good: colors for terminal display console.log(colors.green("✓") + " Done"); // Use log() for structured logging to stderr log("Task completed", { level: "info" });
All utilities respect NO_COLOR and CI environment variables, automatically adjusting their
behavior for different environments.