Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.11.1
github.com/testcontainers/testcontainers-go v0.41.0
golang.org/x/term v0.40.0
golang.org/x/time v0.14.0
gopkg.in/yaml.v3 v3.0.1
)
Expand Down Expand Up @@ -103,7 +104,6 @@ require (
golang.org/x/net v0.50.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/grpc v1.79.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
Expand Down
112 changes: 104 additions & 8 deletions pkg/cli/init.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"bufio"
"context"
"encoding/json"
"fmt"
Expand All @@ -17,6 +18,7 @@ import (
dockerimage "github.com/docker/docker/api/types/image"
dockerclient "github.com/docker/docker/client"
"github.com/spf13/cobra"
"golang.org/x/term"

authclient "github.com/ethpandaops/panda/pkg/auth/client"
authstore "github.com/ethpandaops/panda/pkg/auth/store"
Expand All @@ -32,14 +34,16 @@ const (
)

var (
initDir = configpath.DefaultConfigDir()
initForce bool
initProxyURL string
initSandboxImage string
initServerImage string
initSkipDocker bool
initSkipAuth bool
initSkipStart bool
initDir = configpath.DefaultConfigDir()
initForce bool
initProxyURL string
initSandboxImage string
initServerImage string
initSkipDocker bool
initSkipAuth bool
initSkipStart bool
initDiscordUsername string
initSkipDiscord bool
)

var initCmd = &cobra.Command{
Expand Down Expand Up @@ -73,6 +77,10 @@ func init() {
initCmd.Flags().BoolVar(&initSkipStart, "skip-start", false, "skip starting the server")
initCmd.Flags().BoolVar(&noBrowser, "no-browser", false,
"manual auth flow for SSH/headless environments (auto-detected over SSH)")
initCmd.Flags().StringVar(&initDiscordUsername, "discord-username", "",
"Discord username or user ID to DM on build completion (skips interactive prompt)")
initCmd.Flags().BoolVar(&initSkipDiscord, "skip-discord", false,
"skip the Discord username prompt entirely")
}

func runInit(_ *cobra.Command, _ []string) error {
Expand Down Expand Up @@ -162,6 +170,11 @@ func runInit(_ *cobra.Command, _ []string) error {
fmt.Println("\nSkipping authentication (--skip-auth)")
}

// 4b. Discord username (optional, persisted to config.user.yaml).
if err := initEnsureDiscordUsername(configPath); err != nil {
fmt.Fprintf(os.Stderr, " (could not save Discord username: %v)\n", err)
}

// 5. Start the server.
switch {
case initSkipStart:
Expand Down Expand Up @@ -495,3 +508,86 @@ func initEnsureAuth() (bool, error) {

return false, runAuthLogin(nil, nil)
}

// initEnsureDiscordUsername gathers an optional Discord username and persists
// it under notifications.discord.username so future `panda build` invocations
// notify the user without needing the --discord-dm flag. The prompt is
// suppressed under --skip-discord, when stdin is not a TTY, or when a value is
// already configured. A value supplied via --discord-username always wins.
func initEnsureDiscordUsername(configPath string) error {
if initSkipDiscord {
return nil
}

username := strings.TrimSpace(initDiscordUsername)

if username == "" {
existing, _ := loadConfiguredDiscordUsername(configPath)
if existing != "" {
fmt.Printf("Discord notifications: already configured for %q\n", existing)
return nil
}

if !term.IsTerminal(int(os.Stdin.Fd())) {
return nil
}

fmt.Println()
fmt.Println("Optional: Discord username for build-completion DMs.")
fmt.Println(" Leave blank to skip; you can set it later with --discord-dm on `panda build`.")
fmt.Print("Discord username: ")

scanner := bufio.NewScanner(os.Stdin)
if !scanner.Scan() {
return nil
}

username = strings.TrimSpace(scanner.Text())
}

if username == "" {
return nil
}

if err := saveDiscordUsername(configPath, username); err != nil {
return err
}

fmt.Printf("Saved Discord username %q to config.user.yaml\n", username)

return nil
}

func loadConfiguredDiscordUsername(configPath string) (string, error) {
cfg, err := config.LoadWithUserOverrides(configPath)
if err != nil {
return "", err
}

return cfg.Notifications.Discord.Username, nil
}

func saveDiscordUsername(configPath, username string) error {
userPath := config.UserConfigPath(configPath)

overrides, err := config.LoadUserConfigMap(userPath)
if err != nil {
return err
}

notifications, _ := overrides["notifications"].(map[string]any)
if notifications == nil {
notifications = make(map[string]any, 1)
}

discord, _ := notifications["discord"].(map[string]any)
if discord == nil {
discord = make(map[string]any, 1)
}

discord["username"] = username
notifications["discord"] = discord
overrides["notifications"] = notifications

return config.SaveUserConfig(userPath, overrides)
}