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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ For details about specific commands, use the `--help` flag.
The CLI supports workspace configuration to avoid repeatedly specifying the project name. When you run a command, the CLI will:

1. Check if a project name is provided via command-line flag
2. If not, look for a `stainless-workspace.json` file in the current directory or any parent directory
2. If not, look for a `.stainless/workspace.json` file in the current directory or any parent directory
3. Use the project name from the workspace configuration if found

### Initializing a Workspace
Expand Down
41 changes: 29 additions & 12 deletions pkg/cmd/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@ import (
"github.com/stainless-api/stainless-api-go/option"
"github.com/tidwall/gjson"
"github.com/urfave/cli/v3"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)

const defaultClientID = "stl_client_0001u04Vo1IWoSe0Mwinw2SVuuO3hTkvL"

var authLogin = cli.Command{
Name: "login",
Usage: "Authenticate with Stainless API",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "client-id",
Value: "stl_client_0001u04Vo1IWoSe0Mwinw2SVuuO3hTkvL",
Value: defaultClientID,
Usage: "OAuth client ID",
},
},
Expand All @@ -47,13 +51,9 @@ var authStatus = cli.Command{
HideHelpCommand: true,
}

func handleAuthLogin(ctx context.Context, cmd *cli.Command) error {
cc := getAPICommandContext(cmd)
clientID := cmd.String("client-id")
scope := "openapi:read project:write project:read"
authResult, err := startDeviceFlow(ctx, cmd, cc.client, clientID, scope)
if err != nil {
return err
func authenticate(ctx context.Context, cmd *cli.Command, forceAuthentication bool) error {
if apiKey := os.Getenv("STAINLESS_API_KEY"); apiKey != "" && !forceAuthentication {
return nil
}

config, err := NewAuthConfig()
Expand All @@ -62,6 +62,20 @@ func handleAuthLogin(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("authentication failed")
}

if !forceAuthentication {
if found, err := config.Find(); err == nil && found && config.AccessToken != "" {
return nil
}
}

cc := getAPICommandContext(cmd)
clientID := cmd.String("client-id")
scope := "openapi:read project:write project:read"
authResult, err := startDeviceFlow(ctx, cmd, cc.client, clientID, scope)
if err != nil {
return err
}

config.AccessToken = authResult.AccessToken
config.RefreshToken = authResult.RefreshToken
config.TokenType = authResult.TokenType
Expand All @@ -70,10 +84,14 @@ func handleAuthLogin(ctx context.Context, cmd *cli.Command) error {
Error("Failed to save authentication: %v", err)
return fmt.Errorf("authentication failed")
}
Success("Authentication successful! Your credentials have been saved to " + config.ConfigPath)
Success("Authentication successful! Your credentials have been saved to %s", config.ConfigPath)
return nil
}

func handleAuthLogin(ctx context.Context, cmd *cli.Command) error {
return authenticate(ctx, cmd, true)
}

func handleAuthLogout(ctx context.Context, cmd *cli.Command) error {
config := &AuthConfig{}
found, err := config.Find()
Expand Down Expand Up @@ -153,8 +171,7 @@ func startDeviceFlow(ctx context.Context, cmd *cli.Command, client stainless.Cli
ok, _, err := group.Confirm(cmd, "browser", "Open browser?", "", true)
if err != nil {
return nil, err
}
if ok {
} else if ok {
if err := browser.OpenURL(deviceResponse.VerificationURIComplete); err == nil {
group.Info("Opening browser...")
} else {
Expand Down Expand Up @@ -244,7 +261,7 @@ func getDeviceName() string {
case "linux":
osName = "Linux"
default:
osName = strings.Title(osName)
osName = cases.Title(language.English).String(osName)
}

if username != "" {
Expand Down
20 changes: 10 additions & 10 deletions pkg/cmd/authconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,17 @@ func ConfigDir() (string, error) {
// Returns (false, nil) if config file doesn't exist or is empty (not an error).
// Returns (false, error) if config file exists but failed to load due to an error.
func (config *AuthConfig) Find() (bool, error) {
if config.ConfigPath != "" {
return true, nil
}
if config.ConfigPath == "" {
configDir, err := ConfigDir()
if err != nil {
return false, fmt.Errorf("failed to get config directory: %w", err)
}

configDir, err := ConfigDir()
if err != nil {
return false, fmt.Errorf("failed to get config directory: %w", err)
config.ConfigPath = filepath.Join(configDir, "auth.json")
}

configPath := filepath.Join(configDir, "auth.json")
if _, err := os.Stat(configPath); err == nil {
if _, err := os.Stat(config.ConfigPath); err == nil {
// Config file exists, attempt to load it
err := config.Load(configPath)
err := config.Load(config.ConfigPath)
if err != nil {
return false, err
}
Expand All @@ -71,6 +69,7 @@ func (config *AuthConfig) Load(configPath string) error {
if err != nil {
if os.IsNotExist(err) {
// File doesn't exist - this is not an error, just means no auth config
fmt.Println("No config file!")
return nil
}
return fmt.Errorf("failed to open auth config file %s: %w", configPath, err)
Expand All @@ -84,6 +83,7 @@ func (config *AuthConfig) Load(configPath string) error {
}
if info.Size() == 0 {
// File exists but is empty - this is not an error, treat as no auth config
fmt.Println("Empty config file!")
return nil
}

Expand Down
8 changes: 5 additions & 3 deletions pkg/cmd/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,24 @@ func GetFormTheme(indent int) *huh.Theme {
t := huh.ThemeBase()

grayBright := lipgloss.Color("251")
gray := lipgloss.Color("8")
gray := lipgloss.Color("243")
primary := lipgloss.Color("6")
primaryBright := lipgloss.Color("14")
error := lipgloss.Color("1")

t.Form.Base = t.Form.Base.PaddingLeft(indent * 2)
t.Group.Title = t.Group.Title.Foreground(gray).PaddingBottom(1)
t.Group.Title = t.Group.Title.Foreground(primary).PaddingBottom(1)
t.Group.Description = t.Group.Description.Foreground(gray)

t.Focused.Title = t.Focused.Title.Bold(true)
t.Focused.Title = t.Focused.Title.Foreground(primary).Bold(true)
t.Focused.Base = t.Focused.Base.
BorderLeft(false).
SetString("\b\b" + lipgloss.NewStyle().Foreground(primaryBright).Render("✱")).
PaddingLeft(2)
t.Focused.Description = t.Focused.Description.Foreground(gray).Width(70)
t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(gray)
t.Focused.SelectedPrefix = lipgloss.NewStyle().SetString("[✓] ")
t.Focused.SelectedOption = lipgloss.NewStyle().Foreground(lipgloss.Color("75"))

t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(error)
t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(error)
Expand Down
Loading