add lock subcommand; add --local flag to rm and exec
All checks were successful
CI / build (push) Successful in 23s

This commit is contained in:
Jonathan Apodaca 2025-05-12 09:55:35 -06:00
parent dac1a06e1a
commit 1922138133
4 changed files with 444 additions and 42 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/build /build
/envvault /envvault
.aider* .aider*
.envvault.json.enc

View File

@ -6,16 +6,18 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"maps"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"slices"
"strings"
"time" "time"
"github.com/alecthomas/kong" "github.com/alecthomas/kong"
"golang.org/x/crypto/nacl/secretbox" "golang.org/x/crypto/nacl/secretbox"
"golang.org/x/crypto/scrypt" "golang.org/x/crypto/scrypt"
"golang.org/x/term" "golang.org/x/term"
"slices"
"git.jrop.me/jonathan/envvault/internal" "git.jrop.me/jonathan/envvault/internal"
"git.jrop.me/jonathan/envvault/internal/config" "git.jrop.me/jonathan/envvault/internal/config"
@ -24,9 +26,14 @@ import (
var cachedPassword []byte var cachedPassword []byte
const (
EnvStoreCurrentVersion = 1
)
var ( var (
keyFilePath string keyFilePath string
dbFilePath string dbFilePath string
localDbFilePath string
) )
func init() { func init() {
@ -36,10 +43,79 @@ func init() {
} }
keyFilePath = filepath.Join(homeDir, ".local/cache/envvault/key") keyFilePath = filepath.Join(homeDir, ".local/cache/envvault/key")
dbFilePath = filepath.Join(homeDir, ".local/cache/envvault/db.json.enc") dbFilePath = filepath.Join(homeDir, ".local/cache/envvault/db.json.enc")
localDbFilePath = ".envvault.json.enc"
} }
type EnvStore struct { type EnvStore struct {
Vars map[string]string `json:"vars"` Vars map[string]string `json:"vars"`
Tags map[string][]string `json:"tags,omitempty"`
}
// GetTags returns all tags for a given variable name
func (s *EnvStore) GetTags(varName string) []string {
if s.Tags == nil {
return []string{}
}
return s.Tags[varName]
}
// SetTags sets the tags for a given variable name
func (s *EnvStore) SetTags(varName string, tags []string) {
if s.Tags == nil {
s.Tags = make(map[string][]string)
}
// Create a union of existing and new tags
existingTags := s.Tags[varName]
unionTags := make([]string, 0, len(existingTags)+len(tags))
// Add existing tags to the union
for _, tag := range existingTags {
unionTags = append(unionTags, tag)
}
// Add new tags if they don't already exist
for _, tag := range tags {
if !slices.Contains(unionTags, tag) {
unionTags = append(unionTags, tag)
}
}
s.Tags[varName] = unionTags
}
// RemoveTags removes the specified tags from a variable
func (s *EnvStore) RemoveTags(varName string, tagsToRemove []string) {
if s.Tags == nil || len(s.Tags[varName]) == 0 {
return
}
existingTags := s.Tags[varName]
remainingTags := make([]string, 0, len(existingTags))
// Keep only tags that are not in tagsToRemove
for _, tag := range existingTags {
if !slices.Contains(tagsToRemove, tag) {
remainingTags = append(remainingTags, tag)
}
}
s.Tags[varName] = remainingTags
}
// HasAnyTag returns true if the variable has any of the specified tags
func (s *EnvStore) HasAnyTag(varName string, tags []string) bool {
if len(tags) == 0 {
return true // No tags specified, include all variables
}
varTags := s.GetTags(varName)
for _, tag := range tags {
if slices.Contains(varTags, tag) {
return true
}
}
return false
} }
type CLI struct { type CLI struct {
@ -47,31 +123,47 @@ type CLI struct {
List ListCmd `cmd:"" help:"List all environment variables."` List ListCmd `cmd:"" help:"List all environment variables."`
Add AddCmd `cmd:"" help:"Add an environment variable."` Add AddCmd `cmd:"" help:"Add an environment variable."`
Rm RmCmd `cmd:"" help:"Remove an environment variable."` Rm RmCmd `cmd:"" help:"Remove an environment variable."`
Tag TagCmd `cmd:"" help:"Add or remove tags from an environment variable."`
Exec ExecCmd `cmd:"" help:"Execute a command with environment variables." alias:"x"` Exec ExecCmd `cmd:"" help:"Execute a command with environment variables." alias:"x"`
Rekey RekeyCmd `cmd:"" help:"Change the master password."` Rekey RekeyCmd `cmd:"" help:"Change the master password."`
Daemon DaemonCmd `cmd:"" help:"Start the password caching daemon."` Daemon DaemonCmd `cmd:"" help:"Start the password caching daemon."`
Lock LockCmd `cmd:"" help:"Lock the vault by stopping the password caching daemon."`
} }
type InitCmd struct{} type InitCmd struct{}
type ListCmd struct { type ListCmd struct {
Values bool `short:"v" help:"Show values of environment variables." default:"false"` Values bool `short:"v" help:"Show values of environment variables." default:"false"`
Local bool `short:"l" help:"List only from local vault." default:"false"`
Tags []string `short:"t" help:"Filter by tags (can be specified multiple times)."`
} }
type AddCmd struct { type AddCmd struct {
Name string `arg:"" help:"Name of the environment variable."` Name string `arg:"" help:"Name of the environment variable."`
Value string `arg:"" optional:"" help:"Value of the environment variable."` Value string `arg:"" help:"Value of the environment variable." optional:""`
Local bool ` help:"Add to local vault instead of global." short:"l" default:"false"`
} }
type RmCmd struct { type RmCmd struct {
Name string `arg:"" help:"Name of the environment variable to remove."` Name string `arg:"" help:"Name of the environment variable to remove."`
Local bool ` help:"Remove from local vault only." short:"l" default:"false"`
}
type TagCmd struct {
Name string `arg:"" help:"Name of the environment variable."`
Tags []string `arg:"" help:"Tags to add or remove." optional:""`
Remove bool ` help:"Remove tags instead of adding them." short:"r" default:"false"`
Local bool ` help:"Operate on local vault only." short:"l" default:"false"`
List bool ` help:"List tags for the variable." short:"L" default:"false"`
} }
type ExecCmd struct { type ExecCmd struct {
Env []string `short:"e" help:"Environment variables to set."` Env []string `short:"e" help:"Environment variables to set."`
Cmd string `arg:"" help:"Command to execute." passthrough:"all"` Local bool `short:"l" help:"Use only local vault." default:"false"`
Args []string `arg:"" optional:"" help:"Arguments for the command."` Tags []string `short:"t" help:"Filter by tags (can be specified multiple times)."`
Cmd string ` help:"Command to execute." arg:"" passthrough:"all"`
Args []string ` help:"Arguments for the command." arg:"" optional:""`
} }
type RekeyCmd struct{} type RekeyCmd struct{}
type DaemonCmd struct { type DaemonCmd struct {
Timeout int `help:"Password cache timeout in minutes." default:"5"` Timeout int `help:"Password cache timeout in minutes." default:"5"`
} }
type LockCmd struct{}
func main() { func main() {
internal.Log.Println("Starting envvault") internal.Log.Println("Starting envvault")
@ -86,11 +178,11 @@ func main() {
case "init": case "init":
subcommandInitVault() subcommandInitVault()
case "add <name>": case "add <name>":
subcommandAddEnvVar(cli.Add.Name, cli.Add.Value) subcommandAddEnvVar(cli.Add)
case "add <name> <value>": case "add <name> <value>":
subcommandAddEnvVar(cli.Add.Name, cli.Add.Value) subcommandAddEnvVar(cli.Add)
case "rm <name>": case "rm <name>":
subcommandRmEnvVar(cli.Rm.Name) subcommandRmEnvVar(cli.Rm.Name, cli.Rm.Local)
case "list": case "list":
subcommandListEnvVars(cli.List) subcommandListEnvVars(cli.List)
case "exec <cmd>": case "exec <cmd>":
@ -101,6 +193,12 @@ func main() {
subcommandRekeyVault() subcommandRekeyVault()
case "daemon": case "daemon":
subcommandStartDaemon(cli.Daemon.Timeout) subcommandStartDaemon(cli.Daemon.Timeout)
case "lock":
subcommandLockVault()
case "tag <name>":
subcommandTagEnvVar(cli.Tag)
case "tag <name> <tags>":
subcommandTagEnvVar(cli.Tag)
default: default:
internal.Log.Printf("Unknown command: %s", command) internal.Log.Printf("Unknown command: %s", command)
log.Fatal("Unknown command") log.Fatal("Unknown command")
@ -165,7 +263,7 @@ func subcommandInitVault() { // {{{
// Initialize the db file // Initialize the db file
internal.Log.Println("Initializing empty environment store") internal.Log.Println("Initializing empty environment store")
emptyStore := &EnvStore{Vars: make(map[string]string)} emptyStore := &EnvStore{Vars: make(map[string]string)}
saveEnvStore(emptyStore) saveGlobalEnvStore(emptyStore)
internal.Log.Println("Vault initialized successfully") internal.Log.Println("Vault initialized successfully")
fmt.Fprintln(os.Stderr, "Vault initialized successfully.") fmt.Fprintln(os.Stderr, "Vault initialized successfully.")
@ -177,47 +275,205 @@ func subcommandInitVault() { // {{{
// }}} // }}}
func subcommandAddEnvVar(name, value string) { // {{{ func subcommandAddEnvVar(cmd AddCmd) { // {{{
store := loadEnvStore() var store *EnvStore
if value == "" { if cmd.Local {
fmt.Fprintf(os.Stderr, "Enter value for %s: ", name) store = loadLocalEnvStore()
internal.Log.Printf("Adding environment variable '%s' to local vault", cmd.Name)
} else {
store = loadGlobalEnvStore()
internal.Log.Printf("Adding environment variable '%s' to global vault", cmd.Name)
}
if cmd.Value == "" {
fmt.Fprintf(os.Stderr, "Enter value for %s: ", cmd.Name)
inputValue, err := term.ReadPassword(int(os.Stdin.Fd())) inputValue, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Fprintln(os.Stderr) fmt.Fprintln(os.Stderr)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
value = string(inputValue) cmd.Value = string(inputValue)
}
store.Vars[cmd.Name] = cmd.Value
if cmd.Local {
saveLocalEnvStore(store)
fmt.Fprintf(os.Stderr, "Environment variable '%s' added to local vault.\n", cmd.Name)
} else {
saveGlobalEnvStore(store)
fmt.Fprintf(os.Stderr, "Environment variable '%s' added to global vault.\n", cmd.Name)
} }
store.Vars[name] = value
saveEnvStore(store)
} // }}} } // }}}
func subcommandRmEnvVar(name string) { // {{{ func subcommandRmEnvVar(name string, localOnly bool) { // {{{
store := loadEnvStore() localExists := false
if _, exists := store.Vars[name]; exists {
delete(store.Vars, name) // Check local store if it exists
saveEnvStore(store) if _, err := os.Stat(localDbFilePath); err == nil {
fmt.Fprintf(os.Stderr, "Environment variable '%s' removed.\n", name) localStore := loadLocalEnvStore()
if _, exists := localStore.Vars[name]; exists {
delete(localStore.Vars, name)
saveLocalEnvStore(localStore)
localExists = true
fmt.Fprintf(os.Stderr, "Environment variable '%s' removed from local vault.\n", name)
} else if localOnly {
fmt.Fprintf(os.Stderr, "Environment variable '%s' not found in local vault.\n", name)
}
} else if localOnly {
fmt.Fprintf(os.Stderr, "Local vault does not exist.\n")
}
// Check global store if not local-only
if !localOnly {
globalStore := loadGlobalEnvStore()
if _, exists := globalStore.Vars[name]; exists {
delete(globalStore.Vars, name)
saveGlobalEnvStore(globalStore)
fmt.Fprintf(os.Stderr, "Environment variable '%s' removed from global vault.\n", name)
} else if !localExists {
fmt.Fprintf(os.Stderr, "Environment variable '%s' not found in global vault.\n", name)
}
}
} // }}}
func subcommandTagEnvVar(cmd TagCmd) { // {{{
var store *EnvStore
// Determine which store to use
if cmd.Local {
if _, err := os.Stat(localDbFilePath); os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "Local vault does not exist.\n")
os.Exit(1)
}
store = loadLocalEnvStore()
internal.Log.Printf("Using local vault")
} else { } else {
fmt.Fprintf(os.Stderr, "Environment variable '%s' not found.\n", name) store = loadGlobalEnvStore()
internal.Log.Printf("Using global vault")
}
// Check if the variable exists
if _, exists := store.Vars[cmd.Name]; !exists {
vaultType := "global"
if cmd.Local {
vaultType = "local"
}
fmt.Fprintf(os.Stderr, "Environment variable '%s' not found in %s vault.\n",
cmd.Name,
vaultType)
os.Exit(1)
}
// If list flag is set, just list the tags and return
if cmd.List {
tags := store.GetTags(cmd.Name)
if len(tags) == 0 {
fmt.Fprintf(os.Stderr, "No tags for '%s'\n", cmd.Name)
return
}
for _, tag := range tags {
fmt.Println(tag)
}
return
}
// If no tags provided and not in list mode, show error
if len(cmd.Tags) == 0 {
fmt.Fprintf(os.Stderr, "No tags provided. Use --list to view tags.\n")
os.Exit(1)
}
// Process tags
if cmd.Remove {
internal.Log.Printf("Removing tags %v from variable '%s'", cmd.Tags, cmd.Name)
store.RemoveTags(cmd.Name, cmd.Tags)
fmt.Fprintf(
os.Stderr,
"Removed tags from '%s': %s\n",
cmd.Name,
strings.Join(cmd.Tags, ", "),
)
} else {
internal.Log.Printf("Adding tags %v to variable '%s'", cmd.Tags, cmd.Name)
store.SetTags(cmd.Name, cmd.Tags)
fmt.Fprintf(os.Stderr, "Added tags to '%s': %s\n", cmd.Name, strings.Join(cmd.Tags, ", "))
}
// Save the store
if cmd.Local {
saveLocalEnvStore(store)
} else {
saveGlobalEnvStore(store)
} }
} // }}} } // }}}
func subcommandListEnvVars(cmdArgs ListCmd) { // {{{ func subcommandListEnvVars(cmdArgs ListCmd) { // {{{
store := loadEnvStore() var store *EnvStore
for k, v := range store.Vars {
if cmdArgs.Values { // Determine which store to use
fmt.Printf("%s=%s\n", k, v) if cmdArgs.Local {
if _, err := os.Stat(localDbFilePath); os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "Local vault does not exist.\n")
return
}
store = loadLocalEnvStore()
} else { } else {
fmt.Printf("%s\n", k) // Default: load and merge stores
store = loadEnvStore()
}
// Sort keys for consistent output
var keys []string
for k := range store.Vars {
// Filter by tags if specified
if store.HasAnyTag(k, cmdArgs.Tags) {
keys = append(keys, k)
}
}
slices.Sort(keys)
for _, k := range keys {
v := store.Vars[k]
tags := store.GetTags(k)
tagStr := ""
if len(tags) > 0 {
tagStr = fmt.Sprintf(" (tags: %s)", strings.Join(tags, ", "))
}
if cmdArgs.Values {
fmt.Printf("%s=%s", k, v)
fmt.Fprintf(os.Stderr, "%s", tagStr)
fmt.Println()
} else {
fmt.Printf("%s", k)
fmt.Fprintf(os.Stderr, "%s", tagStr)
fmt.Println()
} }
} }
} // }}} } // }}}
func subcommandExecCommand(cmdArgs ExecCmd) { // {{{ func subcommandExecCommand(cmdArgs ExecCmd) { // {{{
store := loadEnvStore() var store *EnvStore
// Determine which store to use
if cmdArgs.Local {
if _, err := os.Stat(localDbFilePath); os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "Local vault does not exist.\n")
os.Exit(1)
}
store = loadLocalEnvStore()
internal.Log.Println("Using local vault only for execution")
} else {
// Default: load and merge stores
store = loadEnvStore()
internal.Log.Println("Using merged vaults for execution")
}
envVars := os.Environ() envVars := os.Environ()
for candidateEnvName, candidateEnvValue := range store.Vars { for candidateEnvName, candidateEnvValue := range store.Vars {
// Skip if not matching tag filter
if !store.HasAnyTag(candidateEnvName, cmdArgs.Tags) {
continue
}
if len(cmdArgs.Env) == 0 { if len(cmdArgs.Env) == 0 {
// If no env vars are specified, add all env vars // If no env vars are specified, add all env vars
envVars = append(envVars, fmt.Sprintf("%s=%s", candidateEnvName, candidateEnvValue)) envVars = append(envVars, fmt.Sprintf("%s=%s", candidateEnvName, candidateEnvValue))
@ -266,6 +522,29 @@ func subcommandStartDaemon(timeoutMinutes int) { // {{{
} }
} // }}} } // }}}
func subcommandLockVault() { // {{{
internal.Log.Println("Locking vault by stopping daemon")
socketPath := daemon.GetSocketPath()
client := daemon.NewClient(socketPath)
if !client.IsRunning() {
internal.Log.Println("Daemon is not running, nothing to lock")
fmt.Println("Vault is already locked (daemon not running)")
return
}
internal.Log.Println("Sending kill command to daemon")
err := client.KillDaemon()
if err != nil {
internal.Log.Printf("Failed to kill daemon: %v", err)
log.Fatalf("Failed to lock vault: %v", err)
}
internal.Log.Println("Daemon stopped successfully")
fmt.Println("Vault locked successfully")
} // }}}
func subcommandRekeyVault() { // {{{ func subcommandRekeyVault() { // {{{
// Ask for current master password // Ask for current master password
fmt.Fprint(os.Stderr, "Enter current master password: ") fmt.Fprint(os.Stderr, "Enter current master password: ")
@ -307,6 +586,21 @@ func subcommandRekeyVault() { // {{{
log.Fatal("Passwords do not match") log.Fatal("Passwords do not match")
} }
// Check if daemon is running and kill it
socketPath := daemon.GetSocketPath()
client := daemon.NewClient(socketPath)
if client.IsRunning() {
internal.Log.Println("Daemon is running, stopping it before rekey")
fmt.Println("Stopping password daemon before changing master password...")
err := client.KillDaemon()
if err != nil {
internal.Log.Printf("Failed to kill daemon: %v", err)
log.Fatalf("Failed to stop daemon: %v", err)
}
internal.Log.Println("Daemon stopped successfully")
}
// Encrypt the master key with the new password // Encrypt the master key with the new password
newEncryptedKey := encryptKeyWithPassword(masterKey, newPassword) newEncryptedKey := encryptKeyWithPassword(masterKey, newPassword)
@ -460,15 +754,16 @@ func decryptKeyWithPassword(encryptedKey, password []byte) ([]byte, error) { //
return decrypted, nil return decrypted, nil
} // }}} } // }}}
func loadEnvStore() *EnvStore { // {{{ func loadStoreFile(filePath string) *EnvStore { // {{{
key := loadKey() key := loadKey()
if err := os.MkdirAll(filepath.Dir(dbFilePath), 0700); err != nil {
log.Fatal(err) data, err := os.ReadFile(filePath)
}
data, err := os.ReadFile(dbFilePath)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return &EnvStore{Vars: make(map[string]string)} return &EnvStore{
Vars: make(map[string]string),
Tags: make(map[string][]string),
}
} }
log.Fatal(err) log.Fatal(err)
} }
@ -484,13 +779,61 @@ func loadEnvStore() *EnvStore { // {{{
if err := json.Unmarshal(decrypted, &store); err != nil { if err := json.Unmarshal(decrypted, &store); err != nil {
log.Fatal(err) log.Fatal(err)
} }
// Initialize Tags map if it's nil
if store.Tags == nil {
store.Tags = make(map[string][]string)
}
} else { } else {
return &EnvStore{Vars: make(map[string]string)} return &EnvStore{
Vars: make(map[string]string),
Tags: make(map[string][]string),
}
} }
return &store return &store
} // }}} } // }}}
func saveEnvStore(store *EnvStore) { // {{{ func loadGlobalEnvStore() *EnvStore { // {{{
if err := os.MkdirAll(filepath.Dir(dbFilePath), 0700); err != nil {
log.Fatal(err)
}
return loadStoreFile(dbFilePath)
} // }}}
func loadLocalEnvStore() *EnvStore { // {{{
return loadStoreFile(localDbFilePath)
} // }}}
func loadEnvStore() *EnvStore { // {{{
// Load global store first
globalStore := loadGlobalEnvStore()
// Check if local store exists
if _, err := os.Stat(localDbFilePath); os.IsNotExist(err) {
// No local store, just return global
return globalStore
}
// Load local store
localStore := loadLocalEnvStore()
// Merge stores (local takes precedence)
mergedStore := &EnvStore{
Vars: make(map[string]string),
Tags: make(map[string][]string),
}
// Copy all global vars and tags
maps.Copy(mergedStore.Vars, globalStore.Vars)
maps.Copy(mergedStore.Tags, globalStore.Tags)
// Override/add local vars and tags
maps.Copy(mergedStore.Vars, localStore.Vars)
maps.Copy(mergedStore.Tags, localStore.Tags)
return mergedStore
} // }}}
func saveGlobalEnvStore(store *EnvStore) { // {{{
key := loadKey() key := loadKey()
data, err := json.Marshal(store) data, err := json.Marshal(store)
if err != nil { if err != nil {
@ -507,3 +850,21 @@ func saveEnvStore(store *EnvStore) { // {{{
log.Fatal(err) log.Fatal(err)
} }
} // }}} } // }}}
func saveLocalEnvStore(store *EnvStore) { // {{{
key := loadKey()
data, err := json.Marshal(store)
if err != nil {
log.Fatal(err)
}
var nonce [24]byte
if _, err := rand.Read(nonce[:]); err != nil {
log.Fatal(err)
}
encrypted := secretbox.Seal(nonce[:], data, &nonce, &key)
if err := os.WriteFile(localDbFilePath, encrypted, 0600); err != nil {
log.Fatal(err)
}
} // }}}

View File

@ -111,6 +111,29 @@ func (c *Client) RetrievePassword() ([]byte, error) {
return []byte(resp.Data), nil return []byte(resp.Data), nil
} }
// KillDaemon sends a kill command to the daemon to stop it
func (c *Client) KillDaemon() error {
internal.DaemonLog.Printf("Sending kill command to daemon")
msg := Message{
Command: "kill",
}
internal.DaemonLog.Printf("Sending kill message to daemon")
resp, err := c.sendMessage(msg)
if err != nil {
internal.DaemonLog.Printf("Failed to send kill message: %v", err)
return err
}
if !resp.Success {
internal.DaemonLog.Printf("Daemon reported error: %s", resp.Error)
return fmt.Errorf("failed to kill daemon: %s", resp.Error)
}
internal.DaemonLog.Printf("Kill command sent successfully")
return nil
}
// sendMessage sends a message to the daemon and returns the response // sendMessage sends a message to the daemon and returns the response
func (c *Client) sendMessage(msg Message) (*Response, error) { func (c *Client) sendMessage(msg Message) (*Response, error) {
internal.DaemonLog.Printf("Connecting to daemon socket: %s", c.socketPath) internal.DaemonLog.Printf("Connecting to daemon socket: %s", c.socketPath)

View File

@ -217,6 +217,23 @@ func handleConnection(conn net.Conn, cache *PasswordCache) {
internal.DaemonLog.Println("Password retrieved, sending success response") internal.DaemonLog.Println("Password retrieved, sending success response")
encoder.Encode(Response{Success: true, Data: string(password)}) encoder.Encode(Response{Success: true, Data: string(password)})
case "kill":
internal.DaemonLog.Println("Kill command received, shutting down daemon")
encoder.Encode(Response{Success: true})
// Close the connection before exiting
conn.Close()
// Clean up the socket file
os.Remove(conn.LocalAddr().String())
internal.DaemonLog.Printf("Removed socket file: %s", conn.LocalAddr().String())
// Exit with success status
internal.DaemonLog.Println("Daemon shutting down gracefully")
// Use goroutine to allow response to be sent before exit
go func() {
time.Sleep(100 * time.Millisecond)
os.Exit(0)
}()
return
default: default:
internal.DaemonLog.Printf("Unknown command received: %s", msg.Command) internal.DaemonLog.Printf("Unknown command received: %s", msg.Command)
encoder.Encode(Response{Success: false, Error: "Unknown command"}) encoder.Encode(Response{Success: false, Error: "Unknown command"})