Files
secret/internal/cli/completions.go
T
clawbot 0e6a4afb71
check / check (push) Failing after 2s
Make an unlocker's ID the name of its directory (closes #98)
Keychain and Secure Enclave unlocker IDs were the creation time to the
minute plus the host name, and passphrase unlocker IDs the time to the
minute, so two created within one minute shared an ID, and `unlocker
select`, `unlocker remove` and the selection after `unlocker add` acted on
the older one. Every unlocker's ID is now its directory name, unique in
its vault. `vault.ListUnlockers` returns each unlocker's metadata keyed by
that name, so `unlocker list` and shell completion no longer find IDs by
matching metadata. PGP unlocker IDs were `pgp-<fingerprint>`; a second
PGP unlocker for one key is refused by comparing fingerprints in metadata.

Model: opus-5-5
2026-10-04 21:25:00 +02:00

186 lines
5.1 KiB
Go

package cli
import (
"maps"
"slices"
"strings"
"git.eeqj.de/sneak/secret/internal/vault"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)
// getSecretNamesCompletionFunc returns a completion function that provides
// secret names
func getSecretNamesCompletionFunc(fs afero.Fs, stateDir string) func(
cmd *cobra.Command, args []string, toComplete string,
) ([]string, cobra.ShellCompDirective) {
return func(
_ *cobra.Command, _ []string, toComplete string,
) ([]string, cobra.ShellCompDirective) {
// Get current vault
vlt, err := vault.GetCurrentVault(fs, stateDir)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
// Get list of secrets
secrets, err := vlt.ListSecrets()
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
// Filter secrets based on what user has typed
var completions []string
for _, secret := range secrets {
if strings.HasPrefix(secret, toComplete) {
completions = append(completions, secret)
}
}
return completions, cobra.ShellCompDirectiveNoFileComp
}
}
// getUnlockerIDsCompletionFunc returns a completion function that provides
// unlocker IDs, the names of the unlockers' directories in unlockers.d
func getUnlockerIDsCompletionFunc(fs afero.Fs, stateDir string) func(
cmd *cobra.Command, args []string, toComplete string,
) ([]string, cobra.ShellCompDirective) {
return func(
_ *cobra.Command, _ []string, toComplete string,
) ([]string, cobra.ShellCompDirective) {
// Get current vault
vlt, err := vault.GetCurrentVault(fs, stateDir)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
unlockerMetadata, err := vlt.ListUnlockers()
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
var completions []string
for _, id := range slices.Sorted(maps.Keys(unlockerMetadata)) {
if strings.HasPrefix(id, toComplete) {
completions = append(completions, id)
}
}
return completions, cobra.ShellCompDirectiveNoFileComp
}
}
// getVaultNamesCompletionFunc returns a completion function that provides
// vault names
func getVaultNamesCompletionFunc(fs afero.Fs, stateDir string) func(
cmd *cobra.Command, args []string, toComplete string,
) ([]string, cobra.ShellCompDirective) {
return func(
_ *cobra.Command, _ []string, toComplete string,
) ([]string, cobra.ShellCompDirective) {
vaults, err := vault.ListVaults(fs, stateDir)
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
var completions []string
for _, v := range vaults {
if strings.HasPrefix(v, toComplete) {
completions = append(completions, v)
}
}
return completions, cobra.ShellCompDirectiveNoFileComp
}
}
// completeVaultQualifiedSecrets completes "vault:secret" references once a
// colon is present in the input. It completes nothing when the vault part
// is not a valid vault name, so that a name such as ".." cannot list a
// directory outside vaults.d.
func completeVaultQualifiedSecrets(
fs afero.Fs, stateDir, toComplete string,
) []string {
var completions []string
// Complete secret names for the specified vault
parts := strings.SplitN(toComplete, ":", vaultSecretParts)
vaultName := parts[0]
secretPrefix := parts[1]
if vault.ValidateVaultName(vaultName) != nil {
return nil
}
vlt := vault.NewVault(fs, stateDir, vaultName)
secrets, err := vlt.ListSecrets()
if err == nil {
for _, secretName := range secrets {
if strings.HasPrefix(secretName, secretPrefix) {
completions = append(completions, vaultName+":"+secretName)
}
}
}
return completions
}
// completeUnqualifiedVaultSecrets completes vault names (with a ":"
// suffix) and secrets from the current vault
func completeUnqualifiedVaultSecrets(
fs afero.Fs, stateDir, toComplete string,
) []string {
var completions []string
// Complete vault names with ":" suffix
vaults, err := vault.ListVaults(fs, stateDir)
if err == nil {
for _, v := range vaults {
if strings.HasPrefix(v, toComplete) {
completions = append(completions, v+":")
}
}
}
// Also complete secrets from current vault (for within-vault moves)
currentVlt, err := vault.GetCurrentVault(fs, stateDir)
if err == nil {
secrets, err := currentVlt.ListSecrets()
if err == nil {
for _, secretName := range secrets {
if strings.HasPrefix(secretName, toComplete) {
completions = append(completions, secretName)
}
}
}
}
return completions
}
// getVaultSecretCompletionFunc returns a completion function for the
// vault:secret format. It completes vault names with ":" suffix, and
// after ":" it completes secrets from that vault.
func getVaultSecretCompletionFunc(fs afero.Fs, stateDir string) func(
cmd *cobra.Command, args []string, toComplete string,
) ([]string, cobra.ShellCompDirective) {
return func(
_ *cobra.Command, _ []string, toComplete string,
) ([]string, cobra.ShellCompDirective) {
// Check if we're completing after a vault: prefix
if strings.Contains(toComplete, ":") {
return completeVaultQualifiedSecrets(fs, stateDir, toComplete),
cobra.ShellCompDirectiveNoFileComp
}
return completeUnqualifiedVaultSecrets(fs, stateDir, toComplete),
cobra.ShellCompDirectiveNoSpace
}
}