Change NewCLIInstance() and NewCLIInstanceWithFs() to return (*Instance, error) instead of panicking on DetermineStateDir failure. Callers in RunE contexts propagate the error. Callers in command construction (for shell completion) use log.Fatalf. Test callers use t.Fatalf. Addresses review feedback on PR #18.
166 lines
4.9 KiB
Go
166 lines
4.9 KiB
Go
package cli
|
|
|
|
import (
|
|
"log"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/secret/internal/vault"
|
|
"github.com/dustin/go-humanize"
|
|
"github.com/fatih/color"
|
|
"github.com/spf13/afero"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// Version info - these are set at build time
|
|
var ( //nolint:gochecknoglobals // Set at build time
|
|
Version = "dev" //nolint:gochecknoglobals // Set at build time
|
|
GitCommit = "unknown" //nolint:gochecknoglobals // Set at build time
|
|
)
|
|
|
|
// InfoOutput represents the system information for JSON output
|
|
type InfoOutput struct {
|
|
Version string `json:"version"`
|
|
GitCommit string `json:"gitCommit"`
|
|
Author string `json:"author"`
|
|
License string `json:"license"`
|
|
GoVersion string `json:"goVersion"`
|
|
DataDirectory string `json:"dataDirectory"`
|
|
CurrentVault string `json:"currentVault"`
|
|
NumVaults int `json:"numVaults"`
|
|
NumSecrets int `json:"numSecrets"`
|
|
TotalSize int64 `json:"totalSizeBytes"`
|
|
OldestSecret time.Time `json:"oldestSecret,omitempty"`
|
|
LatestSecret time.Time `json:"latestSecret,omitempty"`
|
|
}
|
|
|
|
// newInfoCmd returns the info command
|
|
func newInfoCmd() *cobra.Command {
|
|
cli, err := NewCLIInstance()
|
|
if err != nil {
|
|
log.Fatalf("failed to initialize CLI: %v", err)
|
|
}
|
|
|
|
var jsonOutput bool
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "info",
|
|
Short: "Display system information",
|
|
Long: "Display information about the secret system including version, vault statistics, and storage usage",
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
return cli.Info(cmd, jsonOutput)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().BoolVar(&jsonOutput, "json", false, "Output in JSON format")
|
|
|
|
return cmd
|
|
}
|
|
|
|
// Info displays system information
|
|
func (cli *Instance) Info(cmd *cobra.Command, jsonOutput bool) error {
|
|
info := InfoOutput{
|
|
Version: Version,
|
|
GitCommit: GitCommit,
|
|
Author: "Jeffrey Paul <sneak@sneak.berlin>",
|
|
License: "WTFPL",
|
|
GoVersion: runtime.Version(),
|
|
DataDirectory: cli.stateDir,
|
|
}
|
|
|
|
// Get current vault
|
|
currentVault, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
|
if err == nil {
|
|
info.CurrentVault = currentVault.Name
|
|
}
|
|
|
|
// Count vaults
|
|
vaultsDir := filepath.Join(cli.stateDir, "vaults.d")
|
|
vaultEntries, err := afero.ReadDir(cli.fs, vaultsDir)
|
|
if err == nil {
|
|
for _, entry := range vaultEntries {
|
|
if entry.IsDir() {
|
|
info.NumVaults++
|
|
}
|
|
}
|
|
}
|
|
|
|
// Gather statistics from all vaults
|
|
if info.NumVaults > 0 {
|
|
totalSecrets, totalSize, oldestTime, latestTime, _ := gatherVaultStats(cli.fs, vaultsDir)
|
|
info.NumSecrets = totalSecrets
|
|
info.TotalSize = totalSize
|
|
if !oldestTime.IsZero() {
|
|
info.OldestSecret = oldestTime
|
|
}
|
|
if !latestTime.IsZero() {
|
|
info.LatestSecret = latestTime
|
|
}
|
|
}
|
|
|
|
if jsonOutput {
|
|
encoder := json.NewEncoder(cmd.OutOrStdout())
|
|
encoder.SetIndent("", " ")
|
|
|
|
return encoder.Encode(info)
|
|
}
|
|
|
|
// Pretty print with colors and emoji
|
|
return prettyPrintInfo(cmd.OutOrStdout(), info)
|
|
}
|
|
|
|
// prettyPrintInfo formats and prints the info in a pretty format
|
|
func prettyPrintInfo(w io.Writer, info InfoOutput) error {
|
|
const separatorLength = 40
|
|
|
|
bold := color.New(color.Bold)
|
|
green := color.New(color.FgGreen)
|
|
cyan := color.New(color.FgCyan)
|
|
yellow := color.New(color.FgYellow)
|
|
magenta := color.New(color.FgMagenta)
|
|
|
|
_, _ = fmt.Fprintln(w)
|
|
_, _ = bold.Fprintln(w, "🔐 Secret System Information")
|
|
_, _ = fmt.Fprintln(w, strings.Repeat("─", separatorLength))
|
|
|
|
_, _ = fmt.Fprintf(w, "📦 Version: %s\n", green.Sprint(info.Version))
|
|
_, _ = fmt.Fprintf(w, "🔧 Git Commit: %s\n", cyan.Sprint(info.GitCommit))
|
|
_, _ = fmt.Fprintf(w, "👤 Author: %s\n", cyan.Sprint(info.Author))
|
|
_, _ = fmt.Fprintf(w, "📜 License: %s\n", cyan.Sprint(info.License))
|
|
_, _ = fmt.Fprintf(w, "🐹 Go Version: %s\n", cyan.Sprint(info.GoVersion))
|
|
_, _ = fmt.Fprintf(w, "📁 Data Directory: %s\n", yellow.Sprint(info.DataDirectory))
|
|
|
|
if info.CurrentVault != "" {
|
|
_, _ = fmt.Fprintf(w, "🗄️ Current Vault: %s\n", magenta.Sprint(info.CurrentVault))
|
|
} else {
|
|
_, _ = fmt.Fprintf(w, "🗄️ Current Vault: %s\n", color.RedString("(none)"))
|
|
}
|
|
|
|
_, _ = fmt.Fprintln(w, strings.Repeat("─", separatorLength))
|
|
|
|
_, _ = fmt.Fprintf(w, "🗂️ Vaults: %s\n", bold.Sprint(info.NumVaults))
|
|
_, _ = fmt.Fprintf(w, "🔑 Secrets: %s\n", bold.Sprint(info.NumSecrets))
|
|
if info.TotalSize >= 0 {
|
|
//nolint:gosec // TotalSize is always >= 0
|
|
_, _ = fmt.Fprintf(w, "💾 Total Size: %s\n", bold.Sprint(humanize.Bytes(uint64(info.TotalSize))))
|
|
} else {
|
|
_, _ = fmt.Fprintf(w, "💾 Total Size: %s\n", bold.Sprint("0 B"))
|
|
}
|
|
|
|
if !info.OldestSecret.IsZero() {
|
|
_, _ = fmt.Fprintf(w, "🕰️ Oldest Secret: %s\n", info.OldestSecret.Format("2006-01-02 15:04:05"))
|
|
}
|
|
if !info.LatestSecret.IsZero() {
|
|
_, _ = fmt.Fprintf(w, "✨ Latest Secret: %s\n", info.LatestSecret.Format("2006-01-02 15:04:05"))
|
|
}
|
|
|
|
_, _ = fmt.Fprintln(w)
|
|
|
|
return nil
|
|
}
|