// Package cli implements the bitwarden-go command line: a thin read-path // front end over the library (login check, project/secret discovery, // value fetch). Credentials come from the environment or a 0600 env file // — never from flags or arguments. Values are printed to stdout bare // (exec-style plumbing) and never to stderr; stderr carries only // redacted diagnostics. package cli import ( "context" "flag" "fmt" "io" "os" "path/filepath" bw "git.knownelement.com/ukrrs/mopac-bitwarden-go" "git.knownelement.com/ukrrs/mopac-bitwarden-go/internal/config" ) const usage = `bitwarden-go: Bitwarden Secrets Manager read client (plain REST, no SDK) Usage: bitwarden-go login [-config PATH] verify credentials (no output secrets) bitwarden-go projects [-config PATH] list projects: " " bitwarden-go secrets list [-config PATH] list secrets: " " bitwarden-go get [-config PATH] print one secret value (bare, no newline) bitwarden-go help print this usage is a secret name or secret uuid. Credentials come from BW_* env vars or a 0600 env file (BW_SERVER_URL, BW_ACCESS_TOKEN, or BW_CLIENTID+BW_CLIENTSECRET) — NEVER from flags or arguments. Tokens are memory-only and refreshed before expiry; nothing is ever written to disk. Exit codes: 0 ok, 1 usage/config error, 2 auth or resolution failure. ` // Run executes one command; it returns the process exit code. func Run(args []string, stdout, stderr io.Writer) int { if len(args) == 0 { fmt.Fprint(stderr, usage) return 1 } switch args[0] { case "help", "-h", "--help": fmt.Fprint(stdout, usage) return 0 case "login": return cmdLogin(args[1:], stdout, stderr) case "projects": return cmdProjects(args[1:], stdout, stderr) case "secrets": return cmdSecrets(args[1:], stdout, stderr) case "get": return cmdGet(args[1:], stdout, stderr) default: fmt.Fprintf(stderr, "bitwarden-go: unknown command %q\n\n%s", args[0], usage) return 1 } } // configPath resolves -config / $BITWARDENGO_CONFIG / default location. func configPath(explicit string) string { if explicit != "" { return explicit } if p := os.Getenv("BITWARDENGO_CONFIG"); p != "" { return p } home, err := os.UserHomeDir() if err != nil { return "" } return filepath.Join(home, ".config", "bitwarden-go", "env") } func newFlags(name string) *flag.FlagSet { fs := flag.NewFlagSet(name, flag.ContinueOnError) fs.SetOutput(io.Discard) return fs } func authenticate(stderr io.Writer, explicitConfig string) (*bw.Token, error) { cfg, _, err := config.Load(configPath(explicitConfig)) if err != nil { return nil, err } return bw.Authenticate(context.Background(), bw.Credentials{ BaseURL: cfg.ServerURL, AccessToken: cfg.AccessToken, ClientID: cfg.ClientID, ClientSecret: cfg.ClientSecret, }) } func cmdLogin(args []string, stdout, stderr io.Writer) int { fs := newFlags("login") configFlag := fs.String("config", "", "credentials env file (default $BITWARDENGO_CONFIG, then ~/.config/bitwarden-go/env)") if err := fs.Parse(args); err != nil || fs.NArg() != 0 { fmt.Fprintln(stderr, "bitwarden-go login: no arguments expected") return 1 } tok, err := authenticate(stderr, *configFlag) if err != nil { fmt.Fprintf(stderr, "bitwarden-go: %v\n", err) return 2 } defer tok.Zero() fmt.Fprintf(stdout, "authenticated: account %s, %s\n", tok.AccountID, tok) return 0 } func cmdProjects(args []string, stdout, stderr io.Writer) int { fs := newFlags("projects") configFlag := fs.String("config", "", "credentials env file") if err := fs.Parse(args); err != nil || fs.NArg() != 0 { fmt.Fprintln(stderr, "bitwarden-go projects: no arguments expected") return 1 } tok, err := authenticate(stderr, *configFlag) if err != nil { fmt.Fprintf(stderr, "bitwarden-go: %v\n", err) return 2 } defer tok.Zero() projects, err := bw.ListProjects(context.Background(), tok) if err != nil { fmt.Fprintf(stderr, "bitwarden-go: %v\n", err) return 2 } for _, p := range projects { fmt.Fprintf(stdout, "%s %s\n", p.ID, p.Name) } return 0 } func cmdSecrets(args []string, stdout, stderr io.Writer) int { fs := newFlags("secrets") configFlag := fs.String("config", "", "credentials env file") if err := fs.Parse(args); err != nil { fmt.Fprintln(stderr, "bitwarden-go secrets: usage: bitwarden-go secrets list") return 1 } if fs.NArg() != 1 || fs.Arg(0) != "list" { fmt.Fprintln(stderr, "bitwarden-go secrets: usage: bitwarden-go secrets list") return 1 } tok, err := authenticate(stderr, *configFlag) if err != nil { fmt.Fprintf(stderr, "bitwarden-go: %v\n", err) return 2 } defer tok.Zero() secrets, err := bw.ListSecrets(context.Background(), tok) if err != nil { fmt.Fprintf(stderr, "bitwarden-go: %v\n", err) return 2 } for _, s := range secrets { fmt.Fprintf(stdout, "%s %s\n", s.ID, s.Name) } return 0 } func cmdGet(args []string, stdout, stderr io.Writer) int { fs := newFlags("get") configFlag := fs.String("config", "", "credentials env file") if err := fs.Parse(args); err != nil || fs.NArg() != 1 { fmt.Fprintln(stderr, "bitwarden-go get: exactly one secret name or id required") return 1 } key := fs.Arg(0) tok, err := authenticate(stderr, *configFlag) if err != nil { fmt.Fprintf(stderr, "bitwarden-go: %v\n", err) return 2 } defer tok.Zero() value, err := bw.GetSecret(context.Background(), tok, key) if err != nil { fmt.Fprintf(stderr, "bitwarden-go: %v\n", err) return 2 } // Bare value, no trailing newline: $(bitwarden-go get name) plumbing. fmt.Fprint(stdout, value) return 0 }