feat: lazysecrets list prints everything lazysecrets can find, for fzf and editor pickers #70
No files matched your search
+34
-27
@@ -29,9 +29,10 @@ import (
|
|||||||
//
|
//
|
||||||
// lazysecrets list | fzf --ansi --delimiter='\t' --with-nth='2..-3' --nth='2..' --accept-nth=-1
|
// lazysecrets list | fzf --ansi --delimiter='\t' --with-nth='2..-3' --nth='2..' --accept-nth=-1
|
||||||
//
|
//
|
||||||
// Each line is the kind, a mark (★ and the provider's icon), the name,
|
// Each line is the kind, a mark (★, ◷ and the provider's icon), the name,
|
||||||
// why it couldn't be read (empty unless it failed), the account and the
|
// why it couldn't be read (empty unless it failed), whether it expires
|
||||||
// link, tab-separated. The mark has a field of its own so a search can
|
// soon or has expired (empty unless so), the account and the link,
|
||||||
|
// tab-separated. The mark has a field of its own so a search can
|
||||||
// leave it out (fzf --nth), and ^prod finds names starting with prod. The
|
// leave it out (fzf --nth), and ^prod finds names starting with prod. The
|
||||||
// name is coloured from the theme,
|
// name is coloured from the theme,
|
||||||
// with the provider's icon (as in the TUI: --icons, LAZYSECRETS_ICONS,
|
// with the provider's icon (as in the TUI: --icons, LAZYSECRETS_ICONS,
|
||||||
@@ -45,12 +46,12 @@ func listCmd(providers []provider.Provider, st *store.Store, cachePath string, t
|
|||||||
fs := flag.NewFlagSet("list", flag.ContinueOnError)
|
fs := flag.NewFlagSet("list", flag.ContinueOnError)
|
||||||
cached := fs.Bool("cached", false, "only what's cached, and repos: load nothing from Azure or Databricks")
|
cached := fs.Bool("cached", false, "only what's cached, and repos: load nothing from Azure or Databricks")
|
||||||
maxAge := fs.Duration("max-age", 2*time.Minute, "load cached lists older than this again")
|
maxAge := fs.Duration("max-age", 2*time.Minute, "load cached lists older than this again")
|
||||||
format := fs.String("format", "text", "text: kind, mark, name, error, account and link, tab-separated; json: an object per line")
|
format := fs.String("format", "text", "text: kind, mark, name, error, expiry, account and link, tab-separated; json: an object per line")
|
||||||
color := fs.String("color", "always", "colour the names (text): always or never; fzf needs --ansi")
|
color := fs.String("color", "always", "colour the names (text): always or never; fzf needs --ansi")
|
||||||
iconsOn := fs.Bool("icons", withIcons, "put the provider's Nerd Font icon before names (text)")
|
iconsOn := fs.Bool("icons", withIcons, "put the provider's Nerd Font icon before names (text)")
|
||||||
stars := fs.Bool("stars", true, "put ★ before starred names, in the theme's favouriteColor (text)")
|
stars := fs.Bool("stars", true, "put ★ before starred names, in the theme's favouriteColor (text)")
|
||||||
recent := fs.Bool("recent", withRecent, "put ◷ before names used lately, in mutedColor (text; default ui.recentIcon)")
|
recent := fs.Bool("recent", withRecent, "put ◷ before names used lately, in mutedColor (text; default ui.recentIcon)")
|
||||||
expiry := fs.Bool("expiry", true, "put ⚠ before names expiring within ui.expiryWarningDays, ✗ before expired ones (text)")
|
expiry := fs.Bool("expiry", true, "say which expire within ui.expiryWarningDays (⚠ 12d) or have expired (✗ expired), after the name (text)")
|
||||||
kinds := fs.String("kind", "", "only these kinds, comma-separated: "+strings.Join(listing.Kinds, ", ")+" (vault: also scopes and repo files)")
|
kinds := fs.String("kind", "", "only these kinds, comma-separated: "+strings.Join(listing.Kinds, ", ")+" (vault: also scopes and repo files)")
|
||||||
showErrors := fs.Bool("errors", true, "list the lists that can't be read, with why (text: ✗ lines; json: \"error\")")
|
showErrors := fs.Bool("errors", true, "list the lists that can't be read, with why (text: ✗ lines; json: \"error\")")
|
||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
@@ -72,8 +73,8 @@ func listCmd(providers []provider.Provider, st *store.Store, cachePath string, t
|
|||||||
if *color != "always" && *color != "never" {
|
if *color != "always" && *color != "never" {
|
||||||
return fmt.Errorf("--color: %q is not always or never", *color)
|
return fmt.Errorf("--color: %q is not always or never", *color)
|
||||||
}
|
}
|
||||||
m := marks{theme: theme, icons: icons.None, stars: *stars, recent: *recent, expiry: *expiry,
|
m := marks{theme: theme, icons: icons.None, stars: *stars, recent: *recent, color: *color == "always"}
|
||||||
color: *color == "always", warn: expiryWarn, now: time.Now()}
|
now := time.Now()
|
||||||
if *iconsOn {
|
if *iconsOn {
|
||||||
m.icons = icons.NerdFont
|
m.icons = icons.NerdFont
|
||||||
}
|
}
|
||||||
@@ -85,8 +86,12 @@ func listCmd(providers []provider.Provider, st *store.Store, cachePath string, t
|
|||||||
mark := m.of(e)
|
mark := m.of(e)
|
||||||
name := nameOf(e, theme, colored)
|
name := nameOf(e, theme, colored)
|
||||||
why := errorOf(e, theme, colored)
|
why := errorOf(e, theme, colored)
|
||||||
|
var expires string
|
||||||
|
if *expiry {
|
||||||
|
expires = expiryOf(e, theme, expiryWarn, now, colored)
|
||||||
|
}
|
||||||
// A tab or line break in a name would break the line up.
|
// A tab or line break in a name would break the line up.
|
||||||
_, err := fmt.Fprintf(out, "%s\t%s\t%s\t%s\t%s\t%s\n", e.Kind, mark, oneLine.Replace(name), oneLine.Replace(why), e.Account, e.Link)
|
_, err := fmt.Fprintf(out, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", e.Kind, mark, oneLine.Replace(name), oneLine.Replace(why), expires, e.Account, e.Link)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "json":
|
case "json":
|
||||||
@@ -141,6 +146,22 @@ func listCmd(providers []provider.Provider, st *store.Store, cachePath string, t
|
|||||||
|
|
||||||
var oneLine = strings.NewReplacer("\t", " ", "\r\n", " ", "\n", " ", "\r", " ")
|
var oneLine = strings.NewReplacer("\t", " ", "\r\n", " ", "\n", " ", "\r", " ")
|
||||||
|
|
||||||
|
// expiryOf says, as the TUI does, that a secret, key or certificate has
|
||||||
|
// expired (✗ expired) or will within warn (⚠ 12d), or is "".
|
||||||
|
func expiryOf(e listing.Entry, theme styles.Styles, warn time.Duration, now time.Time, color bool) string {
|
||||||
|
render := renderer(color)
|
||||||
|
left := e.Expires.Sub(now)
|
||||||
|
switch {
|
||||||
|
case e.Expires.IsZero() || left > warn:
|
||||||
|
return ""
|
||||||
|
case left <= 0:
|
||||||
|
return render(theme.Error, "✗ expired")
|
||||||
|
case left < 24*time.Hour:
|
||||||
|
return render(theme.Warning, fmt.Sprintf("⚠ %dh", max(1, int(left.Hours()))))
|
||||||
|
}
|
||||||
|
return render(theme.Warning, fmt.Sprintf("⚠ %dd", int(left.Hours()/24)))
|
||||||
|
}
|
||||||
|
|
||||||
// errorOf is why a list couldn't be read, or "": with colour, a red ✗
|
// errorOf is why a list couldn't be read, or "": with colour, a red ✗
|
||||||
// before it, as the TUI marks lists it can't read.
|
// before it, as the TUI marks lists it can't read.
|
||||||
func errorOf(e listing.Entry, theme styles.Styles, color bool) string {
|
func errorOf(e listing.Entry, theme styles.Styles, color bool) string {
|
||||||
@@ -168,17 +189,14 @@ func nameOf(e listing.Entry, theme styles.Styles, color bool) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// marks says what goes before the name, in a field of its own so a
|
// marks says what goes before the name, in a field of its own so a
|
||||||
// search leaves it out: ★ for a starred entry, ◷ for one used lately,
|
// search leaves it out: ★ for a starred entry and ◷ for one used lately,
|
||||||
// ⚠ for one expiring within warn and ✗ for an expired one, as the TUI
|
// as the TUI marks them, then the provider's icon. The marks are
|
||||||
// marks them, then the provider's icon. The marks are right-aligned in
|
// right-aligned in room for every kind turned on, so they sit by the icon
|
||||||
// room for every kind turned on, so they sit by the icon and names line
|
// and names line up.
|
||||||
// up.
|
|
||||||
type marks struct {
|
type marks struct {
|
||||||
theme styles.Styles
|
theme styles.Styles
|
||||||
icons icons.Set
|
icons icons.Set
|
||||||
stars, recent, expiry, color bool
|
stars, recent, color bool
|
||||||
warn time.Duration // ui.expiryWarningDays
|
|
||||||
now time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m marks) of(e listing.Entry) string {
|
func (m marks) of(e listing.Entry) string {
|
||||||
@@ -197,17 +215,6 @@ func (m marks) of(e listing.Entry) string {
|
|||||||
if m.recent {
|
if m.recent {
|
||||||
mark(e.Recent, m.theme.Muted, "◷")
|
mark(e.Recent, m.theme.Muted, "◷")
|
||||||
}
|
}
|
||||||
if m.expiry {
|
|
||||||
left := e.Expires.Sub(m.now)
|
|
||||||
switch {
|
|
||||||
case e.Expires.IsZero() || left > m.warn:
|
|
||||||
mark(false, m.theme.Muted, "")
|
|
||||||
case left <= 0:
|
|
||||||
mark(true, m.theme.Error, "✗")
|
|
||||||
default:
|
|
||||||
mark(true, m.theme.Warning, "⚠")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pad := strings.Repeat(" ", room-len(parts)) // each mark and its space
|
pad := strings.Repeat(" ", room-len(parts)) // each mark and its space
|
||||||
kind := provider.Kind("")
|
kind := provider.Kind("")
|
||||||
for _, k := range []provider.Kind{provider.KindAzure, provider.KindDatabricks, provider.KindRepo} {
|
for _, k := range []provider.Kind{provider.KindAzure, provider.KindDatabricks, provider.KindRepo} {
|
||||||
|
|||||||
@@ -46,31 +46,43 @@ func TestListMarks(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListRecentAndExpiryMarks(t *testing.T) {
|
func TestListRecentMarks(t *testing.T) {
|
||||||
az := icons.NerdFont.Azure
|
az := icons.NerdFont.Azure
|
||||||
now := time.Date(2026, 10, 14, 12, 0, 0, 0, time.UTC)
|
m := marks{icons: icons.NerdFont, stars: true, recent: true}
|
||||||
m := marks{icons: icons.NerdFont, stars: true, recent: true, expiry: true, warn: 30 * 24 * time.Hour, now: now}
|
|
||||||
for _, c := range []struct {
|
for _, c := range []struct {
|
||||||
e listing.Entry
|
e listing.Entry
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
// Right-aligned in room for three: by the icon, names lined up.
|
// Right-aligned in room for two: by the icon, names lined up.
|
||||||
{listing.Entry{Provider: "azure", Starred: true, Recent: true}, " ★ ◷ " + az},
|
{listing.Entry{Provider: "azure", Starred: true, Recent: true}, "★ ◷ " + az},
|
||||||
{listing.Entry{Provider: "azure", Recent: true}, " ◷ " + az},
|
{listing.Entry{Provider: "azure", Recent: true}, " ◷ " + az},
|
||||||
{listing.Entry{Provider: "azure", Expires: now.Add(-time.Hour)}, " ✗ " + az},
|
{listing.Entry{Provider: "azure"}, " " + az},
|
||||||
{listing.Entry{Provider: "azure", Expires: now.Add(10 * 24 * time.Hour)}, " ⚠ " + az},
|
|
||||||
{listing.Entry{Provider: "azure", Expires: now.Add(60 * 24 * time.Hour)}, " " + az},
|
|
||||||
} {
|
} {
|
||||||
if got := m.of(c.e); got != c.want {
|
if got := m.of(c.e); got != c.want {
|
||||||
t.Errorf("%+v: %q, want %q", c.e, got, c.want)
|
t.Errorf("%+v: %q, want %q", c.e, got, c.want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.color, m.theme = true, styles.Default()
|
}
|
||||||
if got := m.of(listing.Entry{Expires: now.Add(-time.Hour)}); !strings.Contains(got, styles.Default().Error.Render("✗")) {
|
|
||||||
t.Errorf("expired isn't a red ✗: %q", got)
|
func TestListExpiry(t *testing.T) {
|
||||||
|
now := time.Date(2026, 10, 14, 12, 0, 0, 0, time.UTC)
|
||||||
|
warn := 30 * 24 * time.Hour
|
||||||
|
for _, c := range []struct {
|
||||||
|
expires time.Time
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{time.Time{}, ""},
|
||||||
|
{now.Add(-time.Hour), "✗ expired"},
|
||||||
|
{now.Add(12*24*time.Hour + time.Hour), "⚠ 12d"},
|
||||||
|
{now.Add(5 * time.Hour), "⚠ 5h"},
|
||||||
|
{now.Add(60 * 24 * time.Hour), ""},
|
||||||
|
} {
|
||||||
|
if got := expiryOf(listing.Entry{Expires: c.expires}, styles.Default(), warn, now, false); got != c.want {
|
||||||
|
t.Errorf("expires %v: %q, want %q", c.expires, got, c.want)
|
||||||
}
|
}
|
||||||
if got := m.of(listing.Entry{Expires: now.Add(time.Hour)}); !strings.Contains(got, styles.Default().Warning.Render("⚠")) {
|
}
|
||||||
t.Errorf("expiring isn't a ⚠ in the warning colour: %q", got)
|
if got := expiryOf(listing.Entry{Expires: now.Add(-time.Hour)}, styles.Default(), warn, now, true); got != styles.Default().Error.Render("✗ expired") {
|
||||||
|
t.Errorf("expired isn't red: %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1304,7 +1304,8 @@ func TestExpiringInStatus(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
d := newDriverOpts(t, Options{ExpiryWarning: 30 * 24 * time.Hour}, fake.Azure(0))
|
d := newDriverOpts(t, Options{ExpiryWarning: 30 * 24 * time.Hour}, fake.Azure(0))
|
||||||
d.keys("3", "]", "]") // load kv-app-prod's certificates
|
d.keys("3", "]", "]") // load kv-app-prod's certificates
|
||||||
if !strings.Contains(d.screen(), "⚠ 2 expiring") { // old-tls, legacy-token
|
// old-tls and legacy-token.
|
||||||
|
if !strings.Contains(d.screen(), "⚠ 2 expiring") {
|
||||||
t.Errorf("status line:\n%s", d.screen())
|
t.Errorf("status line:\n%s", d.screen())
|
||||||
}
|
}
|
||||||
d.keys("0")
|
d.keys("0")
|
||||||
|
|||||||
Reference in new issue
Block a user