sugarcraft / sugar-cli
Opinionated CLI presentation helpers shaped after charmbracelet/fang's output layer: StatusLine (✓/✗/⚠/ℹ), Banner, Section header, Stage step, HelpText page, Frame, Logo, and stock themes (ansi/plain/charm/dracula/nord/catppuccin, plus terminal-adaptive auto).
Requires
- php: ^8.3
- ext-mbstring: *
- sugarcraft/sugar-core: dev-master
- sugarcraft/sugar-style: dev-master
Requires (Dev)
- phpunit/phpunit: ^10.5
- sugarcraft/sugar-testing: dev-master
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-05 01:01:41 UTC
README
SugarCli
Formerly published as
sugarcraft/candy-kit(renamed 2026-10). Namespace changed:SugarCraft\Kit\→SugarCraft\Cli\.
composer require sugarcraft/sugar-cli
sugar-cli — opinionated CLI presentation helpers that turn ordinary command-line output into something that matches the rest of the SugarCraft stack, for PHP 8.3+. SugarCli is library-only — drop it into any Composer project, no Symfony Console requirement.
<?php use SugarCraft\Cli\StatusLine; use SugarCraft\Cli\Banner; echo Banner::title('MyApp', 'v0.1.0'), "\n\n"; echo StatusLine::info('connecting to https://example.com'), "\n"; echo StatusLine::success('done in 0.4s'), "\n"; echo StatusLine::warn('disk almost full'), "\n"; echo StatusLine::error('connection refused'), "\n";
Components
Theme— palette ofSugarCraft\Style\Styleobjects keyed by status level (success / error / warn / info / prompt / accent / muted).Theme::ansi()ships the colourful palette;Theme::plain()strips all colour;Theme::charm(),dracula(),nord()andcatppuccin()are fixed presets;Theme::auto()adapts to the background colour the terminal reports, andTheme::byName('nord')resolves any of them case-insensitively (roster inTheme::PRESET_NAMES). Bring your own theme by passing styles to the constructor, or chainTheme::build()— the builder requires all seven slots beforebuild()succeeds.Theme::detect()is theansi()palette downgraded to what the output can show, via sugar-core'sColorProfile::detect(): no escape bytes at all when STDOUT is not a tty (myapp --help | less), colour dropped but bold kept underNO_COLORon a terminal, full colour forced byCLICOLOR_FORCE/FORCE_COLOR. As in thecolorprofiledesign, tty-ness is checked beforeNO_COLOR, so a pipe gets plain text whether or notNO_COLORis set. Every presenter called without a theme usesTheme::detect(); an explicitly passed theme is rendered as given — apply the same downgrade to any preset with$theme->withColorProfile(\SugarCraft\Core\Util\ColorProfile::detect(null, STDOUT))(detect()throws anInvalidArgumentExceptionwhen a stream you pass explicitly is not an open resource; the implicitSTDOUTsubstitute never errors).StatusLine—success/error/warn/info/promptstatic helpers returning a themed glyph + space + message. Only the glyph and its space are styled; the message itself stays plain. Every call takes a trailingint|AutoWidth|null $widthbudget: an over-long message is clipped with…to fit (the glyph is never clipped;nullrenders the full message).Banner— render a bordered title block with optional subtitle, rounded by default. Useful for app intros /--versionoutput.Logo— Unicode box-drawing text art:Logo::sugarcraft()(70 cells wide, deliberately not clamped to the terminal) plusLogo::fromAscii($art)for custom multi-line art. Control and non-SGR escape bytes are stripped on ingress, and newlines are the layout;->withColor($hex)tints the art, and a second call replaces the tint.Section— one-line themed dividers:header()label + fill rule, barerule(), and indentedsubHeader()for nesting under a parent. An explicitwidthis a hard cap: an over-long label is cut with…rather than overflowing the line. Omittedwidthdefaults to the terminal width (exportedCOLUMNS, else 80 cells);nullopts out.Stage— per-line step renderers for progressive CLI output:step()(numbered),subStep(), andsubStepWithProgress()(bar or spinner). Messages take the same width budget asStatusLine(tree glyphs and the progress bar are never clipped — the bar's cells are reserved before the message is measured).subStepWithProgress()also takes a trailing?int $frameto pin the spinner glyph whenever bytes must be deterministic (goldens, recorded sessions); omitted, it walks the wall clock.HelpText—--helppage:USAGE(retitle via theusageLabelparameter), an optional description, and titled two-columnKEY descriptionsections. Wraps towidth(omitted = the terminal width via exportedCOLUMNS, else 80 cells;null= never wrap): long descriptions continue at the description column, and switch to a stacked layout (description under the key) when the key column leaves fewer thanHelpText::MIN_DESCRIPTION_WIDTH(16) cells. Keys and section titles are never wrapped or cut — in stacked mode a very long key can overflow the width, so keep keys short.renderRows()renders just the key/description grid, andrenderPage()renders a complete pre-composed page without re-wrapping it.Frame— full-screen application chrome: a double-line box that fills the terminal exactly (Frame::new()->withTitle($bar) ->withStatus($bar)->render($body, $cols, $rows)), with a centred title bar, dividers, and a status bar. Normalises the body to a constant line count and pads ANSI-width-aware so it never overflows the terminal — safe for a SugarCraft app whose frame-diff renderer owns the screen. Titles, status lines and body lines may carry inline SGR styling; every other escape and control byte is stripped on ingress (the SGR-preservingSafeText::styledLine()gate), andwithTitleText()/withBorderStyle()let you style plain text without building escapes yourself. An unclosed SGR on width-fitting content can bleed into the surrounding padding — close your styles or pass them throughwithTitleText().AutoWidth— theint|AutoWidth|null $widthsentinel shared by the presenters:AutoWidth::Autoresolves the terminal width (exportedCOLUMNS, else 80 cells; implausibly large advertised values are discarded as junk), anintmeans exactly that many cells, andnullrenders without a cap. Most presenters default toAuto; the two opt-outs areBanner::title()(nullsizes the box to its content) andHelpText::renderPage()(nullnever re-wraps).
What this is not
sugar-cli is the presentation half of a CLI: it renders strings and leaves every other decision to you.
- No argument parsing, exit codes or output routing. Every helper returns a string; whether it goes to STDOUT or STDERR and what the process exits with is the application's call.
- No interaction machinery. Line editing and prompts live in
sugar-readline; scripted TUI shells incandy-scriptui. This library only decorates the text you already decided to print. - No shell-outs and no terminal ioctls. Width comes from the
environment or an explicit parameter — see
AutoWidthabove. - The charmbracelet/fang label describes the shape of the output, not a parity promise: sugar-cli renders the same kind of status and help pages, without cobra-style typed flag metadata (required/optional/default markers are whatever you put in your key strings).
Demos
CLI page
Tests
cd sugar-cli && composer install && vendor/bin/phpunit
Snapshot tests
Presenter output is pinned via sugar-testing's assertGoldenAnsi
golden-file snapshots under tests/fixtures/. The comparison is
byte-exact and the fixtures are create-only: an existing golden is
never rewritten by a test run, so an intended change to a render must be
re-recorded deliberately — delete the fixture, run once with
UPDATE_GOLDENS=1 vendor/bin/phpunit (or SUGAR_TESTING_UPDATE_GOLDENS=1)
to mint the new canonical bytes, then re-run plain to confirm it holds.
Review the diff before committing: golden churn means every user sees a
different render.
Credits & inspiration
Originally inspired by the Go Charm ecosystem; SugarCraft is developed as a native PHP project.
