Route direct-stdout command output through internal/ui (closes #149)
check / check (pull_request) Successful in 1m45s

version, info, remote info, config, and database delete wrote plain text
straight to stdout, so they were unstyled and ignored --quiet. Output now
falls in two buckets, both governed by internal/ui.

Status lines and confirmations (config init, config set, database
delete) go through the ui message methods: styled, and --quiet silences
them. The data a command exists to produce is written plain, since a
marker would corrupt a table or a parsed document: the version, info, and
remote info reports, the snapshot list table, config get values, and the
--json documents. --quiet silences the human reports and tables but never
the config get value or the --json documents, which a script depends on.
The database delete confirmation prompt is always shown; it is an
interactive exchange the operator must see.

The pure-cli commands reach internal/ui through a small commandUI helper
that builds a ui.Writer on the command's stdout in quiet mode when
--quiet is set. NewForTesting now supplies a UI writer so the quiet gate
is never nil. The README output-style section states the resulting rule.

Model: opus-4-8
This commit is contained in:
2026-09-22 18:03:45 +00:00
parent dd7a610c23
commit b4a539c8f9
14 changed files with 427 additions and 66 deletions
+25 -17
View File
@@ -4,7 +4,6 @@ import (
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
@@ -13,6 +12,7 @@ import (
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"sneak.berlin/go/vaultik/internal/ui"
)
// configFileMode is the permission set for freshly written config files;
@@ -265,7 +265,7 @@ The config is written to the path from --config, $VAULTIK_CONFIG, or
the platform default config directory (e.g. ~/Library/Application Support/
on macOS, ~/.config/ on Linux, /etc/vaultik/ as root).`,
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) error {
RunE: func(cmd *cobra.Command, _ []string) error {
path := configPathForInit()
_, err := os.Stat(path)
@@ -285,8 +285,11 @@ on macOS, ~/.config/ on Linux, /etc/vaultik/ as root).`,
return fmt.Errorf("writing config file: %w", err)
}
_, _ = fmt.Fprintf(os.Stdout, "Config written to %s\n", path)
_, _ = fmt.Fprintln(os.Stdout,
// A written-confirmation, not scriptable output: route it
// through the UI so it is styled and --quiet silences it.
out := commandUI(cmd)
out.Infof("Config written to %s.", path)
out.Infof(
"Edit it to set your age_recipients, snapshots, and storage_url.")
return nil
@@ -328,7 +331,7 @@ func newConfigGetCommand() *cobra.Command {
Use: "get <key>",
Short: "Print a config value by dotted path (e.g. storage_url, compression_level)",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
path, err := ResolveConfigPath()
if err != nil {
return err
@@ -344,8 +347,13 @@ func newConfigGetCommand() *cobra.Command {
return err
}
// The value is scriptable output: it must stay machine-plain
// (no marker, no color) and is never silenced by --quiet, so it
// is written straight to stdout rather than through the UI.
w := cmd.OutOrStdout()
if node.Kind == yaml.ScalarNode {
_, _ = fmt.Fprintln(os.Stdout, node.Value)
_, _ = fmt.Fprintln(w, node.Value)
return nil
}
@@ -355,7 +363,7 @@ func newConfigGetCommand() *cobra.Command {
return fmt.Errorf("marshaling value: %w", err)
}
_, _ = fmt.Fprint(os.Stdout, string(out))
_, _ = fmt.Fprint(w, string(out))
return nil
},
@@ -377,23 +385,23 @@ Examples:
vaultik config set compression_level 9
vaultik config set s3.bucket mybucket # legacy S3 fields still supported`,
Args: cobra.ExactArgs(configSetArgs),
RunE: func(_ *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
path, err := ResolveConfigPath()
if err != nil {
return err
}
return writeConfigSet(os.Stdout, path, args[0], args[1])
return writeConfigSet(commandUI(cmd), path, args[0], args[1])
},
}
}
// writeConfigSet applies key=value to the config at path, writes it back
// owner-only, and confirms the write by printing just the key name to w.
// The value is never echoed: it may be a secret such as
// s3.secret_access_key, and captured stdout or a pasted terminal would
// then leak it.
func writeConfigSet(w io.Writer, path, key, value string) error {
// owner-only, and confirms the write by naming just the key through the
// UI writer (styled, and silenced by --quiet). The value is never
// echoed: it may be a secret such as s3.secret_access_key, and captured
// stdout or a pasted terminal would then leak it.
func writeConfigSet(out *ui.Writer, path, key, value string) error {
root, err := loadYAMLFile(path)
if err != nil {
return err
@@ -404,12 +412,12 @@ func writeConfigSet(w io.Writer, path, key, value string) error {
return err
}
out, err := marshalConfigYAML(root)
data, err := marshalConfigYAML(root)
if err != nil {
return fmt.Errorf("marshaling config: %w", err)
}
err = os.WriteFile(path, out, configFileMode)
err = os.WriteFile(path, data, configFileMode)
if err != nil {
return fmt.Errorf("writing config file: %w", err)
}
@@ -425,7 +433,7 @@ func writeConfigSet(w io.Writer, path, key, value string) error {
}
}
_, _ = fmt.Fprintln(w, key)
out.Infof("Set %s.", key)
return nil
}