package cli import ( "encoding/json" "fmt" "io" "os" "strings" "git.eeqj.de/sneak/secret/internal/secret" "git.eeqj.de/sneak/secret/internal/vault" "github.com/spf13/afero" "github.com/spf13/cobra" ) func newAddCmd() *cobra.Command { cmd := &cobra.Command{ Use: "add ", Short: "Add a secret to the vault", Long: `Add a secret to the current vault. The secret value is read from stdin.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { secret.Debug("Add command RunE starting", "secret_name", args[0]) force, _ := cmd.Flags().GetBool("force") secret.Debug("Got force flag", "force", force) cli := NewCLIInstance() secret.Debug("Created CLI instance, calling AddSecret") return cli.AddSecret(args[0], force) }, } cmd.Flags().BoolP("force", "f", false, "Overwrite existing secret") return cmd } func newGetCmd() *cobra.Command { cmd := &cobra.Command{ Use: "get ", Short: "Retrieve a secret from the vault", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { version, _ := cmd.Flags().GetString("version") cli := NewCLIInstance() return cli.GetSecretWithVersion(args[0], version) }, } cmd.Flags().StringP("version", "v", "", "Get a specific version (default: current)") return cmd } func newListCmd() *cobra.Command { cmd := &cobra.Command{ Use: "list [filter]", Aliases: []string{"ls"}, Short: "List all secrets in the current vault", Long: `List all secrets in the current vault. Optionally filter by substring match in secret name.`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { jsonOutput, _ := cmd.Flags().GetBool("json") var filter string if len(args) > 0 { filter = args[0] } cli := NewCLIInstance() return cli.ListSecrets(jsonOutput, filter) }, } cmd.Flags().Bool("json", false, "Output in JSON format") return cmd } func newImportCmd() *cobra.Command { cmd := &cobra.Command{ Use: "import ", Short: "Import a secret from a file", Long: `Import a secret from a file and store it in the current vault under the given name.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { sourceFile, _ := cmd.Flags().GetString("source") force, _ := cmd.Flags().GetBool("force") cli := NewCLIInstance() return cli.ImportSecret(args[0], sourceFile, force) }, } cmd.Flags().StringP("source", "s", "", "Source file to import from (required)") cmd.Flags().BoolP("force", "f", false, "Overwrite existing secret") _ = cmd.MarkFlagRequired("source") return cmd } // AddSecret adds a secret to the current vault func (cli *CLIInstance) AddSecret(secretName string, force bool) error { secret.Debug("CLI AddSecret starting", "secret_name", secretName, "force", force) // Get current vault secret.Debug("Getting current vault") vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) if err != nil { return err } secret.Debug("Got current vault", "vault_name", vlt.GetName()) // Read secret value from stdin secret.Debug("Reading secret value from stdin") value, err := io.ReadAll(os.Stdin) if err != nil { return fmt.Errorf("failed to read secret value: %w", err) } secret.Debug("Read secret value from stdin", "value_length", len(value)) // Remove trailing newline if present if len(value) > 0 && value[len(value)-1] == '\n' { value = value[:len(value)-1] secret.Debug("Removed trailing newline", "new_length", len(value)) } // Add the secret to the vault secret.Debug("Calling vault.AddSecret", "secret_name", secretName, "value_length", len(value), "force", force) if err := vlt.AddSecret(secretName, value, force); err != nil { secret.Debug("vault.AddSecret failed", "error", err) return err } secret.Debug("vault.AddSecret completed successfully") return nil } // GetSecret retrieves and prints a secret from the current vault func (cli *CLIInstance) GetSecret(secretName string) error { return cli.GetSecretWithVersion(secretName, "") } // GetSecretWithVersion retrieves and prints a specific version of a secret func (cli *CLIInstance) GetSecretWithVersion(secretName string, version string) error { // Get current vault vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) if err != nil { return err } // Get the secret value var value []byte if version == "" { value, err = vlt.GetSecret(secretName) } else { value, err = vlt.GetSecretVersion(secretName, version) } if err != nil { return err } // Print the secret value to stdout fmt.Print(string(value)) return nil } // ListSecrets lists all secrets in the current vault func (cli *CLIInstance) ListSecrets(jsonOutput bool, filter string) error { // Get current vault vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) if err != nil { return err } // Get list of secrets secrets, err := vlt.ListSecrets() if err != nil { return fmt.Errorf("failed to list secrets: %w", err) } // Filter secrets if filter is provided var filteredSecrets []string if filter != "" { for _, secretName := range secrets { if strings.Contains(secretName, filter) { filteredSecrets = append(filteredSecrets, secretName) } } } else { filteredSecrets = secrets } if jsonOutput { // For JSON output, get metadata for each secret secretsWithMetadata := make([]map[string]interface{}, 0, len(filteredSecrets)) for _, secretName := range filteredSecrets { secretInfo := map[string]interface{}{ "name": secretName, } // Try to get metadata using GetSecretObject if secretObj, err := vlt.GetSecretObject(secretName); err == nil { metadata := secretObj.GetMetadata() secretInfo["created_at"] = metadata.CreatedAt secretInfo["updated_at"] = metadata.UpdatedAt } secretsWithMetadata = append(secretsWithMetadata, secretInfo) } output := map[string]interface{}{ "secrets": secretsWithMetadata, } if filter != "" { output["filter"] = filter } jsonBytes, err := json.MarshalIndent(output, "", " ") if err != nil { return fmt.Errorf("failed to marshal JSON: %w", err) } fmt.Println(string(jsonBytes)) } else { // Pretty table output if len(filteredSecrets) == 0 { if filter != "" { fmt.Printf("No secrets found in vault '%s' matching filter '%s'.\n", vlt.GetName(), filter) } else { fmt.Println("No secrets found in current vault.") fmt.Println("Run 'secret add ' to create one.") } return nil } // Get current vault name for display if filter != "" { fmt.Printf("Secrets in vault '%s' matching '%s':\n\n", vlt.GetName(), filter) } else { fmt.Printf("Secrets in vault '%s':\n\n", vlt.GetName()) } fmt.Printf("%-40s %-20s\n", "NAME", "LAST UPDATED") fmt.Printf("%-40s %-20s\n", "----", "------------") for _, secretName := range filteredSecrets { lastUpdated := "unknown" if secretObj, err := vlt.GetSecretObject(secretName); err == nil { metadata := secretObj.GetMetadata() lastUpdated = metadata.UpdatedAt.Format("2006-01-02 15:04") } fmt.Printf("%-40s %-20s\n", secretName, lastUpdated) } fmt.Printf("\nTotal: %d secret(s)", len(filteredSecrets)) if filter != "" { fmt.Printf(" (filtered from %d)", len(secrets)) } fmt.Println() } return nil } // ImportSecret imports a secret from a file func (cli *CLIInstance) ImportSecret(secretName, sourceFile string, force bool) error { // Get current vault vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir) if err != nil { return err } // Read secret value from the source file value, err := afero.ReadFile(cli.fs, sourceFile) if err != nil { return fmt.Errorf("failed to read secret from file %s: %w", sourceFile, err) } // Store the secret in the vault if err := vlt.AddSecret(secretName, value, force); err != nil { return err } fmt.Printf("Successfully imported secret '%s' from file '%s'\n", secretName, sourceFile) return nil }