- fgj pr approve / pr reject: thin shortcuts over 'pr review --approve'
and '--request-changes'. Reject requires a body.
- fgj repo migrate: wrap SDK MigrateRepo. Supports git, github, gitlab,
gitea, gogs services; mirror mode with --mirror-interval; selective
import (wiki/labels/milestones/issues/PRs/releases/LFS); auth via
--auth-token or --auth-username/--auth-password. Defaults owner to
the authenticated user.
- fgj repo create-from-template: wrap SDK CreateRepoFromTemplate with
fine-grained --with-{content,topics,labels,webhooks,git-hooks,avatar}
flags. Template is owner/name; new repo defaults to the current user.
- Rework 'fgj actions secret create' input. New cmd/secret_input.go
resolves values from --body, --body-file (path or '-'), hidden TTY
prompt via term.ReadPassword, or piped stdin. Trims trailing
whitespace, rejects empty values. Replaces fmt.Scanln which broke on
spaces/newlines and echoed input.
- CHANGELOG: v0.4.0 Unreleased section documenting all additions,
changes, and development items.
- README: updated feature list with new commands.
68 lines
1.8 KiB
Go
68 lines
1.8 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
// readSecretValue resolves the value for a secret/token flag from, in order:
|
|
// 1. --body (inline; visible in shell history)
|
|
// 2. --body-file (file path, or "-" for stdin)
|
|
// 3. interactive TTY prompt (hidden)
|
|
// 4. piped stdin
|
|
//
|
|
// Trailing whitespace (including the final newline common in heredocs and
|
|
// `echo ... | fgj ...`) is trimmed. An empty resolved value is rejected so we
|
|
// never silently write an empty secret.
|
|
func readSecretValue(cmd *cobra.Command, label string) (string, error) {
|
|
if v, _ := cmd.Flags().GetString("body"); v != "" {
|
|
return strings.TrimRight(v, "\r\n"), nil
|
|
}
|
|
|
|
if path, _ := cmd.Flags().GetString("body-file"); path != "" {
|
|
var raw []byte
|
|
var err error
|
|
if path == "-" {
|
|
raw, err = io.ReadAll(ios.In)
|
|
} else {
|
|
raw, err = os.ReadFile(path)
|
|
}
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read secret from %q: %w", path, err)
|
|
}
|
|
value := strings.TrimRight(string(raw), "\r\n")
|
|
if value == "" {
|
|
return "", fmt.Errorf("secret value from %q is empty", path)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
if ios.IsStdinTTY() {
|
|
fmt.Fprintf(ios.ErrOut, "Value for %s: ", label)
|
|
pw, err := term.ReadPassword(int(os.Stdin.Fd()))
|
|
fmt.Fprintln(ios.ErrOut)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read secret: %w", err)
|
|
}
|
|
value := strings.TrimRight(string(pw), "\r\n")
|
|
if value == "" {
|
|
return "", fmt.Errorf("secret value is empty")
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
raw, err := io.ReadAll(ios.In)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read secret from stdin: %w", err)
|
|
}
|
|
value := strings.TrimRight(string(raw), "\r\n")
|
|
if value == "" {
|
|
return "", fmt.Errorf("secret value from stdin is empty")
|
|
}
|
|
return value, nil
|
|
}
|