Route direct-stdout command output through internal/ui (closes #149)
version, info, remote info, config, and database delete wrote plain text straight to stdout, so they were unstyled and ignored --quiet. Output is now governed by internal/ui in two buckets. Status lines and confirmations (config init, config set, the database delete prompt) go through the ui message methods and are silenced by --quiet. The data a command exists to produce is written plain -- the version/info/remote-info reports, the snapshot list table, config get values, and the --json documents -- and is never suppressed, since a script depends on it and a marker would corrupt a table or document. The database delete confirmation prompt is always shown. Pure-cli commands reach ui through a small commandUI helper. Model: opus-4-8
This commit was merged in pull request #201.
This commit is contained in:
@@ -13,6 +13,14 @@ import (
|
||||
|
||||
// ShowInfo displays system and configuration information
|
||||
func (v *Vaultik) ShowInfo() error {
|
||||
// The info report is the output this command exists to produce, so it
|
||||
// is written plain (markers would corrupt the aligned report) through
|
||||
// the UI writer's stdout; --quiet silences it like any other
|
||||
// non-error output.
|
||||
if v.UI.Quiet() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// System Information
|
||||
v.stdoutf("=== System Information ===\n")
|
||||
v.stdoutf("OS/Architecture: %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
||||
@@ -213,7 +221,12 @@ func (v *Vaultik) RemoteInfo(jsonOutput bool) error {
|
||||
result.StorageType = storageInfo.Type
|
||||
result.StorageLocation = storageInfo.Location
|
||||
|
||||
if !jsonOutput {
|
||||
// The human report is written only when it is neither the --json
|
||||
// document (which needs stdout to itself) nor silenced by --quiet. The
|
||||
// scan still runs in both cases so --json still gets a full result.
|
||||
showText := !jsonOutput && !v.UI.Quiet()
|
||||
|
||||
if showText {
|
||||
v.stdoutf("=== Remote Storage ===\n")
|
||||
v.stdoutf("Type: %s\n", storageInfo.Type)
|
||||
v.stdoutf("Location: %s\n", storageInfo.Location)
|
||||
@@ -226,7 +239,7 @@ func (v *Vaultik) RemoteInfo(jsonOutput bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if !jsonOutput {
|
||||
if showText {
|
||||
v.stdoutf("Downloading %d manifest(s)...\n", len(snapshotIDs))
|
||||
}
|
||||
|
||||
@@ -234,7 +247,7 @@ func (v *Vaultik) RemoteInfo(jsonOutput bool) error {
|
||||
|
||||
v.populateRemoteInfoResult(result, snapshotMetadata, snapshotIDs, referencedBlobs)
|
||||
|
||||
err = v.scanRemoteBlobStorage(result, referencedBlobs, jsonOutput)
|
||||
err = v.scanRemoteBlobStorage(result, referencedBlobs, showText)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -252,7 +265,9 @@ func (v *Vaultik) RemoteInfo(jsonOutput bool) error {
|
||||
return enc.Encode(result)
|
||||
}
|
||||
|
||||
v.printRemoteInfoTable(result)
|
||||
if showText {
|
||||
v.printRemoteInfoTable(result)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -362,11 +377,13 @@ func (v *Vaultik) populateRemoteInfoResult(
|
||||
}
|
||||
}
|
||||
|
||||
// scanRemoteBlobStorage lists all blobs on remote and computes orphan stats
|
||||
// scanRemoteBlobStorage lists all blobs on remote and computes orphan
|
||||
// stats. showText is true only when the human report is being printed
|
||||
// (not --json, not --quiet), gating the progress line.
|
||||
func (v *Vaultik) scanRemoteBlobStorage(
|
||||
result *RemoteInfoResult, referencedBlobs map[string]int64, jsonOutput bool,
|
||||
result *RemoteInfoResult, referencedBlobs map[string]int64, showText bool,
|
||||
) error {
|
||||
if !jsonOutput {
|
||||
if showText {
|
||||
v.stdoutf("Scanning blobs...\n")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package vaultik_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"sneak.berlin/go/vaultik/internal/ui"
|
||||
"sneak.berlin/go/vaultik/internal/vaultik"
|
||||
)
|
||||
|
||||
// TestShowInfo_QuietSuppressesReport checks that --quiet silences the
|
||||
// whole info report. The report is human-facing status, not a scriptable
|
||||
// value, so under --quiet the command produces nothing (and touches none
|
||||
// of its dependencies, which is why this minimal instance suffices).
|
||||
func TestShowInfo_QuietSuppressesReport(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var out bytes.Buffer
|
||||
|
||||
v := &vaultik.Vaultik{
|
||||
Stdout: &out,
|
||||
UI: ui.NewWithColor(&out, false),
|
||||
}
|
||||
v.UI.SetQuiet(true)
|
||||
|
||||
require.NoError(t, v.ShowInfo())
|
||||
require.Empty(t, out.String(),
|
||||
"the info report must be suppressed under --quiet")
|
||||
}
|
||||
@@ -114,9 +114,15 @@ func (v *Vaultik) ListSnapshots(jsonOutput bool) error {
|
||||
return encoder.Encode(snapshots)
|
||||
}
|
||||
|
||||
err = v.printSnapshotTable(snapshots)
|
||||
if err != nil {
|
||||
return err
|
||||
// The table is the output this command exists to produce, so it is
|
||||
// written plain (markers would corrupt the columns) to the UI writer's
|
||||
// stdout; --quiet silences it. Reconciliation notes below go through
|
||||
// the UI methods, so their warnings still emit under --quiet.
|
||||
if !v.UI.Quiet() {
|
||||
err = v.printSnapshotTable(snapshots)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if remoteErr == nil {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package vaultik_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"sneak.berlin/go/vaultik/internal/log"
|
||||
)
|
||||
|
||||
// TestListSnapshots_QuietSuppressesTableNotJSON is the --quiet contract
|
||||
// for `snapshot list`: the human table is silenced, but the --json
|
||||
// document a script depends on still emits. A local snapshot with its
|
||||
// remote counterpart present is used so there are no drift notes, whose
|
||||
// warnings would emit even under --quiet.
|
||||
func TestListSnapshots_QuietSuppressesTableNotJSON(t *testing.T) {
|
||||
log.Initialize(log.Config{})
|
||||
t.Parallel()
|
||||
|
||||
env := newListEnv(t)
|
||||
|
||||
ts := time.Date(2026, 3, 1, 10, 0, 0, 0, time.UTC)
|
||||
env.addLocal(t, listLocalID, ts)
|
||||
env.addRemote(t, listLocalID, ts)
|
||||
|
||||
env.v.UI.SetQuiet(true)
|
||||
|
||||
// Table mode: nothing on stdout.
|
||||
require.NoError(t, env.v.ListSnapshots(false))
|
||||
require.Empty(t, env.stdout.String(),
|
||||
"the table must be suppressed under --quiet")
|
||||
|
||||
// JSON mode: the document is still written despite the quiet UI.
|
||||
env.stdout.Reset()
|
||||
require.NoError(t, env.v.ListSnapshots(true))
|
||||
|
||||
rows := decodeListJSON(t, env.stdout.String())
|
||||
require.Len(t, rows, 1)
|
||||
require.Equal(t, listLocalID, rows[0].ID,
|
||||
"the --json document must still emit under --quiet")
|
||||
}
|
||||
@@ -215,6 +215,7 @@ func NewForTesting(storage storage.Storer) *TestVaultik {
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
Stdin: stdin,
|
||||
UI: ui.NewWithColor(stdout, false),
|
||||
},
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
|
||||
Reference in New Issue
Block a user