v0.27.0 packaging update
Package / arch (push) Canceled after 0s
Package / deb (push) Canceled after 0s
Package / freebsd (push) Canceled after 0s
Package / image (push) Canceled after 0s
Test / test (push) Canceled after 0s
Test / e2e (push) Canceled after 0s

This commit is contained in:
lixen committed 2026-10-05 15:12:37 -04:00
1 parent b5ac9084c7
commit 60abc97eb0
17 files changed
+1194 -43

No files matched your search

+102
View File
@@ -0,0 +1,102 @@
name: Package
on:
push:
branches: [main]
paths: &paths
- Makefile
- Dockerfile
- go.mod
- go.sum
- config/logwisp.toml
- doc/lw.1
- deploy/package/**
- .github/workflows/package.yml
pull_request:
paths: *paths
workflow_dispatch:
permissions:
contents: read
jobs:
# The AUR package from this commit: makepkg finds the tarball the PKGBUILD
# names next to it and skips the download, then builds, tests and packages.
# checkout's safe.directory lasts only its own step; root owns .git here.
arch:
runs-on: ubuntu-latest
timeout-minutes: 20
container: archlinux:base-devel
steps:
- run: pacman -Syu --noconfirm --needed git go
- uses: actions/checkout@v5
- run: |
ver=$(sed -n 's/^pkgver=//p' deploy/package/arch/PKGBUILD)
dir=$(mktemp -d)
git -c safe.directory="$PWD" archive --prefix="logwisp-$ver/" -o "$dir/logwisp-$ver.tar.gz" HEAD
cp deploy/package/arch/PKGBUILD "$dir"
useradd -m build
chown -R build "$dir"
su build -c "cd '$dir' && makepkg --noconfirm"
pacman -U --noconfirm "$dir"/logwisp-*.pkg.tar.zst
- run: |
lw --version
test -f /usr/share/bash-completion/completions/lw
test -f /usr/share/zsh/site-functions/_lw
test -f /usr/share/fish/vendor_completions.d/lw.fish
pacman -R --noconfirm logwisp
test ! -e /usr/bin/lw
# make deb on a host with systemd: the service runs from the package's
# configuration; remove stops and disables it and leaves the conffile, purge
# removes that too
deb:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- run: make deb
- run: |
sudo apt-get install -y ./bin/logwisp_*.deb
lw --version
test -f /usr/share/zsh/vendor-completions/_lw
sudo systemctl enable --now logwisp
sleep 3
systemctl is-active logwisp || { sudo journalctl -u logwisp --no-pager; exit 1; }
sudo apt-get remove -y logwisp
test ! -e /usr/bin/lw
test ! -e /usr/lib/systemd/system/logwisp.service
test ! -e /etc/systemd/system/multi-user.target.wants/logwisp.service
if systemctl is-active --quiet logwisp; then exit 1; fi
test -f /etc/logwisp/logwisp.toml
sudo apt-get purge -y logwisp
test ! -e /etc/logwisp/logwisp.toml
# BSD make on FreeBSD: build, install, run the rc.d service, uninstall
freebsd:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v5
- uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1.5.9
with:
release: "15.1"
copyback: false
prepare: pkg install -y go127
run: |
set -e
make release
make install
lw --version
pw groupadd logwisp
pw useradd logwisp -g logwisp -d /nonexistent -s /usr/sbin/nologin
service logwisp onestart
sleep 3
service logwisp onestatus
service logwisp onestop
make uninstall
test ! -e /usr/local/bin/lw
image:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
- run: make image image-check
+53 -3
View File
@@ -56,10 +56,17 @@ BINDIR ?= $(PREFIX)/bin
MANDIR ?= $(PREFIX)/share/man
DOCDIR ?= $(PREFIX)/share/doc/logwisp
LICENSEDIR ?= $(PREFIX)/share/licenses/logwisp
BASHCOMPDIR ?= $(PREFIX)/share/bash-completion/completions
# Debian's zsh reads /usr/share/zsh/vendor-completions, not site-functions
ZSHCOMPDIR_DEFAULT != [ '$(PREFIX)' = /usr ] && [ -f /etc/debian_version ] && \
echo /usr/share/zsh/vendor-completions || echo '$(PREFIX)/share/zsh/site-functions'
ZSHCOMPDIR ?= $(ZSHCOMPDIR_DEFAULT)
FISHCOMPDIR ?= $(PREFIX)/share/fish/vendor_completions.d
DEB_MAINTAINER ?= Maintainer <maintainer@example.org>
.DEFAULT_GOAL := help
.PHONY: help check-go build release dev test verify e2e image image-check install uninstall clean version
.PHONY: help check-go build release dev test verify e2e image image-check install uninstall deb completion clean version
help:
@echo "Usage: make <target> [VARIABLE=value ...]"
@@ -69,6 +76,7 @@ help:
@echo " release Build it static, stripped and trimmed (CGO_ENABLED=0)"
@echo " dev Build it with the race detector"
@echo " version Print the version metadata a build embeds"
@echo " completion Regenerate $(PKG_DIR)/completion from lw's command tables"
@echo " clean Remove $(BIN_DIR)/"
@echo "Check:"
@echo " test Run the Go tests"
@@ -79,13 +87,16 @@ help:
@echo " image-check Run it read-only, offline, without capabilities: --version, then"
@echo " --check on $(IMAGE_CHECK_CONFIG)"
@echo "Install (build or release first):"
@echo " install Binary, manual, sample config, service files, licence and docs"
@echo " install Binary, manual, sample config, service files, shell completion,"
@echo " licence and docs"
@echo " uninstall Remove them, keeping $(SYSCONFDIR)/logwisp"
@echo " deb Package a release build as $(BIN_DIR)/logwisp_VERSION_ARCH.deb (dpkg-deb)"
@echo ""
@echo "Variables: GO=$(GO) PREFIX=$(PREFIX) SYSCONFDIR=$(SYSCONFDIR) DESTDIR=$(DESTDIR)"
@echo " INSTALL_OS=$(INSTALL_OS) (the layout install uses: FreeBSD or any other)"
@echo " GO_BUILDFLAGS, GO_LDFLAGS (extra go build and linker flags), E2E (scripts),"
@echo " CONTAINER_ENGINE, IMAGE, IMAGE_TAG, BUILD_CA, IMAGE_BUILD_FLAGS, IMAGE_CHECK_CONFIG"
@echo " CONTAINER_ENGINE, IMAGE, IMAGE_TAG, BUILD_CA, IMAGE_BUILD_FLAGS, IMAGE_CHECK_CONFIG,"
@echo " BASHCOMPDIR, ZSHCOMPDIR, FISHCOMPDIR, DEB_MAINTAINER"
check-go:
@command -v $(GO) >/dev/null 2>&1 || { \
@@ -108,6 +119,10 @@ dev: check-go
test: check-go
$(GO) test ./...
# A test fails when the committed scripts differ from what the tables generate
completion: check-go
$(GO) test ./cmd/lw -run '^TestCompletionScriptsAreCurrent$$' -args -update
# gofmt checks the Go files changed since the branch forked from GOFMT_BASE,
# exact renames aside, or the whole tree without git or that ref: older files
# predate the gate, and AGENTS.md formats what a change touches.
@@ -189,6 +204,9 @@ install:
root='$(DESTDIR)'; etc="$$root$(SYSCONFDIR)/logwisp"; \
put 0755 $(BIN_DIR)/$(BINARY) "$$root$(BINDIR)/$(BINARY)"; \
put 0644 doc/lw.1 "$$root$(MANDIR)/man1/lw.1"; \
put 0644 $(PKG_DIR)/completion/lw.bash "$$root$(BASHCOMPDIR)/lw"; \
put 0644 $(PKG_DIR)/completion/_lw "$$root$(ZSHCOMPDIR)/_lw"; \
put 0644 $(PKG_DIR)/completion/lw.fish "$$root$(FISHCOMPDIR)/lw.fish"; \
put 0644 LICENSE "$$root$(LICENSEDIR)/LICENSE"; \
for src in README.md doc/*.md; do put 0644 "$$src" "$$root$(DOCDIR)/$$src"; done; \
case '$(INSTALL_OS)' in \
@@ -207,6 +225,7 @@ install:
uninstall:
@set -eu; root='$(DESTDIR)'; \
for f in "$$root$(BINDIR)/$(BINARY)" "$$root$(MANDIR)/man1/lw.1" \
"$$root$(BASHCOMPDIR)/lw" "$$root$(ZSHCOMPDIR)/_lw" "$$root$(FISHCOMPDIR)/lw.fish" \
"$$root$(PREFIX)/lib/systemd/system/logwisp.service" \
"$$root$(PREFIX)/lib/sysusers.d/logwisp.conf" "$$root$(PREFIX)/lib/tmpfiles.d/logwisp.conf" \
"$$root$(PREFIX)/etc/rc.d/logwisp" "$$root$(SYSCONFDIR)/logwisp/logwisp.toml.sample"; do \
@@ -218,6 +237,37 @@ uninstall:
done; \
echo "keep $$root$(SYSCONFDIR)/logwisp (configuration and credentials)"
# deb stages install in Debian's layout and takes the architecture from the
# binary. An untagged or modified tree's version sorts below every release and
# by build time, so each build upgrades the last.
deb: release
@set -eu; stage=$(BIN_DIR)/deb; doc="$$stage/usr/share/doc/logwisp"; rm -rf "$$stage"; \
$(MAKE) install DESTDIR="$$stage" PREFIX=/usr SYSCONFDIR=/etc INSTALL_OS=Linux \
LICENSEDIR=/usr/share/doc/logwisp ZSHCOMPDIR=/usr/share/zsh/vendor-completions >/dev/null; \
meta=$$($(GO) version -m $(BIN_DIR)/$(BINARY)); \
case $$meta in *GOOS=linux*) ;; *) echo "$(BIN_DIR)/$(BINARY) is not a Linux build" >&2; exit 1 ;; esac; \
arch=$$(echo "$$meta" | sed -n 's/^[[:space:]]*build[[:space:]]*GOARCH=//p'); \
case $$arch in 386) arch=i386 ;; arm) arch=armhf ;; ppc64le) arch=ppc64el ;; \
mipsle) arch=mipsel ;; mips64le) arch=mips64el ;; esac; \
stamp=$$(echo '$(BUILD_TIME)' | tr -dc 0-9); \
version=$$(echo '$(VERSION)' | sed -e 's/^v//' -e 's/-dirty$$/+dirty/' \
-e 's/-\([0-9][0-9]*\)-g/+\1.g/' -e 's/-/~/g'); \
case $$version in \
[0-9]*.*+dirty) version="$$version.$$stamp" ;; \
[0-9]*.*) ;; \
*) version="0~$$stamp.$$version" ;; \
esac; \
mv "$$doc/LICENSE" "$$doc/copyright"; gzip -9n "$$stage/usr/share/man/man1/lw.1"; \
install -d -m 0755 "$$stage/DEBIAN"; \
for f in postinst prerm postrm; do install -m 0755 $(PKG_DIR)/deb/$$f "$$stage/DEBIAN/$$f"; done; \
echo /etc/logwisp/logwisp.toml > "$$stage/DEBIAN/conffiles"; \
(cd "$$stage" && find usr -type f -exec md5sum {} + | LC_ALL=C sort -k 2) > "$$stage/DEBIAN/md5sums"; \
size=$$(du -sk --apparent-size "$$stage/usr" "$$stage/etc" | awk '{ s += $$1 } END { print s }'); \
{ printf 'Package: logwisp\nVersion: %s\nArchitecture: %s\nMaintainer: %s\nInstalled-Size: %s\n' \
"$$version" "$$arch" "$(DEB_MAINTAINER)" "$$size"; cat $(PKG_DIR)/deb/control; \
sed -e 's/^$$/./' -e 's/^/ /' $(PKG_DIR)/freebsd/pkg-descr; } > "$$stage/DEBIAN/control"; \
dpkg-deb --root-owner-group --build "$$stage" "$(BIN_DIR)/logwisp_$${version}_$$arch.deb"
clean:
rm -rf $(BIN_DIR)
+4 -4
View File
@@ -115,7 +115,7 @@ authentication designs for the rationale and what is deliberately left out.
exit codes, `lw auth`; also the `lw(1)` manual, [`doc/lw.1`](doc/lw.1)
- [Operations](doc/operations.md): running, monitoring, tuning,
troubleshooting
- [To Do](doc/todo.md): planned work: packaging
- [To Do](doc/todo.md): planned work: packaging, `lw --tui` and its shared configuration engine
A fully annotated configuration covering every option ships as
[`config/logwisp.toml`](config/logwisp.toml).
@@ -185,9 +185,9 @@ docker run --rm --read-only --cap-drop ALL --security-opt no-new-privileges \
-v /etc/logwisp:/etc/logwisp:ro logwisp:dev -c /etc/logwisp/logwisp.toml
```
`sudo make install` installs the binary, the manual, a sample configuration
and a systemd unit or FreeBSD rc.d script; see
[Installation](doc/installation.md).
`sudo make install` installs the binary, the manual, shell completion, a
sample configuration and a systemd unit or FreeBSD rc.d script; `make deb`
packages them for Debian and Ubuntu. See [Installation](doc/installation.md).
## System Requirements
+24 -12
View File
@@ -136,18 +136,7 @@ func TestHelpAndManualListEveryOption(t *testing.T) {
}
var help bytes.Buffer
printHelp(&help)
options := map[string]string{} // long -> its short, if lw takes one
for _, long := range config.SpecFlags() {
options[long] = ""
}
for long := range config.Settings() {
options[long] = ""
}
for letter, short := range shorts {
if short.top {
options[short.long] = letter
}
}
options := options()
// mdoc's Fl writes the first dash, and the others as \-
fl := func(flag string) string { return regexp.QuoteMeta("Fl " + strings.ReplaceAll(flag[1:], "-", `\-`)) }
for long, letter := range options {
@@ -271,3 +260,26 @@ func TestTLSCommandsIssueAVerifiableChain(t *testing.T) {
t.Fatalf("second ca: exit %d: %s", code, stderr)
}
}
var update = flag.Bool("update", false, "rewrite the completion scripts: make completion")
// The installed completion scripts are what lw's tables generate
func TestCompletionScriptsAreCurrent(t *testing.T) {
for _, sh := range shells {
script := sh.script()
path := filepath.Join("../../deploy/package/completion", sh.file)
if *update {
if err := os.WriteFile(path, script, 0o644); err != nil {
t.Fatal(err)
}
continue
}
have, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(have, script) {
t.Errorf("%s is not what lw's tables generate: run make completion", path)
}
}
}
+321
View File
@@ -0,0 +1,321 @@
package main
import (
"bytes"
"flag"
"fmt"
"io"
"maps"
"slices"
"strings"
"github.com/lixenwraith/logwisp/internal/config"
)
// compFlag is a flag as a shell completes it: value is its row in a shell's
// values, "" for a switch; repeat lets a shell offer it again.
type compFlag struct {
long, short, value, help string
repeat bool
}
type compCommand struct {
name, summary string
subs []compSub
}
type compSub struct {
name, summary string
flags []compFlag
}
// shell is a completion script make completion writes under
// deploy/package/completion: values completes a flag's value by its kind,
// "text" as the shell would without lw's script, PRESETS the preset names.
type shell struct {
file string
values map[string]string
write func(w io.Writer, top []compFlag, cmds []compCommand, values map[string]string)
}
var shells = []shell{
{"lw.bash", map[string]string{
"text": "COMPREPLY=()",
"dir": "_lw_reply -d",
"preset": "mapfile -t COMPREPLY < <(compgen -W 'PRESETS' -- \"$cur\")",
}, writeBash},
{"_lw", map[string]string{
"text": ":value:_default",
"dir": ":directory:_files -/",
"preset": ":preset:(PRESETS)",
}, writeZsh},
{"lw.fish", map[string]string{
"text": "-r -F",
"dir": "-x -a '(__fish_complete_directories)'",
"preset": "-x -a 'PRESETS'",
}, writeFish},
}
// valueOf is the kind of value an option of lw's own takes when it is not
// text: "" for none, as a bare --color means always.
var valueOf = map[string]string{"preset": "preset", "logging.file.directory": "dir", "color": "", "help": ""}
const generated = "Generated by make completion from lw's own tables: edit cmd/lw, not this file."
// script is the shell's completion for lw as its tables stand
func (s shell) script() []byte {
top, cmds, presets := completionModel()
values := map[string]string{}
for kind, action := range s.values {
values[kind] = strings.ReplaceAll(action, "PRESETS", presets)
}
var b bytes.Buffer
s.write(&b, top, cmds, values)
return b.Bytes()
}
// options maps each long option lw itself takes to its short letter, if any
func options() map[string]string {
options := map[string]string{}
for _, long := range config.SpecFlags() {
options[long] = ""
}
for long := range config.Settings() {
options[long] = ""
}
for letter, short := range shorts {
if short.top {
options[short.long] = letter
}
}
return options
}
// completionModel reads the parser's tables: lw's options, then each
// command's subcommands with the flags their define registers.
func completionModel() (top []compFlag, cmds []compCommand, presets string) {
settings, specFlags, shortOf := config.Settings(), config.SpecFlags(), options()
for _, long := range slices.Sorted(maps.Keys(shortOf)) {
f := compFlag{long: long, short: shortOf[long], value: "text", repeat: slices.Contains(specFlags, long)}
if v, ok := valueOf[long]; ok {
f.value = v
} else if settings[long] {
f.value = ""
}
top = append(top, f)
}
for _, c := range commands {
cmd := compCommand{name: c.name, summary: c.summary}
for _, s := range c.subcommands {
fs := flag.NewFlagSet(s.name, flag.ContinueOnError)
s.define(fs)
sub := compSub{name: s.name, summary: s.summary}
fs.VisitAll(func(f *flag.Flag) {
if f.Usage == "" {
return // an alias, unlisted like a short
}
arg, help := flag.UnquoteUsage(f)
value := arg // "" for a switch; a backquoted `dir` names its row
if _, known := shells[0].values[arg]; arg != "" && !known {
value = "text"
}
sub.flags = append(sub.flags, compFlag{long: f.Name, short: shortFor(f.Name), value: value, help: help})
})
// flag answers -h and --help itself
sub.flags = append(sub.flags, compFlag{long: "help", short: shortFor("help"), help: "print the flags"})
cmd.subs = append(cmd.subs, sub)
}
cmds = append(cmds, cmd)
}
var names []string
for _, p := range config.Presets() {
names = append(names, p.Name)
}
return top, cmds, strings.Join(names, " ")
}
func shortFor(long string) string {
for letter, short := range shorts {
if short.long == long {
return letter
}
}
return ""
}
// words lists the flags as typed: --long, then -s for a short
func words(flags []compFlag, dashes ...string) string {
var w []string
for _, f := range flags {
for _, d := range dashes {
w = append(w, d+f.long)
}
if f.short != "" {
w = append(w, "-"+f.short)
}
}
return strings.Join(w, " ")
}
// bashValues is a case statement completing each valued flag's argument
func bashValues(w io.Writer, indent string, flags []compFlag, values map[string]string, dashes ...string) {
byValue := map[string][]string{}
for _, f := range flags {
if f.value != "" {
byValue[f.value] = append(byValue[f.value], strings.Fields(words([]compFlag{f}, dashes...))...)
}
}
fmt.Fprintf(w, "%scase $prev in\n", indent)
for _, v := range slices.Sorted(maps.Keys(byValue)) {
fmt.Fprintf(w, "%s%s) %s; return ;;\n", indent, strings.Join(byValue[v], "|"), values[v])
}
fmt.Fprintf(w, "%sesac\n", indent)
}
func writeBash(w io.Writer, top []compFlag, cmds []compCommand, values map[string]string) {
var names []string
for _, c := range cmds {
names = append(names, c.name)
}
fmt.Fprintf(w, "# bash completion for lw. %s\n\n", generated)
fmt.Fprintf(w, "_lw_reply() { compopt -o filenames 2>/dev/null; mapfile -t COMPREPLY < <(compgen \"$@\" -- \"$cur\"); }\n\n")
fmt.Fprintf(w, "_lw() {\n")
fmt.Fprintf(w, "\tlocal cur=${COMP_WORDS[COMP_CWORD]} prev='' i=$COMP_CWORD words\n")
fmt.Fprintf(w, "\t((i > 0)) && prev=${COMP_WORDS[i-1]}\n")
fmt.Fprintf(w, "\t# bash splits --key=value at '=': complete the value of the key before it\n")
fmt.Fprintf(w, "\tif [[ $cur == = ]]; then cur=''\n\telif [[ $prev == = ]]; then prev=${COMP_WORDS[i-2]}\n\tfi\n")
fmt.Fprintf(w, "\tcase ${COMP_WORDS[1]}:$i in\n")
fmt.Fprintf(w, "\t*:1) words='%s %s help' ;;\n", words(top, "--"), strings.Join(names, " "))
for _, c := range cmds {
var subs []string
for _, s := range c.subs {
subs = append(subs, s.name)
}
fmt.Fprintf(w, "\t%s:2) words='%s' ;;\n", c.name, strings.Join(subs, " "))
fmt.Fprintf(w, "\t%s:*)\n\t\tcase ${COMP_WORDS[2]} in\n", c.name)
for _, s := range c.subs {
fmt.Fprintf(w, "\t\t%s)\n", s.name)
bashValues(w, "\t\t\t", s.flags, values, "--", "-")
fmt.Fprintf(w, "\t\t\twords='%s' ;;\n", words(s.flags, "--"))
}
fmt.Fprintf(w, "\t\tesac ;;\n")
}
fmt.Fprintf(w, "\t*)\n")
bashValues(w, "\t\t", top, values, "--")
fmt.Fprintf(w, "\t\twords='%s' ;;\n", words(top, "--"))
fmt.Fprintf(w, "\tesac\n")
fmt.Fprintf(w, "\tmapfile -t COMPREPLY < <(compgen -W \"$words\" -- \"$cur\")\n}\n\n")
fmt.Fprintf(w, "# An empty reply falls back to file names, as without this script\n")
fmt.Fprintf(w, "complete -o bashdefault -o default -F _lw lw\n")
}
// zshQuote makes help text safe inside a single-quoted _arguments spec
func zshQuote(s string) string {
return strings.NewReplacer("'", `'\''`, "]", `\]`).Replace(s)
}
// zshSpecs are _arguments specs for flags; a value follows '=' or as the next
// word, never attached to a short, the forms lw parses.
func zshSpecs(flags []compFlag, values map[string]string) []string {
var specs []string
for _, f := range flags {
help, action, eq := "", "", ""
if f.help != "" {
help = "[" + zshQuote(f.help) + "]"
}
if f.value != "" {
action, eq = values[f.value], "="
}
names, repeat := fmt.Sprintf("'(-%s --%s)'{-%s%s,--%s%s}", f.short, f.long, f.short, eq, f.long, eq), ""
if f.repeat {
names, repeat = fmt.Sprintf("'*'{-%s%s,--%s%s}", f.short, eq, f.long, eq), "*"
}
switch tail := help + action; {
case f.short == "":
specs = append(specs, fmt.Sprintf("'%s--%s%s%s'", repeat, f.long, eq, tail))
case tail == "":
specs = append(specs, names)
default:
specs = append(specs, names+"'"+tail+"'")
}
}
return specs
}
func writeZsh(w io.Writer, top []compFlag, cmds []compCommand, values map[string]string) {
fmt.Fprintf(w, "#compdef lw\n# zsh completion for lw. %s\n\n", generated)
fmt.Fprintf(w, "local -a described\n")
fmt.Fprintf(w, "case ${words[2]}:$CURRENT in\n")
for _, c := range cmds {
fmt.Fprintf(w, "%s:2) ;;\n%s:3)\n\tdescribed=(\n", c.name, c.name)
for _, s := range c.subs {
fmt.Fprintf(w, "\t\t'%s:%s'\n", s.name, zshQuote(s.summary))
}
fmt.Fprintf(w, "\t)\n\t_describe -t commands 'lw %s command' described\n\treturn ;;\n", c.name)
// _arguments reads words from the second: make that the subcommand's first flag
fmt.Fprintf(w, "%s:*)\n\tshift 2 words\n\t(( CURRENT -= 2 ))\n\tcase ${words[1]} in\n", c.name)
for _, s := range c.subs {
fmt.Fprintf(w, "\t%s) _arguments -S \\\n\t\t%s ;;\n", s.name, strings.Join(zshSpecs(s.flags, values), " \\\n\t\t"))
}
fmt.Fprintf(w, "\tesac\n\treturn ;;\n")
}
fmt.Fprintf(w, "esac\n")
fmt.Fprintf(w, "if (( CURRENT == 2 )) && [[ $PREFIX != -* ]]; then\n\tdescribed=(\n")
for _, c := range cmds {
fmt.Fprintf(w, "\t\t'%s:%s'\n", c.name, zshQuote(c.summary))
}
fmt.Fprintf(w, "\t\t'help:Print the help and exit'\n\t)\n")
fmt.Fprintf(w, "\t_describe -t commands 'lw command' described\n\treturn\nfi\n")
fmt.Fprintf(w, "_arguments -S \\\n\t%s\n", strings.Join(zshSpecs(top, values), " \\\n\t"))
}
// fishQuote makes text safe inside a single-quoted fish string
func fishQuote(s string) string {
return strings.NewReplacer(`\`, `\\`, "'", `\'`).Replace(s)
}
// fishFlags completes a short as an old-style option (-o): lw takes no
// bundled shorts and no value attached to one.
func fishFlags(w io.Writer, cond string, flags []compFlag, values map[string]string) {
for _, f := range flags {
line := fmt.Sprintf("complete -c lw -n '%s' -l %s", cond, f.long)
if f.short != "" {
line += " -o " + f.short
}
if f.value != "" {
line += " " + values[f.value]
}
if f.help != "" {
line += " -d '" + fishQuote(f.help) + "'"
}
fmt.Fprintln(w, line)
}
}
func writeFish(w io.Writer, top []compFlag, cmds []compCommand, values map[string]string) {
fmt.Fprintf(w, "# fish completion for lw. %s\n\n", generated)
fmt.Fprintf(w, "# __lw_in WORD...: the command line so far is lw WORD...\n")
fmt.Fprintf(w, "function __lw_in\n\tset -l w (commandline -opc)\n\tset -l n (math (count $argv) + 1)\n")
fmt.Fprintf(w, "\ttest (count $w) -ge $n; and test \"$w[2..$n]\" = \"$argv\"\nend\n\n")
fmt.Fprintf(w, "complete -c lw -f\n")
fmt.Fprintf(w, "set -l __lw_first 'test (count (commandline -opc)) -eq 1'\n")
for _, c := range cmds {
fmt.Fprintf(w, "complete -c lw -n $__lw_first -a %s -d '%s'\n", c.name, fishQuote(c.summary))
}
fmt.Fprintf(w, "complete -c lw -n $__lw_first -a help -d 'Print the help and exit'\n")
var cond []string
for _, c := range cmds {
cond = append(cond, "__lw_in "+c.name)
}
fishFlags(w, "not "+strings.Join(cond, "; and not "), top, values)
for _, c := range cmds {
fmt.Fprintf(w, "\n")
for _, s := range c.subs {
fmt.Fprintf(w, "complete -c lw -n '__lw_in %s; and test (count (commandline -opc)) -eq 2' -a %s -d '%s'\n",
c.name, s.name, fishQuote(s.summary))
}
for _, s := range c.subs {
fishFlags(w, "__lw_in "+c.name+" "+s.name, s.flags, values)
}
}
}
+191
View File
@@ -0,0 +1,191 @@
#compdef lw
# zsh completion for lw. Generated by make completion from lw's own tables: edit cmd/lw, not this file.
local -a described
case ${words[2]}:$CURRENT in
auth:2) ;;
auth:3)
described=(
'add-user:Add a user to a credentials file, or replace its password'
'remove-user:Remove a user from a credentials file'
'token:Log in to an http sink or http_chain source and print a bearer token'
'stream:Log in to a tcp sink and copy its stream to stdout until interrupted'
)
_describe -t commands 'lw auth command' described
return ;;
auth:*)
shift 2 words
(( CURRENT -= 2 ))
case ${words[1]} in
add-user) _arguments -S \
'--credentials=[credentials file, created when missing]:value:_default' \
'--generate[generate a new password, replacing any --password-file; needed to rotate without one]' \
'--password-file=[read the password from file when it exists, else write a generated one there]:value:_default' \
'(-u --user)'{-u=,--user=}'[user name]:value:_default' \
'(-h --help)'{-h,--help}'[print the flags]' ;;
remove-user) _arguments -S \
'--credentials=[credentials file]:value:_default' \
'(-u --user)'{-u=,--user=}'[user name]:value:_default' \
'(-h --help)'{-h,--help}'[print the flags]' ;;
token) _arguments -S \
'--ca-file=[CA file that verifies the server (default: system roots)]:value:_default' \
'--cert-file=[client certificate file, for listeners with tls.client_auth]:value:_default' \
'--key-file=[client key file]:value:_default' \
'--password-file=[file holding the password]:value:_default' \
'--pin-sha256=[sha256//BASE64 pin of the server key, in place of --ca-file (a self_signed listener logs it)]:value:_default' \
'--server-name=[name the server certificate must carry (default: the host)]:value:_default' \
'--unbound[log in without channel binding, to an http sink behind a TLS-terminating proxy (auth.trusted_proxies)]' \
'--url=[listener URL, https://HOST:PORT or https://[IPV6\]:PORT (with --unbound, a proxy'\''s mount URL may carry a path)]:value:_default' \
'(-u --user)'{-u=,--user=}'[user name]:value:_default' \
'(-h --help)'{-h,--help}'[print the flags]' ;;
stream) _arguments -S \
'--addr=[tcp sink address, HOST:PORT or [IPV6\]:PORT]:value:_default' \
'--ca-file=[CA file that verifies the server (default: system roots)]:value:_default' \
'--cert-file=[client certificate file, for listeners with tls.client_auth]:value:_default' \
'--key-file=[client key file]:value:_default' \
'--password-file=[file holding the password]:value:_default' \
'--pin-sha256=[sha256//BASE64 pin of the server key, in place of --ca-file (a self_signed listener logs it)]:value:_default' \
'--server-name=[name the server certificate must carry (default: the host)]:value:_default' \
'(-u --user)'{-u=,--user=}'[user name]:value:_default' \
'(-h --help)'{-h,--help}'[print the flags]' ;;
esac
return ;;
tls:2) ;;
tls:3)
described=(
'ca:Create a certificate authority, DIR/ca.crt and DIR/ca.key, that signs certificates only'
'cert:Issue a certificate and key, OUT/NAME.crt and OUT/NAME.key, from DIR/ca.crt and DIR/ca.key'
)
_describe -t commands 'lw tls command' described
return ;;
tls:*)
shift 2 words
(( CURRENT -= 2 ))
case ${words[1]} in
ca) _arguments -S \
'--days=[validity in n days]:value:_default' \
'--dir=[directory to write ca.crt and ca.key in, created when missing]:directory:_files -/' \
'--name=[subject common name]:value:_default' \
'(-h --help)'{-h,--help}'[print the flags]' ;;
cert) _arguments -S \
'--ca-dir=[directory holding ca.crt and ca.key (lw tls ca)]:directory:_files -/' \
'--client[for dialers presenting a certificate (TLS client authentication)]' \
'--days=[validity in n days, at most the CA'\''s]:value:_default' \
'--hosts=[DNS names and IP addresses, '\'','\'' between them (default with --server: --name)]:value:_default' \
'--name=[subject common name, the mtls identity, and the file names]:value:_default' \
'--out=[directory to write in (default: --ca-dir)]:directory:_files -/' \
'--server[for listeners (TLS server authentication)]' \
'(-h --help)'{-h,--help}'[print the flags]' ;;
esac
return ;;
preset:2) ;;
preset:3)
described=(
'pipe:Standard input to standard output, line for line: lw without a file'
'tail:Follow files to standard output, like tail -F'
'serve:Serve files or standard input live: a browser viewer at /, SSE at /stream'
'edge:Forward files or standard input to an aggregator over TLS, authenticated'
'aggregator:Receive from edges over TLS, authenticated, to files or standard output'
)
_describe -t commands 'lw preset command' described
return ;;
preset:*)
shift 2 words
(( CURRENT -= 2 ))
case ${words[1]} in
pipe) _arguments -S \
'--format=[raw|txt|json]:value:_default' \
'(-h --help)'{-h,--help}'[print the flags]' ;;
tail) _arguments -S \
'--format=[raw|txt|json]:value:_default' \
'--from=[start|end: where reading a file starts]:value:_default' \
'--path=[file, directory (its files) or glob to follow]:value:_default' \
'(-h --help)'{-h,--help}'[print the flags]' ;;
serve) _arguments -S \
'--allow=[addresses or CIDRs that may connect, of listen'\''s family (either behind proxy=); default: all; '\'','\'' between them]:value:_default' \
'--cert=[certificate file, for tls=files]:value:_default' \
'--deny=[addresses or CIDRs refused, allow or not; '\'','\'' between them]:value:_default' \
'--format=[raw|txt|json]:value:_default' \
'--from=[start|end: where reading a file starts]:value:_default' \
'--hosts=[names and addresses, beyond this host'\''s, of a self or issuer certificate; '\'','\'' between them]:value:_default' \
'--issuer_cert=[CA certificate file (lw tls ca), for tls=issuer]:value:_default' \
'--issuer_key=[CA key file, for tls=issuer]:value:_default' \
'--key=[key file, for tls=files]:value:_default' \
'--listen=[HOST:PORT; [IPV6\]:PORT]:value:_default' \
'--path=[file, directory (its files) or glob to follow; default: standard input]:value:_default' \
'--proxy=[addresses or CIDRs of the TLS-terminating proxies browsers come through; '\'','\'' between them]:value:_default' \
'--tls=[off|self|issuer|files: a certificate made at startup (self-signed or from issuer_*), or files]:value:_default' \
'--users=[credentials file (lw auth add-user): readers log in with SCRAM]:value:_default' \
'--viewer=[true: the login page and viewer for users, behind proxy; without users the viewer is always on]:value:_default' \
'(-h --help)'{-h,--help}'[print the flags]' ;;
edge) _arguments -S \
'--ca=[CA file that verifies the aggregator; default: system roots]:value:_default' \
'--cert=[client certificate file, for an aggregator with client_ca]:value:_default' \
'--from=[start|end: where reading a file starts]:value:_default' \
'--key=[client key file]:value:_default' \
'--node=[origin label; default: this host'\''s name]:value:_default' \
'--password_file=[file holding the SCRAM password]:value:_default' \
'--path=[file, directory (its files) or glob to follow; default: standard input]:value:_default' \
'--pin=[sha256//BASE64 of the aggregator'\''s key, which it logs with tls=self]:value:_default' \
'--server_name=[name the aggregator'\''s certificate carries; default: to'\''s host]:value:_default' \
'--to=[the aggregator'\''s HOST:PORT; [IPV6\]:PORT]:value:_default' \
'--transport=[tcp|http: tcp_chain or http_chain]:value:_default' \
'(-u --user)'{-u=,--user=}'[SCRAM user (lw auth add-user on the aggregator)]:value:_default' \
'(-h --help)'{-h,--help}'[print the flags]' ;;
aggregator) _arguments -S \
'--allow=[addresses or CIDRs that may connect, of listen'\''s family (either behind proxy=); default: all; '\'','\'' between them]:value:_default' \
'--cert=[certificate file, for tls=files]:value:_default' \
'--client_ca=[CA file that verifies edge certificates (mTLS)]:value:_default' \
'--deny=[addresses or CIDRs refused, allow or not; '\'','\'' between them]:value:_default' \
'--format=[raw|txt|json]:value:_default' \
'--hosts=[names and addresses, beyond this host'\''s, of a self or issuer certificate; '\'','\'' between them]:value:_default' \
'--issuer_cert=[CA certificate file (lw tls ca), for tls=issuer]:value:_default' \
'--issuer_key=[CA key file, for tls=issuer]:value:_default' \
'--key=[key file, for tls=files]:value:_default' \
'--listen=[HOST:PORT; [IPV6\]:PORT; [::\] for IPv6 only]:value:_default' \
'--out=[directory for the received entries; default: standard output]:value:_default' \
'--tls=[self|issuer|files: a certificate made at startup (self-signed or from issuer_*), or files]:value:_default' \
'--transport=[tcp|http: tcp_chain or http_chain]:value:_default' \
'--users=[credentials file (lw auth add-user): edges log in with SCRAM]:value:_default' \
'(-h --help)'{-h,--help}'[print the flags]' ;;
esac
return ;;
esac
if (( CURRENT == 2 )) && [[ $PREFIX != -* ]]; then
described=(
'auth:SCRAM users, bearer tokens, tcp sink viewing'
'tls:A CA and certificates for TLS and mTLS'
'preset:Show the pipeline a preset makes, as TOML'
'help:Print the help and exit'
)
_describe -t commands 'lw command' described
return
fi
_arguments -S \
'--auto_reload' \
'(-t --check)'{-t,--check} \
'--color' \
'(-c --config)'{-c=,--config=}':value:_default' \
'--dump' \
'*--filter=:value:_default' \
'*--format=:value:_default' \
'*--heartbeat=:value:_default' \
'(-h --help)'{-h,--help} \
'--logging.console.target=:value:_default' \
'--logging.file.directory=:directory:_files -/' \
'--logging.file.max_size_mb=:value:_default' \
'--logging.file.max_total_size_mb=:value:_default' \
'--logging.file.name=:value:_default' \
'--logging.file.retention_hours=:value:_default' \
'--logging.format=:value:_default' \
'--logging.level=:value:_default' \
'--logging.output=:value:_default' \
'--logging.sanitization=:value:_default' \
'*--pipeline=:value:_default' \
'*'{-p=,--preset=}':preset:(pipe tail serve edge aggregator)' \
'(-q --quiet)'{-q,--quiet} \
'*--rate-limit=:value:_default' \
'*--sink=:value:_default' \
'*--source=:value:_default' \
'--status_reporter' \
'(-V --version)'{-V,--version}
+95
View File
@@ -0,0 +1,95 @@
# bash completion for lw. Generated by make completion from lw's own tables: edit cmd/lw, not this file.
_lw_reply() { compopt -o filenames 2>/dev/null; mapfile -t COMPREPLY < <(compgen "$@" -- "$cur"); }
_lw() {
local cur=${COMP_WORDS[COMP_CWORD]} prev='' i=$COMP_CWORD words
((i > 0)) && prev=${COMP_WORDS[i-1]}
# bash splits --key=value at '=': complete the value of the key before it
if [[ $cur == = ]]; then cur=''
elif [[ $prev == = ]]; then prev=${COMP_WORDS[i-2]}
fi
case ${COMP_WORDS[1]}:$i in
*:1) words='--auto_reload --check -t --color --config -c --dump --filter --format --heartbeat --help -h --logging.console.target --logging.file.directory --logging.file.max_size_mb --logging.file.max_total_size_mb --logging.file.name --logging.file.retention_hours --logging.format --logging.level --logging.output --logging.sanitization --pipeline --preset -p --quiet -q --rate-limit --sink --source --status_reporter --version -V auth tls preset help' ;;
auth:2) words='add-user remove-user token stream' ;;
auth:*)
case ${COMP_WORDS[2]} in
add-user)
case $prev in
--credentials|-credentials|--password-file|-password-file|--user|-user|-u) COMPREPLY=(); return ;;
esac
words='--credentials --generate --password-file --user -u --help -h' ;;
remove-user)
case $prev in
--credentials|-credentials|--user|-user|-u) COMPREPLY=(); return ;;
esac
words='--credentials --user -u --help -h' ;;
token)
case $prev in
--ca-file|-ca-file|--cert-file|-cert-file|--key-file|-key-file|--password-file|-password-file|--pin-sha256|-pin-sha256|--server-name|-server-name|--url|-url|--user|-user|-u) COMPREPLY=(); return ;;
esac
words='--ca-file --cert-file --key-file --password-file --pin-sha256 --server-name --unbound --url --user -u --help -h' ;;
stream)
case $prev in
--addr|-addr|--ca-file|-ca-file|--cert-file|-cert-file|--key-file|-key-file|--password-file|-password-file|--pin-sha256|-pin-sha256|--server-name|-server-name|--user|-user|-u) COMPREPLY=(); return ;;
esac
words='--addr --ca-file --cert-file --key-file --password-file --pin-sha256 --server-name --user -u --help -h' ;;
esac ;;
tls:2) words='ca cert' ;;
tls:*)
case ${COMP_WORDS[2]} in
ca)
case $prev in
--dir|-dir) _lw_reply -d; return ;;
--days|-days|--name|-name) COMPREPLY=(); return ;;
esac
words='--days --dir --name --help -h' ;;
cert)
case $prev in
--ca-dir|-ca-dir|--out|-out) _lw_reply -d; return ;;
--days|-days|--hosts|-hosts|--name|-name) COMPREPLY=(); return ;;
esac
words='--ca-dir --client --days --hosts --name --out --server --help -h' ;;
esac ;;
preset:2) words='pipe tail serve edge aggregator' ;;
preset:*)
case ${COMP_WORDS[2]} in
pipe)
case $prev in
--format|-format) COMPREPLY=(); return ;;
esac
words='--format --help -h' ;;
tail)
case $prev in
--format|-format|--from|-from|--path|-path) COMPREPLY=(); return ;;
esac
words='--format --from --path --help -h' ;;
serve)
case $prev in
--allow|-allow|--cert|-cert|--deny|-deny|--format|-format|--from|-from|--hosts|-hosts|--issuer_cert|-issuer_cert|--issuer_key|-issuer_key|--key|-key|--listen|-listen|--path|-path|--proxy|-proxy|--tls|-tls|--users|-users|--viewer|-viewer) COMPREPLY=(); return ;;
esac
words='--allow --cert --deny --format --from --hosts --issuer_cert --issuer_key --key --listen --path --proxy --tls --users --viewer --help -h' ;;
edge)
case $prev in
--ca|-ca|--cert|-cert|--from|-from|--key|-key|--node|-node|--password_file|-password_file|--path|-path|--pin|-pin|--server_name|-server_name|--to|-to|--transport|-transport|--user|-user|-u) COMPREPLY=(); return ;;
esac
words='--ca --cert --from --key --node --password_file --path --pin --server_name --to --transport --user -u --help -h' ;;
aggregator)
case $prev in
--allow|-allow|--cert|-cert|--client_ca|-client_ca|--deny|-deny|--format|-format|--hosts|-hosts|--issuer_cert|-issuer_cert|--issuer_key|-issuer_key|--key|-key|--listen|-listen|--out|-out|--tls|-tls|--transport|-transport|--users|-users) COMPREPLY=(); return ;;
esac
words='--allow --cert --client_ca --deny --format --hosts --issuer_cert --issuer_key --key --listen --out --tls --transport --users --help -h' ;;
esac ;;
*)
case $prev in
--logging.file.directory) _lw_reply -d; return ;;
--preset|-p) mapfile -t COMPREPLY < <(compgen -W 'pipe tail serve edge aggregator' -- "$cur"); return ;;
--config|-c|--filter|--format|--heartbeat|--logging.console.target|--logging.file.max_size_mb|--logging.file.max_total_size_mb|--logging.file.name|--logging.file.retention_hours|--logging.format|--logging.level|--logging.output|--logging.sanitization|--pipeline|--rate-limit|--sink|--source) COMPREPLY=(); return ;;
esac
words='--auto_reload --check -t --color --config -c --dump --filter --format --heartbeat --help -h --logging.console.target --logging.file.directory --logging.file.max_size_mb --logging.file.max_total_size_mb --logging.file.name --logging.file.retention_hours --logging.format --logging.level --logging.output --logging.sanitization --pipeline --preset -p --quiet -q --rate-limit --sink --source --status_reporter --version -V' ;;
esac
mapfile -t COMPREPLY < <(compgen -W "$words" -- "$cur")
}
# An empty reply falls back to file names, as without this script
complete -o bashdefault -o default -F _lw lw
+145
View File
@@ -0,0 +1,145 @@
# fish completion for lw. Generated by make completion from lw's own tables: edit cmd/lw, not this file.
# __lw_in WORD...: the command line so far is lw WORD...
function __lw_in
set -l w (commandline -opc)
set -l n (math (count $argv) + 1)
test (count $w) -ge $n; and test "$w[2..$n]" = "$argv"
end
complete -c lw -f
set -l __lw_first 'test (count (commandline -opc)) -eq 1'
complete -c lw -n $__lw_first -a auth -d 'SCRAM users, bearer tokens, tcp sink viewing'
complete -c lw -n $__lw_first -a tls -d 'A CA and certificates for TLS and mTLS'
complete -c lw -n $__lw_first -a preset -d 'Show the pipeline a preset makes, as TOML'
complete -c lw -n $__lw_first -a help -d 'Print the help and exit'
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l auto_reload
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l check -o t
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l color
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l config -o c -r -F
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l dump
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l filter -r -F
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l format -r -F
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l heartbeat -r -F
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l help -o h
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.console.target -r -F
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.file.directory -x -a '(__fish_complete_directories)'
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.file.max_size_mb -r -F
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.file.max_total_size_mb -r -F
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.file.name -r -F
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.file.retention_hours -r -F
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.format -r -F
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.level -r -F
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.output -r -F
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.sanitization -r -F
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l pipeline -r -F
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l preset -o p -x -a 'pipe tail serve edge aggregator'
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l quiet -o q
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l rate-limit -r -F
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l sink -r -F
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l source -r -F
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l status_reporter
complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l version -o V
complete -c lw -n '__lw_in auth; and test (count (commandline -opc)) -eq 2' -a add-user -d 'Add a user to a credentials file, or replace its password'
complete -c lw -n '__lw_in auth; and test (count (commandline -opc)) -eq 2' -a remove-user -d 'Remove a user from a credentials file'
complete -c lw -n '__lw_in auth; and test (count (commandline -opc)) -eq 2' -a token -d 'Log in to an http sink or http_chain source and print a bearer token'
complete -c lw -n '__lw_in auth; and test (count (commandline -opc)) -eq 2' -a stream -d 'Log in to a tcp sink and copy its stream to stdout until interrupted'
complete -c lw -n '__lw_in auth add-user' -l credentials -r -F -d 'credentials file, created when missing'
complete -c lw -n '__lw_in auth add-user' -l generate -d 'generate a new password, replacing any --password-file; needed to rotate without one'
complete -c lw -n '__lw_in auth add-user' -l password-file -r -F -d 'read the password from file when it exists, else write a generated one there'
complete -c lw -n '__lw_in auth add-user' -l user -o u -r -F -d 'user name'
complete -c lw -n '__lw_in auth add-user' -l help -o h -d 'print the flags'
complete -c lw -n '__lw_in auth remove-user' -l credentials -r -F -d 'credentials file'
complete -c lw -n '__lw_in auth remove-user' -l user -o u -r -F -d 'user name'
complete -c lw -n '__lw_in auth remove-user' -l help -o h -d 'print the flags'
complete -c lw -n '__lw_in auth token' -l ca-file -r -F -d 'CA file that verifies the server (default: system roots)'
complete -c lw -n '__lw_in auth token' -l cert-file -r -F -d 'client certificate file, for listeners with tls.client_auth'
complete -c lw -n '__lw_in auth token' -l key-file -r -F -d 'client key file'
complete -c lw -n '__lw_in auth token' -l password-file -r -F -d 'file holding the password'
complete -c lw -n '__lw_in auth token' -l pin-sha256 -r -F -d 'sha256//BASE64 pin of the server key, in place of --ca-file (a self_signed listener logs it)'
complete -c lw -n '__lw_in auth token' -l server-name -r -F -d 'name the server certificate must carry (default: the host)'
complete -c lw -n '__lw_in auth token' -l unbound -d 'log in without channel binding, to an http sink behind a TLS-terminating proxy (auth.trusted_proxies)'
complete -c lw -n '__lw_in auth token' -l url -r -F -d 'listener URL, https://HOST:PORT or https://[IPV6]:PORT (with --unbound, a proxy\'s mount URL may carry a path)'
complete -c lw -n '__lw_in auth token' -l user -o u -r -F -d 'user name'
complete -c lw -n '__lw_in auth token' -l help -o h -d 'print the flags'
complete -c lw -n '__lw_in auth stream' -l addr -r -F -d 'tcp sink address, HOST:PORT or [IPV6]:PORT'
complete -c lw -n '__lw_in auth stream' -l ca-file -r -F -d 'CA file that verifies the server (default: system roots)'
complete -c lw -n '__lw_in auth stream' -l cert-file -r -F -d 'client certificate file, for listeners with tls.client_auth'
complete -c lw -n '__lw_in auth stream' -l key-file -r -F -d 'client key file'
complete -c lw -n '__lw_in auth stream' -l password-file -r -F -d 'file holding the password'
complete -c lw -n '__lw_in auth stream' -l pin-sha256 -r -F -d 'sha256//BASE64 pin of the server key, in place of --ca-file (a self_signed listener logs it)'
complete -c lw -n '__lw_in auth stream' -l server-name -r -F -d 'name the server certificate must carry (default: the host)'
complete -c lw -n '__lw_in auth stream' -l user -o u -r -F -d 'user name'
complete -c lw -n '__lw_in auth stream' -l help -o h -d 'print the flags'
complete -c lw -n '__lw_in tls; and test (count (commandline -opc)) -eq 2' -a ca -d 'Create a certificate authority, DIR/ca.crt and DIR/ca.key, that signs certificates only'
complete -c lw -n '__lw_in tls; and test (count (commandline -opc)) -eq 2' -a cert -d 'Issue a certificate and key, OUT/NAME.crt and OUT/NAME.key, from DIR/ca.crt and DIR/ca.key'
complete -c lw -n '__lw_in tls ca' -l days -r -F -d 'validity in n days'
complete -c lw -n '__lw_in tls ca' -l dir -x -a '(__fish_complete_directories)' -d 'directory to write ca.crt and ca.key in, created when missing'
complete -c lw -n '__lw_in tls ca' -l name -r -F -d 'subject common name'
complete -c lw -n '__lw_in tls ca' -l help -o h -d 'print the flags'
complete -c lw -n '__lw_in tls cert' -l ca-dir -x -a '(__fish_complete_directories)' -d 'directory holding ca.crt and ca.key (lw tls ca)'
complete -c lw -n '__lw_in tls cert' -l client -d 'for dialers presenting a certificate (TLS client authentication)'
complete -c lw -n '__lw_in tls cert' -l days -r -F -d 'validity in n days, at most the CA\'s'
complete -c lw -n '__lw_in tls cert' -l hosts -r -F -d 'DNS names and IP addresses, \',\' between them (default with --server: --name)'
complete -c lw -n '__lw_in tls cert' -l name -r -F -d 'subject common name, the mtls identity, and the file names'
complete -c lw -n '__lw_in tls cert' -l out -x -a '(__fish_complete_directories)' -d 'directory to write in (default: --ca-dir)'
complete -c lw -n '__lw_in tls cert' -l server -d 'for listeners (TLS server authentication)'
complete -c lw -n '__lw_in tls cert' -l help -o h -d 'print the flags'
complete -c lw -n '__lw_in preset; and test (count (commandline -opc)) -eq 2' -a pipe -d 'Standard input to standard output, line for line: lw without a file'
complete -c lw -n '__lw_in preset; and test (count (commandline -opc)) -eq 2' -a tail -d 'Follow files to standard output, like tail -F'
complete -c lw -n '__lw_in preset; and test (count (commandline -opc)) -eq 2' -a serve -d 'Serve files or standard input live: a browser viewer at /, SSE at /stream'
complete -c lw -n '__lw_in preset; and test (count (commandline -opc)) -eq 2' -a edge -d 'Forward files or standard input to an aggregator over TLS, authenticated'
complete -c lw -n '__lw_in preset; and test (count (commandline -opc)) -eq 2' -a aggregator -d 'Receive from edges over TLS, authenticated, to files or standard output'
complete -c lw -n '__lw_in preset pipe' -l format -r -F -d 'raw|txt|json'
complete -c lw -n '__lw_in preset pipe' -l help -o h -d 'print the flags'
complete -c lw -n '__lw_in preset tail' -l format -r -F -d 'raw|txt|json'
complete -c lw -n '__lw_in preset tail' -l from -r -F -d 'start|end: where reading a file starts'
complete -c lw -n '__lw_in preset tail' -l path -r -F -d 'file, directory (its files) or glob to follow'
complete -c lw -n '__lw_in preset tail' -l help -o h -d 'print the flags'
complete -c lw -n '__lw_in preset serve' -l allow -r -F -d 'addresses or CIDRs that may connect, of listen\'s family (either behind proxy=); default: all; \',\' between them'
complete -c lw -n '__lw_in preset serve' -l cert -r -F -d 'certificate file, for tls=files'
complete -c lw -n '__lw_in preset serve' -l deny -r -F -d 'addresses or CIDRs refused, allow or not; \',\' between them'
complete -c lw -n '__lw_in preset serve' -l format -r -F -d 'raw|txt|json'
complete -c lw -n '__lw_in preset serve' -l from -r -F -d 'start|end: where reading a file starts'
complete -c lw -n '__lw_in preset serve' -l hosts -r -F -d 'names and addresses, beyond this host\'s, of a self or issuer certificate; \',\' between them'
complete -c lw -n '__lw_in preset serve' -l issuer_cert -r -F -d 'CA certificate file (lw tls ca), for tls=issuer'
complete -c lw -n '__lw_in preset serve' -l issuer_key -r -F -d 'CA key file, for tls=issuer'
complete -c lw -n '__lw_in preset serve' -l key -r -F -d 'key file, for tls=files'
complete -c lw -n '__lw_in preset serve' -l listen -r -F -d 'HOST:PORT; [IPV6]:PORT'
complete -c lw -n '__lw_in preset serve' -l path -r -F -d 'file, directory (its files) or glob to follow; default: standard input'
complete -c lw -n '__lw_in preset serve' -l proxy -r -F -d 'addresses or CIDRs of the TLS-terminating proxies browsers come through; \',\' between them'
complete -c lw -n '__lw_in preset serve' -l tls -r -F -d 'off|self|issuer|files: a certificate made at startup (self-signed or from issuer_*), or files'
complete -c lw -n '__lw_in preset serve' -l users -r -F -d 'credentials file (lw auth add-user): readers log in with SCRAM'
complete -c lw -n '__lw_in preset serve' -l viewer -r -F -d 'true: the login page and viewer for users, behind proxy; without users the viewer is always on'
complete -c lw -n '__lw_in preset serve' -l help -o h -d 'print the flags'
complete -c lw -n '__lw_in preset edge' -l ca -r -F -d 'CA file that verifies the aggregator; default: system roots'
complete -c lw -n '__lw_in preset edge' -l cert -r -F -d 'client certificate file, for an aggregator with client_ca'
complete -c lw -n '__lw_in preset edge' -l from -r -F -d 'start|end: where reading a file starts'
complete -c lw -n '__lw_in preset edge' -l key -r -F -d 'client key file'
complete -c lw -n '__lw_in preset edge' -l node -r -F -d 'origin label; default: this host\'s name'
complete -c lw -n '__lw_in preset edge' -l password_file -r -F -d 'file holding the SCRAM password'
complete -c lw -n '__lw_in preset edge' -l path -r -F -d 'file, directory (its files) or glob to follow; default: standard input'
complete -c lw -n '__lw_in preset edge' -l pin -r -F -d 'sha256//BASE64 of the aggregator\'s key, which it logs with tls=self'
complete -c lw -n '__lw_in preset edge' -l server_name -r -F -d 'name the aggregator\'s certificate carries; default: to\'s host'
complete -c lw -n '__lw_in preset edge' -l to -r -F -d 'the aggregator\'s HOST:PORT; [IPV6]:PORT'
complete -c lw -n '__lw_in preset edge' -l transport -r -F -d 'tcp|http: tcp_chain or http_chain'
complete -c lw -n '__lw_in preset edge' -l user -o u -r -F -d 'SCRAM user (lw auth add-user on the aggregator)'
complete -c lw -n '__lw_in preset edge' -l help -o h -d 'print the flags'
complete -c lw -n '__lw_in preset aggregator' -l allow -r -F -d 'addresses or CIDRs that may connect, of listen\'s family (either behind proxy=); default: all; \',\' between them'
complete -c lw -n '__lw_in preset aggregator' -l cert -r -F -d 'certificate file, for tls=files'
complete -c lw -n '__lw_in preset aggregator' -l client_ca -r -F -d 'CA file that verifies edge certificates (mTLS)'
complete -c lw -n '__lw_in preset aggregator' -l deny -r -F -d 'addresses or CIDRs refused, allow or not; \',\' between them'
complete -c lw -n '__lw_in preset aggregator' -l format -r -F -d 'raw|txt|json'
complete -c lw -n '__lw_in preset aggregator' -l hosts -r -F -d 'names and addresses, beyond this host\'s, of a self or issuer certificate; \',\' between them'
complete -c lw -n '__lw_in preset aggregator' -l issuer_cert -r -F -d 'CA certificate file (lw tls ca), for tls=issuer'
complete -c lw -n '__lw_in preset aggregator' -l issuer_key -r -F -d 'CA key file, for tls=issuer'
complete -c lw -n '__lw_in preset aggregator' -l key -r -F -d 'key file, for tls=files'
complete -c lw -n '__lw_in preset aggregator' -l listen -r -F -d 'HOST:PORT; [IPV6]:PORT; [::] for IPv6 only'
complete -c lw -n '__lw_in preset aggregator' -l out -r -F -d 'directory for the received entries; default: standard output'
complete -c lw -n '__lw_in preset aggregator' -l tls -r -F -d 'self|issuer|files: a certificate made at startup (self-signed or from issuer_*), or files'
complete -c lw -n '__lw_in preset aggregator' -l transport -r -F -d 'tcp|http: tcp_chain or http_chain'
complete -c lw -n '__lw_in preset aggregator' -l users -r -F -d 'credentials file (lw auth add-user): edges log in with SCRAM'
complete -c lw -n '__lw_in preset aggregator' -l help -o h -d 'print the flags'
+4
View File
@@ -0,0 +1,4 @@
Section: admin
Priority: optional
Homepage: https://github.com/lixenwraith/logwisp
Description: Pipeline-based log transport with TLS and SCRAM authentication
+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
# sysusers and tmpfiles need no running systemd (a chroot); a first install
# leaves the service to systemctl enable --now logwisp, an upgrade restarts it
# if it runs. As in debhelper's scripts, only the account is fatal.
set -e
if [ "$1" = configure ]; then
if command -v systemd-sysusers >/dev/null; then systemd-sysusers logwisp.conf; fi
if command -v systemd-tmpfiles >/dev/null; then systemd-tmpfiles --create logwisp.conf || true; fi
if [ -d /run/systemd/system ]; then
systemctl daemon-reload || true
if [ -n "$2" ]; then systemctl try-restart logwisp.service || true; fi
fi
fi
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
# A purge removes logwisp.toml; credentials in /etc/logwisp, /var/lib/logwisp,
# /var/log/logwisp and the logwisp user stay, as make uninstall keeps them.
set -e
if [ -d /run/systemd/system ]; then systemctl daemon-reload || true; fi
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh
set -e
if [ "$1" = remove ]; then
if [ -d /run/systemd/system ]; then systemctl stop logwisp.service || true; fi
systemctl disable logwisp.service >/dev/null 2>&1 || true
fi
+9
View File
@@ -33,6 +33,15 @@ PORTDOCS= *
post-install:
${INSTALL_MAN} ${WRKSRC}/doc/lw.1 ${STAGEDIR}${PREFIX}/share/man/man1
${MKDIR} ${STAGEDIR}${PREFIX}/share/bash-completion/completions \
${STAGEDIR}${PREFIX}/share/fish/vendor_completions.d \
${STAGEDIR}${PREFIX}/share/zsh/site-functions
${INSTALL_DATA} ${WRKSRC}/deploy/package/completion/lw.bash \
${STAGEDIR}${PREFIX}/share/bash-completion/completions/lw
${INSTALL_DATA} ${WRKSRC}/deploy/package/completion/lw.fish \
${STAGEDIR}${PREFIX}/share/fish/vendor_completions.d
${INSTALL_DATA} ${WRKSRC}/deploy/package/completion/_lw \
${STAGEDIR}${PREFIX}/share/zsh/site-functions
${MKDIR} ${STAGEDIR}${ETCDIR}
${INSTALL_DATA} ${WRKSRC}/config/logwisp.toml ${STAGEDIR}${ETCDIR}/logwisp.toml.sample
+3
View File
@@ -1,3 +1,6 @@
bin/lw
@sample %%ETCDIR%%/logwisp.toml.sample
share/bash-completion/completions/lw
share/fish/vendor_completions.d/lw.fish
share/man/man1/lw.1.gz
share/zsh/site-functions/_lw
+1 -1
View File
@@ -30,7 +30,7 @@ streams, or downstream LogWisp nodes.
- [CLI](cli.md): flags, signals, exit codes, `lw auth`; also the `lw(1)`
manual, [`lw.1`](lw.1)
- [Operations](operations.md): running, monitoring, tuning, troubleshooting
- [To Do](todo.md): planned work: packaging
- [To Do](todo.md): planned work: packaging, `lw --tui` and its shared configuration engine
A fully annotated configuration covering every option lives at
[`config/logwisp.toml`](../config/logwisp.toml).
+43 -9
View File
@@ -27,6 +27,9 @@ The Makefile works with GNU make and BSD make alike. Targets:
- `make release`: the same, static (`CGO_ENABLED=0`), `-trimpath`, stripped
- `make dev`: built with the race detector
- `make version`: the metadata a build would embed
- `make completion`: regenerate the bash, zsh and fish completion in
`deploy/package/completion/` from `cmd/lw`'s `shorts` and `commands`
tables; `go test ./cmd/lw` fails while they differ
- `make clean`: remove `bin/`
- Check
- `make test`: the Go tests
@@ -39,7 +42,7 @@ The Makefile works with GNU make and BSD make alike. Targets:
- Container
- `make image`, `make image-check`: see [Container Image](#container-image)
- Install
- `make install`, `make uninstall`: see [Installing](#installing)
- `make install`, `make uninstall`, `make deb`: see [Installing](#installing)
Variables:
@@ -70,6 +73,11 @@ first. It honours `DESTDIR`, `PREFIX`, `SYSCONFDIR` and `BINDIR` (default
- FreeBSD defaults: `PREFIX=/usr/local`, `SYSCONFDIR=/usr/local/etc`.
- `INSTALL_OS` (default: `uname -s`) picks the layout, so
`INSTALL_OS=FreeBSD DESTDIR=...` stages the FreeBSD one on another system.
- `BASHCOMPDIR`, `ZSHCOMPDIR`, `FISHCOMPDIR`: where the shell completion
goes, by default `$PREFIX/share/bash-completion/completions`,
`$PREFIX/share/zsh/site-functions` (`/usr/share/zsh/vendor-completions`
for `PREFIX=/usr` on Debian and Ubuntu, whose zsh reads only that) and
`$PREFIX/share/fish/vendor_completions.d`.
```bash
make release
@@ -79,6 +87,8 @@ sudo make install PREFIX=/usr/local
Every system gets:
- `$BINDIR/lw` and the manual `$PREFIX/share/man/man1/lw.1`
- completion for `lw`, its commands, flags, files and preset names:
`lw`, `_lw` and `lw.fish` in the three directories above
- `$PREFIX/share/doc/logwisp/` (this documentation) and
`$PREFIX/share/licenses/logwisp/LICENSE`
@@ -146,6 +156,28 @@ rc.conf variables:
- `logwisp_args`: extra `lw` options, e.g. `--logging.level=debug` (default
empty); `logwisp_flags` goes to `daemon(8)`, not to `lw`
### Debian and Ubuntu (.deb)
`make deb` builds a release and packages what `make install` stages, in
Debian's layout, as `bin/logwisp_VERSION_ARCH.deb`, the architecture taken
from the binary. A release tag gives the version; an untagged or modified
tree's sorts below every release and by build time, so each build upgrades
the last. `DEB_MAINTAINER` names the maintainer.
```bash
make deb
sudo apt install ./bin/logwisp_*.deb
sudo systemctl enable --now logwisp
```
- Installing creates the `logwisp` account and its directories where
`systemd-sysusers` and `systemd-tmpfiles` exist; the service waits to be
enabled, and an upgrade restarts it if it runs.
- `/etc/logwisp/logwisp.toml` is a conffile: an upgrade keeps your edits,
and only a purge removes it.
- Removing stops and disables the service. The account, its directories
and any credentials in `/etc/logwisp` stay, as with `make uninstall`.
### Uninstall
`sudo make uninstall` (with the `PREFIX` used to install) removes everything
@@ -235,18 +267,20 @@ docker run -d --name logwisp \
## Packaging Status
The foundation for distribution packages is in place:
In place:
- the `lw` binary name, and `make install` with `DESTDIR`, `PREFIX` and
`SYSCONFDIR`
- the `doc/lw.1` manual
- service files in `deploy/package/`: `logwisp.service`, `logwisp.sysusers`,
`logwisp.tmpfiles` and the FreeBSD `logwisp.rc`
- `make install` with `DESTDIR`, `PREFIX` and `SYSCONFDIR`, the manual,
shell completion and the service files in `deploy/package/`
- `make deb`
- skeletons: `deploy/package/arch/PKGBUILD` and a `sysutils/logwisp` port in
`deploy/package/freebsd/`
- `.github/workflows/package.yml`, on changes to them: `makepkg` on the
PKGBUILD in an Arch container, the `.deb` installed, started, removed and
purged on Ubuntu, BSD make building, installing and running the rc.d
service on FreeBSD, and `make image image-check`
Still missing: release tags, which both skeletons download; finishing and
submitting the AUR package and the port; Debian packaging.
Still missing: release tags, which both skeletons download; submitting the
AUR package and the port; a Debian source package for a PPA or the archive.
[To Do](todo.md) has the steps.
## Verification
+175 -14
View File
@@ -11,14 +11,18 @@ The foundation exists:
24.04 and 26.04, and the FreeBSD 15.1 ports tree.
- The canonical module path `github.com/lixenwraith/logwisp`, which
`go install`, FreeBSD's `USES=go:modules` and Debian's dh-golang expect.
- `make install` with `DESTDIR`, `PREFIX` and `SYSCONFDIR`.
- The `doc/lw.1` manual.
- Service files in `deploy/package/`: systemd unit, sysusers, tmpfiles and the
FreeBSD rc.d script.
- `make install` with `DESTDIR`, `PREFIX` and `SYSCONFDIR`: the binary, the
`doc/lw.1` manual, bash, zsh and fish completion generated from `cmd/lw`'s
`shorts` and `commands` tables, and the service files in `deploy/package/`
(systemd unit, sysusers, tmpfiles, FreeBSD rc.d script).
- `make deb`, a binary package built with `dpkg-deb`.
- Skeletons in `deploy/package/arch/` and `deploy/package/freebsd/`.
- `.github/workflows/package.yml`: `makepkg` on the PKGBUILD, the `.deb`
installed, started and purged, BSD make and the rc.d service on FreeBSD
15.1, and `make image image-check`.
What remains. Steps 1 to 3 are the maintainer's: they need push rights and an
identity, and both skeletons download the tagged source.
What remains is the maintainer's: it needs push rights, an identity or a
release tag, and both skeletons download the tagged source.
1. Tag the release on the merged main commit, `vX.Y.Z`:
```
@@ -52,11 +56,168 @@ identity, and both skeletons download the tagged source.
- `portlint -AC`, `poudriere testport` on 14.x and 15.x jails, then a
Bugzilla report with the port directory and the UIDs/GIDs diff.
4. Debian:
- Debian policy wants every Go dependency packaged; the four `lixenwraith`
libraries are not.
- Either package them too (dh-golang), or start with an `.deb` built by the
Makefile `install` target (nfpm or `dpkg-deb`) and a PPA, and move to the
archive later.
5. Shell completion for bash, zsh and fish, installed by `make install`.
6. A packaging CI job: build the AUR package in an Arch container and the port
in a FreeBSD VM, run `lw --version` and `make image-check`.
- `make deb` serves direct installs. A PPA builds from a signed source
package without network access: a `debian/` directory (changelog,
control, rules) and the Go modules vendored into the source tarball.
- The archive wants every Go dependency packaged (dh-golang); the four
`lixenwraith` libraries are not.
- `lintian` on the `make deb` package reports `no-changelog` (a source
package's `debian/changelog` provides it), `statically-linked-binary`
(lw is static by design) and `maintainer-script-calls-systemctl` (dh's
scripts use `deb-systemd-helper` instead).
5. With a tag, the packaging workflow's FreeBSD job can build the port itself
from a ports tree (`make stage check-plist` in a copy of
`deploy/package/freebsd/`), rather than BSD make on the checkout.
## 2. `lw --tui` and the configuration engine it shares with lixen.com
`lw --tui` composes pipelines in a full-screen terminal UI, then either runs
them at once or prints one form of them, chosen on exit: a command line,
environment variables, or a configuration file. Its logic is a pure engine that
lixen.com's planned Configure tab (lixencom `doc/todo.md`) runs as WebAssembly,
so no part of LogWisp is rewritten in JavaScript.
What exists:
- Option structs with toml tags for every plugin and flow stage
(`internal/config/config.go`), presets as rows with typed parameters
(`preset.go`), the command-line and environment grammar (`spec.go`),
`ValidateConfig`, `--check` and `--dump`.
- `internal/config` already compiles for `GOOS=js`; it links no net/http,
crypto/tls or terminal code.
- `github.com/lixenwraith/terminal` (pinned) and its `tui` package: regions,
layout, boxes, List, Tree, TabBar, StatusBar, TextField, Form, Modal,
ConfirmDialog, scrolling, mouse. `github.com/lixenwraith/color`: RGB,
blending, RGBTo256, RGBTo16.
What is missing:
- A machine-readable option schema. Defaults, enums and required checks live
inside the plugin constructors (about 50 `Default*` constants, 24 inline
validators); `doc/sources.md`, `doc/sinks.md`, `config/logwisp.toml` and the
site's catalogue copy them by hand, and the site's copy already lags.
- Offline plugin validation: the constructors mix their checks with reading
certificates, credentials and files.
- Typed command-line values: `lw --sink http,port=8080 --dump` prints
`port = "8080"`, a file `port = 8080`.
- A host-independent preset: `fileSource` (`preset.go`) decides directory or
pattern with `os.Stat`, so off the target host `path=/var/log` expands to
directory `/var`, pattern `log`.
- Emitters: only parsers exist for the three forms.
Steps:
1. Schema, in `internal/config`, as the one source of truth.
- Tags on the option structs: `default:`, `help:`, and `lw:` for required,
enum, min and max, what zero means (default, none, unlimited), hints
(path, regex, file) and roles (listener, dialer) on the shared tls, auth
and acl tables.
- One catalogue table, one row per plugin type: role, network role, single
instance, summary, option struct. `RegisterSource`/`RegisterSink` panic on
a type without a row.
- `Decode[T](role, type, map)` applies defaults, unknown-key and tag checks,
then the struct's pure `Check()`. Every constructor decodes through it and
keeps only I/O; the `Default*` constants and inline validators go.
- Pure cross-field checks become methods that tlsx, authz and netacl call
first: TLS role rules, auth bindings without file reads, ACL parsing, the
http sink's paths, filter regexes.
- `Coerce` converts command-line and environment strings, and JSON floats,
to the declared kinds; `spec.go` uses it, so a command line dumps typed.
- `ValidateConfig` adds unique plugin ids per pipeline and decodes every
plugin, naming the path (`pipelines[0].plugin_sinks[view].config.port`).
It opens nothing; `lw --check` stays the authority on files.
- Keep under test what moves: file sink `min_disk_free_mb` (negative means
100, 0 means none), a backoff maximum below the minimum, format `text`.
2. Engine, `internal/compose`.
- Imports internal/config, internal/version and lixenwraith/toml only; a
test fails when `go list -deps` shows net, crypto/tls, os/exec, os/signal,
x/term or the auth, log and terminal modules.
- Starts: empty; a preset, with a `stat` function that is nil off the host
(the UI then asks "directory or file pattern"); a loaded configuration; a
pasted command line (wrapping `cliSpecs` and `buildPipelines`).
- Operations: add or remove a source, sink or filter; set or unset an
option; move a filter; validate.
- Emitters, one form per call:
- Command line: `--pipeline NAME`, `id=`, the rate limit policy and
heartbeat `enabled` always explicit (their command-line defaults differ
from the file's); `--key=value` when a value starts with `-`; `,` `=`
`\` escaped; POSIX-quoted.
- Environment: `LOGWISP_SOURCE_1` and the rest, single-quoted
`KEY='value'` lines; an error for more than one pipeline, which the
environment form cannot hold.
- File: the TOML `lw --dump` prints for those pipelines.
3. `lw --schema`, a parser switch like `--help` (not a Config field, so no
file or environment can trigger it). It prints one JSON document: version;
settings with kind, default, enum, flag and variable; the pipeline flags;
flow stage order; the tls, auth and acl tables; sources, sinks, presets.
The site generates its catalogue from it.
4. In `lixenwraith/terminal`, released and pinned before the TUI:
- Full-screen 8- and 16-color output (SGR 30-37, 90-97, 40-47) and the
default colors 39/49; it emits only 256-color and truecolor today.
- Detection as `inline` does it: 16 colors on text consoles, none under
`NO_COLOR`, refusal on `TERM=dumb`, and an override for LogWisp's `color`
key.
- Drawing on `/dev/tty`, so `lw --tui > logwisp.toml` and `cmd | lw --tui`
keep stdin and stdout as data.
- Bracketed paste into text fields.
- `tui` widgets the site's controls have: segmented choice, filterable
option list, toggle, typed fields (default as placeholder, required mark,
error and help line, folded groups), a focus ring; every widget styled
from one `tui.Theme`.
- In `color`, optional: export the 16 xterm reference shades. RGBTo16 maps
the site's hues badly (green to cyan, red to bright black), so the
8-color tier is chosen by hand.
5. `lw --tui`.
- A parser switch, no short form. With `-c FILE`, pipeline flags or
`LOGWISP_SOURCE` it starts from that composition, and never rewrites FILE
(TOML encoding drops its comments); bare, it opens the preset menu (pipe,
tail, serve, edge, aggregator, empty).
- The pipeline as the site draws it: SOURCES, FLOW, SINKS; a node is a
tinted bar, its type, listens or dials, and a summary line; the four flow
stages in order inside a dashed box, the ones off dimmed; wires drawn from
per-cell connectivity so every junction is right. The inspector sits
below at 80 to 109 columns, beside at 110 and over; under 64 columns the
stages stack, as on the site.
```
● SOURCES ● FLOW ● SINKS
▌ file ─┐ ╭╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╮ ┌─▌ http listens
▌ /var/log *.log │ ╎ ▌ rate_limit 400/s ╎ │ ▌ :8080 /stream
█ tcp_chain listens─┴─►╎ ▌ filters -TRACE ╎─►┤
█ :9000 tls scram ╎ ▌ format txt ╎ └─▌ console
╎ ○ heartbeat off ╎ ▌ stdout
╰╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╯
── tcp_chain · source · id src-2 ─────────────────── ⏎ edit tab next ──
host 0.0.0.0 default format raw [txt] json
port * 9000▌ tls ▸ on (auth, acl folded)
✓ valid a add d delete ␣ on/off J/K move p preset o output ? help
```
- Keys: arrows or hjkl move, Enter edits, `a` adds from the catalogue, `d`
deletes, Space switches a flow stage, `J`/`K` reorder filters, `p`
presets, `[` `]` switch pipelines, `c` lists errors, `o` output, `?`
help, `q` quits.
- Theme: the site's dark tokens as one role table in four tiers. Truecolor
and 256 colors paint the site's background; 8 colors and mono keep the
terminal's. Sources cyan, flow violet, sinks green, accent blue, muted
grey, errors red. A selection always changes a glyph (bar, `▸`,
underline), never only a shade. Glyphs: Unicode, a CP437 set on text
consoles, ASCII outside UTF-8.
- Output, `o`: Run hands `config.Args` built from the composition to the
usual `config.Load`, after the terminal is released and before anything
reads stdin; reloads keep the pipelines, and `--tui --check` and `--tui
--dump` follow. Or exactly one of command line, environment and file,
printed to stdout once the screen closes.
6. WebAssembly: `cmd/lwconf` (`js && wasm`) exposes schema, preset, parse,
validate and emit to the page as JSON strings. `make wasm` builds
`bin/lwconf.wasm` with the version stamped, copies the toolchain's
`wasm_exec.js`, and prints both checksums. A prototype measured about 6 MB,
1.7 MB gzipped, ready in about 60 ms in Chromium; `wasm_exec.js` uses no
eval, so the page needs only `'wasm-unsafe-eval'` in its `script-src`.
Verify:
- A Go test in `cmd/lw`: for every preset, an edge and aggregator pair,
escaped values and two pipelines, the pipelines loaded from the emitted
command line, file and environment (one pipeline) are equal. It runs in an
empty directory with the `LOGWISP_` environment cleared, since a discovered
file would add settings.
- A schema test: every option has a kind, every enum values, every catalogue
row a factory; `TestEveryPluginRejectsUnknownKeys` still passes.
- CI builds `cmd/lwconf` for `GOOS=js` and runs a node test of its calls.
- The diagram rendered into a cell buffer at 80x24 and 64x24, per tier; a
scripted session on a pseudo-terminal for Run and each output.