feat: adds --readonly and --pin flags, and displays Size column in list by default
This commit is contained in:
parent
e5b6dcd187
commit
5bcd3581dd
46 changed files with 711 additions and 177 deletions
186
README.md
186
README.md
|
|
@ -26,6 +26,7 @@
|
||||||
- plaintext exports in 7 different formats,
|
- plaintext exports in 7 different formats,
|
||||||
- support for all [binary data](https://github.com/Llywelwyn/pda#binary),
|
- support for all [binary data](https://github.com/Llywelwyn/pda#binary),
|
||||||
- expiring keys with a [time-to-live](https://github.com/Llywelwyn/pda#ttl),
|
- expiring keys with a [time-to-live](https://github.com/Llywelwyn/pda#ttl),
|
||||||
|
- [read-only](https://github.com/Llywelwyn/pda#read-only--pinned) keys and [pinned](https://github.com/Llywelwyn/pda#read-only--pinned) entries,
|
||||||
- built-in [diagnostics](https://github.com/Llywelwyn/pda#doctor) and [configuration](https://github.com/Llywelwyn/pda#config),
|
- built-in [diagnostics](https://github.com/Llywelwyn/pda#doctor) and [configuration](https://github.com/Llywelwyn/pda#config),
|
||||||
|
|
||||||
and more, written in pure Go, and inspired by [skate](https://github.com/charmbracelet/skate) and [nb](https://github.com/xwmx/nb).
|
and more, written in pure Go, and inspired by [skate](https://github.com/charmbracelet/skate) and [nb](https://github.com/xwmx/nb).
|
||||||
|
|
@ -55,6 +56,7 @@ and more, written in pure Go, and inspired by [skate](https://github.com/charmbr
|
||||||
- [Templates](https://github.com/Llywelwyn/pda#templates)
|
- [Templates](https://github.com/Llywelwyn/pda#templates)
|
||||||
- [Filtering](https://github.com/Llywelwyn/pda#filtering)
|
- [Filtering](https://github.com/Llywelwyn/pda#filtering)
|
||||||
- [TTL](https://github.com/Llywelwyn/pda#ttl)
|
- [TTL](https://github.com/Llywelwyn/pda#ttl)
|
||||||
|
- [Read-only & Pinned](https://github.com/Llywelwyn/pda#read-only--pinned)
|
||||||
- [Binary](https://github.com/Llywelwyn/pda#binary)
|
- [Binary](https://github.com/Llywelwyn/pda#binary)
|
||||||
- [Encryption](https://github.com/Llywelwyn/pda#encryption)
|
- [Encryption](https://github.com/Llywelwyn/pda#encryption)
|
||||||
- [Doctor](https://github.com/Llywelwyn/pda#doctor)
|
- [Doctor](https://github.com/Llywelwyn/pda#doctor)
|
||||||
|
|
@ -152,6 +154,9 @@ pda set name "Alice" --safe
|
||||||
pda set name "Bob" --safe
|
pda set name "Bob" --safe
|
||||||
pda get name
|
pda get name
|
||||||
# Alice
|
# Alice
|
||||||
|
|
||||||
|
# --readonly to protect a key from modification.
|
||||||
|
pda set api-url "https://prod.example.com" --readonly
|
||||||
```
|
```
|
||||||
|
|
||||||
<p align="center"></p><!-- spacer -->
|
<p align="center"></p><!-- spacer -->
|
||||||
|
|
@ -202,12 +207,17 @@ pda mv name name2 --safe
|
||||||
pda mv name name2 -y
|
pda mv name name2 -y
|
||||||
```
|
```
|
||||||
|
|
||||||
`pda cp` to make a copy.
|
`pda cp` to make a copy. All metadata is preserved.
|
||||||
```bash
|
```bash
|
||||||
pda cp name name2
|
pda cp name name2
|
||||||
|
|
||||||
# 'mv --copy' and 'cp' are aliases. Either one works.
|
# 'mv --copy' and 'cp' are aliases. Either one works.
|
||||||
pda mv name name2 --copy
|
pda mv name name2 --copy
|
||||||
|
|
||||||
|
# Read-only keys can't be moved or overwritten without --force.
|
||||||
|
pda mv readonly-key newname
|
||||||
|
# FAIL cannot move 'readonly-key': key is read-only
|
||||||
|
pda mv readonly-key newname --force
|
||||||
```
|
```
|
||||||
|
|
||||||
<p align="center"></p><!-- spacer -->
|
<p align="center"></p><!-- spacer -->
|
||||||
|
|
@ -216,7 +226,7 @@ pda mv name name2 --copy
|
||||||
```bash
|
```bash
|
||||||
pda rm kitty
|
pda rm kitty
|
||||||
|
|
||||||
# Remove multiple keys, within the same or different stores.
|
# Remove multiple keys.
|
||||||
pda rm kitty dog@animals
|
pda rm kitty dog@animals
|
||||||
|
|
||||||
# Mix exact keys with glob patterns.
|
# Mix exact keys with glob patterns.
|
||||||
|
|
@ -232,32 +242,42 @@ pda rm kitty -i
|
||||||
|
|
||||||
# --yes/-y to auto-accept all confirmation prompts.
|
# --yes/-y to auto-accept all confirmation prompts.
|
||||||
pda rm kitty -y
|
pda rm kitty -y
|
||||||
|
|
||||||
|
# Read-only keys can't be deleted without --force.
|
||||||
|
pda rm protected-key
|
||||||
|
# FAIL cannot remove 'protected-key': key is read-only
|
||||||
|
pda rm protected-key --force
|
||||||
```
|
```
|
||||||
|
|
||||||
<p align="center"></p><!-- spacer -->
|
<p align="center"></p><!-- spacer -->
|
||||||
|
|
||||||
`pda ls` to see what you've got stored. By default it lists the contents of all stores. Pass a store name to check only the given store. Checking a specific store is faster than checking everything, but the slowdown should be insignificant unless you have masses of different stores. `list.always_show_all_stores` can be set to false to list `store.default_store_name` by default.
|
`pda ls` to see what you've got stored. The default columns are `meta,size,ttl,store,key,value`. Meta is a 4-char flag string showing `(e)ncrypted (w)ritable (t)tl (p)inned`, or a dash for an unset flag. Pinned entries sort to the top.
|
||||||
|
|
||||||
|
By default it lists the contents of all stores. Pass a store name to check only the given store. Checking a specific store is faster than checking everything, but the slowdown should be insignificant unless you have masses of different stores. `list.always_show_all_stores` can be set to false to list only the default store when none is specified.
|
||||||
```bash
|
```bash
|
||||||
pda ls
|
pda ls
|
||||||
# Key Store Value TTL
|
# Meta Size TTL Store Key Value
|
||||||
# dogs default four legged mammals no expiry
|
# -w-p 5 - store todo don't forget this
|
||||||
# name default Alice no expiry
|
# ---- 23 - store url https://prod.example.com
|
||||||
|
# -w-- 5 - store name Alice
|
||||||
|
|
||||||
# Narrow to a single store.
|
# Narrow to a single store.
|
||||||
pda ls @default
|
pda ls @store
|
||||||
|
|
||||||
# Or filter stores by glob pattern.
|
# Or filter stores by glob pattern.
|
||||||
pda ls --store "prod*"
|
pda ls --store "prod*"
|
||||||
|
|
||||||
|
# Suppress or add columns with --no-X flags.
|
||||||
|
# --no-X suppresses. --no-X=false adds even if not in default config.
|
||||||
|
|
||||||
# Or as CSV.
|
# Or as CSV.
|
||||||
pda ls --format csv
|
pda ls --format csv
|
||||||
# Key,Store,Value,TTL
|
# Meta,Size,TTL,Store,Key,Value
|
||||||
# dogs,default,four legged mammals,no expiry
|
# -w--,5,-,store,name,Alice
|
||||||
# name,default,Alice,no expiry
|
|
||||||
|
|
||||||
# Or as a JSON array.
|
# Or as a JSON array.
|
||||||
pda ls --format json
|
pda ls --format json
|
||||||
# [{"key":"dogs","value":"four legged mammals","encoding":"text","store":"default"},{"key":"name","value":"Alice","encoding":"text","store":"default"}]
|
# [{"key":"name","value":"Alice","encoding":"text","store":"store"}]
|
||||||
|
|
||||||
# Or TSV, Markdown, HTML, NDJSON.
|
# Or TSV, Markdown, HTML, NDJSON.
|
||||||
|
|
||||||
|
|
@ -273,12 +293,12 @@ pda ls --count --key "d*"
|
||||||
Long values are truncated to fit the terminal. Use `--full`/`-f` to show the complete value.
|
Long values are truncated to fit the terminal. Use `--full`/`-f` to show the complete value.
|
||||||
```bash
|
```bash
|
||||||
pda ls
|
pda ls
|
||||||
# Key Value TTL
|
# Key Value
|
||||||
# note this is a very long (..30 more chars) no expiry
|
# note this is a very long (..30 more chars)
|
||||||
|
|
||||||
pda ls --full
|
pda ls --full
|
||||||
# Key Value TTL
|
# Key Value
|
||||||
# note this is a very long value that keeps on going and going no expiry
|
# note this is a very long value that keeps on going and going
|
||||||
```
|
```
|
||||||
|
|
||||||
<p align="center"></p><!-- spacer -->
|
<p align="center"></p><!-- spacer -->
|
||||||
|
|
@ -296,7 +316,7 @@ pda export --value "**https**"
|
||||||
|
|
||||||
<p align="center"></p><!-- spacer -->
|
<p align="center"></p><!-- spacer -->
|
||||||
|
|
||||||
`pda import` to import it all back. By default, each entry is routed to the store it came from (via the `"store"` field in the NDJSON). If no `"store"` field is present, entries go to the default store. Pass a store name as a positional argument to force all entries into one store. Existing keys are updated and new keys are added.
|
`pda import` to import it all back. By default, each entry is routed to the store it came from (via the `"store"` field in the NDJSON). If no `"store"` field is present, entries go to `store.default_store_name`. Pass a store name as a positional argument to force all entries into one store. Existing keys are updated and new keys are added.
|
||||||
```bash
|
```bash
|
||||||
# Entries are routed to their original stores.
|
# Entries are routed to their original stores.
|
||||||
pda import -f my_backup
|
pda import -f my_backup
|
||||||
|
|
@ -330,17 +350,18 @@ pda set alice@birthdays 11/11/1998
|
||||||
pda list-stores
|
pda list-stores
|
||||||
# Keys Size Store
|
# Keys Size Store
|
||||||
# 2 1.8k @birthdays
|
# 2 1.8k @birthdays
|
||||||
# 12 4.2k @default
|
# 12 4.2k @store
|
||||||
|
|
||||||
# Just the names.
|
# Just the names.
|
||||||
pda list-stores --short
|
pda list-stores --short
|
||||||
# @birthdays
|
# @birthdays
|
||||||
# @default
|
# @store
|
||||||
|
|
||||||
# Check out a specific store.
|
# Check out a specific store.
|
||||||
pda ls @birthdays --no-header --no-ttl
|
pda ls @birthdays
|
||||||
# alice 11/11/1998
|
# Store Key Value
|
||||||
# bob 05/12/1980
|
# birthdays alice 11/11/1998
|
||||||
|
# birthdays bob 05/12/1980
|
||||||
|
|
||||||
# Export it.
|
# Export it.
|
||||||
pda export birthdays > friends_birthdays
|
pda export birthdays > friends_birthdays
|
||||||
|
|
@ -551,7 +572,7 @@ pda get hello --no-template
|
||||||
|
|
||||||
`*` wildcards a word or series of characters, stopping at separator boundaries (the default separators are `/-_.@:` and space).
|
`*` wildcards a word or series of characters, stopping at separator boundaries (the default separators are `/-_.@:` and space).
|
||||||
```bash
|
```bash
|
||||||
pda ls --no-values --no-header
|
pda ls
|
||||||
# cat
|
# cat
|
||||||
# dog
|
# dog
|
||||||
# cog
|
# cog
|
||||||
|
|
@ -639,16 +660,19 @@ pda ls --key "19[90-99]"
|
||||||
`--value` filters by value content using the same glob syntax.
|
`--value` filters by value content using the same glob syntax.
|
||||||
```bash
|
```bash
|
||||||
pda ls --value "**localhost**"
|
pda ls --value "**localhost**"
|
||||||
# db-url postgres://localhost:5432 no expiry
|
# Key Value
|
||||||
|
# db-url postgres://localhost:5432
|
||||||
|
|
||||||
# Combine key and value filters.
|
# Combine key and value filters.
|
||||||
pda ls --key "db*" --value "**localhost**"
|
pda ls --key "db*" --value "**localhost**"
|
||||||
# db-url postgres://localhost:5432 no expiry
|
# Key Value
|
||||||
|
# db-url postgres://localhost:5432
|
||||||
|
|
||||||
# Multiple --value patterns are OR'd.
|
# Multiple --value patterns are OR'd.
|
||||||
pda ls --value "**world**" --value "42"
|
pda ls --value "**world**" --value "42"
|
||||||
# greeting hello world no expiry
|
# Key Value
|
||||||
# number 42 no expiry
|
# greeting hello world
|
||||||
|
# number 42
|
||||||
```
|
```
|
||||||
|
|
||||||
<p align="center"></p><!-- spacer -->
|
<p align="center"></p><!-- spacer -->
|
||||||
|
|
@ -682,34 +706,118 @@ pda set session2 "xyz" --ttl 54m10s
|
||||||
`list` shows expiration in the TTL column by default.
|
`list` shows expiration in the TTL column by default.
|
||||||
```bash
|
```bash
|
||||||
pda ls
|
pda ls
|
||||||
# Key Value TTL
|
# TTL Key Value
|
||||||
# session 123 in 59m30s
|
# 59m30s session 123
|
||||||
# session2 xyz in 51m40s
|
# 51m40s session2 xyz
|
||||||
```
|
```
|
||||||
|
|
||||||
`export` and `import` persist the expiry date. Expirations will continue ticking down regardless of if they're actively in a store or not - the expiry is just a timestamp, not a timer.
|
`export` and `import` persist the expiry date. Expirations will continue ticking down regardless of if they're actively in a store or not - the expiry is just a timestamp, not a timer.
|
||||||
|
|
||||||
<p align="center"></p><!-- spacer -->
|
<p align="center"></p><!-- spacer -->
|
||||||
|
|
||||||
`pda meta` views or modifies metadata (TTL, encryption) without changing a key's value.
|
`pda meta` views or modifies metadata (TTL, encryption, read-only, pinned) without changing a key's value. Changes print an ok message describing what was done.
|
||||||
```bash
|
```bash
|
||||||
# View metadata for a key.
|
# View metadata for a key.
|
||||||
pda meta session
|
pda meta session
|
||||||
# key: session@default
|
# key: session@store
|
||||||
# secret: false
|
# secret: false
|
||||||
# expires: in 59m30s
|
# writable: true
|
||||||
|
# pinned: false
|
||||||
|
# expires: 59m30s
|
||||||
|
|
||||||
# Set or change TTL.
|
# Set or change TTL.
|
||||||
pda meta session --ttl 2h
|
pda meta session --ttl 2h
|
||||||
|
# ok set ttl to 2h session
|
||||||
|
|
||||||
# Clear TTL.
|
# Clear TTL.
|
||||||
pda meta session --ttl never
|
pda meta session --ttl never
|
||||||
|
# ok cleared ttl session
|
||||||
|
|
||||||
# Encrypt an existing plaintext key.
|
# Encrypt a key.
|
||||||
pda meta api-key --encrypt
|
pda meta api-key --encrypt
|
||||||
|
# ok encrypted api-key
|
||||||
|
|
||||||
# Decrypt an encrypted key.
|
# Decrypt an encrypted key.
|
||||||
pda meta api-key --decrypt
|
pda meta api-key --decrypt
|
||||||
|
# ok decrypted api-key
|
||||||
|
|
||||||
|
# Mark a key as read-only.
|
||||||
|
pda meta api-url --readonly
|
||||||
|
# ok made readonly api-url
|
||||||
|
|
||||||
|
# Make it writable.
|
||||||
|
pda meta api-url --writable
|
||||||
|
# ok made writable api-url
|
||||||
|
|
||||||
|
# Pin a key to the top of the list.
|
||||||
|
pda meta todo --pin
|
||||||
|
# ok pinned todo
|
||||||
|
|
||||||
|
# Unpin.
|
||||||
|
pda meta todo --unpin
|
||||||
|
# ok unpinned todo
|
||||||
|
|
||||||
|
# Or combine multiple changes.
|
||||||
|
pda meta session --readonly --pin
|
||||||
|
# ok made readonly, pinned session
|
||||||
|
|
||||||
|
# Modifying a read-only key requires making it writable, or just forcing it.
|
||||||
|
pda meta api-url --ttl 1h
|
||||||
|
# FAIL cannot meta 'api-url': key is read-only
|
||||||
|
pda meta api-url --ttl 1h --force
|
||||||
|
# ok set ttl to 1h api-url
|
||||||
|
```
|
||||||
|
|
||||||
|
<p align="center"></p><!-- spacer -->
|
||||||
|
|
||||||
|
### Read-only & Pinned
|
||||||
|
|
||||||
|
Keys can be marked **read-only** to prevent accidental modification, and **pinned** to sort to the top of list output. Both flags are shown in the `Meta` column as part of the 4-char flag string: `ewtp` (encrypted, writable, ttl, pinned).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set flags at creation time.
|
||||||
|
pda set api-url "https://prod.example.com" --readonly
|
||||||
|
pda set important "remember this" --pin
|
||||||
|
|
||||||
|
# Or toggle them with meta.
|
||||||
|
pda meta api-url --readonly
|
||||||
|
# ok made readonly api-url
|
||||||
|
pda meta api-url --writable
|
||||||
|
# ok made writable api-url
|
||||||
|
pda meta important --pin
|
||||||
|
# ok pinned important
|
||||||
|
|
||||||
|
# Or alongside an edit.
|
||||||
|
pda edit notes --readonly --pin
|
||||||
|
```
|
||||||
|
|
||||||
|
<p align="center"></p><!-- spacer -->
|
||||||
|
|
||||||
|
Read-only keys are protected from `set`, `rm`, `mv`, and `edit`. Use `--force` to bypass.
|
||||||
|
```bash
|
||||||
|
pda set api-url "new value"
|
||||||
|
# FAIL cannot set 'api-url': key is read-only
|
||||||
|
|
||||||
|
pda set api-url "new value" --force
|
||||||
|
# overwrites despite read-only
|
||||||
|
|
||||||
|
pda rm api-url --force
|
||||||
|
pda mv api-url new-name --force
|
||||||
|
```
|
||||||
|
|
||||||
|
<p align="center"></p><!-- spacer -->
|
||||||
|
|
||||||
|
`cp` can copy a read-only key freely (since the source isn't modified), and the copy preserves the read-only flag. Overwriting a read-only destination is blocked without `--force`.
|
||||||
|
|
||||||
|
<p align="center"></p><!-- spacer -->
|
||||||
|
|
||||||
|
Pinned entries sort to the top of `list` output, preserving alphabetical order within the pinned and unpinned groups.
|
||||||
|
```bash
|
||||||
|
pda ls
|
||||||
|
# Meta Key Value
|
||||||
|
# -w-p important remember this
|
||||||
|
# -w-- name Alice
|
||||||
|
# -w-- other foo
|
||||||
```
|
```
|
||||||
|
|
||||||
<p align="center"></p><!-- spacer -->
|
<p align="center"></p><!-- spacer -->
|
||||||
|
|
@ -780,7 +888,7 @@ pda export
|
||||||
|
|
||||||
<p align="center"></p><!-- spacer -->
|
<p align="center"></p><!-- spacer -->
|
||||||
|
|
||||||
`mv`, `cp`, and `import` all preserve encryption. Overwriting an encrypted key without `--encrypt` will warn you.
|
`mv`, `cp`, and `import` all preserve encryption, read-only, and pinned flags. Overwriting an encrypted key without `--encrypt` will warn you.
|
||||||
```bash
|
```bash
|
||||||
pda cp api-key api-key-backup
|
pda cp api-key api-key-backup
|
||||||
# still encrypted
|
# still encrypted
|
||||||
|
|
@ -795,8 +903,8 @@ pda set api-key "oops"
|
||||||
If the identity file is missing, encrypted values are inaccessible but not lost. Keys are still visible, and the ciphertext is preserved through reads and writes.
|
If the identity file is missing, encrypted values are inaccessible but not lost. Keys are still visible, and the ciphertext is preserved through reads and writes.
|
||||||
```bash
|
```bash
|
||||||
pda ls
|
pda ls
|
||||||
# Key Value TTL
|
# Meta Key Value
|
||||||
# api-key locked (identity file missing) no expiry
|
# ew-- api-key locked (identity file missing)
|
||||||
|
|
||||||
pda get api-key
|
pda get api-key
|
||||||
# FAIL cannot get 'api-key': secret is locked (identity file missing)
|
# FAIL cannot get 'api-key': secret is locked (identity file missing)
|
||||||
|
|
@ -930,7 +1038,7 @@ always_encrypt = false
|
||||||
|
|
||||||
[store]
|
[store]
|
||||||
# store name used when none is specified
|
# store name used when none is specified
|
||||||
default_store_name = "default"
|
default_store_name = "store"
|
||||||
# prompt y/n before deleting whole store
|
# prompt y/n before deleting whole store
|
||||||
always_prompt_delete = true
|
always_prompt_delete = true
|
||||||
# prompt y/n before store overwrites
|
# prompt y/n before store overwrites
|
||||||
|
|
@ -945,8 +1053,8 @@ default_list_format = "table"
|
||||||
always_show_full_values = false
|
always_show_full_values = false
|
||||||
# suppress the header row
|
# suppress the header row
|
||||||
always_hide_header = false
|
always_hide_header = false
|
||||||
# columns and order, accepts: key,store,value,ttl
|
# columns and order, accepts: meta,size,ttl,store,key,value
|
||||||
default_columns = "key,store,value,ttl"
|
default_columns = "meta,size,ttl,store,key,value"
|
||||||
|
|
||||||
[git]
|
[git]
|
||||||
# auto fetch whenever a change happens
|
# auto fetch whenever a change happens
|
||||||
|
|
|
||||||
|
|
@ -104,14 +104,14 @@ func defaultConfig() Config {
|
||||||
AlwaysPromptOverwrite: false,
|
AlwaysPromptOverwrite: false,
|
||||||
},
|
},
|
||||||
Store: StoreConfig{
|
Store: StoreConfig{
|
||||||
DefaultStoreName: "default",
|
DefaultStoreName: "store",
|
||||||
AlwaysPromptDelete: true,
|
AlwaysPromptDelete: true,
|
||||||
AlwaysPromptOverwrite: true,
|
AlwaysPromptOverwrite: true,
|
||||||
},
|
},
|
||||||
List: ListConfig{
|
List: ListConfig{
|
||||||
AlwaysShowAllStores: true,
|
AlwaysShowAllStores: true,
|
||||||
DefaultListFormat: "table",
|
DefaultListFormat: "table",
|
||||||
DefaultColumns: "key,store,value,ttl",
|
DefaultColumns: "meta,size,ttl,store,key,value",
|
||||||
},
|
},
|
||||||
Git: GitConfig{
|
Git: GitConfig{
|
||||||
AutoFetch: false,
|
AutoFetch: false,
|
||||||
|
|
|
||||||
|
|
@ -134,8 +134,8 @@ func TestConfigFieldsStringField(t *testing.T) {
|
||||||
if f.Kind != reflect.String {
|
if f.Kind != reflect.String {
|
||||||
t.Errorf("store.default_store_name Kind = %v, want String", f.Kind)
|
t.Errorf("store.default_store_name Kind = %v, want String", f.Kind)
|
||||||
}
|
}
|
||||||
if f.Value != "default" {
|
if f.Value != "store" {
|
||||||
t.Errorf("store.default_store_name Value = %v, want 'default'", f.Value)
|
t.Errorf("store.default_store_name Value = %v, want 'store'", f.Value)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,8 @@ func del(cmd *cobra.Command, args []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
force, _ := cmd.Flags().GetBool("force")
|
||||||
|
|
||||||
var removedNames []string
|
var removedNames []string
|
||||||
for _, dbName := range storeOrder {
|
for _, dbName := range storeOrder {
|
||||||
st := byStore[dbName]
|
st := byStore[dbName]
|
||||||
|
|
@ -123,6 +125,9 @@ func del(cmd *cobra.Command, args []string) error {
|
||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
return fmt.Errorf("cannot remove '%s': no such key", t.full)
|
return fmt.Errorf("cannot remove '%s': no such key", t.full)
|
||||||
}
|
}
|
||||||
|
if entries[idx].ReadOnly && !force {
|
||||||
|
return fmt.Errorf("cannot remove '%s': key is read-only", t.full)
|
||||||
|
}
|
||||||
entries = append(entries[:idx], entries[idx+1:]...)
|
entries = append(entries[:idx], entries[idx+1:]...)
|
||||||
removedNames = append(removedNames, t.display)
|
removedNames = append(removedNames, t.display)
|
||||||
}
|
}
|
||||||
|
|
@ -137,6 +142,7 @@ func del(cmd *cobra.Command, args []string) error {
|
||||||
func init() {
|
func init() {
|
||||||
delCmd.Flags().BoolP("interactive", "i", false, "prompt yes/no for each deletion")
|
delCmd.Flags().BoolP("interactive", "i", false, "prompt yes/no for each deletion")
|
||||||
delCmd.Flags().BoolP("yes", "y", false, "skip all confirmation prompts")
|
delCmd.Flags().BoolP("yes", "y", false, "skip all confirmation prompts")
|
||||||
|
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.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)")
|
||||||
|
|
|
||||||
42
cmd/edit.go
42
cmd/edit.go
|
|
@ -48,10 +48,21 @@ func edit(cmd *cobra.Command, args []string) error {
|
||||||
encryptFlag, _ := cmd.Flags().GetBool("encrypt")
|
encryptFlag, _ := cmd.Flags().GetBool("encrypt")
|
||||||
decryptFlag, _ := cmd.Flags().GetBool("decrypt")
|
decryptFlag, _ := cmd.Flags().GetBool("decrypt")
|
||||||
preserveNewline, _ := cmd.Flags().GetBool("preserve-newline")
|
preserveNewline, _ := cmd.Flags().GetBool("preserve-newline")
|
||||||
|
force, _ := cmd.Flags().GetBool("force")
|
||||||
|
readonlyFlag, _ := cmd.Flags().GetBool("readonly")
|
||||||
|
writableFlag, _ := cmd.Flags().GetBool("writable")
|
||||||
|
pinFlag, _ := cmd.Flags().GetBool("pin")
|
||||||
|
unpinFlag, _ := cmd.Flags().GetBool("unpin")
|
||||||
|
|
||||||
if encryptFlag && decryptFlag {
|
if encryptFlag && decryptFlag {
|
||||||
return fmt.Errorf("cannot edit '%s': --encrypt and --decrypt are mutually exclusive", args[0])
|
return fmt.Errorf("cannot edit '%s': --encrypt and --decrypt are mutually exclusive", args[0])
|
||||||
}
|
}
|
||||||
|
if readonlyFlag && writableFlag {
|
||||||
|
return fmt.Errorf("cannot edit '%s': --readonly and --writable are mutually exclusive", args[0])
|
||||||
|
}
|
||||||
|
if pinFlag && unpinFlag {
|
||||||
|
return fmt.Errorf("cannot edit '%s': --pin and --unpin are mutually exclusive", args[0])
|
||||||
|
}
|
||||||
|
|
||||||
// Load identity
|
// Load identity
|
||||||
var identity *age.X25519Identity
|
var identity *age.X25519Identity
|
||||||
|
|
@ -87,6 +98,9 @@ func edit(cmd *cobra.Command, args []string) error {
|
||||||
original = nil
|
original = nil
|
||||||
} else {
|
} else {
|
||||||
entry = &entries[idx]
|
entry = &entries[idx]
|
||||||
|
if entry.ReadOnly && !force {
|
||||||
|
return fmt.Errorf("cannot edit '%s': key is read-only", args[0])
|
||||||
|
}
|
||||||
if entry.Locked {
|
if entry.Locked {
|
||||||
return fmt.Errorf("cannot edit '%s': secret is locked (identity file missing)", args[0])
|
return fmt.Errorf("cannot edit '%s': secret is locked (identity file missing)", args[0])
|
||||||
}
|
}
|
||||||
|
|
@ -149,7 +163,7 @@ func edit(cmd *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for no-op
|
// Check for no-op
|
||||||
noMetaFlags := ttlStr == "" && !encryptFlag && !decryptFlag
|
noMetaFlags := ttlStr == "" && !encryptFlag && !decryptFlag && !readonlyFlag && !writableFlag && !pinFlag && !unpinFlag
|
||||||
if bytes.Equal(original, newValue) && noMetaFlags {
|
if bytes.Equal(original, newValue) && noMetaFlags {
|
||||||
infof("no changes to '%s'", spec.Display())
|
infof("no changes to '%s'", spec.Display())
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -164,9 +178,11 @@ func edit(cmd *cobra.Command, args []string) error {
|
||||||
// Build or update entry
|
// Build or update entry
|
||||||
if creating {
|
if creating {
|
||||||
newEntry := Entry{
|
newEntry := Entry{
|
||||||
Key: spec.Key,
|
Key: spec.Key,
|
||||||
Value: newValue,
|
Value: newValue,
|
||||||
Secret: encryptFlag,
|
Secret: encryptFlag,
|
||||||
|
ReadOnly: readonlyFlag,
|
||||||
|
Pinned: pinFlag,
|
||||||
}
|
}
|
||||||
if ttlStr != "" {
|
if ttlStr != "" {
|
||||||
expiresAt, err := parseTTLString(ttlStr)
|
expiresAt, err := parseTTLString(ttlStr)
|
||||||
|
|
@ -199,6 +215,19 @@ func edit(cmd *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
entry.Secret = false
|
entry.Secret = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if readonlyFlag {
|
||||||
|
entry.ReadOnly = true
|
||||||
|
}
|
||||||
|
if writableFlag {
|
||||||
|
entry.ReadOnly = false
|
||||||
|
}
|
||||||
|
if pinFlag {
|
||||||
|
entry.Pinned = true
|
||||||
|
}
|
||||||
|
if unpinFlag {
|
||||||
|
entry.Pinned = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := writeStoreFile(p, entries, recipients); err != nil {
|
if err := writeStoreFile(p, entries, recipients); err != nil {
|
||||||
|
|
@ -219,5 +248,10 @@ func init() {
|
||||||
editCmd.Flags().BoolP("encrypt", "e", false, "encrypt the value at rest")
|
editCmd.Flags().BoolP("encrypt", "e", false, "encrypt the value at rest")
|
||||||
editCmd.Flags().BoolP("decrypt", "d", false, "decrypt the value (store as plaintext)")
|
editCmd.Flags().BoolP("decrypt", "d", false, "decrypt the value (store as plaintext)")
|
||||||
editCmd.Flags().Bool("preserve-newline", false, "keep trailing newlines added by the editor")
|
editCmd.Flags().Bool("preserve-newline", false, "keep trailing newlines added by the editor")
|
||||||
|
editCmd.Flags().Bool("force", false, "bypass read-only protection")
|
||||||
|
editCmd.Flags().Bool("readonly", false, "mark the key as read-only")
|
||||||
|
editCmd.Flags().Bool("writable", false, "clear the read-only flag")
|
||||||
|
editCmd.Flags().Bool("pin", false, "pin the key (sorts to top in list)")
|
||||||
|
editCmd.Flags().Bool("unpin", false, "unpin the key")
|
||||||
rootCmd.AddCommand(editCmd)
|
rootCmd.AddCommand(editCmd)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
223
cmd/list.go
223
cmd/list.go
|
|
@ -67,6 +67,8 @@ var columnNames = map[string]columnKind{
|
||||||
"key": columnKey,
|
"key": columnKey,
|
||||||
"store": columnStore,
|
"store": columnStore,
|
||||||
"value": columnValue,
|
"value": columnValue,
|
||||||
|
"meta": columnMeta,
|
||||||
|
"size": columnSize,
|
||||||
"ttl": columnTTL,
|
"ttl": columnTTL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,7 +77,7 @@ func validListColumns(v string) error {
|
||||||
for _, raw := range strings.Split(v, ",") {
|
for _, raw := range strings.Split(v, ",") {
|
||||||
tok := strings.TrimSpace(raw)
|
tok := strings.TrimSpace(raw)
|
||||||
if _, ok := columnNames[tok]; !ok {
|
if _, ok := columnNames[tok]; !ok {
|
||||||
return fmt.Errorf("must be a comma-separated list of 'key', 'store', 'value', 'ttl' (got '%s')", tok)
|
return fmt.Errorf("must be a comma-separated list of 'key', 'store', 'value', 'meta', 'size', 'ttl' (got '%s')", tok)
|
||||||
}
|
}
|
||||||
if seen[tok] {
|
if seen[tok] {
|
||||||
return fmt.Errorf("duplicate column '%s'", tok)
|
return fmt.Errorf("duplicate column '%s'", tok)
|
||||||
|
|
@ -103,7 +105,10 @@ var (
|
||||||
listBase64 bool
|
listBase64 bool
|
||||||
listCount bool
|
listCount bool
|
||||||
listNoKeys bool
|
listNoKeys bool
|
||||||
|
listNoStore bool
|
||||||
listNoValues bool
|
listNoValues bool
|
||||||
|
listNoMeta bool
|
||||||
|
listNoSize bool
|
||||||
listNoTTL bool
|
listNoTTL bool
|
||||||
listFull bool
|
listFull bool
|
||||||
listAll bool
|
listAll bool
|
||||||
|
|
@ -120,6 +125,8 @@ const (
|
||||||
columnValue
|
columnValue
|
||||||
columnTTL
|
columnTTL
|
||||||
columnStore
|
columnStore
|
||||||
|
columnMeta
|
||||||
|
columnSize
|
||||||
)
|
)
|
||||||
|
|
||||||
var listCmd = &cobra.Command{
|
var listCmd = &cobra.Command{
|
||||||
|
|
@ -131,10 +138,9 @@ By default, list shows entries from every store. Pass a store name as a
|
||||||
positional argument to narrow to a single store, or use --store/-s with a
|
positional argument to narrow to a single store, or use --store/-s with a
|
||||||
glob pattern to filter by store name.
|
glob pattern to filter by store name.
|
||||||
|
|
||||||
The Store column is always shown so entries can be distinguished across
|
Use --key/-k and --value/-v to filter by key or value glob, and --store/-s
|
||||||
stores. Use --key/-k and --value/-v to filter by key or value glob, and
|
to filter by store name. All filters are repeatable and OR'd within the
|
||||||
--store/-s to filter by store name. All filters are repeatable and OR'd
|
same flag.`,
|
||||||
within the same flag.`,
|
|
||||||
Aliases: []string{"ls"},
|
Aliases: []string{"ls"},
|
||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
RunE: list,
|
RunE: list,
|
||||||
|
|
@ -178,19 +184,35 @@ func list(cmd *cobra.Command, args []string) error {
|
||||||
targetDB = "@" + dbName
|
targetDB = "@" + dbName
|
||||||
}
|
}
|
||||||
|
|
||||||
if listNoKeys && listNoValues && listNoTTL {
|
columns := parseColumns(config.List.DefaultColumns)
|
||||||
return withHint(fmt.Errorf("cannot ls '%s': no columns selected", targetDB), "disable --no-keys, --no-values, or --no-ttl")
|
|
||||||
|
// Each --no-X flag: if explicitly true, remove the column;
|
||||||
|
// if explicitly false (--no-X=false), add the column if missing.
|
||||||
|
type colToggle struct {
|
||||||
|
flag string
|
||||||
|
kind columnKind
|
||||||
|
}
|
||||||
|
for _, ct := range []colToggle{
|
||||||
|
{"no-keys", columnKey},
|
||||||
|
{"no-store", columnStore},
|
||||||
|
{"no-values", columnValue},
|
||||||
|
{"no-meta", columnMeta},
|
||||||
|
{"no-size", columnSize},
|
||||||
|
{"no-ttl", columnTTL},
|
||||||
|
} {
|
||||||
|
if !cmd.Flags().Changed(ct.flag) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val, _ := cmd.Flags().GetBool(ct.flag)
|
||||||
|
if val {
|
||||||
|
columns = slices.DeleteFunc(columns, func(c columnKind) bool { return c == ct.kind })
|
||||||
|
} else if !slices.Contains(columns, ct.kind) {
|
||||||
|
columns = append(columns, ct.kind)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
columns := parseColumns(config.List.DefaultColumns)
|
if len(columns) == 0 {
|
||||||
if listNoKeys {
|
return withHint(fmt.Errorf("cannot ls '%s': no columns selected", targetDB), "disable some --no-* flags")
|
||||||
columns = slices.DeleteFunc(columns, func(c columnKind) bool { return c == columnKey })
|
|
||||||
}
|
|
||||||
if listNoValues {
|
|
||||||
columns = slices.DeleteFunc(columns, func(c columnKind) bool { return c == columnValue })
|
|
||||||
}
|
|
||||||
if listNoTTL {
|
|
||||||
columns = slices.DeleteFunc(columns, func(c columnKind) bool { return c == columnTTL })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
keyPatterns, err := cmd.Flags().GetStringSlice("key")
|
keyPatterns, err := cmd.Flags().GetStringSlice("key")
|
||||||
|
|
@ -271,6 +293,17 @@ func list(cmd *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stable sort: pinned entries first, preserving alphabetical order within each group
|
||||||
|
slices.SortStableFunc(filtered, func(a, b Entry) int {
|
||||||
|
if a.Pinned && !b.Pinned {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if !a.Pinned && b.Pinned {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
if listCount {
|
if listCount {
|
||||||
fmt.Fprintln(cmd.OutOrStdout(), len(filtered))
|
fmt.Fprintln(cmd.OutOrStdout(), len(filtered))
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -330,7 +363,7 @@ func list(cmd *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Table-based formats
|
// Table-based formats
|
||||||
showValues := !listNoValues
|
showValues := slices.Contains(columns, columnValue)
|
||||||
tw := table.NewWriter()
|
tw := table.NewWriter()
|
||||||
tw.SetOutputMirror(output)
|
tw.SetOutputMirror(output)
|
||||||
tw.SetStyle(table.StyleDefault)
|
tw.SetStyle(table.StyleDefault)
|
||||||
|
|
@ -384,16 +417,32 @@ func list(cmd *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
case columnStore:
|
case columnStore:
|
||||||
if tty {
|
if tty {
|
||||||
row = append(row, dimStyle.Sprint(e.StoreName))
|
row = append(row, text.Colors{text.Bold, text.FgYellow}.Sprint(e.StoreName))
|
||||||
} else {
|
} else {
|
||||||
row = append(row, e.StoreName)
|
row = append(row, e.StoreName)
|
||||||
}
|
}
|
||||||
|
case columnMeta:
|
||||||
|
if tty {
|
||||||
|
row = append(row, colorizeMeta(e))
|
||||||
|
} else {
|
||||||
|
row = append(row, entryMetaString(e))
|
||||||
|
}
|
||||||
|
case columnSize:
|
||||||
|
sizeStr := formatSize(len(e.Value))
|
||||||
|
if tty {
|
||||||
|
if len(e.Value) >= 1000 {
|
||||||
|
sizeStr = text.Colors{text.Bold, text.FgGreen}.Sprint(sizeStr)
|
||||||
|
} else {
|
||||||
|
sizeStr = text.FgGreen.Sprint(sizeStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
row = append(row, sizeStr)
|
||||||
case columnTTL:
|
case columnTTL:
|
||||||
ttlStr := formatExpiry(e.ExpiresAt)
|
ttlStr := formatExpiry(e.ExpiresAt)
|
||||||
if tty && e.ExpiresAt == 0 {
|
if tty && e.ExpiresAt == 0 {
|
||||||
ttlStr = dimStyle.Sprint(ttlStr)
|
ttlStr = dimStyle.Sprint(ttlStr)
|
||||||
}
|
}
|
||||||
row = append(row, ttlStr)
|
row = append(row, ttlStr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tw.AppendRow(row)
|
tw.AppendRow(row)
|
||||||
|
|
@ -484,6 +533,10 @@ func headerRow(columns []columnKind, tty bool) table.Row {
|
||||||
row = append(row, h("Store"))
|
row = append(row, h("Store"))
|
||||||
case columnValue:
|
case columnValue:
|
||||||
row = append(row, h("Value"))
|
row = append(row, h("Value"))
|
||||||
|
case columnMeta:
|
||||||
|
row = append(row, h("Meta"))
|
||||||
|
case columnSize:
|
||||||
|
row = append(row, h("Size"))
|
||||||
case columnTTL:
|
case columnTTL:
|
||||||
row = append(row, h("TTL"))
|
row = append(row, h("TTL"))
|
||||||
}
|
}
|
||||||
|
|
@ -494,12 +547,13 @@ func headerRow(columns []columnKind, tty bool) table.Row {
|
||||||
const (
|
const (
|
||||||
keyColumnWidthCap = 30
|
keyColumnWidthCap = 30
|
||||||
storeColumnWidthCap = 20
|
storeColumnWidthCap = 20
|
||||||
|
sizeColumnWidthCap = 10
|
||||||
ttlColumnWidthCap = 20
|
ttlColumnWidthCap = 20
|
||||||
)
|
)
|
||||||
|
|
||||||
// columnLayout holds the resolved max widths for each column kind.
|
// columnLayout holds the resolved max widths for each column kind.
|
||||||
type columnLayout struct {
|
type columnLayout struct {
|
||||||
key, store, value, ttl int
|
key, store, value, meta, size, ttl int
|
||||||
}
|
}
|
||||||
|
|
||||||
// computeLayout derives column widths from the terminal size and actual
|
// computeLayout derives column widths from the terminal size and actual
|
||||||
|
|
@ -509,7 +563,16 @@ func computeLayout(columns []columnKind, out io.Writer, entries []Entry) columnL
|
||||||
var lay columnLayout
|
var lay columnLayout
|
||||||
termWidth := detectTerminalWidth(out)
|
termWidth := detectTerminalWidth(out)
|
||||||
|
|
||||||
// Scan entries for actual max key/store/TTL content widths.
|
// Meta column is always exactly 4 chars wide (ewtp).
|
||||||
|
lay.meta = 4
|
||||||
|
|
||||||
|
// Ensure columns are at least as wide as their headers.
|
||||||
|
lay.key = len("Key")
|
||||||
|
lay.store = len("Store")
|
||||||
|
lay.size = len("Size")
|
||||||
|
lay.ttl = len("TTL")
|
||||||
|
|
||||||
|
// Scan entries for actual max key/store/size/TTL content widths.
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
if w := utf8.RuneCountInString(e.Key); w > lay.key {
|
if w := utf8.RuneCountInString(e.Key); w > lay.key {
|
||||||
lay.key = w
|
lay.key = w
|
||||||
|
|
@ -517,6 +580,9 @@ func computeLayout(columns []columnKind, out io.Writer, entries []Entry) columnL
|
||||||
if w := utf8.RuneCountInString(e.StoreName); w > lay.store {
|
if w := utf8.RuneCountInString(e.StoreName); w > lay.store {
|
||||||
lay.store = w
|
lay.store = w
|
||||||
}
|
}
|
||||||
|
if w := utf8.RuneCountInString(formatSize(len(e.Value))); w > lay.size {
|
||||||
|
lay.size = w
|
||||||
|
}
|
||||||
if w := utf8.RuneCountInString(formatExpiry(e.ExpiresAt)); w > lay.ttl {
|
if w := utf8.RuneCountInString(formatExpiry(e.ExpiresAt)); w > lay.ttl {
|
||||||
lay.ttl = w
|
lay.ttl = w
|
||||||
}
|
}
|
||||||
|
|
@ -527,6 +593,9 @@ func computeLayout(columns []columnKind, out io.Writer, entries []Entry) columnL
|
||||||
if lay.store > storeColumnWidthCap {
|
if lay.store > storeColumnWidthCap {
|
||||||
lay.store = storeColumnWidthCap
|
lay.store = storeColumnWidthCap
|
||||||
}
|
}
|
||||||
|
if lay.size > sizeColumnWidthCap {
|
||||||
|
lay.size = sizeColumnWidthCap
|
||||||
|
}
|
||||||
if lay.ttl > ttlColumnWidthCap {
|
if lay.ttl > ttlColumnWidthCap {
|
||||||
lay.ttl = ttlColumnWidthCap
|
lay.ttl = ttlColumnWidthCap
|
||||||
}
|
}
|
||||||
|
|
@ -541,7 +610,7 @@ func computeLayout(columns []columnKind, out io.Writer, entries []Entry) columnL
|
||||||
return lay
|
return lay
|
||||||
}
|
}
|
||||||
|
|
||||||
// Give the value column whatever is left after key and TTL.
|
// Give the value column whatever is left after fixed-width columns.
|
||||||
lay.value = available
|
lay.value = available
|
||||||
for _, col := range columns {
|
for _, col := range columns {
|
||||||
switch col {
|
switch col {
|
||||||
|
|
@ -549,6 +618,10 @@ func computeLayout(columns []columnKind, out io.Writer, entries []Entry) columnL
|
||||||
lay.value -= lay.key
|
lay.value -= lay.key
|
||||||
case columnStore:
|
case columnStore:
|
||||||
lay.value -= lay.store
|
lay.value -= lay.store
|
||||||
|
case columnMeta:
|
||||||
|
lay.value -= lay.meta
|
||||||
|
case columnSize:
|
||||||
|
lay.value -= lay.size
|
||||||
case columnTTL:
|
case columnTTL:
|
||||||
lay.value -= lay.ttl
|
lay.value -= lay.ttl
|
||||||
}
|
}
|
||||||
|
|
@ -568,31 +641,40 @@ func applyColumnWidths(tw table.Writer, columns []columnKind, out io.Writer, lay
|
||||||
|
|
||||||
var configs []table.ColumnConfig
|
var configs []table.ColumnConfig
|
||||||
for i, col := range columns {
|
for i, col := range columns {
|
||||||
var maxW int
|
cc := table.ColumnConfig{Number: i + 1}
|
||||||
var enforcer func(string, int) string
|
|
||||||
switch col {
|
switch col {
|
||||||
case columnKey:
|
case columnKey:
|
||||||
maxW = lay.key
|
cc.WidthMax = lay.key
|
||||||
enforcer = text.Trim
|
cc.WidthMaxEnforcer = text.Trim
|
||||||
case columnStore:
|
case columnStore:
|
||||||
maxW = lay.store
|
cc.WidthMax = lay.store
|
||||||
enforcer = text.Trim
|
cc.WidthMaxEnforcer = text.Trim
|
||||||
|
cc.Align = text.AlignRight
|
||||||
|
cc.AlignHeader = text.AlignRight
|
||||||
case columnValue:
|
case columnValue:
|
||||||
maxW = lay.value
|
cc.WidthMax = lay.value
|
||||||
if full {
|
if full {
|
||||||
enforcer = text.WrapText
|
cc.WidthMaxEnforcer = text.WrapText
|
||||||
}
|
}
|
||||||
// When !full, values are already pre-truncated by
|
// When !full, values are already pre-truncated by
|
||||||
// summariseValue — no enforcer needed.
|
// summariseValue — no enforcer needed.
|
||||||
|
case columnMeta:
|
||||||
|
cc.WidthMax = lay.meta
|
||||||
|
cc.WidthMaxEnforcer = text.Trim
|
||||||
|
cc.Align = text.AlignRight
|
||||||
|
cc.AlignHeader = text.AlignRight
|
||||||
|
case columnSize:
|
||||||
|
cc.WidthMax = lay.size
|
||||||
|
cc.WidthMaxEnforcer = text.Trim
|
||||||
|
cc.Align = text.AlignRight
|
||||||
|
cc.AlignHeader = text.AlignRight
|
||||||
case columnTTL:
|
case columnTTL:
|
||||||
maxW = lay.ttl
|
cc.WidthMax = lay.ttl
|
||||||
enforcer = text.Trim
|
cc.WidthMaxEnforcer = text.Trim
|
||||||
|
cc.Align = text.AlignRight
|
||||||
|
cc.AlignHeader = text.AlignRight
|
||||||
}
|
}
|
||||||
configs = append(configs, table.ColumnConfig{
|
configs = append(configs, cc)
|
||||||
Number: i + 1,
|
|
||||||
WidthMax: maxW,
|
|
||||||
WidthMaxEnforcer: enforcer,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
tw.SetColumnConfigs(configs)
|
tw.SetColumnConfigs(configs)
|
||||||
}
|
}
|
||||||
|
|
@ -615,6 +697,64 @@ func detectTerminalWidth(out io.Writer) int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// entryMetaString returns a 4-char flag string: (e)ncrypted (w)ritable (t)tl (p)inned.
|
||||||
|
func entryMetaString(e Entry) string {
|
||||||
|
var b [4]byte
|
||||||
|
if e.Secret {
|
||||||
|
b[0] = 'e'
|
||||||
|
} else {
|
||||||
|
b[0] = '-'
|
||||||
|
}
|
||||||
|
if !e.ReadOnly {
|
||||||
|
b[1] = 'w'
|
||||||
|
} else {
|
||||||
|
b[1] = '-'
|
||||||
|
}
|
||||||
|
if e.ExpiresAt > 0 {
|
||||||
|
b[2] = 't'
|
||||||
|
} else {
|
||||||
|
b[2] = '-'
|
||||||
|
}
|
||||||
|
if e.Pinned {
|
||||||
|
b[3] = 'p'
|
||||||
|
} else {
|
||||||
|
b[3] = '-'
|
||||||
|
}
|
||||||
|
return string(b[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// colorizeMeta returns a colorized meta string for TTY display.
|
||||||
|
// e=bold+yellow, w=bold+red, t=bold+green, p=bold+yellow, unset=dim.
|
||||||
|
func colorizeMeta(e Entry) string {
|
||||||
|
dim := text.Colors{text.Faint}
|
||||||
|
yellow := text.Colors{text.Bold, text.FgYellow}
|
||||||
|
red := text.Colors{text.Bold, text.FgRed}
|
||||||
|
green := text.Colors{text.Bold, text.FgGreen}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
if e.Secret {
|
||||||
|
b.WriteString(yellow.Sprint("e"))
|
||||||
|
} else {
|
||||||
|
b.WriteString(dim.Sprint("-"))
|
||||||
|
}
|
||||||
|
if !e.ReadOnly {
|
||||||
|
b.WriteString(red.Sprint("w"))
|
||||||
|
} else {
|
||||||
|
b.WriteString(dim.Sprint("-"))
|
||||||
|
}
|
||||||
|
if e.ExpiresAt > 0 {
|
||||||
|
b.WriteString(green.Sprint("t"))
|
||||||
|
} else {
|
||||||
|
b.WriteString(dim.Sprint("-"))
|
||||||
|
}
|
||||||
|
if e.Pinned {
|
||||||
|
b.WriteString(yellow.Sprint("p"))
|
||||||
|
} else {
|
||||||
|
b.WriteString(dim.Sprint("-"))
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
func renderTable(tw table.Writer) {
|
func renderTable(tw table.Writer) {
|
||||||
switch listFormat.String() {
|
switch listFormat.String() {
|
||||||
case "tsv":
|
case "tsv":
|
||||||
|
|
@ -635,7 +775,10 @@ func init() {
|
||||||
listCmd.Flags().BoolVarP(&listBase64, "base64", "b", false, "view binary data as base64")
|
listCmd.Flags().BoolVarP(&listBase64, "base64", "b", false, "view binary data as base64")
|
||||||
listCmd.Flags().BoolVarP(&listCount, "count", "c", false, "print only the count of matching entries")
|
listCmd.Flags().BoolVarP(&listCount, "count", "c", false, "print only the count of matching entries")
|
||||||
listCmd.Flags().BoolVar(&listNoKeys, "no-keys", false, "suppress the key column")
|
listCmd.Flags().BoolVar(&listNoKeys, "no-keys", false, "suppress the key column")
|
||||||
|
listCmd.Flags().BoolVar(&listNoStore, "no-store", false, "suppress the store column")
|
||||||
listCmd.Flags().BoolVar(&listNoValues, "no-values", false, "suppress the value column")
|
listCmd.Flags().BoolVar(&listNoValues, "no-values", false, "suppress the value column")
|
||||||
|
listCmd.Flags().BoolVar(&listNoMeta, "no-meta", false, "suppress the meta column")
|
||||||
|
listCmd.Flags().BoolVar(&listNoSize, "no-size", false, "suppress the size column")
|
||||||
listCmd.Flags().BoolVar(&listNoTTL, "no-ttl", false, "suppress the TTL column")
|
listCmd.Flags().BoolVar(&listNoTTL, "no-ttl", false, "suppress the TTL column")
|
||||||
listCmd.Flags().BoolVarP(&listFull, "full", "f", false, "show full values without truncation")
|
listCmd.Flags().BoolVarP(&listFull, "full", "f", false, "show full values without truncation")
|
||||||
listCmd.Flags().BoolVar(&listNoHeader, "no-header", false, "suppress the header row")
|
listCmd.Flags().BoolVar(&listNoHeader, "no-header", false, "suppress the header row")
|
||||||
|
|
|
||||||
70
cmd/meta.go
70
cmd/meta.go
|
|
@ -2,6 +2,7 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
@ -9,13 +10,10 @@ import (
|
||||||
var metaCmd = &cobra.Command{
|
var metaCmd = &cobra.Command{
|
||||||
Use: "meta KEY[@STORE]",
|
Use: "meta KEY[@STORE]",
|
||||||
Short: "View or modify metadata for a key",
|
Short: "View or modify metadata for a key",
|
||||||
Long: `View or modify metadata (TTL, encryption) for a key without changing its value.
|
Long: `View or modify metadata (TTL, encryption, read-only, pinned) for a key
|
||||||
|
without changing its value.
|
||||||
|
|
||||||
With no flags, displays the key's current metadata. Use flags to modify:
|
With no flags, displays the key's current metadata. Pass flags to modify.`,
|
||||||
--ttl DURATION Set expiry (e.g. 30m, 2h)
|
|
||||||
--ttl never Remove expiry
|
|
||||||
--encrypt Encrypt the value at rest
|
|
||||||
--decrypt Decrypt the value (store as plaintext)`,
|
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: meta,
|
RunE: meta,
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
|
|
@ -52,23 +50,46 @@ func meta(cmd *cobra.Command, args []string) error {
|
||||||
ttlStr, _ := cmd.Flags().GetString("ttl")
|
ttlStr, _ := cmd.Flags().GetString("ttl")
|
||||||
encryptFlag, _ := cmd.Flags().GetBool("encrypt")
|
encryptFlag, _ := cmd.Flags().GetBool("encrypt")
|
||||||
decryptFlag, _ := cmd.Flags().GetBool("decrypt")
|
decryptFlag, _ := cmd.Flags().GetBool("decrypt")
|
||||||
|
readonlyFlag, _ := cmd.Flags().GetBool("readonly")
|
||||||
|
writableFlag, _ := cmd.Flags().GetBool("writable")
|
||||||
|
pinFlag, _ := cmd.Flags().GetBool("pin")
|
||||||
|
unpinFlag, _ := cmd.Flags().GetBool("unpin")
|
||||||
|
force, _ := cmd.Flags().GetBool("force")
|
||||||
|
|
||||||
if encryptFlag && decryptFlag {
|
if encryptFlag && decryptFlag {
|
||||||
return fmt.Errorf("cannot meta '%s': --encrypt and --decrypt are mutually exclusive", args[0])
|
return fmt.Errorf("cannot meta '%s': --encrypt and --decrypt are mutually exclusive", args[0])
|
||||||
}
|
}
|
||||||
|
if readonlyFlag && writableFlag {
|
||||||
|
return fmt.Errorf("cannot meta '%s': --readonly and --writable are mutually exclusive", args[0])
|
||||||
|
}
|
||||||
|
if pinFlag && unpinFlag {
|
||||||
|
return fmt.Errorf("cannot meta '%s': --pin and --unpin are mutually exclusive", args[0])
|
||||||
|
}
|
||||||
|
|
||||||
// View mode: no flags set
|
// View mode: no flags set
|
||||||
if ttlStr == "" && !encryptFlag && !decryptFlag {
|
isModify := ttlStr != "" || encryptFlag || decryptFlag || readonlyFlag || writableFlag || pinFlag || unpinFlag
|
||||||
|
if !isModify {
|
||||||
expiresStr := "never"
|
expiresStr := "never"
|
||||||
if entry.ExpiresAt > 0 {
|
if entry.ExpiresAt > 0 {
|
||||||
expiresStr = formatExpiry(entry.ExpiresAt)
|
expiresStr = formatExpiry(entry.ExpiresAt)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(cmd.OutOrStdout(), " key: %s\n", spec.Full())
|
fmt.Fprintf(cmd.OutOrStdout(), " key: %s\n", spec.Full())
|
||||||
fmt.Fprintf(cmd.OutOrStdout(), " secret: %v\n", entry.Secret)
|
fmt.Fprintf(cmd.OutOrStdout(), " secret: %v\n", entry.Secret)
|
||||||
|
fmt.Fprintf(cmd.OutOrStdout(), " writable: %v\n", !entry.ReadOnly)
|
||||||
|
fmt.Fprintf(cmd.OutOrStdout(), " pinned: %v\n", entry.Pinned)
|
||||||
fmt.Fprintf(cmd.OutOrStdout(), " expires: %s\n", expiresStr)
|
fmt.Fprintf(cmd.OutOrStdout(), " expires: %s\n", expiresStr)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read-only enforcement: --readonly and --writable always work without --force,
|
||||||
|
// but other modifications on a read-only key require --force.
|
||||||
|
if entry.ReadOnly && !force && !readonlyFlag && !writableFlag {
|
||||||
|
onlyPinChange := !encryptFlag && !decryptFlag && ttlStr == "" && (pinFlag || unpinFlag)
|
||||||
|
if !onlyPinChange {
|
||||||
|
return fmt.Errorf("cannot meta '%s': key is read-only", args[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Modification mode — may need identity for encrypt
|
// Modification mode — may need identity for encrypt
|
||||||
if encryptFlag {
|
if encryptFlag {
|
||||||
identity, err = ensureIdentity()
|
identity, err = ensureIdentity()
|
||||||
|
|
@ -81,12 +102,19 @@ func meta(cmd *cobra.Command, args []string) error {
|
||||||
return fmt.Errorf("cannot meta '%s': %v", args[0], err)
|
return fmt.Errorf("cannot meta '%s': %v", args[0], err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var changes []string
|
||||||
|
|
||||||
if ttlStr != "" {
|
if ttlStr != "" {
|
||||||
expiresAt, err := parseTTLString(ttlStr)
|
expiresAt, err := parseTTLString(ttlStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot meta '%s': %v", args[0], err)
|
return fmt.Errorf("cannot meta '%s': %v", args[0], err)
|
||||||
}
|
}
|
||||||
entry.ExpiresAt = expiresAt
|
entry.ExpiresAt = expiresAt
|
||||||
|
if expiresAt == 0 {
|
||||||
|
changes = append(changes, "cleared ttl")
|
||||||
|
} else {
|
||||||
|
changes = append(changes, "set ttl to "+ttlStr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if encryptFlag {
|
if encryptFlag {
|
||||||
|
|
@ -97,6 +125,7 @@ func meta(cmd *cobra.Command, args []string) error {
|
||||||
return fmt.Errorf("cannot meta '%s': secret is locked (identity file missing)", args[0])
|
return fmt.Errorf("cannot meta '%s': secret is locked (identity file missing)", args[0])
|
||||||
}
|
}
|
||||||
entry.Secret = true
|
entry.Secret = true
|
||||||
|
changes = append(changes, "encrypted")
|
||||||
}
|
}
|
||||||
|
|
||||||
if decryptFlag {
|
if decryptFlag {
|
||||||
|
|
@ -107,18 +136,43 @@ func meta(cmd *cobra.Command, args []string) error {
|
||||||
return fmt.Errorf("cannot meta '%s': secret is locked (identity file missing)", args[0])
|
return fmt.Errorf("cannot meta '%s': secret is locked (identity file missing)", args[0])
|
||||||
}
|
}
|
||||||
entry.Secret = false
|
entry.Secret = false
|
||||||
|
changes = append(changes, "decrypted")
|
||||||
|
}
|
||||||
|
|
||||||
|
if readonlyFlag {
|
||||||
|
entry.ReadOnly = true
|
||||||
|
changes = append(changes, "made readonly")
|
||||||
|
}
|
||||||
|
if writableFlag {
|
||||||
|
entry.ReadOnly = false
|
||||||
|
changes = append(changes, "made writable")
|
||||||
|
}
|
||||||
|
if pinFlag {
|
||||||
|
entry.Pinned = true
|
||||||
|
changes = append(changes, "pinned")
|
||||||
|
}
|
||||||
|
if unpinFlag {
|
||||||
|
entry.Pinned = false
|
||||||
|
changes = append(changes, "unpinned")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := writeStoreFile(p, entries, recipients); err != nil {
|
if err := writeStoreFile(p, entries, recipients); err != nil {
|
||||||
return fmt.Errorf("cannot meta '%s': %v", args[0], err)
|
return fmt.Errorf("cannot meta '%s': %v", args[0], err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return autoSync("meta " + spec.Display())
|
summary := strings.Join(changes, ", ")
|
||||||
|
okf("%s %s", summary, spec.Display())
|
||||||
|
return autoSync(summary + " " + spec.Display())
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
metaCmd.Flags().String("ttl", "", "set expiry (e.g. 30m, 2h) or 'never' to clear")
|
metaCmd.Flags().String("ttl", "", "set expiry (e.g. 30m, 2h) or 'never' to clear")
|
||||||
metaCmd.Flags().BoolP("encrypt", "e", false, "encrypt the value at rest")
|
metaCmd.Flags().BoolP("encrypt", "e", false, "encrypt the value at rest")
|
||||||
metaCmd.Flags().BoolP("decrypt", "d", false, "decrypt the value (store as plaintext)")
|
metaCmd.Flags().BoolP("decrypt", "d", false, "decrypt the value (store as plaintext)")
|
||||||
|
metaCmd.Flags().Bool("readonly", false, "mark the key as read-only")
|
||||||
|
metaCmd.Flags().Bool("writable", false, "clear the read-only flag")
|
||||||
|
metaCmd.Flags().Bool("pin", false, "pin the key (sorts to top in list)")
|
||||||
|
metaCmd.Flags().Bool("unpin", false, "unpin the key")
|
||||||
|
metaCmd.Flags().Bool("force", false, "bypass read-only protection for metadata changes")
|
||||||
rootCmd.AddCommand(metaCmd)
|
rootCmd.AddCommand(metaCmd)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
cmd/mv.go
16
cmd/mv.go
|
|
@ -71,6 +71,7 @@ func mvImpl(cmd *cobra.Command, args []string, keepSource bool) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
force, _ := cmd.Flags().GetBool("force")
|
||||||
promptOverwrite := !yes && (interactive || config.Key.AlwaysPromptOverwrite)
|
promptOverwrite := !yes && (interactive || config.Key.AlwaysPromptOverwrite)
|
||||||
|
|
||||||
identity, _ := loadIdentity()
|
identity, _ := loadIdentity()
|
||||||
|
|
@ -103,6 +104,11 @@ func mvImpl(cmd *cobra.Command, args []string, keepSource bool) error {
|
||||||
}
|
}
|
||||||
srcEntry := srcEntries[srcIdx]
|
srcEntry := srcEntries[srcIdx]
|
||||||
|
|
||||||
|
// Block moving a read-only source (move removes the source)
|
||||||
|
if !keepSource && srcEntry.ReadOnly && !force {
|
||||||
|
return fmt.Errorf("cannot move '%s': key is read-only", fromSpec.Key)
|
||||||
|
}
|
||||||
|
|
||||||
sameStore := fromSpec.DB == toSpec.DB
|
sameStore := fromSpec.DB == toSpec.DB
|
||||||
|
|
||||||
// Check destination for overwrite prompt
|
// Check destination for overwrite prompt
|
||||||
|
|
@ -121,6 +127,10 @@ func mvImpl(cmd *cobra.Command, args []string, keepSource bool) error {
|
||||||
|
|
||||||
dstIdx := findEntry(dstEntries, toSpec.Key)
|
dstIdx := findEntry(dstEntries, toSpec.Key)
|
||||||
|
|
||||||
|
if dstIdx >= 0 && dstEntries[dstIdx].ReadOnly && !force {
|
||||||
|
return fmt.Errorf("cannot overwrite '%s': key is read-only", toSpec.Key)
|
||||||
|
}
|
||||||
|
|
||||||
if safe && dstIdx >= 0 {
|
if safe && dstIdx >= 0 {
|
||||||
infof("skipped '%s': already exists", toSpec.Display())
|
infof("skipped '%s': already exists", toSpec.Display())
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -137,13 +147,15 @@ func mvImpl(cmd *cobra.Command, args []string, keepSource bool) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write destination entry — preserve secret status
|
// Write destination entry — preserve metadata
|
||||||
newEntry := Entry{
|
newEntry := Entry{
|
||||||
Key: toSpec.Key,
|
Key: toSpec.Key,
|
||||||
Value: srcEntry.Value,
|
Value: srcEntry.Value,
|
||||||
ExpiresAt: srcEntry.ExpiresAt,
|
ExpiresAt: srcEntry.ExpiresAt,
|
||||||
Secret: srcEntry.Secret,
|
Secret: srcEntry.Secret,
|
||||||
Locked: srcEntry.Locked,
|
Locked: srcEntry.Locked,
|
||||||
|
ReadOnly: srcEntry.ReadOnly,
|
||||||
|
Pinned: srcEntry.Pinned,
|
||||||
}
|
}
|
||||||
|
|
||||||
if sameStore {
|
if sameStore {
|
||||||
|
|
@ -197,9 +209,11 @@ func init() {
|
||||||
mvCmd.Flags().BoolP("interactive", "i", false, "prompt before overwriting destination")
|
mvCmd.Flags().BoolP("interactive", "i", false, "prompt before overwriting destination")
|
||||||
mvCmd.Flags().BoolP("yes", "y", false, "skip all confirmation prompts")
|
mvCmd.Flags().BoolP("yes", "y", false, "skip all confirmation prompts")
|
||||||
mvCmd.Flags().Bool("safe", false, "do not overwrite if the destination already exists")
|
mvCmd.Flags().Bool("safe", false, "do not overwrite if the destination already exists")
|
||||||
|
mvCmd.Flags().Bool("force", false, "bypass read-only protection")
|
||||||
rootCmd.AddCommand(mvCmd)
|
rootCmd.AddCommand(mvCmd)
|
||||||
cpCmd.Flags().BoolP("interactive", "i", false, "prompt before overwriting destination")
|
cpCmd.Flags().BoolP("interactive", "i", false, "prompt before overwriting destination")
|
||||||
cpCmd.Flags().BoolP("yes", "y", false, "skip all confirmation prompts")
|
cpCmd.Flags().BoolP("yes", "y", false, "skip all confirmation prompts")
|
||||||
cpCmd.Flags().Bool("safe", false, "do not overwrite if the destination already exists")
|
cpCmd.Flags().Bool("safe", false, "do not overwrite if the destination already exists")
|
||||||
|
cpCmd.Flags().Bool("force", false, "bypass read-only protection")
|
||||||
rootCmd.AddCommand(cpCmd)
|
rootCmd.AddCommand(cpCmd)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ type Entry struct {
|
||||||
ExpiresAt uint64 // Unix timestamp; 0 = never expires
|
ExpiresAt uint64 // Unix timestamp; 0 = never expires
|
||||||
Secret bool // encrypted on disk
|
Secret bool // encrypted on disk
|
||||||
Locked bool // secret but no identity available to decrypt
|
Locked bool // secret but no identity available to decrypt
|
||||||
|
ReadOnly bool // cannot be modified without --force
|
||||||
|
Pinned bool // sorts to top in list output
|
||||||
StoreName string // populated by list --all
|
StoreName string // populated by list --all
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,6 +54,8 @@ type jsonEntry struct {
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
Encoding string `json:"encoding,omitempty"`
|
Encoding string `json:"encoding,omitempty"`
|
||||||
ExpiresAt *int64 `json:"expires_at,omitempty"`
|
ExpiresAt *int64 `json:"expires_at,omitempty"`
|
||||||
|
ReadOnly *bool `json:"readonly,omitempty"`
|
||||||
|
Pinned *bool `json:"pinned,omitempty"`
|
||||||
Store string `json:"store,omitempty"`
|
Store string `json:"store,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,6 +153,8 @@ func decodeJsonEntry(je jsonEntry, identity *age.X25519Identity) (Entry, error)
|
||||||
if je.ExpiresAt != nil {
|
if je.ExpiresAt != nil {
|
||||||
expiresAt = uint64(*je.ExpiresAt)
|
expiresAt = uint64(*je.ExpiresAt)
|
||||||
}
|
}
|
||||||
|
readOnly := je.ReadOnly != nil && *je.ReadOnly
|
||||||
|
pinned := je.Pinned != nil && *je.Pinned
|
||||||
|
|
||||||
if je.Encoding == "secret" {
|
if je.Encoding == "secret" {
|
||||||
ciphertext, err := base64.StdEncoding.DecodeString(je.Value)
|
ciphertext, err := base64.StdEncoding.DecodeString(je.Value)
|
||||||
|
|
@ -156,14 +162,14 @@ func decodeJsonEntry(je jsonEntry, identity *age.X25519Identity) (Entry, error)
|
||||||
return Entry{}, fmt.Errorf("decode secret for '%s': %w", je.Key, err)
|
return Entry{}, fmt.Errorf("decode secret for '%s': %w", je.Key, err)
|
||||||
}
|
}
|
||||||
if identity == nil {
|
if identity == nil {
|
||||||
return Entry{Key: je.Key, Value: ciphertext, ExpiresAt: expiresAt, Secret: true, Locked: true}, nil
|
return Entry{Key: je.Key, Value: ciphertext, ExpiresAt: expiresAt, Secret: true, Locked: true, ReadOnly: readOnly, Pinned: pinned}, nil
|
||||||
}
|
}
|
||||||
plaintext, err := decrypt(ciphertext, identity)
|
plaintext, err := decrypt(ciphertext, identity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
warnf("cannot decrypt '%s': %v", je.Key, err)
|
warnf("cannot decrypt '%s': %v", je.Key, err)
|
||||||
return Entry{Key: je.Key, Value: ciphertext, ExpiresAt: expiresAt, Secret: true, Locked: true}, nil
|
return Entry{Key: je.Key, Value: ciphertext, ExpiresAt: expiresAt, Secret: true, Locked: true, ReadOnly: readOnly, Pinned: pinned}, nil
|
||||||
}
|
}
|
||||||
return Entry{Key: je.Key, Value: plaintext, ExpiresAt: expiresAt, Secret: true}, nil
|
return Entry{Key: je.Key, Value: plaintext, ExpiresAt: expiresAt, Secret: true, ReadOnly: readOnly, Pinned: pinned}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var value []byte
|
var value []byte
|
||||||
|
|
@ -179,7 +185,7 @@ func decodeJsonEntry(je jsonEntry, identity *age.X25519Identity) (Entry, error)
|
||||||
default:
|
default:
|
||||||
return Entry{}, fmt.Errorf("unsupported encoding '%s' for '%s'", je.Encoding, je.Key)
|
return Entry{}, fmt.Errorf("unsupported encoding '%s' for '%s'", je.Encoding, je.Key)
|
||||||
}
|
}
|
||||||
return Entry{Key: je.Key, Value: value, ExpiresAt: expiresAt}, nil
|
return Entry{Key: je.Key, Value: value, ExpiresAt: expiresAt, ReadOnly: readOnly, Pinned: pinned}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeJsonEntry(e Entry, recipients []age.Recipient) (jsonEntry, error) {
|
func encodeJsonEntry(e Entry, recipients []age.Recipient) (jsonEntry, error) {
|
||||||
|
|
@ -188,6 +194,14 @@ func encodeJsonEntry(e Entry, recipients []age.Recipient) (jsonEntry, error) {
|
||||||
ts := int64(e.ExpiresAt)
|
ts := int64(e.ExpiresAt)
|
||||||
je.ExpiresAt = &ts
|
je.ExpiresAt = &ts
|
||||||
}
|
}
|
||||||
|
if e.ReadOnly {
|
||||||
|
t := true
|
||||||
|
je.ReadOnly = &t
|
||||||
|
}
|
||||||
|
if e.Pinned {
|
||||||
|
t := true
|
||||||
|
je.Pinned = &t
|
||||||
|
}
|
||||||
|
|
||||||
if e.Secret && e.Locked {
|
if e.Secret && e.Locked {
|
||||||
// Passthrough: Value holds raw ciphertext, re-encode as-is
|
// Passthrough: Value holds raw ciphertext, re-encode as-is
|
||||||
|
|
|
||||||
20
cmd/set.go
20
cmd/set.go
|
|
@ -133,8 +133,14 @@ func set(cmd *cobra.Command, args []string) error {
|
||||||
return fmt.Errorf("cannot set '%s': %v", args[0], err)
|
return fmt.Errorf("cannot set '%s': %v", args[0], err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
force, _ := cmd.Flags().GetBool("force")
|
||||||
|
|
||||||
idx := findEntry(entries, spec.Key)
|
idx := findEntry(entries, spec.Key)
|
||||||
|
|
||||||
|
if idx >= 0 && entries[idx].ReadOnly && !force {
|
||||||
|
return fmt.Errorf("cannot set '%s': key is read-only", args[0])
|
||||||
|
}
|
||||||
|
|
||||||
if safe && idx >= 0 {
|
if safe && idx >= 0 {
|
||||||
infof("skipped '%s': already exists", spec.Display())
|
infof("skipped '%s': already exists", spec.Display())
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -157,10 +163,15 @@ func set(cmd *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pinFlag, _ := cmd.Flags().GetBool("pin")
|
||||||
|
readonlyFlag, _ := cmd.Flags().GetBool("readonly")
|
||||||
|
|
||||||
entry := Entry{
|
entry := Entry{
|
||||||
Key: spec.Key,
|
Key: spec.Key,
|
||||||
Value: value,
|
Value: value,
|
||||||
Secret: secret,
|
Secret: secret,
|
||||||
|
ReadOnly: readonlyFlag,
|
||||||
|
Pinned: pinFlag,
|
||||||
}
|
}
|
||||||
if ttl != 0 {
|
if ttl != 0 {
|
||||||
entry.ExpiresAt = uint64(time.Now().Add(ttl).Unix())
|
entry.ExpiresAt = uint64(time.Now().Add(ttl).Unix())
|
||||||
|
|
@ -185,5 +196,8 @@ func init() {
|
||||||
setCmd.Flags().BoolP("interactive", "i", false, "prompt before overwriting an existing key")
|
setCmd.Flags().BoolP("interactive", "i", false, "prompt before overwriting an existing key")
|
||||||
setCmd.Flags().BoolP("encrypt", "e", false, "encrypt the value at rest using age")
|
setCmd.Flags().BoolP("encrypt", "e", false, "encrypt the value at rest using age")
|
||||||
setCmd.Flags().Bool("safe", false, "do not overwrite if the key already exists")
|
setCmd.Flags().Bool("safe", false, "do not overwrite if the key already exists")
|
||||||
|
setCmd.Flags().Bool("force", false, "bypass read-only protection")
|
||||||
|
setCmd.Flags().Bool("pin", false, "pin the key (sorts to top in list)")
|
||||||
|
setCmd.Flags().Bool("readonly", false, "mark the key as read-only")
|
||||||
setCmd.Flags().StringP("file", "f", "", "read value from a file")
|
setCmd.Flags().StringP("file", "f", "", "read value from a file")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -262,14 +262,14 @@ func validateDBName(name string) error {
|
||||||
|
|
||||||
func formatExpiry(expiresAt uint64) string {
|
func formatExpiry(expiresAt uint64) string {
|
||||||
if expiresAt == 0 {
|
if expiresAt == 0 {
|
||||||
return "none"
|
return "-"
|
||||||
}
|
}
|
||||||
expiry := time.Unix(int64(expiresAt), 0).UTC()
|
expiry := time.Unix(int64(expiresAt), 0).UTC()
|
||||||
remaining := time.Until(expiry)
|
remaining := time.Until(expiry)
|
||||||
if remaining <= 0 {
|
if remaining <= 0 {
|
||||||
return "expired"
|
return "expired"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("in %s", remaining.Round(time.Second))
|
return remaining.Round(time.Second).String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseTTLString parses a TTL string that is either a duration (e.g. "30m", "2h")
|
// parseTTLString parses a TTL string that is either a duration (e.g. "30m", "2h")
|
||||||
|
|
|
||||||
2
testdata/config-get.ct
vendored
2
testdata/config-get.ct
vendored
|
|
@ -2,7 +2,7 @@ $ pda config get display_ascii_art
|
||||||
true
|
true
|
||||||
|
|
||||||
$ pda config get store.default_store_name
|
$ pda config get store.default_store_name
|
||||||
default
|
store
|
||||||
|
|
||||||
$ pda config get git.auto_commit
|
$ pda config get git.auto_commit
|
||||||
false
|
false
|
||||||
|
|
|
||||||
4
testdata/config-list.ct
vendored
4
testdata/config-list.ct
vendored
|
|
@ -4,14 +4,14 @@ key.always_prompt_delete = false
|
||||||
key.always_prompt_glob_delete = true
|
key.always_prompt_glob_delete = true
|
||||||
key.always_prompt_overwrite = false
|
key.always_prompt_overwrite = false
|
||||||
key.always_encrypt = false
|
key.always_encrypt = false
|
||||||
store.default_store_name = default
|
store.default_store_name = store
|
||||||
store.always_prompt_delete = true
|
store.always_prompt_delete = true
|
||||||
store.always_prompt_overwrite = true
|
store.always_prompt_overwrite = true
|
||||||
list.always_show_all_stores = true
|
list.always_show_all_stores = true
|
||||||
list.default_list_format = table
|
list.default_list_format = table
|
||||||
list.always_show_full_values = false
|
list.always_show_full_values = false
|
||||||
list.always_hide_header = false
|
list.always_hide_header = false
|
||||||
list.default_columns = key,store,value,ttl
|
list.default_columns = meta,size,ttl,store,key,value
|
||||||
git.auto_fetch = false
|
git.auto_fetch = false
|
||||||
git.auto_commit = false
|
git.auto_commit = false
|
||||||
git.auto_push = false
|
git.auto_push = false
|
||||||
|
|
|
||||||
10
testdata/config-set.ct
vendored
10
testdata/config-set.ct
vendored
|
|
@ -32,7 +32,7 @@ json
|
||||||
|
|
||||||
# Invalid list columns
|
# Invalid list columns
|
||||||
$ pda config set list.default_columns foo --> FAIL
|
$ pda config set list.default_columns foo --> FAIL
|
||||||
FAIL cannot set 'list.default_columns': must be a comma-separated list of 'key', 'store', 'value', 'ttl' (got 'foo')
|
FAIL cannot set 'list.default_columns': must be a comma-separated list of 'key', 'store', 'value', 'meta', 'size', 'ttl' (got 'foo')
|
||||||
|
|
||||||
# Duplicate columns
|
# Duplicate columns
|
||||||
$ pda config set list.default_columns key,key --> FAIL
|
$ pda config set list.default_columns key,key --> FAIL
|
||||||
|
|
@ -50,9 +50,9 @@ FAIL unknown config key 'git.auto_comit'
|
||||||
hint did you mean 'git.auto_commit'?
|
hint did you mean 'git.auto_commit'?
|
||||||
|
|
||||||
# Reset changed values so subsequent tests see defaults
|
# Reset changed values so subsequent tests see defaults
|
||||||
$ pda config set store.default_store_name default
|
$ pda config set store.default_store_name store
|
||||||
$ pda config set list.default_list_format table
|
$ pda config set list.default_list_format table
|
||||||
$ pda config set list.default_columns key,store,value,ttl
|
$ pda config set list.default_columns meta,size,ttl,store,key,value
|
||||||
ok store.default_store_name set to 'default'
|
ok store.default_store_name set to 'store'
|
||||||
ok list.default_list_format set to 'table'
|
ok list.default_list_format set to 'table'
|
||||||
ok list.default_columns set to 'key,store,value,ttl'
|
ok list.default_columns set to 'meta,size,ttl,store,key,value'
|
||||||
|
|
|
||||||
20
testdata/help-list.ct
vendored
20
testdata/help-list.ct
vendored
|
|
@ -6,10 +6,9 @@ By default, list shows entries from every store. Pass a store name as a
|
||||||
positional argument to narrow to a single store, or use --store/-s with a
|
positional argument to narrow to a single store, or use --store/-s with a
|
||||||
glob pattern to filter by store name.
|
glob pattern to filter by store name.
|
||||||
|
|
||||||
The Store column is always shown so entries can be distinguished across
|
Use --key/-k and --value/-v to filter by key or value glob, and --store/-s
|
||||||
stores. Use --key/-k and --value/-v to filter by key or value glob, and
|
to filter by store name. All filters are repeatable and OR'd within the
|
||||||
--store/-s to filter by store name. All filters are repeatable and OR'd
|
same flag.
|
||||||
within the same flag.
|
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
pda list [STORE] [flags]
|
pda list [STORE] [flags]
|
||||||
|
|
@ -27,6 +26,9 @@ Flags:
|
||||||
-k, --key strings filter keys with glob pattern (repeatable)
|
-k, --key strings filter keys with glob pattern (repeatable)
|
||||||
--no-header suppress the header row
|
--no-header suppress the header row
|
||||||
--no-keys suppress the key column
|
--no-keys suppress the key column
|
||||||
|
--no-meta suppress the meta column
|
||||||
|
--no-size suppress the size column
|
||||||
|
--no-store suppress the store column
|
||||||
--no-ttl suppress the TTL column
|
--no-ttl suppress the TTL column
|
||||||
--no-values suppress the value column
|
--no-values suppress the value column
|
||||||
-s, --store strings filter stores with glob pattern (repeatable)
|
-s, --store strings filter stores with glob pattern (repeatable)
|
||||||
|
|
@ -37,10 +39,9 @@ By default, list shows entries from every store. Pass a store name as a
|
||||||
positional argument to narrow to a single store, or use --store/-s with a
|
positional argument to narrow to a single store, or use --store/-s with a
|
||||||
glob pattern to filter by store name.
|
glob pattern to filter by store name.
|
||||||
|
|
||||||
The Store column is always shown so entries can be distinguished across
|
Use --key/-k and --value/-v to filter by key or value glob, and --store/-s
|
||||||
stores. Use --key/-k and --value/-v to filter by key or value glob, and
|
to filter by store name. All filters are repeatable and OR'd within the
|
||||||
--store/-s to filter by store name. All filters are repeatable and OR'd
|
same flag.
|
||||||
within the same flag.
|
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
pda list [STORE] [flags]
|
pda list [STORE] [flags]
|
||||||
|
|
@ -58,6 +59,9 @@ Flags:
|
||||||
-k, --key strings filter keys with glob pattern (repeatable)
|
-k, --key strings filter keys with glob pattern (repeatable)
|
||||||
--no-header suppress the header row
|
--no-header suppress the header row
|
||||||
--no-keys suppress the key column
|
--no-keys suppress the key column
|
||||||
|
--no-meta suppress the meta column
|
||||||
|
--no-size suppress the size column
|
||||||
|
--no-store suppress the store column
|
||||||
--no-ttl suppress the TTL column
|
--no-ttl suppress the TTL column
|
||||||
--no-values suppress the value column
|
--no-values suppress the value column
|
||||||
-s, --store strings filter stores with glob pattern (repeatable)
|
-s, --store strings filter stores with glob pattern (repeatable)
|
||||||
|
|
|
||||||
2
testdata/help-remove.ct
vendored
2
testdata/help-remove.ct
vendored
|
|
@ -9,6 +9,7 @@ Aliases:
|
||||||
remove, rm
|
remove, rm
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
|
--force bypass read-only protection
|
||||||
-h, --help help for remove
|
-h, --help help for remove
|
||||||
-i, --interactive prompt yes/no for each deletion
|
-i, --interactive prompt yes/no for each deletion
|
||||||
-k, --key strings delete keys matching glob pattern (repeatable)
|
-k, --key strings delete keys matching glob pattern (repeatable)
|
||||||
|
|
@ -24,6 +25,7 @@ Aliases:
|
||||||
remove, rm
|
remove, rm
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
|
--force bypass read-only protection
|
||||||
-h, --help help for remove
|
-h, --help help for remove
|
||||||
-i, --interactive prompt yes/no for each deletion
|
-i, --interactive prompt yes/no for each deletion
|
||||||
-k, --key strings delete keys matching glob pattern (repeatable)
|
-k, --key strings delete keys matching glob pattern (repeatable)
|
||||||
|
|
|
||||||
6
testdata/help-set.ct
vendored
6
testdata/help-set.ct
vendored
|
|
@ -23,8 +23,11 @@ Aliases:
|
||||||
Flags:
|
Flags:
|
||||||
-e, --encrypt encrypt the value at rest using age
|
-e, --encrypt encrypt the value at rest using age
|
||||||
-f, --file string read value from a file
|
-f, --file string read value from a file
|
||||||
|
--force bypass read-only protection
|
||||||
-h, --help help for set
|
-h, --help help for set
|
||||||
-i, --interactive prompt before overwriting an existing key
|
-i, --interactive prompt before overwriting an existing key
|
||||||
|
--pin pin the key (sorts to top in list)
|
||||||
|
--readonly mark the key as read-only
|
||||||
--safe do not overwrite if the key already exists
|
--safe do not overwrite if the key already exists
|
||||||
-t, --ttl duration expire the key after the provided duration (e.g. 24h, 30m)
|
-t, --ttl duration expire the key after the provided duration (e.g. 24h, 30m)
|
||||||
Set a key to a given value or stdin. Optionally specify a store.
|
Set a key to a given value or stdin. Optionally specify a store.
|
||||||
|
|
@ -50,7 +53,10 @@ Aliases:
|
||||||
Flags:
|
Flags:
|
||||||
-e, --encrypt encrypt the value at rest using age
|
-e, --encrypt encrypt the value at rest using age
|
||||||
-f, --file string read value from a file
|
-f, --file string read value from a file
|
||||||
|
--force bypass read-only protection
|
||||||
-h, --help help for set
|
-h, --help help for set
|
||||||
-i, --interactive prompt before overwriting an existing key
|
-i, --interactive prompt before overwriting an existing key
|
||||||
|
--pin pin the key (sorts to top in list)
|
||||||
|
--readonly mark the key as read-only
|
||||||
--safe do not overwrite if the key already exists
|
--safe do not overwrite if the key already exists
|
||||||
-t, --ttl duration expire the key after the provided duration (e.g. 24h, 30m)
|
-t, --ttl duration expire the key after the provided duration (e.g. 24h, 30m)
|
||||||
|
|
|
||||||
4
testdata/list-all-suppressed-err.ct
vendored
4
testdata/list-all-suppressed-err.ct
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
# Error when all columns are suppressed
|
# Error when all columns are suppressed
|
||||||
$ pda set a@las 1
|
$ pda set a@las 1
|
||||||
$ pda ls las --no-keys --no-values --no-ttl --> FAIL
|
$ pda ls las --no-keys --no-store --no-values --no-meta --no-size --no-ttl --> FAIL
|
||||||
FAIL cannot ls '@las': no columns selected
|
FAIL cannot ls '@las': no columns selected
|
||||||
hint disable --no-keys, --no-values, or --no-ttl
|
hint disable some --no-* flags
|
||||||
|
|
|
||||||
20
testdata/list-all.ct
vendored
20
testdata/list-all.ct
vendored
|
|
@ -2,25 +2,25 @@
|
||||||
$ pda set lax@laa 1
|
$ pda set lax@laa 1
|
||||||
$ pda set lax@lab 2
|
$ pda set lax@lab 2
|
||||||
$ pda ls --key "lax" --format tsv
|
$ pda ls --key "lax" --format tsv
|
||||||
Key Store Value TTL
|
Meta Size TTL Store Key Value
|
||||||
lax laa 1 none
|
-w-- 1 - laa lax 1
|
||||||
lax lab 2 none
|
-w-- 1 - lab lax 2
|
||||||
$ pda ls --key "lax" --count
|
$ pda ls --key "lax" --count
|
||||||
2
|
2
|
||||||
$ pda ls --key "lax" --format json
|
$ pda ls --key "lax" --format json
|
||||||
[{"key":"lax","value":"1","encoding":"text","store":"laa"},{"key":"lax","value":"2","encoding":"text","store":"lab"}]
|
[{"key":"lax","value":"1","encoding":"text","store":"laa"},{"key":"lax","value":"2","encoding":"text","store":"lab"}]
|
||||||
# Positional arg narrows to one store
|
# Positional arg narrows to one store
|
||||||
$ pda ls laa --key "lax" --format tsv
|
$ pda ls laa --key "lax" --format tsv
|
||||||
Key Store Value TTL
|
Meta Size TTL Store Key Value
|
||||||
lax laa 1 none
|
-w-- 1 - laa lax 1
|
||||||
# --store glob filter
|
# --store glob filter
|
||||||
$ pda ls --store "la?" --key "lax" --format tsv
|
$ pda ls --store "la?" --key "lax" --format tsv
|
||||||
Key Store Value TTL
|
Meta Size TTL Store Key Value
|
||||||
lax laa 1 none
|
-w-- 1 - laa lax 1
|
||||||
lax lab 2 none
|
-w-- 1 - lab lax 2
|
||||||
$ pda ls --store "laa" --key "lax" --format tsv
|
$ pda ls --store "laa" --key "lax" --format tsv
|
||||||
Key Store Value TTL
|
Meta Size TTL Store Key Value
|
||||||
lax laa 1 none
|
-w-- 1 - laa lax 1
|
||||||
# --store cannot be combined with positional arg
|
# --store cannot be combined with positional arg
|
||||||
$ pda ls --store "laa" laa --> FAIL
|
$ pda ls --store "laa" laa --> FAIL
|
||||||
FAIL cannot use --store with a store argument
|
FAIL cannot use --store with a store argument
|
||||||
|
|
|
||||||
4
testdata/list-config-columns.ct
vendored
4
testdata/list-config-columns.ct
vendored
|
|
@ -7,5 +7,5 @@ Key Value
|
||||||
a 1
|
a 1
|
||||||
|
|
||||||
# Reset
|
# Reset
|
||||||
$ pda config set list.default_columns key,store,value,ttl
|
$ pda config set list.default_columns meta,size,ttl,store,key,value
|
||||||
ok list.default_columns set to 'key,store,value,ttl'
|
ok list.default_columns set to 'meta,size,ttl,store,key,value'
|
||||||
|
|
|
||||||
2
testdata/list-config-hide-header.ct
vendored
2
testdata/list-config-hide-header.ct
vendored
|
|
@ -3,7 +3,7 @@ $ pda config set list.always_hide_header true
|
||||||
$ pda set a@lchh 1
|
$ pda set a@lchh 1
|
||||||
$ pda ls lchh --format tsv
|
$ pda ls lchh --format tsv
|
||||||
ok list.always_hide_header set to 'true'
|
ok list.always_hide_header set to 'true'
|
||||||
a lchh 1 none
|
-w-- 1 - lchh a 1
|
||||||
|
|
||||||
# Reset
|
# Reset
|
||||||
$ pda config set list.always_hide_header false
|
$ pda config set list.always_hide_header false
|
||||||
|
|
|
||||||
6
testdata/list-format-csv.ct
vendored
6
testdata/list-format-csv.ct
vendored
|
|
@ -2,6 +2,6 @@
|
||||||
$ pda set a@csv 1
|
$ pda set a@csv 1
|
||||||
$ pda set b@csv 2
|
$ pda set b@csv 2
|
||||||
$ pda ls csv --format csv
|
$ pda ls csv --format csv
|
||||||
Key,Store,Value,TTL
|
Meta,Size,TTL,Store,Key,Value
|
||||||
a,csv,1,none
|
-w--,1,-,csv,a,1
|
||||||
b,csv,2,none
|
-w--,1,-,csv,b,2
|
||||||
|
|
|
||||||
8
testdata/list-format-markdown.ct
vendored
8
testdata/list-format-markdown.ct
vendored
|
|
@ -2,7 +2,7 @@
|
||||||
$ pda set a@md 1
|
$ pda set a@md 1
|
||||||
$ pda set b@md 2
|
$ pda set b@md 2
|
||||||
$ pda ls md --format markdown
|
$ pda ls md --format markdown
|
||||||
| Key | Store | Value | TTL |
|
| Meta | Size | TTL | Store | Key | Value |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| a | md | 1 | none |
|
| -w-- | 1 | - | md | a | 1 |
|
||||||
| b | md | 2 | none |
|
| -w-- | 1 | - | md | b | 2 |
|
||||||
|
|
|
||||||
10
testdata/list-key-filter.ct
vendored
10
testdata/list-key-filter.ct
vendored
|
|
@ -2,11 +2,11 @@ $ pda set a1@lg 1
|
||||||
$ pda set a2@lg 2
|
$ pda set a2@lg 2
|
||||||
$ pda set b1@lg 3
|
$ pda set b1@lg 3
|
||||||
$ pda ls lg --key "a*" --format tsv
|
$ pda ls lg --key "a*" --format tsv
|
||||||
Key Store Value TTL
|
Meta Size TTL Store Key Value
|
||||||
a1 lg 1 none
|
-w-- 1 - lg a1 1
|
||||||
a2 lg 2 none
|
-w-- 1 - lg a2 2
|
||||||
$ pda ls lg --key "b*" --format tsv
|
$ pda ls lg --key "b*" --format tsv
|
||||||
Key Store Value TTL
|
Meta Size TTL Store Key Value
|
||||||
b1 lg 3 none
|
-w-- 1 - lg b1 3
|
||||||
$ pda ls lg --key "c*" --> FAIL
|
$ pda ls lg --key "c*" --> FAIL
|
||||||
FAIL cannot ls '@lg': no matches for key pattern 'c*'
|
FAIL cannot ls '@lg': no matches for key pattern 'c*'
|
||||||
|
|
|
||||||
8
testdata/list-key-value-filter.ct
vendored
8
testdata/list-key-value-filter.ct
vendored
|
|
@ -2,10 +2,10 @@ $ pda set dburl@kv postgres://localhost:5432
|
||||||
$ pda set apiurl@kv https://api.example.com
|
$ pda set apiurl@kv https://api.example.com
|
||||||
$ pda set dbpass@kv s3cret
|
$ pda set dbpass@kv s3cret
|
||||||
$ pda ls kv -k "db*" -v "**localhost**" --format tsv
|
$ pda ls kv -k "db*" -v "**localhost**" --format tsv
|
||||||
Key Store Value TTL
|
Meta Size TTL Store Key Value
|
||||||
dburl kv postgres://localhost:5432 none
|
-w-- 25 - kv dburl postgres://localhost:5432
|
||||||
$ pda ls kv -k "*url*" -v "**example**" --format tsv
|
$ pda ls kv -k "*url*" -v "**example**" --format tsv
|
||||||
Key Store Value TTL
|
Meta Size TTL Store Key Value
|
||||||
apiurl kv https://api.example.com none
|
-w-- 23 - kv apiurl https://api.example.com
|
||||||
$ pda ls kv -k "db*" -v "**nomatch**" --> FAIL
|
$ pda ls kv -k "db*" -v "**nomatch**" --> FAIL
|
||||||
FAIL cannot ls '@kv': no matches for key pattern 'db*' and value pattern '**nomatch**'
|
FAIL cannot ls '@kv': no matches for key pattern 'db*' and value pattern '**nomatch**'
|
||||||
|
|
|
||||||
9
testdata/list-meta-column.ct
vendored
Normal file
9
testdata/list-meta-column.ct
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Meta column shows ewtp flags
|
||||||
|
$ pda set plain@lmc hello
|
||||||
|
$ pda set readonly@lmc world --readonly
|
||||||
|
$ pda set pinned@lmc foo --pin
|
||||||
|
$ pda ls lmc --format tsv --no-ttl --no-size
|
||||||
|
Meta Store Key Value
|
||||||
|
-w-p lmc pinned foo
|
||||||
|
-w-- lmc plain hello
|
||||||
|
---- lmc readonly world
|
||||||
2
testdata/list-no-header.ct
vendored
2
testdata/list-no-header.ct
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
# --no-header suppresses the header row
|
# --no-header suppresses the header row
|
||||||
$ pda set a@nh 1
|
$ pda set a@nh 1
|
||||||
$ pda ls nh --format tsv --no-header
|
$ pda ls nh --format tsv --no-header
|
||||||
a nh 1 none
|
-w-- 1 - nh a 1
|
||||||
|
|
|
||||||
4
testdata/list-no-keys.ct
vendored
4
testdata/list-no-keys.ct
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
# --no-keys suppresses the key column
|
# --no-keys suppresses the key column
|
||||||
$ pda set a@nk 1
|
$ pda set a@nk 1
|
||||||
$ pda ls nk --format tsv --no-keys
|
$ pda ls nk --format tsv --no-keys
|
||||||
Store Value TTL
|
Meta Size TTL Store Value
|
||||||
nk 1 none
|
-w-- 1 - nk 1
|
||||||
|
|
|
||||||
4
testdata/list-no-ttl.ct
vendored
4
testdata/list-no-ttl.ct
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
# --no-ttl suppresses the TTL column
|
# --no-ttl suppresses the TTL column
|
||||||
$ pda set a@nt 1
|
$ pda set a@nt 1
|
||||||
$ pda ls nt --format tsv --no-ttl
|
$ pda ls nt --format tsv --no-ttl
|
||||||
Key Store Value
|
Meta Size Store Key Value
|
||||||
a nt 1
|
-w-- 1 nt a 1
|
||||||
|
|
|
||||||
4
testdata/list-no-values.ct
vendored
4
testdata/list-no-values.ct
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
# --no-values suppresses the value column
|
# --no-values suppresses the value column
|
||||||
$ pda set a@nv 1
|
$ pda set a@nv 1
|
||||||
$ pda ls nv --format tsv --no-values
|
$ pda ls nv --format tsv --no-values
|
||||||
Key Store TTL
|
Meta Size TTL Store Key
|
||||||
a nv none
|
-w-- 1 - nv a
|
||||||
|
|
|
||||||
9
testdata/list-pinned-sort.ct
vendored
Normal file
9
testdata/list-pinned-sort.ct
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Pinned entries sort to the top
|
||||||
|
$ pda set alpha@lps one
|
||||||
|
$ pda set beta@lps two
|
||||||
|
$ pda set gamma@lps three --pin
|
||||||
|
$ pda ls lps --format tsv --no-meta --no-size
|
||||||
|
TTL Store Key Value
|
||||||
|
- lps gamma three
|
||||||
|
- lps alpha one
|
||||||
|
- lps beta two
|
||||||
8
testdata/list-stores.ct
vendored
8
testdata/list-stores.ct
vendored
|
|
@ -2,8 +2,8 @@
|
||||||
$ pda set a@lsalpha 1
|
$ pda set a@lsalpha 1
|
||||||
$ pda set b@lsbeta 2
|
$ pda set b@lsbeta 2
|
||||||
$ pda ls lsalpha --format tsv
|
$ pda ls lsalpha --format tsv
|
||||||
Key Store Value TTL
|
Meta Size TTL Store Key Value
|
||||||
a lsalpha 1 none
|
-w-- 1 - lsalpha a 1
|
||||||
$ pda ls lsbeta --format tsv
|
$ pda ls lsbeta --format tsv
|
||||||
Key Store Value TTL
|
Meta Size TTL Store Key Value
|
||||||
b lsbeta 2 none
|
-w-- 1 - lsbeta b 2
|
||||||
|
|
|
||||||
12
testdata/list-value-filter.ct
vendored
12
testdata/list-value-filter.ct
vendored
|
|
@ -3,13 +3,13 @@ $ fecho tmpval hello world
|
||||||
$ pda set greeting@vt < tmpval
|
$ pda set greeting@vt < tmpval
|
||||||
$ pda set number@vt 42
|
$ pda set number@vt 42
|
||||||
$ pda ls vt --value "**world**" --format tsv
|
$ pda ls vt --value "**world**" --format tsv
|
||||||
Key Store Value TTL
|
Meta Size TTL Store Key Value
|
||||||
greeting vt hello world (..1 more chars) none
|
-w-- 12 - vt greeting hello world (..1 more chars)
|
||||||
$ pda ls vt --value "**https**" --format tsv
|
$ pda ls vt --value "**https**" --format tsv
|
||||||
Key Store Value TTL
|
Meta Size TTL Store Key Value
|
||||||
url vt https://example.com none
|
-w-- 19 - vt url https://example.com
|
||||||
$ pda ls vt --value "*" --format tsv
|
$ pda ls vt --value "*" --format tsv
|
||||||
Key Store Value TTL
|
Meta Size TTL Store Key Value
|
||||||
number vt 42 none
|
-w-- 2 - vt number 42
|
||||||
$ pda ls vt --value "**nomatch**" --> FAIL
|
$ pda ls vt --value "**nomatch**" --> FAIL
|
||||||
FAIL cannot ls '@vt': no matches for value pattern '**nomatch**'
|
FAIL cannot ls '@vt': no matches for value pattern '**nomatch**'
|
||||||
|
|
|
||||||
6
testdata/list-value-multi-filter.ct
vendored
6
testdata/list-value-multi-filter.ct
vendored
|
|
@ -3,6 +3,6 @@ $ fecho tmpval hello world
|
||||||
$ pda set greeting@vm < tmpval
|
$ pda set greeting@vm < tmpval
|
||||||
$ pda set number@vm 42
|
$ pda set number@vm 42
|
||||||
$ pda ls vm --value "**world**" --value "42" --format tsv
|
$ pda ls vm --value "**world**" --value "42" --format tsv
|
||||||
Key Store Value TTL
|
Meta Size TTL Store Key Value
|
||||||
greeting vm hello world (..1 more chars) none
|
-w-- 12 - vm greeting hello world (..1 more chars)
|
||||||
number vm 42 none
|
-w-- 2 - vm number 42
|
||||||
|
|
|
||||||
3
testdata/meta-decrypt.ct
vendored
3
testdata/meta-decrypt.ct
vendored
|
|
@ -1,9 +1,12 @@
|
||||||
# Decrypt an existing encrypted key
|
# Decrypt an existing encrypted key
|
||||||
$ pda set --encrypt hello@md world
|
$ pda set --encrypt hello@md world
|
||||||
$ pda meta hello@md --decrypt
|
$ pda meta hello@md --decrypt
|
||||||
|
ok decrypted hello@md
|
||||||
$ pda meta hello@md
|
$ pda meta hello@md
|
||||||
key: hello@md
|
key: hello@md
|
||||||
secret: false
|
secret: false
|
||||||
|
writable: true
|
||||||
|
pinned: false
|
||||||
expires: never
|
expires: never
|
||||||
$ pda get hello@md
|
$ pda get hello@md
|
||||||
world
|
world
|
||||||
|
|
|
||||||
3
testdata/meta-encrypt.ct
vendored
3
testdata/meta-encrypt.ct
vendored
|
|
@ -1,9 +1,12 @@
|
||||||
# Encrypt an existing plaintext key
|
# Encrypt an existing plaintext key
|
||||||
$ pda set hello@me world
|
$ pda set hello@me world
|
||||||
$ pda meta hello@me --encrypt
|
$ pda meta hello@me --encrypt
|
||||||
|
ok encrypted hello@me
|
||||||
$ pda meta hello@me
|
$ pda meta hello@me
|
||||||
key: hello@me
|
key: hello@me
|
||||||
secret: true
|
secret: true
|
||||||
|
writable: true
|
||||||
|
pinned: false
|
||||||
expires: never
|
expires: never
|
||||||
# Value should still be retrievable
|
# Value should still be retrievable
|
||||||
$ pda get hello@me
|
$ pda get hello@me
|
||||||
|
|
|
||||||
24
testdata/meta-pin.ct
vendored
Normal file
24
testdata/meta-pin.ct
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# --pin marks a key as pinned
|
||||||
|
$ pda set a@mp hello
|
||||||
|
$ pda meta a@mp --pin
|
||||||
|
ok pinned a@mp
|
||||||
|
$ pda meta a@mp
|
||||||
|
key: a@mp
|
||||||
|
secret: false
|
||||||
|
writable: true
|
||||||
|
pinned: true
|
||||||
|
expires: never
|
||||||
|
|
||||||
|
# --unpin clears the pinned flag
|
||||||
|
$ pda meta a@mp --unpin
|
||||||
|
ok unpinned a@mp
|
||||||
|
$ pda meta a@mp
|
||||||
|
key: a@mp
|
||||||
|
secret: false
|
||||||
|
writable: true
|
||||||
|
pinned: false
|
||||||
|
expires: never
|
||||||
|
|
||||||
|
# --pin and --unpin are mutually exclusive
|
||||||
|
$ pda meta a@mp --pin --unpin --> FAIL
|
||||||
|
FAIL cannot meta 'a@mp': --pin and --unpin are mutually exclusive
|
||||||
24
testdata/meta-readonly.ct
vendored
Normal file
24
testdata/meta-readonly.ct
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# --readonly marks a key as read-only
|
||||||
|
$ pda set a@mro hello
|
||||||
|
$ pda meta a@mro --readonly
|
||||||
|
ok made readonly a@mro
|
||||||
|
$ pda meta a@mro
|
||||||
|
key: a@mro
|
||||||
|
secret: false
|
||||||
|
writable: false
|
||||||
|
pinned: false
|
||||||
|
expires: never
|
||||||
|
|
||||||
|
# --writable clears the read-only flag
|
||||||
|
$ pda meta a@mro --writable
|
||||||
|
ok made writable a@mro
|
||||||
|
$ pda meta a@mro
|
||||||
|
key: a@mro
|
||||||
|
secret: false
|
||||||
|
writable: true
|
||||||
|
pinned: false
|
||||||
|
expires: never
|
||||||
|
|
||||||
|
# --readonly and --writable are mutually exclusive
|
||||||
|
$ pda meta a@mro --readonly --writable --> FAIL
|
||||||
|
FAIL cannot meta 'a@mro': --readonly and --writable are mutually exclusive
|
||||||
4
testdata/meta-ttl.ct
vendored
4
testdata/meta-ttl.ct
vendored
|
|
@ -1,11 +1,15 @@
|
||||||
# Set TTL on a key, then view it (just verify no error, can't match dynamic time)
|
# Set TTL on a key, then view it (just verify no error, can't match dynamic time)
|
||||||
$ pda set hello@mt world
|
$ pda set hello@mt world
|
||||||
$ pda meta hello@mt --ttl 1h
|
$ pda meta hello@mt --ttl 1h
|
||||||
|
ok set ttl to 1h hello@mt
|
||||||
|
|
||||||
# Clear TTL with --ttl never
|
# Clear TTL with --ttl never
|
||||||
$ pda set --ttl 1h expiring@mt val
|
$ pda set --ttl 1h expiring@mt val
|
||||||
$ pda meta expiring@mt --ttl never
|
$ pda meta expiring@mt --ttl never
|
||||||
|
ok cleared ttl expiring@mt
|
||||||
$ pda meta expiring@mt
|
$ pda meta expiring@mt
|
||||||
key: expiring@mt
|
key: expiring@mt
|
||||||
secret: false
|
secret: false
|
||||||
|
writable: true
|
||||||
|
pinned: false
|
||||||
expires: never
|
expires: never
|
||||||
|
|
|
||||||
4
testdata/meta.ct
vendored
4
testdata/meta.ct
vendored
|
|
@ -3,6 +3,8 @@ $ pda set hello@m world
|
||||||
$ pda meta hello@m
|
$ pda meta hello@m
|
||||||
key: hello@m
|
key: hello@m
|
||||||
secret: false
|
secret: false
|
||||||
|
writable: true
|
||||||
|
pinned: false
|
||||||
expires: never
|
expires: never
|
||||||
|
|
||||||
# View metadata for an encrypted key
|
# View metadata for an encrypted key
|
||||||
|
|
@ -10,4 +12,6 @@ $ pda set --encrypt secret@m hunter2
|
||||||
$ pda meta secret@m
|
$ pda meta secret@m
|
||||||
key: secret@m
|
key: secret@m
|
||||||
secret: true
|
secret: true
|
||||||
|
writable: true
|
||||||
|
pinned: false
|
||||||
expires: never
|
expires: never
|
||||||
|
|
|
||||||
4
testdata/multistore.ct
vendored
4
testdata/multistore.ct
vendored
|
|
@ -6,5 +6,5 @@ bar
|
||||||
$ pda get x@ms2
|
$ pda get x@ms2
|
||||||
y
|
y
|
||||||
$ pda ls ms2 --format tsv
|
$ pda ls ms2 --format tsv
|
||||||
Key Store Value TTL
|
Meta Size TTL Store Key Value
|
||||||
x ms2 y none
|
-w-- 1 - ms2 x y
|
||||||
|
|
|
||||||
23
testdata/mv-readonly.ct
vendored
Normal file
23
testdata/mv-readonly.ct
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Cannot move a read-only key without --force
|
||||||
|
$ pda set a@mvro hello --readonly
|
||||||
|
$ pda mv a@mvro b@mvro --> FAIL
|
||||||
|
FAIL cannot move 'a': key is read-only
|
||||||
|
|
||||||
|
# --force bypasses read-only protection
|
||||||
|
$ pda mv a@mvro b@mvro --force
|
||||||
|
ok renamed a@mvro to b@mvro
|
||||||
|
|
||||||
|
# Copy preserves readonly metadata
|
||||||
|
$ pda cp b@mvro c@mvro
|
||||||
|
ok copied b@mvro to c@mvro
|
||||||
|
$ pda meta c@mvro
|
||||||
|
key: c@mvro
|
||||||
|
secret: false
|
||||||
|
writable: false
|
||||||
|
pinned: false
|
||||||
|
expires: never
|
||||||
|
|
||||||
|
# Cannot overwrite a read-only destination
|
||||||
|
$ pda set d@mvro new
|
||||||
|
$ pda mv d@mvro c@mvro --> FAIL
|
||||||
|
FAIL cannot overwrite 'c': key is read-only
|
||||||
6
testdata/remove-dedupe.ct
vendored
6
testdata/remove-dedupe.ct
vendored
|
|
@ -2,9 +2,9 @@
|
||||||
$ pda set foo@rdd 1
|
$ pda set foo@rdd 1
|
||||||
$ pda set bar@rdd 2
|
$ pda set bar@rdd 2
|
||||||
$ pda ls rdd --format tsv
|
$ pda ls rdd --format tsv
|
||||||
Key Store Value TTL
|
Meta Size TTL Store Key Value
|
||||||
bar rdd 2 none
|
-w-- 1 - rdd bar 2
|
||||||
foo rdd 1 none
|
-w-- 1 - rdd foo 1
|
||||||
$ pda rm foo@rdd --key "*@rdd" -y
|
$ pda rm foo@rdd --key "*@rdd" -y
|
||||||
$ pda get bar@rdd --> FAIL
|
$ pda get bar@rdd --> FAIL
|
||||||
FAIL cannot get 'bar@rdd': no such key
|
FAIL cannot get 'bar@rdd': no such key
|
||||||
|
|
|
||||||
7
testdata/remove-readonly.ct
vendored
Normal file
7
testdata/remove-readonly.ct
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Cannot remove a read-only key without --force
|
||||||
|
$ pda set a@rmro hello --readonly
|
||||||
|
$ pda rm a@rmro --> FAIL
|
||||||
|
FAIL cannot remove 'a@rmro': key is read-only
|
||||||
|
|
||||||
|
# --force bypasses read-only protection
|
||||||
|
$ pda rm a@rmro --force
|
||||||
8
testdata/set-pin.ct
vendored
Normal file
8
testdata/set-pin.ct
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# --pin marks a key as pinned
|
||||||
|
$ pda set b@sp beta --pin
|
||||||
|
$ pda meta b@sp
|
||||||
|
key: b@sp
|
||||||
|
secret: false
|
||||||
|
writable: true
|
||||||
|
pinned: true
|
||||||
|
expires: never
|
||||||
17
testdata/set-readonly.ct
vendored
Normal file
17
testdata/set-readonly.ct
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# --readonly marks a key as read-only
|
||||||
|
$ pda set a@sro hello --readonly
|
||||||
|
$ pda meta a@sro
|
||||||
|
key: a@sro
|
||||||
|
secret: false
|
||||||
|
writable: false
|
||||||
|
pinned: false
|
||||||
|
expires: never
|
||||||
|
|
||||||
|
# Cannot overwrite a read-only key without --force
|
||||||
|
$ pda set a@sro world --> FAIL
|
||||||
|
FAIL cannot set 'a@sro': key is read-only
|
||||||
|
|
||||||
|
# --force bypasses read-only protection
|
||||||
|
$ pda set a@sro world --force
|
||||||
|
$ pda get a@sro
|
||||||
|
world
|
||||||
Loading…
Add table
Add a link
Reference in a new issue