Simple design system for CLIs and CI tools.
When you make a CLI tool, especially one that runs on CI or outputs to a log file, it can be challenging to make the output be human-readable, easy to scan at a glance, and structured enough that users can quickly find the information they need. This is especially true when color and rich formatting options are not available. This tool aims to solve all of these problems with a simple set of design elements that can be combined to create beautiful, readable output.
A well-formed program should generally follow this structure:
- Document Header (TITLE)
- One short intro (NOTE / PLAN) — what this run will do
- Phases (H1 sections) in order
- Inside each phase: repeated Steps (H2 + commands + result)
- Optional: Artifacts, Metrics, Debug
- Summary + Footer (DONE)
========================================================================
RELEASE PIPELINE — payments-api
========================================================================
NOTE: Build the service, run tests, and publish artifacts for deployment.
------------------------------------------------------------------------
## Setup
------------------------------------------------------------------------
WHY: Ensure deterministic tooling and consistent dependencies in CI.
ENV:
node: 20.11.1
pnpm: 9.1.0
os: ubuntu-22.04
----------
### Enable toolchain
----------
$ corepack enable
$ node --version
$ pnpm --version
OK: toolchain ready
----------
### Install dependencies
----------
$ pnpm install --frozen-lockfile
OK: dependencies installed
------------------------------------------------------------------------
## Build
------------------------------------------------------------------------
PLAN: Produce production-ready output in `dist/`.
----------
### Compile TypeScript
----------
$ pnpm run build
OK: build finished in 18s
----------
### Bundle server
----------
$ pnpm run bundle
OK: bundle created
ARTIFACTS:
- dist/server.js
- dist/server.js.map
------------------------------------------------------------------------
## Test
------------------------------------------------------------------------
----------
### Unit tests
----------
$ pnpm test
OK: unit tests passed
----------
### Integration tests
----------
$ pnpm run test:integration
!!! WARNING: integration tests skipped
detail: INTEGRATION=0
------------------------------------------------------------------------
## Deploy (dry run)
------------------------------------------------------------------------
NOTE: Validate deploy commands without publishing changes.
----------
### Validate deploy script
----------
$ ./scripts/deploy.sh --dry-run
OK: deploy validation passed
METRICS:
setup_time: 22s
build_time: 18s
test_time: 41s
total_time: 1m21s
DEBUG:
git_sha: 1a2b3c4d
branch: main
runner: github-actions
SUMMARY:
status: success
duration: 1m21s
artifacts: 2
DONE: RELEASE PIPELINE — payments-apiChoose how you want to use sh-style:
- CLI Tool - Use the
logcommand in your shell scripts and CI pipelines - Deno, Bun, and Node.js Library - Import and use sh-style directly in Deno, Node.js, or Bun
- Go Library - Import and use sh-style directly in your Go programs
A CLI tool implementing the sh-style design system for plain-text CI typography.
Download a pre-built binary from the GitHub Releases page. Binaries are available for Linux (x86_64, aarch64), macOS (x86_64, aarch64), and Windows (x86_64).
go build -o log .
./log title "Hello World"========================================================================
Hello World
========================================================================
The log CLI supports two modes:
- Direct subcommands - Generate individual elements (e.g.,
log phase "Setup") - JSONL rendering - Read structured events from stdin (
log render)
log title <text...> # Document title (centered, single rules)
log phase <text...> # H1 section (full-width rules)
log step <text...> # H2 subsection (short rules)log msg <text...> # Plain paragraph (no prefix)
log note <text...> # NOTE: message
log why <text...> # WHY: message
log plan <text...> # PLAN: message
log ok <text...> # OK: message
log done <text...> # DONE: messagelog cmd <text...> # Display shell command with $ prefixlog warn <text...> [--detail <line>]... # Warning with optional details
log error <line1> [line2]... # Error box with multiple lineslog kv <LABEL> <key=value>... # Key-value block (ENV, SUMMARY, etc.)
log list <LABEL> <item>... # List block (ARTIFACTS, FILES, etc.)Stream JSONL commands from stdin:
log render # Read JSONL from stdin and renderExample:
echo '{"command":"title","lines":["Hello World"]}' | log render
cat events.jsonl | log renderJSONL Format: All commands use consistent structure with command and lines properties:
{"command":"title","lines":["My Title"]}
{"command":"phase","lines":["Setup"]}
{"command":"step","lines":["Install dependencies"]}
{"command":"msg","lines":["Plain paragraph text with no label prefix."]}
{"command":"note","lines":["Starting process..."]}
{"command":"cmd","lines":["npm install"]}
{"command":"ok","lines":["build completed"]}
{"command":"warn","lines":["Warning text","detail 1","detail 2"]}
{"command":"error","lines":["Error line 1","Error line 2"]}
{"command":"kv","lines":["ENV","node: 20.11.1","os: ubuntu-22.04"]}
{"command":"list","lines":["ARTIFACTS","dist/app.js","dist/app.map"]}Format rules:
- Simple commands (
title,phase,step,msg,note,why,plan,ok,done,cmd):lines[0]is the text warn:lines[0]is warning text,lines[1..]are optional detailserror:lines[0..]are all error lineskv:lines[0]is label,lines[1..]are "key: value" pairslist:lines[0]is label,lines[1..]are items
Set the fixed width for rules and boxes using the DOC_WIDTH environment variable (default: 72):
export DOC_WIDTH=80
log title "Wider output"- No truncation - all text is wrapped, never cut off
- Character preservation - including repeated spaces
- Deterministic output - suitable for snapshot testing
- Streaming - render events immediately as they arrive
- Fixed-width output - configurable via
DOC_WIDTH - Automatic spacing - elements add appropriate whitespace automatically
Simple build script:
#!/bin/bash
log title "BUILD PIPELINE"
log phase "Setup"
log cmd "npm install"
log ok "dependencies installed"
log phase "Build"
log step "Compile TypeScript"
log cmd "npm run build"
log ok "build completed in 12s"
log step "Bundle application"
log cmd "npm run bundle"
log ok "bundle created"
log list ARTIFACTS dist/app.js dist/app.cssError handling:
if ! npm test; then
log error "Tests failed" "exit code: $?" "run npm test for details"
exit 1
fiUsing JSONL from another language:
cat > build.jsonl << 'EOF'
{"command":"title","lines":["BUILD PIPELINE"]}
{"command":"phase","lines":["Setup"]}
{"command":"cmd","lines":["npm install"]}
{"command":"ok","lines":["dependencies installed"]}
EOF
cat build.jsonl | log renderUse sh-style directly in your Deno, Node.js, or Bun code. This cross-runtime wrapper bundles the compiled Go binary and executes CLI commands under the hood, providing a clean TypeScript API that works across all major JavaScript runtimes.
See the Deno, Bun, and Node.js Library documentation for installation, usage examples, and API reference.
Use sh-style directly in your Go programs by importing the root package. The library provides both a Logger interface for structured logging and standalone functions for quick one-off formatting.
go get github.com/levibostian/sh-styleCreate a logger instance for structured output:
package main
import (
"github.com/levibostian/sh-style"
)
func main() {
// Create a logger with width 72
logger := shstyle.NewLogger(72)
logger.Title("My Build Script")
logger.Phase("Setup")
logger.Step("Installing dependencies")
logger.Cmd("go mod download")
logger.Ok("Dependencies installed")
logger.Done("Setup complete!")
}Create logger from environment:
// Uses DOC_WIDTH env var (defaults to 72)
logger := shstyle.NewLoggerFromEnv()
logger.Title("My Application")Custom width:
logger := shstyle.NewLogger(50)
logger.Title("Narrow Output")
// Or change width later
logger.SetWidth(80)For quick one-off formatting, use the standalone functions:
package main
import (
"github.com/levibostian/sh-style"
)
func main() {
shstyle.Title("Quick Example")
shstyle.Phase("One-off formatting")
shstyle.Note("You can use standalone functions")
shstyle.Done("Example complete!")
}With custom width:
shstyle.TitleWithWidth("Custom Width", 50)
shstyle.PhaseWithWidth("Narrow output", 50)logger.Title(text string) // Document title (centered, single rules)
logger.Phase(text string) // H1 section (full-width rules)
logger.Step(text string) // H2 subsection (short rules)logger.Msg(text string) // Plain paragraph (no prefix)
logger.Note(text string) // NOTE: message
logger.Why(text string) // WHY: message
logger.Plan(text string) // PLAN: message
logger.Ok(text string) // OK: message
logger.Done(text string) // DONE: messagelogger.Cmd(text string) // Display shell command with $ prefixlogger.Warn(text string, details ...string) // Warning with optional details
logger.Error(lines ...string) // Error box with multiple lineslogger.Kv(label string, entries [][2]string) // Key-value block
logger.List(label string, items []string) // List blockSet the fixed width for rules and boxes using the DOC_WIDTH environment variable (default: 72):
import "os"
os.Setenv("DOC_WIDTH", "80")
logger := shstyle.NewLoggerFromEnv()
logger.Title("Wider output")Or use explicit width:
logger := shstyle.NewLogger(80)
logger.Title("Wider output")- No truncation - all text is wrapped, never cut off
- Character preservation - including repeated spaces
- Deterministic output - suitable for snapshot testing
- Fixed-width output - configurable via
DOC_WIDTHor constructor - Automatic spacing - elements add appropriate whitespace automatically
- Zero dependencies - pure Go implementation
Build script:
package main
import (
"github.com/levibostian/sh-style"
)
func main() {
logger := shstyle.NewLogger(72)
logger.Title("BUILD PIPELINE")
logger.Phase("Setup")
logger.Cmd("go mod download")
logger.Ok("dependencies installed")
logger.Phase("Build")
logger.Step("Compile application")
logger.Cmd("go build -o app .")
logger.Ok("build completed")
logger.List("Artifacts", []string{
"app",
"README.md",
"LICENSE",
})
}Environment variables:
logger.Kv("Build Environment", [][2]string{
{"GOOS", "linux"},
{"GOARCH", "amd64"},
{"CGO_ENABLED", "0"},
})Error handling:
if err := runTests(); err != nil {
shstyle.Error("Tests failed", err.Error(), "run go test for details")
os.Exit(1)
}Warnings:
logger.Warn("Deprecated function used",
"Function 'OldAPI' is deprecated",
"Use 'NewAPI' instead",
)Install Go (optionally run mise install to install it).
Build the CLI for your current platform:
make buildBuild binaries for all platforms (Linux, macOS, Windows × amd64/arm64):
make build-allThis compiles 6 binaries to dist/ and copies them to deno/bin/ for the Deno wrapper.
Run all tests:
make testOr manually:
make build # Required first
go test -v .The test suite includes:
- Render mode tests - Test JSONL parsing and rendering
- Wrapper tests - Test CLI, Deno, and Go wrapper consistency
- Error handling tests - Test CLI argument validation
All wrapper tests compare against the golden output file at tests/expected-output.txt.
make clean- Remove all built binariesmake help- Show all available targets
shstyle.go # Public API - standalone functions
logger.go # Logger type and methods
render.go # Rendering functions for all elements
wrap.go # Text wrapping & formatting utilities
cli/
main.go # CLI entrypoint
commands.go # CLI argument parsing & command types
tests/
expected-output.txt # Single source of truth for wrapper output
scripts/
test-cli.sh # CLI wrapper test script
test-deno.ts # Deno wrapper test script
test-go.go # Go wrapper test script
fixtures/
happy.jsonl # Example JSONL input
happy.out # Expected output
deno/
mod.ts # Deno wrapper library (execs Go binary)
deno.json # JSR package config
scripts/
build-binaries.ts # Cross-compile Go binaries for Deno wrapper
MIT (see LICENSE file in repository root)