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
var delStoreCmd = &cobra.Command{
Use: "remove-store STORE",
Short: "Delete a store",
Aliases: []string{"rms"},
Args: cobra.ExactArgs(1),
RunE: delStore,
SilenceUsage: true,
Use: "remove-store STORE",
Short: "Delete a store",
Aliases: []string{"rms"},
Args: cobra.ExactArgs(1),
ValidArgsFunction: completeStores,
RunE: delStore,
SilenceUsage: true,
}
func delStore(cmd *cobra.Command, args []string) error {

View file

@ -34,9 +34,10 @@ import (
var delCmd = &cobra.Command{
Use: "remove KEY[@STORE] [KEY[@STORE] ...]",
Short: "Delete one or more keys",
Aliases: []string{"rm"},
Args: cobra.ArbitraryArgs,
RunE: del,
Aliases: []string{"rm"},
Args: cobra.ArbitraryArgs,
ValidArgsFunction: completeKeys,
RunE: del,
SilenceUsage: true,
}
@ -145,6 +146,7 @@ func init() {
delCmd.Flags().Bool("force", false, "bypass read-only protection")
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.RegisterFlagCompletionFunc("store", completeStoreFlag)
delCmd.Flags().StringSliceP("value", "v", nil, "delete entries matching value glob pattern (repeatable)")
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
to modify metadata in the same operation.`,
Aliases: []string{"e"},
Args: cobra.ExactArgs(1),
RunE: edit,
Aliases: []string{"e"},
Args: cobra.ExactArgs(1),
ValidArgsFunction: completeKeys,
RunE: edit,
SilenceUsage: true,
}

View file

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

View file

@ -46,9 +46,10 @@ additional argument after the initial KEY being fetched.
For example:
pda set greeting 'Hello, {{ .NAME }}!'
pda get greeting NAME=World`,
Aliases: []string{"g"},
Args: cobra.MinimumNArgs(1),
RunE: get,
Aliases: []string{"g"},
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: completeKeys,
RunE: get,
SilenceUsage: true,
}
@ -63,8 +64,9 @@ additional argument after the initial KEY being fetched.
For example:
pda set greeting 'Hello, {{ .NAME }}!'
pda run greeting NAME=World`,
Args: cobra.MinimumNArgs(1),
RunE: run,
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: completeKeys,
RunE: run,
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
to filter by store name. All filters are repeatable and OR'd within the
same flag.`,
Aliases: []string{"ls"},
Args: cobra.MaximumNArgs(1),
RunE: list,
SilenceUsage: true,
Aliases: []string{"ls"},
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: completeStores,
RunE: list,
SilenceUsage: true,
}
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().StringSliceP("key", "k", nil, "filter keys 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)")
rootCmd.AddCommand(listCmd)
}

View file

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

View file

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

View file

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

View file

@ -37,12 +37,13 @@ import (
)
var restoreCmd = &cobra.Command{
Use: "import [STORE]",
Short: "Restore key/value pairs from an NDJSON dump",
Aliases: []string{},
Args: cobra.MaximumNArgs(1),
RunE: restore,
SilenceUsage: true,
Use: "import [STORE]",
Short: "Restore key/value pairs from an NDJSON dump",
Aliases: []string{},
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: completeStores,
RunE: restore,
SilenceUsage: true,
}
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().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.RegisterFlagCompletionFunc("store", completeStoreFlag)
restoreCmd.Flags().BoolP("interactive", "i", false, "prompt before overwriting existing keys")
restoreCmd.Flags().Bool("drop", false, "drop existing entries before restoring (full replace)")
rootCmd.AddCommand(restoreCmd)

View file

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

View file

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