Compare commits

...

10 commits

14 changed files with 391 additions and 498 deletions

687
README.md

File diff suppressed because it is too large Load diff

83
cmd/completions.go Normal file
View file

@ -0,0 +1,83 @@
package cmd
import (
"strings"
"github.com/spf13/cobra"
)
// completeKeys returns key[@store] completions for the current toComplete prefix.
// It handles three cases:
// - No "@" typed yet: return all keys from all stores (as "key@store")
// - "@" typed with partial store: return store-scoped completions
// - "key@store" with known store: return keys from that store
func completeKeys(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
store := &Store{}
stores, err := store.AllStores()
if err != nil || len(stores) == 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
var completions []string
parts := strings.SplitN(toComplete, "@", 2)
if len(parts) == 2 {
// User typed "something@" — complete keys within matching stores.
prefix := parts[0]
dbFilter := strings.ToLower(parts[1])
for _, db := range stores {
if !strings.HasPrefix(db, dbFilter) {
continue
}
keys, err := store.Keys(db)
if err != nil {
continue
}
for _, k := range keys {
if prefix == "" || strings.HasPrefix(k, strings.ToLower(prefix)) {
completions = append(completions, k+"@"+db)
}
}
}
} else {
// No "@" yet — offer key@store for every key in every store.
lowerPrefix := strings.ToLower(toComplete)
for _, db := range stores {
keys, err := store.Keys(db)
if err != nil {
continue
}
for _, k := range keys {
full := k + "@" + db
if strings.HasPrefix(full, lowerPrefix) || strings.HasPrefix(k, lowerPrefix) {
completions = append(completions, full)
}
}
}
}
return completions, cobra.ShellCompDirectiveNoFileComp
}
// completeStores returns store name completions.
func completeStores(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
store := &Store{}
stores, err := store.AllStores()
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
var completions []string
lowerPrefix := strings.ToLower(toComplete)
for _, db := range stores {
if strings.HasPrefix(db, lowerPrefix) {
completions = append(completions, db)
}
}
return completions, cobra.ShellCompDirectiveNoFileComp
}
// completeStoreFlag is a completion function for --store / -s string slice flags.
func completeStoreFlag(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completeStores(cmd, args, toComplete)
}

View file

@ -33,12 +33,13 @@ import (
// delStoreCmd represents the set command // delStoreCmd represents the set command
var delStoreCmd = &cobra.Command{ var delStoreCmd = &cobra.Command{
Use: "remove-store STORE", Use: "remove-store STORE",
Short: "Delete a store", Short: "Delete a store",
Aliases: []string{"rms"}, Aliases: []string{"rms"},
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: delStore, ValidArgsFunction: completeStores,
SilenceUsage: true, RunE: delStore,
SilenceUsage: true,
} }
func delStore(cmd *cobra.Command, args []string) error { func delStore(cmd *cobra.Command, args []string) error {

View file

@ -34,9 +34,10 @@ import (
var delCmd = &cobra.Command{ var delCmd = &cobra.Command{
Use: "remove KEY[@STORE] [KEY[@STORE] ...]", Use: "remove KEY[@STORE] [KEY[@STORE] ...]",
Short: "Delete one or more keys", Short: "Delete one or more keys",
Aliases: []string{"rm"}, Aliases: []string{"rm"},
Args: cobra.ArbitraryArgs, Args: cobra.ArbitraryArgs,
RunE: del, ValidArgsFunction: completeKeys,
RunE: del,
SilenceUsage: true, SilenceUsage: true,
} }
@ -145,6 +146,7 @@ func init() {
delCmd.Flags().Bool("force", false, "bypass read-only protection") delCmd.Flags().Bool("force", false, "bypass read-only protection")
delCmd.Flags().StringSliceP("key", "k", nil, "delete keys matching glob pattern (repeatable)") delCmd.Flags().StringSliceP("key", "k", nil, "delete keys matching glob pattern (repeatable)")
delCmd.Flags().StringSliceP("store", "s", nil, "target stores matching glob pattern (repeatable)") delCmd.Flags().StringSliceP("store", "s", nil, "target stores matching glob pattern (repeatable)")
delCmd.RegisterFlagCompletionFunc("store", completeStoreFlag)
delCmd.Flags().StringSliceP("value", "v", nil, "delete entries matching value glob pattern (repeatable)") delCmd.Flags().StringSliceP("value", "v", nil, "delete entries matching value glob pattern (repeatable)")
rootCmd.AddCommand(delCmd) rootCmd.AddCommand(delCmd)
} }

View file

@ -22,9 +22,10 @@ Binary values are presented as base64 for editing and decoded back on save.
Metadata flags (--ttl, --encrypt, --decrypt) can be passed alongside the edit Metadata flags (--ttl, --encrypt, --decrypt) can be passed alongside the edit
to modify metadata in the same operation.`, to modify metadata in the same operation.`,
Aliases: []string{"e"}, Aliases: []string{"e"},
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: edit, ValidArgsFunction: completeKeys,
RunE: edit,
SilenceUsage: true, SilenceUsage: true,
} }

View file

@ -31,6 +31,7 @@ var exportCmd = &cobra.Command{
Short: "Export store as NDJSON (alias for list --format ndjson)", Short: "Export store as NDJSON (alias for list --format ndjson)",
Aliases: []string{}, Aliases: []string{},
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
ValidArgsFunction: completeStores,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
listFormat = "ndjson" listFormat = "ndjson"
return list(cmd, args) return list(cmd, args)
@ -41,6 +42,7 @@ var exportCmd = &cobra.Command{
func init() { func init() {
exportCmd.Flags().StringSliceP("key", "k", nil, "filter keys with glob pattern (repeatable)") exportCmd.Flags().StringSliceP("key", "k", nil, "filter keys with glob pattern (repeatable)")
exportCmd.Flags().StringSliceP("store", "s", nil, "filter stores with glob pattern (repeatable)") exportCmd.Flags().StringSliceP("store", "s", nil, "filter stores with glob pattern (repeatable)")
exportCmd.RegisterFlagCompletionFunc("store", completeStoreFlag)
exportCmd.Flags().StringSliceP("value", "v", nil, "filter values with glob pattern (repeatable)") exportCmd.Flags().StringSliceP("value", "v", nil, "filter values with glob pattern (repeatable)")
rootCmd.AddCommand(exportCmd) rootCmd.AddCommand(exportCmd)
} }

View file

@ -46,9 +46,10 @@ additional argument after the initial KEY being fetched.
For example: For example:
pda set greeting 'Hello, {{ .NAME }}!' pda set greeting 'Hello, {{ .NAME }}!'
pda get greeting NAME=World`, pda get greeting NAME=World`,
Aliases: []string{"g"}, Aliases: []string{"g"},
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
RunE: get, ValidArgsFunction: completeKeys,
RunE: get,
SilenceUsage: true, SilenceUsage: true,
} }
@ -63,8 +64,9 @@ additional argument after the initial KEY being fetched.
For example: For example:
pda set greeting 'Hello, {{ .NAME }}!' pda set greeting 'Hello, {{ .NAME }}!'
pda run greeting NAME=World`, pda run greeting NAME=World`,
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
RunE: run, ValidArgsFunction: completeKeys,
RunE: run,
SilenceUsage: true, SilenceUsage: true,
} }

View file

@ -141,10 +141,11 @@ glob pattern to filter by store name.
Use --key/-k and --value/-v to filter by key or value glob, and --store/-s Use --key/-k and --value/-v to filter by key or value glob, and --store/-s
to filter by store name. All filters are repeatable and OR'd within the to filter by store name. All filters are repeatable and OR'd within the
same flag.`, same flag.`,
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
RunE: list, ValidArgsFunction: completeStores,
SilenceUsage: true, RunE: list,
SilenceUsage: true,
} }
func list(cmd *cobra.Command, args []string) error { func list(cmd *cobra.Command, args []string) error {
@ -785,6 +786,7 @@ func init() {
listCmd.Flags().VarP(&listFormat, "format", "o", "output format (table|tsv|csv|markdown|html|ndjson|json)") listCmd.Flags().VarP(&listFormat, "format", "o", "output format (table|tsv|csv|markdown|html|ndjson|json)")
listCmd.Flags().StringSliceP("key", "k", nil, "filter keys with glob pattern (repeatable)") listCmd.Flags().StringSliceP("key", "k", nil, "filter keys with glob pattern (repeatable)")
listCmd.Flags().StringSliceP("store", "s", nil, "filter stores with glob pattern (repeatable)") listCmd.Flags().StringSliceP("store", "s", nil, "filter stores with glob pattern (repeatable)")
listCmd.RegisterFlagCompletionFunc("store", completeStoreFlag)
listCmd.Flags().StringSliceP("value", "v", nil, "filter values with glob pattern (repeatable)") listCmd.Flags().StringSliceP("value", "v", nil, "filter values with glob pattern (repeatable)")
rootCmd.AddCommand(listCmd) rootCmd.AddCommand(listCmd)
} }

View file

@ -14,8 +14,9 @@ var metaCmd = &cobra.Command{
without changing its value. without changing its value.
With no flags, displays the key's current metadata. Pass flags to modify.`, With no flags, displays the key's current metadata. Pass flags to modify.`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: meta, ValidArgsFunction: completeKeys,
RunE: meta,
SilenceUsage: true, SilenceUsage: true,
} }

View file

@ -33,12 +33,13 @@ import (
// mvStoreCmd represents the move-store command // mvStoreCmd represents the move-store command
var mvStoreCmd = &cobra.Command{ var mvStoreCmd = &cobra.Command{
Use: "move-store FROM TO", Use: "move-store FROM TO",
Short: "Rename a store", Short: "Rename a store",
Aliases: []string{"mvs"}, Aliases: []string{"mvs"},
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),
RunE: mvStore, ValidArgsFunction: completeStores,
SilenceUsage: true, RunE: mvStore,
SilenceUsage: true,
} }
func mvStore(cmd *cobra.Command, args []string) error { func mvStore(cmd *cobra.Command, args []string) error {

View file

@ -30,21 +30,23 @@ import (
) )
var cpCmd = &cobra.Command{ var cpCmd = &cobra.Command{
Use: "copy FROM[@STORE] TO[@STORE]", Use: "copy FROM[@STORE] TO[@STORE]",
Aliases: []string{"cp"}, Aliases: []string{"cp"},
Short: "Make a copy of a key", Short: "Make a copy of a key",
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),
RunE: cp, ValidArgsFunction: completeKeys,
SilenceUsage: true, RunE: cp,
SilenceUsage: true,
} }
var mvCmd = &cobra.Command{ var mvCmd = &cobra.Command{
Use: "move FROM[@STORE] TO[@STORE]", Use: "move FROM[@STORE] TO[@STORE]",
Aliases: []string{"mv"}, Aliases: []string{"mv"},
Short: "Move a key", Short: "Move a key",
Args: cobra.ExactArgs(2), Args: cobra.ExactArgs(2),
RunE: mv, ValidArgsFunction: completeKeys,
SilenceUsage: true, RunE: mv,
SilenceUsage: true,
} }
func cp(cmd *cobra.Command, args []string) error { func cp(cmd *cobra.Command, args []string) error {

View file

@ -37,12 +37,13 @@ import (
) )
var restoreCmd = &cobra.Command{ var restoreCmd = &cobra.Command{
Use: "import [STORE]", Use: "import [STORE]",
Short: "Restore key/value pairs from an NDJSON dump", Short: "Restore key/value pairs from an NDJSON dump",
Aliases: []string{}, Aliases: []string{},
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
RunE: restore, ValidArgsFunction: completeStores,
SilenceUsage: true, RunE: restore,
SilenceUsage: true,
} }
func restore(cmd *cobra.Command, args []string) error { func restore(cmd *cobra.Command, args []string) error {
@ -323,6 +324,7 @@ func init() {
restoreCmd.Flags().StringP("file", "f", "", "path to an NDJSON dump (defaults to stdin)") restoreCmd.Flags().StringP("file", "f", "", "path to an NDJSON dump (defaults to stdin)")
restoreCmd.Flags().StringSliceP("key", "k", nil, "restore keys matching glob pattern (repeatable)") restoreCmd.Flags().StringSliceP("key", "k", nil, "restore keys matching glob pattern (repeatable)")
restoreCmd.Flags().StringSliceP("store", "s", nil, "restore entries from stores matching glob pattern (repeatable)") restoreCmd.Flags().StringSliceP("store", "s", nil, "restore entries from stores matching glob pattern (repeatable)")
restoreCmd.RegisterFlagCompletionFunc("store", completeStoreFlag)
restoreCmd.Flags().BoolP("interactive", "i", false, "prompt before overwriting existing keys") restoreCmd.Flags().BoolP("interactive", "i", false, "prompt before overwriting existing keys")
restoreCmd.Flags().Bool("drop", false, "drop existing entries before restoring (full replace)") restoreCmd.Flags().Bool("drop", false, "drop existing entries before restoring (full replace)")
rootCmd.AddCommand(restoreCmd) rootCmd.AddCommand(restoreCmd)

View file

@ -50,9 +50,10 @@ For example:
'Hello, {{ default "World" .NAME }}' will default to World if NAME is blank. 'Hello, {{ default "World" .NAME }}' will default to World if NAME is blank.
'Hello, {{ require .NAME }}' will error if NAME is blank. 'Hello, {{ require .NAME }}' will error if NAME is blank.
'{{ enum .NAME "Alice" "Bob" }}' allows only NAME=Alice or NAME=Bob.`, '{{ enum .NAME "Alice" "Bob" }}' allows only NAME=Alice or NAME=Bob.`,
Aliases: []string{"s"}, Aliases: []string{"s"},
Args: cobra.RangeArgs(1, 2), Args: cobra.RangeArgs(1, 2),
RunE: set, ValidArgsFunction: completeKeys,
RunE: set,
SilenceUsage: true, SilenceUsage: true,
} }

View file

@ -28,7 +28,7 @@ import (
) )
var ( var (
version = "pda! 2025.52 Christmas release" version = "pda! 2026.14"
) )
// versionCmd represents the version command // versionCmd represents the version command