Skip to content

Commit 723bc51

Browse files
areinclaude
andcommitted
Add optional sample query to dune auth onboarding
After saving the API key, prompt the user to run a sample DuneSQL query (latest 5 Ethereum blocks) to verify their setup works. - Only triggers in interactive terminals; skips silently in CI/scripts - Defaults to yes ([Y/n]) for frictionless onboarding - Uses a shared bufio.Reader for both the key and confirmation prompts to avoid stdin buffering issues - Sample query failure is non-fatal (prints to stderr, doesn't fail auth) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1df3c1d commit 723bc51

1 file changed

Lines changed: 81 additions & 7 deletions

File tree

cmd/auth/auth.go

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,29 @@ package auth
22

33
import (
44
"bufio"
5+
"errors"
56
"fmt"
67
"os"
78
"strings"
9+
"time"
810

911
"github.com/duneanalytics/cli/authconfig"
12+
"github.com/duneanalytics/cli/output"
13+
"github.com/duneanalytics/duneapi-client-go/config"
14+
"github.com/duneanalytics/duneapi-client-go/dune"
15+
"github.com/duneanalytics/duneapi-client-go/models"
1016
"github.com/spf13/cobra"
1117
"golang.org/x/term"
1218
)
1319

20+
const (
21+
queryStateCompleted = "QUERY_STATE_COMPLETED"
22+
23+
// sampleQueryMaxRetries is the error-retry ceiling for the sample query poll loop.
24+
// Matches the CLI default: 300s timeout / 2s poll interval = 150.
25+
sampleQueryMaxRetries = 150
26+
)
27+
1428
// NewAuthCmd returns the `auth` command.
1529
func NewAuthCmd() *cobra.Command {
1630
return &cobra.Command{
@@ -29,20 +43,20 @@ func runAuth(cmd *cobra.Command, _ []string) error {
2943
key = os.Getenv("DUNE_API_KEY")
3044
}
3145

46+
// stdinReader is shared across all interactive prompts so a single
47+
// buffered reader is used for the underlying stream.
48+
var stdinReader *bufio.Reader
49+
3250
if key == "" {
3351
if stdin, ok := cmd.InOrStdin().(*os.File); ok && !term.IsTerminal(int(stdin.Fd())) {
3452
return fmt.Errorf("no API key provided; pass --api-key, set DUNE_API_KEY, or run dune auth in an interactive terminal")
3553
}
3654

37-
var (
38-
reader = cmd.InOrStdin()
39-
)
55+
stdinReader = bufio.NewReader(cmd.InOrStdin())
4056

4157
fmt.Fprint(cmd.ErrOrStderr(), "Enter your Dune API key: ")
42-
scanner := bufio.NewScanner(reader)
43-
if scanner.Scan() {
44-
key = strings.TrimSpace(scanner.Text())
45-
}
58+
line, _ := stdinReader.ReadString('\n')
59+
key = strings.TrimSpace(line)
4660
}
4761

4862
if key == "" {
@@ -63,5 +77,65 @@ func runAuth(cmd *cobra.Command, _ []string) error {
6377

6478
p, _ := authconfig.Path()
6579
fmt.Fprintf(cmd.OutOrStdout(), "API key saved to %s\n", p)
80+
81+
if err := maybeSampleQuery(cmd, key, stdinReader); err != nil {
82+
fmt.Fprintf(cmd.ErrOrStderr(), "Sample query failed: %v\n", err)
83+
}
84+
85+
return nil
86+
}
87+
88+
const sampleSQL = "SELECT number, time, gas_used, base_fee_per_gas FROM ethereum.blocks ORDER BY number DESC LIMIT 5"
89+
90+
func maybeSampleQuery(cmd *cobra.Command, apiKey string, reader *bufio.Reader) error {
91+
// Only prompt in interactive terminals.
92+
if reader == nil {
93+
// Non-interactive path (key was provided via flag or env var).
94+
// Create a reader to check for a terminal; if not interactive, skip.
95+
stdin, ok := cmd.InOrStdin().(*os.File)
96+
if !ok || !term.IsTerminal(int(stdin.Fd())) {
97+
return nil
98+
}
99+
reader = bufio.NewReader(cmd.InOrStdin())
100+
}
101+
102+
fmt.Fprint(cmd.ErrOrStderr(), "\nWould you like to run a sample query to verify your setup? [Y/n] ")
103+
line, _ := reader.ReadString('\n')
104+
answer := strings.TrimSpace(strings.ToLower(line))
105+
if answer != "" && answer != "y" && answer != "yes" {
106+
return nil
107+
}
108+
109+
fmt.Fprintf(cmd.ErrOrStderr(), "\nRunning: %s\n\n", sampleSQL)
110+
111+
client := dune.NewDuneClient(config.FromAPIKey(apiKey))
112+
exec, err := client.RunSQL(models.ExecuteSQLRequest{
113+
SQL: sampleSQL,
114+
Performance: "medium",
115+
})
116+
if err != nil {
117+
return fmt.Errorf("executing query: %w", err)
118+
}
119+
120+
resp, err := exec.WaitGetResults(2*time.Second, sampleQueryMaxRetries)
121+
if err != nil {
122+
return fmt.Errorf("waiting for results: %w", err)
123+
}
124+
125+
if resp.State != queryStateCompleted {
126+
msg := fmt.Sprintf("query finished with state %s", resp.State)
127+
if resp.Error != nil {
128+
msg += fmt.Sprintf(": %s", resp.Error.Message)
129+
}
130+
return errors.New(msg)
131+
}
132+
133+
w := cmd.OutOrStdout()
134+
columns := resp.Result.Metadata.ColumnNames
135+
rows := output.ResultRowsToStrings(resp.Result.Rows, columns)
136+
output.PrintTable(w, columns, rows)
137+
fmt.Fprintf(w, "\n%d rows\n", len(resp.Result.Rows))
138+
fmt.Fprintln(w, "\nYou're all set! Run `dune query run-sql --sql \"YOUR SQL\"` to execute your own queries.")
139+
66140
return nil
67141
}

0 commit comments

Comments
 (0)