package cmd import ( "fmt" "os" "os/exec" "strings" "envault/vault" "github.com/spf13/cobra" ) func init() { rootCmd.AddCommand(newRunCmd()) } func newRunCmd() *cobra.Command { var useKeychain bool var saveKeychain bool cmd := &cobra.Command{ Use: "run -- [args...]", Short: "Run a command with secrets injected as environment variables", Long: `Injects secrets into the environment and replaces the current process with the given command. Works seamlessly with PyCharm, VS Code, and other IDEs. Authentication priority: 1. Active session (envault open) — no prompt, recommended 2. macOS Keychain (--keychain flag) — no prompt 3. Password prompt (fallback) — always works PyCharm setup: Terminal: envault open (once, valid 8h) Run Config: envault run -- python -m uvicorn main:app --reload`, Example: ` envault run -- uvicorn main:app --reload envault run -- python main.py envault run --save-keychain -- echo "keychain saved" envault run --keychain -- python -m uvicorn main:app --reload`, Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { var vars map[string]string // Priority 1: active session (envault open) if sv := trySession(); sv != nil { vars = sv } else if useKeychain || saveKeychain { // Priority 2: macOS Keychain password, err := vault.KeychainGet() if err != nil { fmt.Fprintln(os.Stderr, "No password in Keychain — prompting once.") password, err = promptAndOpen() if err != nil { return err } if err := vault.KeychainSet(password); err != nil { fmt.Fprintf(os.Stderr, "Warning: could not save to Keychain: %v\n", err) } else { fmt.Fprintln(os.Stderr, "Password saved to macOS Keychain.") } } path, err := resolveVaultPath() if err != nil { return err } v, err := vault.Open(path, password) if err != nil { return err } vars = v.GetAll(resolveProject()) } else { // Priority 3: password prompt v, _, _, err := openVault() if err != nil { return err } vars = v.GetAll(resolveProject()) } // Inject secrets into environment for k, val := range vars { os.Setenv(k, val) } // For terraform: also inject as TF_VAR_ (hyphens → underscores) if len(args) > 0 && (args[0] == "terraform" || strings.HasSuffix(args[0], "/terraform")) { for k, val := range vars { tfKey := "TF_VAR_" + strings.ReplaceAll(strings.ToLower(k), "-", "_") if os.Getenv(tfKey) == "" { os.Setenv(tfKey, val) } } } // Find and exec the command (replaces current process on Unix) bin, err := exec.LookPath(args[0]) if err != nil { return fmt.Errorf("command not found: %s", args[0]) } return replaceProcess(bin, args) }, } cmd.Flags().BoolVarP(&useKeychain, "keychain", "k", false, "read master password from macOS Keychain (no prompt)") cmd.Flags().BoolVar(&saveKeychain, "save-keychain", false, "prompt once, save to macOS Keychain, then run") // Stop flag parsing at the first non-flag argument so flags like --reload // are passed through to the child command, not interpreted by ev. cmd.Flags().SetInterspersed(false) return cmd } func promptAndOpen() (string, error) { path, err := resolveVaultPath() if err != nil { return "", err } if !vault.Exists(path) { fmt.Fprintln(os.Stderr, "No vault found – creating a new one.") p1, err := vault.PromptPassword("Set master password: ") if err != nil { return "", err } p2, err := vault.PromptPassword("Confirm master password: ") if err != nil { return "", err } if p1 != p2 { return "", fmt.Errorf("passwords do not match") } return p1, nil } return vault.PromptPassword("Master password: ") }