Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
430875f200
111 files changed
+22958
No files matched your search
@@ -0,0 +1,166 @@
|
||||
// Package commands defines custom commands: shell commands from the
|
||||
// config, run on the selected item with its details filled in.
|
||||
//
|
||||
// customCommands:
|
||||
// - key: K
|
||||
// context: secrets
|
||||
// description: Create Kubernetes secret
|
||||
// command: kubectl create secret generic {{.Name}} -n {{.Prompt.namespace}} --from-file=value=/dev/stdin
|
||||
// value: stdin
|
||||
// prompts: [{name: namespace, title: Namespace, default: default}]
|
||||
// output: popup
|
||||
//
|
||||
// Fields are shell-quoted when filled in ({{raw .Note}} opts out), and
|
||||
// the secret value is never one of them: value: stdin or value: env:NAME
|
||||
// hands it to the command without putting it on its command line.
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// Contexts say where a command applies: the pane with focus (the detail
|
||||
// pane counts as secrets), or anywhere.
|
||||
const (
|
||||
Sources = "sources"
|
||||
Vaults = "vaults"
|
||||
Secrets = "secrets"
|
||||
Global = "global"
|
||||
)
|
||||
|
||||
// Outputs say how a command runs.
|
||||
const (
|
||||
Terminal = "terminal" // in the full terminal, lazysecrets stepping aside
|
||||
Popup = "popup" // output captured and shown in a popup
|
||||
Quiet = "quiet" // in the background, success or failure in the footer
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
Key string `yaml:"key"`
|
||||
Context string `yaml:"context"`
|
||||
Description string `yaml:"description"`
|
||||
Command string `yaml:"command"`
|
||||
Value string `yaml:"value"` // "", "stdin" or "env:NAME"
|
||||
Confirm *bool `yaml:"confirm"` // defaults to true when the value is passed
|
||||
Output string `yaml:"output"` // defaults to quiet
|
||||
Prompts []Prompt `yaml:"prompts"`
|
||||
}
|
||||
|
||||
// A Prompt asks for text before the command runs: {{.Prompt.name}}.
|
||||
type Prompt struct {
|
||||
Name string `yaml:"name"`
|
||||
Title string `yaml:"title"`
|
||||
Default string `yaml:"default"`
|
||||
}
|
||||
|
||||
var envName = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
||||
|
||||
// Validate checks a command from the config.
|
||||
func (c Command) Validate() error {
|
||||
name := c.Description
|
||||
if name == "" {
|
||||
name = c.Command
|
||||
}
|
||||
switch {
|
||||
case c.Command == "":
|
||||
return fmt.Errorf("custom command %q has no command", c.Description)
|
||||
case c.Context != Sources && c.Context != Vaults && c.Context != Secrets && c.Context != Global:
|
||||
return fmt.Errorf("custom command %q: context %q is not sources, vaults, secrets or global", name, c.Context)
|
||||
case c.Output != "" && c.Output != Terminal && c.Output != Popup && c.Output != Quiet:
|
||||
return fmt.Errorf("custom command %q: output %q is not terminal, popup or quiet", name, c.Output)
|
||||
}
|
||||
if c.Value != "" {
|
||||
if c.Context != Secrets {
|
||||
return fmt.Errorf("custom command %q: value needs context: secrets", name)
|
||||
}
|
||||
env, isEnv := strings.CutPrefix(c.Value, "env:")
|
||||
if c.Value != "stdin" && !(isEnv && envName.MatchString(env)) {
|
||||
return fmt.Errorf("custom command %q: value %q is not stdin or env:NAME", name, c.Value)
|
||||
}
|
||||
if c.Value == "stdin" && c.Output == Terminal {
|
||||
return fmt.Errorf("custom command %q: output: terminal needs the terminal's input; use value: env:NAME", name)
|
||||
}
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, p := range c.Prompts {
|
||||
if p.Name == "" || seen[p.Name] {
|
||||
return fmt.Errorf("custom command %q: prompts need distinct names", name)
|
||||
}
|
||||
seen[p.Name] = true
|
||||
}
|
||||
// A trial run catches unknown fields ({{.Nmae}}) and prompts.
|
||||
trial := Item{Prompt: map[string]Q{}}
|
||||
for _, p := range c.Prompts {
|
||||
trial.Prompt[p.Name] = ""
|
||||
}
|
||||
if _, err := c.Render(trial); err != nil {
|
||||
return fmt.Errorf("custom command %q: %w", name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Confirms reports whether to ask before running.
|
||||
func (c Command) Confirms() bool {
|
||||
if c.Confirm != nil {
|
||||
return *c.Confirm
|
||||
}
|
||||
return c.Value != ""
|
||||
}
|
||||
|
||||
// Out is the output mode, quiet unless set.
|
||||
func (c Command) Out() string {
|
||||
if c.Output == "" {
|
||||
return Quiet
|
||||
}
|
||||
return c.Output
|
||||
}
|
||||
|
||||
// Item is what's filled into a command: the selected item's details,
|
||||
// shell-quoted when printed, and the answers to its prompts.
|
||||
type Item struct {
|
||||
Kind, Account, Source, SourceID Q
|
||||
Vault, VaultURL Q
|
||||
Name, Type, Version Q
|
||||
Link, Reference, URL, Note, Expires Q
|
||||
Prompt map[string]Q
|
||||
}
|
||||
|
||||
// Q is a field, shell-quoted when printed in a template.
|
||||
type Q string
|
||||
|
||||
func (q Q) String() string { return Quote(string(q)) }
|
||||
|
||||
var safe = regexp.MustCompile(`^[A-Za-z0-9_./:@%+=,-]+$`)
|
||||
|
||||
// Quote quotes s for a POSIX shell, leaving plain words as they are.
|
||||
func Quote(s string) string {
|
||||
if safe.MatchString(s) {
|
||||
return s
|
||||
}
|
||||
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
|
||||
}
|
||||
|
||||
func (c Command) template() (*template.Template, error) {
|
||||
return template.New("command").Option("missingkey=error").
|
||||
Funcs(template.FuncMap{"raw": func(q Q) string { return string(q) }}).
|
||||
Parse(c.Command)
|
||||
}
|
||||
|
||||
// Render fills the item into the command.
|
||||
func (c Command) Render(it Item) (string, error) {
|
||||
t, err := c.template()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if it.Prompt == nil {
|
||||
it.Prompt = map[string]Q{}
|
||||
}
|
||||
var b strings.Builder
|
||||
if err := t.Execute(&b, it); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
Reference in new issue
Block a user