feat(diary): component loggers through a redacting console and daily stack
- New diary module: For, Bootstrap, Setup, SetVerbosity, Reset, Close, ParseConfig - Proxy component handler safe in package vars; styled console handler in bonfire's palette - Daily writer: one file per day, 0640, lazy open, retention, disables itself on open failure - Every channel leaf wrapped by sunscreen.Wrap; stack via slog.NewMultiHandler - bonfire: ColorMode, Palette, DefaultPalette, DetectProfile (lipgloss v2, colorprofile) - Generated main runs diary.Bootstrap first and diary.Setup after compass.Load - Ignore /storage/ and /examples/hello/storage/
This commit is contained in:
@@ -12,7 +12,7 @@ bonfire is the console layer of SummerCMS. Plugins and framework modules describ
|
||||
|
||||
## Role in the framework
|
||||
|
||||
A dependency-free base layer for console commands. `pact` uses it so plugins can contribute commands, and `lagoon`, `conga`, `cabana`, `lighthouse`, `flare` and `surf` build their own commands (migrate, workers, serve, admin tools) on it. Keeping it separate means every module describes commands the same way without depending on the CLI binary.
|
||||
A base layer for console commands with no framework dependencies. `pact` uses it so plugins can contribute commands, and `lagoon`, `conga`, `cabana`, `lighthouse`, `flare` and `surf` build their own commands (migrate, workers, serve, admin tools) on it. Keeping it separate means every module describes commands the same way without depending on the CLI binary.
|
||||
|
||||
Use cases:
|
||||
|
||||
@@ -107,6 +107,10 @@ if catalog.Has("blog:import") {
|
||||
| `bonfire.ErrUnknownCommand` | Returned by `bonfire.Call` when no command has the name. |
|
||||
| `bonfire.Catalog` | An immutable copy of a binary's command list; the generated app main publishes one on the app. |
|
||||
| `bonfire.NewCatalog` | Builds a `bonfire.Catalog` from a command slice. |
|
||||
| `bonfire.ColorMode` | How console output decides on color: `bonfire.ColorAuto` (terminal and environment), `bonfire.ColorAlways` or `bonfire.ColorNever`. |
|
||||
| `bonfire.Palette` | The console styles (`Success`, `Current`, `Warning`, `Error`, `ErrorBold`, `Muted`, `Header`), shared with the `diary` console log lines. |
|
||||
| `bonfire.DefaultPalette` | Returns the palette ported from SSU's status styles: green success, cyan current, yellow warning, red and bold red errors, gray muted text, bold headers. |
|
||||
| `bonfire.DetectProfile` | Returns the color profile for a writer under a `bonfire.ColorMode`, honouring `NO_COLOR`, `TERM=dumb` and `FORCE_COLOR`; render palette styles through a `colorprofile.Writer` with it. |
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -123,7 +127,7 @@ Without these variables, color is enabled only when stdout is a terminal.
|
||||
## Dependencies
|
||||
|
||||
- SummerCMS modules: none.
|
||||
- Third-party: `github.com/spf13/cobra`, `golang.org/x/term`.
|
||||
- Third-party: `github.com/spf13/cobra`, `golang.org/x/term`, `charm.land/lipgloss/v2` and `github.com/charmbracelet/colorprofile` (decided in `.planning/notes/2026-10-21-console-charm-stack.md`).
|
||||
- Standard library: `bufio`, `context`, `errors`, `fmt`, `io`, `os`, `strconv`, `strings`, `sync`, `time`, `unicode/utf8`.
|
||||
|
||||
## Testing
|
||||
|
||||
100
modules/bonfire/styles.go
Normal file
100
modules/bonfire/styles.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package bonfire
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/charmbracelet/colorprofile"
|
||||
)
|
||||
|
||||
// ColorMode selects how console output decides on color: from the terminal
|
||||
// and the environment, always, or never.
|
||||
type ColorMode int
|
||||
|
||||
const (
|
||||
// ColorAuto colors output when the writer is a terminal and the
|
||||
// environment allows it (NO_COLOR and TERM=dumb disable color,
|
||||
// FORCE_COLOR enables it on any writer).
|
||||
ColorAuto ColorMode = iota
|
||||
// ColorAlways colors output whatever the writer is (--ansi).
|
||||
ColorAlways
|
||||
// ColorNever writes no escape codes at all (--no-ansi).
|
||||
ColorNever
|
||||
)
|
||||
|
||||
// Palette is the shared console look: the status styles of list, help,
|
||||
// widgets and log lines. Render through a writer that applies the color
|
||||
// profile (DetectProfile), because lipgloss always emits ANSI codes.
|
||||
type Palette struct {
|
||||
// Success marks completed work (green).
|
||||
Success lipgloss.Style
|
||||
// Current marks the item in progress and informational lines (cyan).
|
||||
Current lipgloss.Style
|
||||
// Warning marks warnings (yellow).
|
||||
Warning lipgloss.Style
|
||||
// Error marks failures (red).
|
||||
Error lipgloss.Style
|
||||
// ErrorBold marks fatal failures and error levels (bold red).
|
||||
ErrorBold lipgloss.Style
|
||||
// Muted marks secondary detail such as times and keys (gray).
|
||||
Muted lipgloss.Style
|
||||
// Header marks headings and names (bold).
|
||||
Header lipgloss.Style
|
||||
}
|
||||
|
||||
// DefaultPalette returns the console palette, ported from SSU's status
|
||||
// styles: green success, cyan current, yellow warning, red and bold red
|
||||
// errors, gray (240) muted text and bold headers.
|
||||
func DefaultPalette() Palette {
|
||||
return Palette{
|
||||
Success: lipgloss.NewStyle().Foreground(lipgloss.Color("2")),
|
||||
Current: lipgloss.NewStyle().Foreground(lipgloss.Color("6")),
|
||||
Warning: lipgloss.NewStyle().Foreground(lipgloss.Color("3")),
|
||||
Error: lipgloss.NewStyle().Foreground(lipgloss.Color("1")),
|
||||
ErrorBold: lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Bold(true),
|
||||
Muted: lipgloss.NewStyle().Foreground(lipgloss.Color("240")),
|
||||
Header: lipgloss.NewStyle().Bold(true),
|
||||
}
|
||||
}
|
||||
|
||||
// DetectProfile returns the color profile for console output written to w.
|
||||
// ColorNever gives colorprofile.NoTTY (no escape codes at all). ColorAlways
|
||||
// gives the detected profile raised to at least 256 colors. ColorAuto treats
|
||||
// a non-empty FORCE_COLOR as ColorAlways unless NO_COLOR is set or TERM is
|
||||
// dumb, and otherwise detects the profile from w and environ, which honours
|
||||
// NO_COLOR and TERM=dumb. A nil environ means os.Environ().
|
||||
func DetectProfile(w io.Writer, mode ColorMode, environ []string) colorprofile.Profile {
|
||||
if mode == ColorNever {
|
||||
return colorprofile.NoTTY
|
||||
}
|
||||
if environ == nil {
|
||||
environ = os.Environ()
|
||||
}
|
||||
if mode == ColorAuto && forceColor(environ) {
|
||||
mode = ColorAlways
|
||||
}
|
||||
p := colorprofile.Detect(w, environ)
|
||||
if mode == ColorAlways && p < colorprofile.ANSI256 {
|
||||
p = colorprofile.ANSI256
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func forceColor(environ []string) bool {
|
||||
if envValue(environ, "NO_COLOR") != "" || envValue(environ, "TERM") == "dumb" {
|
||||
return false
|
||||
}
|
||||
return envValue(environ, "FORCE_COLOR") != ""
|
||||
}
|
||||
|
||||
func envValue(environ []string, key string) string {
|
||||
for i := len(environ) - 1; i >= 0; i-- {
|
||||
k, v, ok := strings.Cut(environ[i], "=")
|
||||
if ok && k == key {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user