diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b069996..08e82c4 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.14" + ".": "0.1.0-alpha.15" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index dda9ad4..9836b19 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/stainless%2Fstainless-v0-345f2d483473fda78e89643698c6084bbd6fe9b565044aa79ad1b21e6bdf4e09.yml -openapi_spec_hash: 6780cb3fbff80ca1791cac9f073019ca +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/stainless%2Fstainless-v0-ddcc03297d5649ca543ccff5be72ca1c148696cdddf63cb640e3f8a9b86ab59e.yml +openapi_spec_hash: 2870606e51060e9080104b1089f28b83 config_hash: 5fc708f77aa1d07b7376eb5cbb78f389 diff --git a/CHANGELOG.md b/CHANGELOG.md index b08a84a..1749e3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.1.0-alpha.15 (2025-06-18) + +Full Changelog: [v0.1.0-alpha.14...v0.1.0-alpha.15](https://github.com/stainless-api/stainless-api-cli/compare/v0.1.0-alpha.14...v0.1.0-alpha.15) + +### Features + +* add workspace status command ([adbc44f](https://github.com/stainless-api/stainless-api-cli/commit/adbc44f0539114437690008d87f16fa4a3ce0a4d)) +* make branch parameter required for builds create ([0735363](https://github.com/stainless-api/stainless-api-cli/commit/07353638344ae98aaeff14e107730945ea3ff6ab)) +* polish around logging ([e969a2d](https://github.com/stainless-api/stainless-api-cli/commit/e969a2dc7356dd4b9b0864f963a582dfbed43344)) +* polish around logging ([a3fbc71](https://github.com/stainless-api/stainless-api-cli/commit/a3fbc71f38a195885b405aad8bec0c423694d692)) + + +### Chores + +* **internal:** codegen related update ([2dd834f](https://github.com/stainless-api/stainless-api-cli/commit/2dd834fed74e180a2ea70a4b8edf0bbb84a76d25)) + ## 0.1.0-alpha.14 (2025-06-17) Full Changelog: [v0.1.0-alpha.13...v0.1.0-alpha.14](https://github.com/stainless-api/stainless-api-cli/compare/v0.1.0-alpha.13...v0.1.0-alpha.14) diff --git a/README.md b/README.md index b112a37..786bdb2 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,11 @@ stl builds create [--allow-empty] [--project ] For details about specific commands, use the `--help` flag. +## Global Flags + +- `--debug` - Enable debug logging (includes HTTP request/response details) +- `--version`, `-v` - Show the CLI version + ## Workspace Configuration The CLI supports workspace configuration to avoid repeatedly specifying the project name. When you run a command, the CLI will: diff --git a/pkg/cmd/auth.go b/pkg/cmd/auth.go index d3ba0b4..5d086b5 100644 --- a/pkg/cmd/auth.go +++ b/pkg/cmd/auth.go @@ -56,9 +56,10 @@ func handleAuthLogin(ctx context.Context, cmd *cli.Command) error { return err } if err := SaveAuthConfig(config); err != nil { - return fmt.Errorf("%s", au.Red(fmt.Sprintf("Failed to save authentication: %v", err))) + Error("Failed to save authentication: %v", err) + return fmt.Errorf("authentication failed") } - fmt.Printf("%s %s\n", au.BrightGreen("✱"), "Authentication successful! Your credentials have been saved.") + Success("Authentication successful! Your credentials have been saved.") return nil } @@ -134,7 +135,7 @@ func handleAuthLogout(ctx context.Context, cmd *cli.Command) error { configPath := filepath.Join(configDir, "auth.json") if _, err := os.Stat(configPath); os.IsNotExist(err) { - fmt.Println(au.BrightYellow("No active session found.")) + Warn("No active session found.") return nil } @@ -142,14 +143,14 @@ func handleAuthLogout(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("failed to remove auth file: %v", err) } - fmt.Printf("%s %s\n", au.BrightGreen("✱"), "Successfully logged out.") + Success("Successfully logged out.") return nil } func handleAuthStatus(ctx context.Context, cmd *cli.Command) error { // Check for API key in environment variables first if apiKey := os.Getenv("STAINLESS_API_KEY"); apiKey != "" { - fmt.Printf("%s %s\n", au.BrightGreen("✱"), "Authenticated via STAINLESS_API_KEY environment variable") + Success("Authenticated via STAINLESS_API_KEY environment variable") return nil } @@ -160,17 +161,17 @@ func handleAuthStatus(ctx context.Context, cmd *cli.Command) error { } if config == nil { - fmt.Printf("%s %s\n", au.BrightYellow("✱"), "Not logged in.") + Warn("Not logged in.") return nil } // If we have a config file with a token - fmt.Printf("%s %s\n", au.BrightGreen("✱"), "Authenticated via saved credentials") + group := Success("Authenticated via saved credentials") // Show a truncated version of the token for verification if len(config.AccessToken) > 10 { truncatedToken := config.AccessToken[:5] + "..." + config.AccessToken[len(config.AccessToken)-5:] - fmt.Printf("Token: %s\n", truncatedToken) + group.Property("token", truncatedToken) } return nil @@ -217,13 +218,13 @@ func StartDeviceFlow(clientID, scope string) (*AuthConfig, error) { } if err := browser.OpenURL(deviceResponse.VerificationURIComplete); err != nil { - fmt.Println() - fmt.Printf("To authenticate, visit %s\n", au.Hyperlink(deviceResponse.VerificationURI, deviceResponse.VerificationURI)) - fmt.Printf("and enter this code %s\n", au.Bold(deviceResponse.UserCode)) - fmt.Println() - fmt.Printf("Or navigate to this URL %s\n", au.Hyperlink(deviceResponse.VerificationURIComplete, deviceResponse.VerificationURIComplete)) + group := Info("To authenticate, visit the verification URL") + group.Property("url", deviceResponse.VerificationURI) + group.Property("code", deviceResponse.UserCode) + group.Property("direct_url", deviceResponse.VerificationURIComplete) } else { - fmt.Printf("Browser opened to %s\n", au.Hyperlink(deviceResponse.VerificationURIComplete, deviceResponse.VerificationURIComplete)) + group := Info("Browser opened") + group.Property("url", deviceResponse.VerificationURIComplete) } return pollForToken( @@ -242,7 +243,7 @@ func pollForToken(clientID, deviceCode string, interval, expiresIn int) (*AuthCo deadline := time.Now().Add(time.Duration(expiresIn) * time.Second) pollInterval := time.Duration(interval) * time.Second - fmt.Println(au.BrightBlack("Waiting for authentication to complete...")) + Progress("Waiting for authentication to complete...") for time.Now().Before(deadline) { time.Sleep(pollInterval) @@ -319,9 +320,10 @@ func getClientOptions() []option.RequestOption { } // Add default project from workspace config if available - projectName := GetProjectNameFromConfig() - if projectName != "" { - options = append(options, option.WithProject(projectName)) + var workspaceConfig WorkspaceConfig + found, err := workspaceConfig.Find() + if err == nil && found && workspaceConfig.Project != "" { + options = append(options, option.WithProject(workspaceConfig.Project)) } return options diff --git a/pkg/cmd/build.go b/pkg/cmd/build.go index d334a0b..c18e4b8 100644 --- a/pkg/cmd/build.go +++ b/pkg/cmd/build.go @@ -148,6 +148,7 @@ var buildsCreate = cli.Command{ Kind: jsonflag.Body, Path: "branch", }, + Required: true, }, &jsonflag.JSONStringFlag{ Name: "commit-message", @@ -308,7 +309,7 @@ func handleBuildsCreate(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } - fmt.Fprintf(os.Stderr, "%s Creating build...\n", au.BrightCyan("✱")) + Progress("Creating build...") params := stainlessv0.BuildNewParams{} res, err := cc.client.Builds.New( context.TODO(), @@ -320,10 +321,11 @@ func handleBuildsCreate(ctx context.Context, cmd *cli.Command) error { } // Print the build ID to stderr - fmt.Fprintf(os.Stderr, " %s Build created: %s\n", au.BrightGreen("•"), au.Bold(res.ID)) + buildGroup := Success("Build created") + buildGroup.Property("build_id", res.ID) if cmd.Bool("wait") { - fmt.Fprintf(os.Stderr, "%s Waiting for build to complete...\n", au.BrightCyan("✱")) + waitGroup := Progress("Waiting for build to complete...") ticker := time.NewTicker(3 * time.Second) defer ticker.Stop() @@ -359,17 +361,11 @@ func handleBuildsCreate(ctx context.Context, cmd *cli.Command) error { // Only print completed statuses with a green checkmark if isTargetCompleted(target.status) { - fmt.Fprintf(os.Stderr, " %s Target %s: %s\n", - au.BrightGreen("•"), - target.name, - string(target.status)) + waitGroup.Success("Target %s: %s", target.name, string(target.status)) anyCompleted = true } else if target.status == "failed" { // For failures, use red text - fmt.Fprintf(os.Stderr, " %s Target %s: %s\n", - au.BrightRed("•"), - target.name, - au.BrightRed(string(target.status))) + waitGroup.Error("Target %s: %s", target.name, string(target.status)) } // Don't print in-progress status updates } @@ -381,7 +377,7 @@ func handleBuildsCreate(ctx context.Context, cmd *cli.Command) error { if (allCompleted || anyCompleted) && len(targets) > 0 { if allCompleted { - fmt.Fprintf(os.Stderr, " %s Build completed successfully\n", au.BrightGreen("✱")) + waitGroup.Success("Build completed successfully") break loop } } @@ -392,11 +388,11 @@ func handleBuildsCreate(ctx context.Context, cmd *cli.Command) error { } if cmd.Bool("pull") { - fmt.Fprintf(os.Stderr, "%s Pulling build outputs...\n", au.BrightCyan("✱")) + pullGroup := Progress("Pulling build outputs...") if err := pullBuildOutputs(context.TODO(), cc.client, *res); err != nil { - fmt.Fprintf(os.Stderr, "%s Failed to pull outputs: %v\n", au.BrightRed("✱"), err) + pullGroup.Error("Failed to pull outputs: %v", err) } else { - fmt.Fprintf(os.Stderr, "%s Successfully pulled all outputs\n", au.BrightGreen("✱")) + pullGroup.Success("Successfully pulled all outputs") } } } @@ -441,8 +437,7 @@ func pullBuildOutputs(ctx context.Context, client stainlessv0.Client, res stainl for i, target := range targets { targetDir := fmt.Sprintf("%s-%s", res.Project, target) - fmt.Fprintf(os.Stderr, "%s [%d/%d] Pulling %s → %s\n", - au.BrightCyan("✱"), i+1, len(targets), au.Bold(target), au.Cyan(targetDir)) + targetGroup := Progress("[%d/%d] Pulling %s → %s", i+1, len(targets), target, targetDir) // Get the output details outputRes, err := client.Builds.TargetOutputs.Get( @@ -455,21 +450,18 @@ func pullBuildOutputs(ctx context.Context, client stainlessv0.Client, res stainl }, ) if err != nil { - fmt.Fprintf(os.Stderr, "%s Failed to get output details for %s: %v\n", - au.BrightRed("✱"), target, err) + targetGroup.Error("Failed to get output details for %s: %v", target, err) continue } // Handle based on output type err = pullOutput(outputRes.Output, outputRes.URL, outputRes.Ref, targetDir) if err != nil { - fmt.Fprintf(os.Stderr, "%s Failed to pull %s: %v\n", - au.BrightRed("✱"), target, err) + targetGroup.Error("Failed to pull %s: %v", target, err) continue } - fmt.Fprintf(os.Stderr, " %s Successfully pulled to %s\n", - au.BrightBlack("•"), au.Cyan(targetDir)) + targetGroup.Success("Successfully pulled to %s", targetDir) if i < len(targets)-1 { fmt.Fprintf(os.Stderr, "\n") @@ -483,7 +475,7 @@ func pullBuildOutputs(ctx context.Context, client stainlessv0.Client, res stainl func pullOutput(output, url, ref, targetDir string) error { // Remove existing directory if it exists if _, err := os.Stat(targetDir); err == nil { - fmt.Fprintf(os.Stderr, " %s Removing existing directory %s\n", au.BrightBlack("•"), targetDir) + Info("Removing existing directory %s", targetDir) if err := os.RemoveAll(targetDir); err != nil { return fmt.Errorf("failed to remove existing directory %s: %v", targetDir, err) } @@ -497,8 +489,8 @@ func pullOutput(output, url, ref, targetDir string) error { switch output { case "git": // Clone the repository - fmt.Fprintf(os.Stderr, " %s Cloning repository\n", au.BrightBlack("•")) - fmt.Fprintf(os.Stderr, " %s Checking out ref %s\n", au.BrightBlack("•"), au.Bold(ref)) + gitGroup := Info("Cloning repository") + gitGroup.Property("ref", ref) cmd := exec.Command("git", "clone", url, targetDir) var stderr bytes.Buffer @@ -519,8 +511,9 @@ func pullOutput(output, url, ref, targetDir string) error { case "url": // Download the tar file - fmt.Fprintf(os.Stderr, " %s Downloading archive %s\n", au.BrightBlack("•"), au.Underline(url)) - fmt.Fprintf(os.Stderr, " %s Extracting to %s\n", au.BrightBlack("•"), targetDir) + downloadGroup := Info("Downloading archive") + downloadGroup.Property("url", url) + downloadGroup.Property("target", targetDir) // Create a temporary file for the tar download tmpFile, err := os.CreateTemp("", "stainless-*.tar.gz") @@ -598,10 +591,11 @@ func handleBuildsCompare(ctx context.Context, cmd *cli.Command) error { // getAPICommandWithWorkspaceDefaults applies workspace defaults before initializing API command func getAPICommandContextWithWorkspaceDefaults(cmd *cli.Command) (*apiCommandContext, error) { cc := getAPICommandContext(cmd) - config, configPath, err := FindWorkspaceConfig() - if err == nil && config != nil { + var config WorkspaceConfig + found, err := config.Find() + if err == nil && found { // Get the directory containing the workspace config file - configDir := filepath.Dir(configPath) + configDir := filepath.Dir(config.ConfigPath) if !cmd.IsSet("openapi-spec") && !cmd.IsSet("oas") && config.OpenAPISpec != "" { // Resolve OpenAPI spec path relative to workspace config directory diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index b9c208c..818e942 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -7,8 +7,9 @@ import ( ) var Command = cli.Command{ - Name: "stl", - Usage: "CLI for the stainless API", + Name: "stl", + Usage: "CLI for the stainless API", + Version: Version, Flags: []cli.Flag{ &cli.BoolFlag{ Name: "debug", @@ -28,7 +29,8 @@ var Command = cli.Command{ { Name: "workspace", Commands: []*cli.Command{ - &initWorkspaceCommand, + &workspaceInit, + &workspaceStatus, }, }, diff --git a/pkg/cmd/form.go b/pkg/cmd/form.go new file mode 100644 index 0000000..8b8898f --- /dev/null +++ b/pkg/cmd/form.go @@ -0,0 +1,60 @@ +package cmd + +import ( + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" +) + +// GetFormTheme returns the standard huh theme used across all forms +func GetFormTheme() *huh.Theme { + t := huh.ThemeBase() + + grayBright := lipgloss.Color("251") + gray := lipgloss.Color("8") + primary := lipgloss.Color("6") + primaryBright := lipgloss.Color("14") + error := lipgloss.Color("1") + + t.Group.Title = t.Group.Title.Foreground(gray).PaddingBottom(1) + + t.Focused.Title = t.Focused.Title.Foreground(primaryBright).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) + t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(gray) + + t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(error) + t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(error) + + t.Blurred.Title = t.Blurred.Title.Foreground(primary).Bold(true) + t.Blurred.Base = t.Blurred.Base. + Foreground(grayBright). + BorderLeft(false). + SetString("\b\b" + lipgloss.NewStyle().Foreground(primary).Render("✱")). + PaddingLeft(2) + t.Blurred.Description = t.Blurred.Description.Foreground(gray) + t.Blurred.TextInput.Placeholder = t.Blurred.TextInput.Placeholder.Foreground(gray) + + return t +} + +// GetFormKeyMap returns the standard huh keymap used across all forms +func GetFormKeyMap() *huh.KeyMap { + keyMap := huh.NewDefaultKeyMap() + keyMap.Input.AcceptSuggestion = key.NewBinding( + key.WithKeys("tab"), + key.WithHelp("tab", "complete"), + ) + keyMap.Input.Next = key.NewBinding( + key.WithKeys("tab", "down", "enter"), + key.WithHelp("enter", "next"), + ) + keyMap.Input.Prev = key.NewBinding( + key.WithKeys("shift+tab", "up"), + key.WithHelp("shift+tab", "back"), + ) + return keyMap +} diff --git a/pkg/cmd/form_theme.go b/pkg/cmd/form_theme.go deleted file mode 100644 index c024244..0000000 --- a/pkg/cmd/form_theme.go +++ /dev/null @@ -1,43 +0,0 @@ -package cmd - -import ( - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/huh" - "github.com/charmbracelet/lipgloss" -) - -// GetFormTheme returns the standard huh theme used across all forms -func GetFormTheme() *huh.Theme { - theme := huh.ThemeBase() - theme.Group.Title = theme.Group.Title.Foreground(lipgloss.Color("8")).PaddingBottom(1) - - theme.Focused.Title = theme.Focused.Title.Foreground(lipgloss.Color("6")).Bold(true) - theme.Focused.Base = theme.Focused.Base.BorderForeground(lipgloss.Color("240")) - theme.Focused.Description = theme.Focused.Description.Foreground(lipgloss.Color("8")) - theme.Focused.TextInput.Placeholder = theme.Focused.TextInput.Placeholder.Foreground(lipgloss.Color("8")) - - theme.Blurred.Title = theme.Blurred.Title.Foreground(lipgloss.Color("6")).Bold(true) - theme.Blurred.Base = theme.Blurred.Base.Foreground(lipgloss.Color("251")) - theme.Blurred.Description = theme.Blurred.Description.Foreground(lipgloss.Color("8")) - theme.Blurred.TextInput.Placeholder = theme.Blurred.TextInput.Placeholder.Foreground(lipgloss.Color("8")) - - return theme -} - -// GetFormKeyMap returns the standard huh keymap used across all forms -func GetFormKeyMap() *huh.KeyMap { - keyMap := huh.NewDefaultKeyMap() - keyMap.Input.AcceptSuggestion = key.NewBinding( - key.WithKeys("tab"), - key.WithHelp("tab", "complete"), - ) - keyMap.Input.Next = key.NewBinding( - key.WithKeys("tab", "down", "enter"), - key.WithHelp("tab/↓/enter", "next"), - ) - keyMap.Input.Prev = key.NewBinding( - key.WithKeys("shift+tab", "up"), - key.WithHelp("shift+tab/↑", "previous"), - ) - return keyMap -} diff --git a/pkg/cmd/print.go b/pkg/cmd/print.go new file mode 100644 index 0000000..14b8f54 --- /dev/null +++ b/pkg/cmd/print.go @@ -0,0 +1,84 @@ +package cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/logrusorgru/aurora/v4" +) + +// Group represents a nested logging group +type Group struct { + prefix string + indent int +} + +func Info(format string, args ...any) Group { + return Group{}.Info(format, args...) +} + +func Property(key, msg string) Group { + return Group{}.Property(key, msg) +} + +func Progress(format string, args ...any) Group { + return Group{}.Progress(format, args...) +} + +func Error(format string, args ...any) Group { + return Group{}.Error(format, args...) +} + +func Warn(format string, args ...any) Group { + return Group{}.Warn(format, args...) +} + +func Success(format string, args ...any) Group { + return Group{}.Success(format, args...) +} + +func Spacer() { + fmt.Fprintf(os.Stderr, "\n") +} + +func (g Group) Info(format string, args ...any) Group { + indentStr := strings.Repeat(" ", g.indent) + msg := fmt.Sprintf(format, args...) + fmt.Fprintf(os.Stderr, "%s%s %s\n", indentStr, aurora.BrightBlue("✱"), msg) + return Group{prefix: "i", indent: g.indent + 1} +} + +func (g Group) Property(key, msg string) Group { + indentStr := strings.Repeat(" ", g.indent) + fmt.Fprintf(os.Stderr, "%s%s %s %s\n", indentStr, aurora.Cyan("✱"), aurora.Cyan(key), msg) + return Group{prefix: "✱", indent: g.indent + 1} +} + +func (g Group) Progress(format string, args ...any) Group { + indentStr := strings.Repeat(" ", g.indent) + msg := fmt.Sprintf(format, args...) + fmt.Fprintf(os.Stderr, "%s%s %s\n", indentStr, aurora.Bold("✱"), msg) + return Group{prefix: "✱", indent: g.indent + 1} +} + +func (g Group) Error(format string, args ...any) Group { + indentStr := strings.Repeat(" ", g.indent) + msg := fmt.Sprintf(format, args...) + fmt.Fprintf(os.Stderr, "%s%s %s\n", indentStr, aurora.BrightRed("✗"), msg) + return Group{prefix: "✗", indent: g.indent + 1} +} + +func (g Group) Warn(format string, args ...any) Group { + indentStr := strings.Repeat(" ", g.indent) + msg := fmt.Sprintf(format, args...) + fmt.Fprintf(os.Stderr, "%s%s %s\n", indentStr, aurora.BrightYellow("!"), msg) + return Group{prefix: "!", indent: g.indent + 1} +} + +func (g Group) Success(format string, args ...any) Group { + indentStr := strings.Repeat(" ", g.indent) + msg := fmt.Sprintf(format, args...) + fmt.Fprintf(os.Stderr, "%s%s %s\n", indentStr, aurora.BrightGreen("✓"), msg) + return Group{prefix: "✓", indent: g.indent + 1} +} diff --git a/pkg/cmd/project.go b/pkg/cmd/project.go index 2c3f478..2810928 100644 --- a/pkg/cmd/project.go +++ b/pkg/cmd/project.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/charmbracelet/huh" - "github.com/logrusorgru/aurora/v4" "github.com/stainless-api/stainless-api-cli/pkg/jsonflag" "github.com/stainless-api/stainless-api-go" "github.com/stainless-api/stainless-api-go/option" @@ -182,8 +181,7 @@ func handleProjectsCreate(ctx context.Context, cmd *cli.Command) error { allValuesProvided := org != "" && projectName != "" if !allValuesProvided { - fmt.Println("Creating a new project...") - fmt.Println() + Info("Creating a new project...") // Fetch available organizations for suggestions orgs := fetchUserOrgs(ctx) @@ -196,7 +194,7 @@ func handleProjectsCreate(ctx context.Context, cmd *cli.Command) error { form := huh.NewForm( huh.NewGroup( huh.NewInput(). - Title("organization"). + Title("org"). Value(&org). Suggestions(orgs). Description("Enter the organization for this project"). @@ -207,7 +205,7 @@ func handleProjectsCreate(ctx context.Context, cmd *cli.Command) error { return nil }), huh.NewInput(). - Title("project name"). + Title("project"). Value(&projectName). DescriptionFunc(func() string { if projectName == "" { @@ -230,23 +228,12 @@ func handleProjectsCreate(ctx context.Context, cmd *cli.Command) error { Description("Select target languages for code generation"). Options(availableTargets...). Value(&selectedTargets), - huh.NewInput(). - Title("OpenAPI spec path"). + huh.NewFilePicker(). + Title("openapi_spec"). Description("Relative path to your OpenAPI spec file"). - Placeholder("openapi.yml"). - Validate(func(s string) error { - s = strings.TrimSpace(s) - if s == "" { - return fmt.Errorf("openapi spec is required") - } - if _, err := os.Stat(s); os.IsNotExist(err) { - return fmt.Errorf("file '%s' does not exist", s) - } - return nil - }). Value(&openAPISpec), huh.NewInput(). - Title("Stainless config path (optional)"). + Title("stainless_config (optional)"). Description("Relative path to your Stainless config file"). Placeholder("openapi.stainless.yml"). Validate(func(s string) error { @@ -270,19 +257,18 @@ func handleProjectsCreate(ctx context.Context, cmd *cli.Command) error { // Generate slug from project name slug := nameToSlug(projectName) - fmt.Printf("%s organization: %s\n", aurora.Bold("✱"), org) - fmt.Printf("%s project name: %s\n", aurora.Bold("✱"), projectName) - fmt.Printf("%s slug: %s\n", aurora.Bold("✱"), slug) + Property("organization", org) + Property("project_name", projectName) + Property("slug", slug) if len(selectedTargets) > 0 { - fmt.Printf("%s targets: %s\n", aurora.Bold("✱"), strings.Join(selectedTargets, ", ")) + Property("targets", strings.Join(selectedTargets, ", ")) } if openAPISpec != "" { - fmt.Printf("%s openapi spec: %s\n", aurora.Bold("✱"), openAPISpec) + Property("openapi_spec", openAPISpec) } if stainlessConfig != "" { - fmt.Printf("%s stainless config: %s\n", aurora.Bold("✱"), stainlessConfig) + Property("stainless_config", stainlessConfig) } - fmt.Println() // Set the flag values so the JSONFlag middleware can pick them up cmd.Set("org", org) @@ -333,24 +319,29 @@ func handleProjectsCreate(ctx context.Context, cmd *cli.Command) error { } if !allValuesProvided { - fmt.Printf("%s %s\n", aurora.BrightGreen("✱"), fmt.Sprintf("Project created successfully")) + Success("Project created successfully") } fmt.Printf("%s\n", ColorizeJSON(res.RawJSON(), os.Stdout)) // Initialize workspace if requested if initWorkspace { - fmt.Println() - fmt.Printf("Initializing workspace configuration...\n") + Info("Initializing workspace configuration...") // Use the same project name (slug) for workspace initialization slug := nameToSlug(projectName) - err := InitWorkspaceConfig(slug, openAPISpec, stainlessConfig) + config, err := NewWorkspaceConfig(slug, openAPISpec, stainlessConfig) + if err != nil { + Error("Failed to create workspace config: %v", err) + return fmt.Errorf("project created but workspace initialization failed: %v", err) + } + + err = config.Save() if err != nil { - fmt.Printf("%s Failed to initialize workspace: %v\n", aurora.BrightRed("✱"), err) + Error("Failed to save workspace config: %v", err) return fmt.Errorf("project created but workspace initialization failed: %v", err) } - fmt.Printf("%s %s\n", aurora.BrightGreen("✱"), fmt.Sprintf("Workspace initialized")) + Success("Workspace initialized") } return nil diff --git a/pkg/cmd/util.go b/pkg/cmd/util.go index c5fb3ad..63224d0 100644 --- a/pkg/cmd/util.go +++ b/pkg/cmd/util.go @@ -265,15 +265,18 @@ func GetProjectName(cmd *cli.Command, flagName string) string { } // Otherwise, try to get from workspace config - configProjectName := GetProjectNameFromConfig() - if configProjectName != "" { + var config WorkspaceConfig + found, err := config.Find() + if err == nil && found && config.Project != "" { // Log that we're using the workspace config if in interactive mode if isTerminal(os.Stdout) { - fmt.Printf("%s %s\n", au.BrightBlue("i"), fmt.Sprintf("Using project '%s' from workspace config", configProjectName)) + group := Info("Using project from workspace config") + group.Property("project", config.Project) } + return config.Project } - return configProjectName + return "" } // CheckInteractiveAndInitWorkspace checks if running in interactive mode and prompts to init workspace if needed @@ -284,21 +287,27 @@ func CheckInteractiveAndInitWorkspace(cmd *cli.Command, projectName string) { } // Check if workspace config exists - config, _, _ := FindWorkspaceConfig() - if config != nil { + var config WorkspaceConfig + found, _ := config.Find() + if found { return } // Prompt user to initialize workspace var answer string - fmt.Printf("%s %s", au.BrightYellow("?"), fmt.Sprintf("Would you like to initialize a workspace config with project '%s'? [y/N] ", projectName)) + fmt.Fprintf(os.Stderr, "%s Would you like to initialize a workspace config with project '%s'? [y/N] ", au.BrightYellow("?"), projectName) fmt.Scanln(&answer) if strings.ToLower(answer) == "y" || strings.ToLower(answer) == "yes" { - if err := InitWorkspaceConfig(projectName, "", ""); err != nil { - fmt.Printf("%s %s\n", au.BrightRed("✱"), fmt.Sprintf("Failed to initialize workspace: %v", err)) + config, err := NewWorkspaceConfig(projectName, "", "") + if err != nil { + Error("Failed to create workspace config: %v", err) return } - fmt.Printf("%s %s\n", au.BrightGreen("✱"), fmt.Sprintf("Workspace initialized with project: %s", projectName)) + if err := config.Save(); err != nil { + Error("Failed to save workspace config: %v", err) + return + } + Success("Workspace initialized with project: %s", projectName) } } diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go new file mode 100644 index 0000000..625dcd5 --- /dev/null +++ b/pkg/cmd/version.go @@ -0,0 +1,5 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +const Version = "0.0.1-alpha.0" // x-release-please-version diff --git a/pkg/cmd/workspace.go b/pkg/cmd/workspace.go index 315e7ff..84f40d7 100644 --- a/pkg/cmd/workspace.go +++ b/pkg/cmd/workspace.go @@ -4,21 +4,18 @@ package cmd import ( "context" - "encoding/json" "fmt" "maps" "os" "path/filepath" "slices" - "strings" "github.com/charmbracelet/huh" - "github.com/logrusorgru/aurora/v4" "github.com/stainless-api/stainless-api-go" "github.com/urfave/cli/v3" ) -var initWorkspaceCommand = cli.Command{ +var workspaceInit = cli.Command{ Name: "init", Usage: "Initialize stainless workspace configuration in current directory", Flags: []cli.Flag{ @@ -27,25 +24,33 @@ var initWorkspaceCommand = cli.Command{ Usage: "Project name to use for this workspace", }, &cli.StringFlag{ - Name: "openapi-spec", + Name: "openapi-spec", Aliases: []string{"oas"}, - Usage: "Path to OpenAPI spec file", + Usage: "Path to OpenAPI spec file", }, &cli.StringFlag{ - Name: "stainless-config", + Name: "stainless-config", Aliases: []string{"config"}, - Usage: "Path to Stainless config file", + Usage: "Path to Stainless config file", }, }, - Action: handleInitWorkspace, + Action: handleWorkspaceInit, HideHelpCommand: true, } -func handleInitWorkspace(ctx context.Context, cmd *cli.Command) error { +var workspaceStatus = cli.Command{ + Name: "status", + Usage: "Show workspace configuration status", + Action: handleWorkspaceStatus, + HideHelpCommand: true, +} + +func handleWorkspaceInit(ctx context.Context, cmd *cli.Command) error { // Check for existing workspace configuration - existingConfig, existingPath, err := FindWorkspaceConfig() - if err == nil && existingConfig != nil { - fmt.Printf("Existing workspace detected: %s (project: %s)\n", aurora.Bold(existingPath), existingConfig.Project) + var existingConfig WorkspaceConfig + found, err := existingConfig.Find() + if err == nil && found { + Info("Existing workspace detected: %s (project: %s)", existingConfig.ConfigPath, existingConfig.Project) } // Get current directory and show where the file will be written @@ -54,8 +59,8 @@ func handleInitWorkspace(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("failed to get current directory: %v", err) } configPath := filepath.Join(dir, "stainless-workspace.json") - fmt.Printf("Writing workspace config to: %s\n", aurora.Bold(configPath)) - fmt.Println() + Info("Writing workspace config to: %s", configPath) + Spacer() // Get values from flags or prepare for interactive prompt projectName := cmd.String("project") @@ -82,18 +87,17 @@ func handleInitWorkspace(ctx context.Context, cmd *cli.Command) error { form := huh.NewForm( huh.NewGroup( huh.NewInput(). - Title("project name"). + Title("project"). Value(&projectName). Suggestions(slices.Collect(maps.Keys(projectInfoMap))). - Description("Enter the stainless project for this workspace"). - Validate(createProjectValidator(projectInfoMap)), + Description("Enter the stainless project for this workspace"), huh.NewInput(). - Title("OpenAPI spec path (optional)"). + Title("openapi_spec (optional)"). Description("Relative path to your OpenAPI spec file"). Placeholder("openapi.yml"). Value(&openAPISpec), huh.NewInput(). - Title("Stainless config path (optional)"). + Title("stainless_config (optional)"). Description("Relative path to your Stainless config file"). Placeholder("openapi.stainless.yml"). Value(&stainlessConfig), @@ -104,20 +108,26 @@ func handleInitWorkspace(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("failed to get workspace configuration: %v", err) } - fmt.Printf("%s project name: %s\n", aurora.Bold("✱"), projectName) + group := Success("Configuration summary:") + group.Property("project_name", projectName) if openAPISpec != "" { - fmt.Printf("%s openapi spec: %s\n", aurora.Bold("✱"), openAPISpec) + group.Property("openapi_spec", openAPISpec) } if stainlessConfig != "" { - fmt.Printf("%s stainless config: %s\n", aurora.Bold("✱"), stainlessConfig) + group.Property("stainless_config", stainlessConfig) } } - if err := InitWorkspaceConfig(projectName, openAPISpec, stainlessConfig); err != nil { - return fmt.Errorf("failed to initialize workspace: %v", err) + config, err := NewWorkspaceConfig(projectName, openAPISpec, stainlessConfig) + if err != nil { + return fmt.Errorf("failed to create workspace config: %v", err) + } + + if err := config.Save(); err != nil { + return fmt.Errorf("failed to save workspace config: %v", err) } - fmt.Printf("%s %s\n", aurora.BrightGreen("✱"), fmt.Sprintf("Workspace initialized")) + Success("Workspace initialized") return nil } @@ -150,106 +160,6 @@ func fetchUserProjects(ctx context.Context) map[string]projectInfo { return projectInfoMap } -func createProjectValidator(projectInfoMap map[string]projectInfo) func(string) error { - attemptCount := 0 - lastProjectName := "" - - return func(projectName string) error { - if projectName != lastProjectName { - attemptCount = 0 - lastProjectName = projectName - } - if strings.TrimSpace(projectName) == "" { - return fmt.Errorf("project name is required") - } - if _, exists := projectInfoMap[projectName]; exists { - return nil - } - - attemptCount++ - if attemptCount == 1 { - return fmt.Errorf("project '%s' not found in accessible projects (press Enter again to proceed anyway)", projectName) - } - // Allow bypass on second attempt - return nil - } -} - -// WorkspaceConfig stores workspace-level configuration -type WorkspaceConfig struct { - Project string `json:"project"` - OpenAPISpec string `json:"openapi_spec,omitempty"` - StainlessConfig string `json:"stainless_config,omitempty"` -} - -// FindWorkspaceConfig searches for a stainless-workspace.json file starting from the current directory -// and moving up to parent directories until found or root is reached -func FindWorkspaceConfig() (*WorkspaceConfig, string, error) { - // Start from current working directory - dir, err := os.Getwd() - if err != nil { - return nil, "", err - } - - for { - configPath := filepath.Join(dir, "stainless-workspace.json") - if _, err := os.Stat(configPath); err == nil { - // Found config file - config, err := LoadWorkspaceConfig(configPath) - return config, configPath, err - } - - // Move up to parent directory - parent := filepath.Dir(dir) - if parent == dir { - // Reached root directory - return nil, "", nil - } - dir = parent - } -} - -// LoadWorkspaceConfig loads the workspace config from the specified path -func LoadWorkspaceConfig(configPath string) (*WorkspaceConfig, error) { - file, err := os.Open(configPath) - if err != nil { - if os.IsNotExist(err) { - return nil, nil - } - return nil, err - } - defer file.Close() - - var config WorkspaceConfig - if err := json.NewDecoder(file).Decode(&config); err != nil { - return nil, err - } - - return &config, nil -} - -// SaveWorkspaceConfig saves the workspace config to the specified path -func SaveWorkspaceConfig(configPath string, config *WorkspaceConfig) error { - file, err := os.Create(configPath) - if err != nil { - return err - } - defer file.Close() - - encoder := json.NewEncoder(file) - encoder.SetIndent("", " ") - return encoder.Encode(config) -} - -// GetProjectNameFromConfig returns the project name from workspace config if available -func GetProjectNameFromConfig() string { - config, _, err := FindWorkspaceConfig() - if err != nil || config == nil || config.Project == "" { - return "" - } - return config.Project -} - // findOpenAPISpec searches for common OpenAPI spec files in the current directory func findOpenAPISpec() string { commonOpenAPIFiles := []string{ @@ -287,20 +197,61 @@ func findStainlessConfig() string { return "" } -// InitWorkspaceConfig initializes a new workspace config in the current directory -func InitWorkspaceConfig(projectName, openAPISpec, stainlessConfig string) error { +func handleWorkspaceStatus(ctx context.Context, cmd *cli.Command) error { + // Look for workspace configuration + var config WorkspaceConfig + found, err := config.Find() + if err != nil { + return fmt.Errorf("error searching for workspace config: %v", err) + } + + if !found { + group := Warn("No workspace configuration found") + group.Info("Run 'stl workspace init' to initialize a workspace in this directory.") + return nil + } + // Get current working directory - dir, err := os.Getwd() + cwd, err := os.Getwd() if err != nil { - return err + return fmt.Errorf("failed to get current directory: %v", err) } - configPath := filepath.Join(dir, "stainless-workspace.json") - config := WorkspaceConfig{ - Project: projectName, - OpenAPISpec: openAPISpec, - StainlessConfig: stainlessConfig, + // Get relative path from cwd to config file + relPath, err := filepath.Rel(cwd, config.ConfigPath) + if err != nil { + relPath = config.ConfigPath // fallback to absolute path } - return SaveWorkspaceConfig(configPath, &config) + group := Success("Workspace configuration found") + group.Property("path", relPath) + group.Property("project", config.Project) + + if config.OpenAPISpec != "" { + // Check if OpenAPI spec file exists + configDir := filepath.Dir(config.ConfigPath) + specPath := filepath.Join(configDir, config.OpenAPISpec) + if _, err := os.Stat(specPath); err == nil { + group.Property("openapi_spec", config.OpenAPISpec) + } else { + group.Property("openapi_spec", config.OpenAPISpec+" (not found)") + } + } else { + group.Property("openapi_spec", "(not configured)") + } + + if config.StainlessConfig != "" { + // Check if Stainless config file exists + configDir := filepath.Dir(config.ConfigPath) + stainlessPath := filepath.Join(configDir, config.StainlessConfig) + if _, err := os.Stat(stainlessPath); err == nil { + group.Property("stainless_config", config.StainlessConfig) + } else { + group.Property("stainless_config", config.StainlessConfig+" (not found)") + } + } else { + group.Property("stainless_config", "(not configured)") + } + + return nil } diff --git a/pkg/cmd/workspaceconfig.go b/pkg/cmd/workspaceconfig.go new file mode 100644 index 0000000..d4bd755 --- /dev/null +++ b/pkg/cmd/workspaceconfig.go @@ -0,0 +1,102 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" +) + +// WorkspaceConfig stores workspace-level configuration +type WorkspaceConfig struct { + Project string `json:"project"` + OpenAPISpec string `json:"openapi_spec,omitempty"` + StainlessConfig string `json:"stainless_config,omitempty"` + + ConfigPath string `json:"-"` +} + +// Find searches for a stainless-workspace.json file starting from the current directory +// and moving up to parent directories until found or root is reached +func (config *WorkspaceConfig) Find() (bool, error) { + dir, err := os.Getwd() + if err != nil { + return false, err + } + + for { + configPath := filepath.Join(dir, "stainless-workspace.json") + if _, err := os.Stat(configPath); err == nil { + // Found config file + err := config.Load(configPath) + if err != nil { + return false, err + } + // Check if the config was actually loaded (not empty) + if config.ConfigPath != "" { + return true, nil + } + // File exists but is empty, continue searching + } + + parent := filepath.Dir(dir) + if parent == dir { + // At root directory + return false, nil + } + dir = parent + } +} + +func (config *WorkspaceConfig) Load(configPath string) error { + file, err := os.Open(configPath) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return fmt.Errorf("failed to open workspace config file %s: %w", configPath, err) + } + defer file.Close() + + // Check if file is empty + info, err := file.Stat() + if err != nil { + return fmt.Errorf("failed to get file info for %s: %w", configPath, err) + } + if info.Size() == 0 { + // File is empty, treat as if no config exists + return nil + } + + if err := json.NewDecoder(file).Decode(config); err != nil { + return fmt.Errorf("failed to parse workspace config file %s: %w", configPath, err) + } + config.ConfigPath = configPath + return nil +} + +func (config *WorkspaceConfig) Save() error { + file, err := os.Create(config.ConfigPath) + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + return encoder.Encode(config) +} + +func NewWorkspaceConfig(projectName, openAPISpec, stainlessConfig string) (*WorkspaceConfig, error) { + dir, err := os.Getwd() + if err != nil { + return nil, err + } + + return &WorkspaceConfig{ + Project: projectName, + OpenAPISpec: openAPISpec, + StainlessConfig: stainlessConfig, + ConfigPath: filepath.Join(dir, "stainless-workspace.json"), + }, nil +} diff --git a/stainless-openapi.yml b/stainless-openapi.yml new file mode 100644 index 0000000..09909cb --- /dev/null +++ b/stainless-openapi.yml @@ -0,0 +1,2273 @@ +{ + "openapi": "3.1.0", + "info": { + "version": "1.0.0", + "title": "My API" + }, + "servers": [ + { + "url": "v1" + } + ], + "paths": { + "/v0/openapi": { + "get": { + "requestBody": { + "content": { + "application/json": {} + } + }, + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + } + } + } + }, + "/v0/orgs": { + "get": { + "summary": "List organizations the user has access to", + "requestBody": { + "content": { + "application/json": {} + } + }, + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "org" + ] + }, + "slug": { + "type": "string" + }, + "display_name": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "object", + "slug", + "display_name" + ] + } + }, + "has_more": { + "type": "boolean" + }, + "next_cursor": { + "type": "string" + } + }, + "required": [ + "data", + "has_more" + ] + } + } + } + } + } + } + }, + "/v0/orgs/{org}": { + "get": { + "summary": "Retrieve an organization by name", + "parameters": [ + { + "in": "path", + "name": "org", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/json": {} + } + }, + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "org" + ] + }, + "slug": { + "type": "string" + }, + "display_name": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "object", + "slug", + "display_name" + ] + } + } + } + } + } + } + }, + "/v0/projects": { + "get": { + "summary": "List projects in an organization", + "parameters": [ + { + "in": "query", + "name": "org", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "cursor", + "schema": { + "type": "string", + "description": "Pagination cursor from a previous response" + } + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 100, + "default": 10, + "description": "Maximum number of projects to return, defaults to 10 (maximum: 100)" + } + } + ], + "requestBody": { + "content": { + "application/json": {} + } + }, + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "project" + ] + }, + "slug": { + "type": "string" + }, + "display_name": { + "type": [ + "string", + "null" + ] + }, + "org": { + "type": "string" + }, + "config_repo": { + "type": "string" + }, + "targets": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "object", + "slug", + "display_name", + "org", + "config_repo", + "targets" + ] + } + }, + "has_more": { + "type": "boolean" + }, + "next_cursor": { + "type": "string" + } + }, + "required": [ + "data", + "has_more" + ] + } + } + } + } + } + }, + "post": { + "summary": "Create a new project", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "slug": { + "type": "string", + "description": "Project name/slug" + }, + "org": { + "type": "string", + "description": "Organization name" + }, + "display_name": { + "type": "string", + "description": "Human-readable project name" + }, + "targets": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "Targets to generate for" + }, + "revision": { + "type": "object", + "propertyNames": { + "type": "string", + "description": "File path" + }, + "additionalProperties": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "File content" + } + }, + "required": [ + "content" + ] + }, + "description": "File contents to commit" + } + }, + "required": [ + "slug", + "org", + "display_name", + "targets", + "revision" + ] + } + } + } + }, + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "project" + ] + }, + "slug": { + "type": "string" + }, + "display_name": { + "type": [ + "string", + "null" + ] + }, + "org": { + "type": "string" + }, + "config_repo": { + "type": "string" + }, + "targets": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "object", + "slug", + "display_name", + "org", + "config_repo", + "targets" + ] + } + } + } + } + } + } + }, + "/v0/projects/{project}": { + "get": { + "summary": "Retrieve a project by name", + "parameters": [ + { + "in": "path", + "name": "project", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/json": {} + } + }, + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "project" + ] + }, + "slug": { + "type": "string" + }, + "display_name": { + "type": [ + "string", + "null" + ] + }, + "org": { + "type": "string" + }, + "config_repo": { + "type": "string" + }, + "targets": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "object", + "slug", + "display_name", + "org", + "config_repo", + "targets" + ] + } + } + } + } + } + }, + "patch": { + "summary": "Update a project's properties", + "parameters": [ + { + "in": "path", + "name": "project", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "display_name": { + "type": [ + "string", + "null" + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "project" + ] + }, + "slug": { + "type": "string" + }, + "display_name": { + "type": [ + "string", + "null" + ] + }, + "org": { + "type": "string" + }, + "config_repo": { + "type": "string" + }, + "targets": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "object", + "slug", + "display_name", + "org", + "config_repo", + "targets" + ] + } + } + } + } + } + } + }, + "/v0/projects/{project}/branches": { + "post": { + "summary": "Create a new branch for a project", + "parameters": [ + { + "in": "path", + "name": "project", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "branch": { + "type": "string", + "description": "Name of the new project branch." + }, + "branch_from": { + "type": "string", + "description": "Branch or commit SHA to branch from." + }, + "force": { + "type": "boolean", + "default": false, + "description": "Whether to throw an error if the branch already exists. Defaults to false." + } + }, + "required": [ + "branch", + "branch_from" + ] + } + } + } + }, + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectBranch" + } + } + } + } + } + } + }, + "/v0/projects/{project}/branches/{branch}": { + "get": { + "summary": "Retrieve a project branch", + "parameters": [ + { + "in": "path", + "name": "project", + "schema": { + "type": "string" + }, + "required": true + }, + { + "in": "path", + "name": "branch", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/json": {} + } + }, + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectBranch" + } + } + } + } + } + } + }, + "/v0/projects/{project}/configs": { + "get": { + "summary": "Retrieve configuration files for a project", + "parameters": [ + { + "in": "path", + "name": "project", + "schema": { + "type": "string" + }, + "required": true + }, + { + "in": "query", + "name": "branch", + "schema": { + "type": "string", + "default": "main", + "description": "Branch name, defaults to \"main\"" + } + } + ], + "requestBody": { + "content": { + "application/json": {} + } + }, + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "type": "object", + "propertyNames": { + "type": "string", + "description": "File path" + }, + "additionalProperties": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "The file content" + } + }, + "required": [ + "content" + ] + }, + "description": "Config files contents" + } + } + } + } + } + } + }, + "/v0/projects/{project}/configs/guess": { + "post": { + "summary": "Generate configuration suggestions based on an OpenAPI spec", + "parameters": [ + { + "in": "path", + "name": "project", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "branch": { + "type": "string", + "default": "main", + "description": "Branch name" + }, + "spec": { + "type": "string", + "description": "OpenAPI spec" + } + }, + "required": [ + "spec" + ] + } + } + } + }, + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "type": "object", + "propertyNames": { + "type": "string", + "description": "File path" + }, + "additionalProperties": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "The file content" + } + }, + "required": [ + "content" + ] + }, + "description": "Config files contents" + } + } + } + } + } + } + }, + "/v0/builds": { + "get": { + "summary": "List builds for a project", + "parameters": [ + { + "in": "query", + "name": "project", + "schema": { + "type": "string", + "description": "Project name" + }, + "required": true + }, + { + "in": "query", + "name": "branch", + "schema": { + "type": "string", + "description": "Branch name" + } + }, + { + "in": "query", + "name": "revision", + "schema": { + "anyOf": [ + { + "type": "string", + "description": "A config commit SHA used for the build" + }, + { + "type": "object", + "propertyNames": { + "type": "string", + "description": "File path" + }, + "additionalProperties": { + "type": "object", + "properties": { + "hash": { + "type": "string", + "description": "File content hash" + } + }, + "required": [ + "hash" + ] + }, + "description": "Hash of the files used for the build" + } + ], + "default": {} + } + }, + { + "in": "query", + "name": "cursor", + "schema": { + "type": "string", + "description": "Pagination cursor from a previous response" + } + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 100, + "default": 10, + "description": "Maximum number of builds to return, defaults to 10 (maximum: 100)" + } + } + ], + "requestBody": { + "content": { + "application/json": {} + } + }, + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BuildObject" + } + }, + "has_more": { + "type": "boolean" + }, + "next_cursor": { + "type": "string" + } + }, + "required": [ + "data", + "has_more" + ] + } + } + } + } + } + }, + "post": { + "summary": "Create a new build", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "Project name" + }, + "revision": { + "anyOf": [ + { + "type": "string", + "description": "A branch name, commit SHA, or merge command in the format \"base..head\"" + }, + { + "type": "object", + "propertyNames": { + "type": "string", + "description": "File path" + }, + "additionalProperties": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "File content" + } + }, + "required": [ + "content" + ] + }, + "description": "File contents to commit directly" + } + ], + "description": "Specifies what to build: a branch name, commit SHA, merge command (\"base..head\"), or file contents" + }, + "branch": { + "type": "string", + "description": "Optional branch to use. If not specified, defaults to \"main\". When using a branch name or merge command as revision, this must match or be omitted." + }, + "targets": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "node", + "typescript", + "python", + "go", + "java", + "kotlin", + "ruby", + "terraform", + "cli", + "php", + "csharp" + ] + }, + "description": "Optional list of SDK targets to build. If not specified, all configured targets will be built." + }, + "commit_message": { + "type": "string", + "description": "Optional commit message to use when creating a new commit." + }, + "allow_empty": { + "type": "boolean", + "default": false, + "description": "Whether to allow empty commits (no changes). Defaults to false." + } + }, + "required": [ + "project", + "revision" + ] + } + } + } + }, + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BuildObject" + } + } + } + } + } + } + }, + "/v0/builds/{buildId}": { + "get": { + "summary": "Retrieve a build by ID", + "parameters": [ + { + "in": "path", + "name": "buildId", + "schema": { + "type": "string", + "description": "Build ID" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/json": {} + } + }, + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BuildObject" + } + } + } + } + } + } + }, + "/v0/build_target_outputs": { + "get": { + "summary": "Download the output of a build target", + "parameters": [ + { + "in": "query", + "name": "build_id", + "schema": { + "type": "string", + "description": "Build ID" + }, + "required": true + }, + { + "in": "query", + "name": "target", + "schema": { + "type": "string", + "enum": [ + "node", + "typescript", + "python", + "go", + "java", + "kotlin", + "ruby", + "terraform", + "cli", + "php", + "csharp" + ], + "description": "SDK language target name" + }, + "required": true + }, + { + "in": "query", + "name": "type", + "schema": { + "type": "string", + "enum": [ + "source" + ], + "description": "Type of output to download: source code" + }, + "required": true + }, + { + "in": "query", + "name": "output", + "schema": { + "type": "string", + "enum": [ + "url", + "git" + ], + "default": "url", + "description": "Output format: url (download URL) or git (temporary access token)" + } + } + ], + "requestBody": { + "content": { + "application/json": {} + } + }, + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "output": { + "type": "string", + "enum": [ + "url" + ] + }, + "url": { + "type": "string", + "description": "URL for direct download" + } + }, + "required": [ + "output", + "url" + ] + }, + { + "type": "object", + "properties": { + "output": { + "type": "string", + "enum": [ + "git" + ] + }, + "token": { + "type": "string", + "description": "Temporary GitHub access token" + }, + "url": { + "type": "string", + "description": "URL to git remote" + }, + "ref": { + "type": "string", + "description": "Git reference (commit SHA, branch, or tag)" + } + }, + "required": [ + "output", + "token", + "url", + "ref" + ] + } + ] + } + } + } + } + } + } + }, + "/v0/builds/compare": { + "post": { + "summary": "Creates two builds whose outputs can be compared directly", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "Project name" + }, + "base": { + "type": "object", + "properties": { + "revision": { + "anyOf": [ + { + "type": "string", + "description": "A branch name, commit SHA, or merge command in the format \"base..head\"" + }, + { + "type": "object", + "propertyNames": { + "type": "string", + "description": "File path" + }, + "additionalProperties": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "File content" + } + }, + "required": [ + "content" + ] + }, + "description": "File contents to commit directly" + } + ], + "description": "Specifies what to build: a branch name, a commit SHA, or file contents" + }, + "branch": { + "type": "string", + "description": "Optional branch to use. If not specified, defaults to \"main\". When using a branch name as revision, this must match or be omitted." + }, + "commit_message": { + "type": "string", + "description": "Optional commit message to use when creating a new commit." + } + }, + "required": [ + "revision" + ], + "description": "Parameters for the base build" + }, + "head": { + "type": "object", + "properties": { + "revision": { + "anyOf": [ + { + "type": "string", + "description": "A branch name, commit SHA, or merge command in the format \"base..head\"" + }, + { + "type": "object", + "propertyNames": { + "type": "string", + "description": "File path" + }, + "additionalProperties": { + "type": "object", + "properties": { + "content": { + "type": "string", + "description": "File content" + } + }, + "required": [ + "content" + ] + }, + "description": "File contents to commit directly" + } + ], + "description": "Specifies what to build: a branch name, a commit SHA, or file contents" + }, + "branch": { + "type": "string", + "description": "Optional branch to use. If not specified, defaults to \"main\". When using a branch name as revision, this must match or be omitted." + }, + "commit_message": { + "type": "string", + "description": "Optional commit message to use when creating a new commit." + } + }, + "required": [ + "revision" + ], + "description": "Parameters for the head build" + }, + "targets": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "node", + "typescript", + "python", + "go", + "java", + "kotlin", + "ruby", + "terraform", + "cli", + "php", + "csharp" + ] + }, + "description": "Optional list of SDK targets to build. If not specified, all configured targets will be built." + } + }, + "required": [ + "project", + "base", + "head" + ] + } + } + } + }, + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "base": { + "$ref": "#/components/schemas/BuildObject" + }, + "head": { + "$ref": "#/components/schemas/BuildObject" + } + }, + "required": [ + "base", + "head" + ] + } + } + } + } + } + } + }, + "/v0/projects/{projectName}/snippets/request": { + "post": { + "parameters": [ + { + "in": "path", + "name": "projectName", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "language": { + "type": "string", + "enum": [ + "node", + "typescript", + "python", + "go", + "java", + "kotlin", + "ruby", + "terraform", + "cli", + "php", + "csharp" + ] + }, + "version": { + "type": "string", + "enum": [ + "next", + "latest_released" + ] + }, + "request": { + "anyOf": [ + { + "type": "object", + "properties": { + "method": { + "type": "string" + }, + "path": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "in": { + "type": "string", + "enum": [ + "path", + "query", + "header", + "cookie" + ] + }, + "value": {} + }, + "required": [ + "name", + "in" + ] + } + }, + "body": { + "type": "object", + "properties": { + "fileParam": { + "type": "string" + }, + "filePath": { + "type": "string" + } + } + } + }, + "required": [ + "method", + "path", + "parameters" + ] + }, + { + "type": "object", + "properties": { + "method": { + "type": "string" + }, + "url": { + "type": "string" + }, + "postData": { + "type": "object", + "properties": { + "mimeType": { + "type": "string" + }, + "text": { + "type": "string" + } + }, + "required": [ + "mimeType", + "text" + ] + }, + "queryString": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "name", + "value" + ] + } + } + }, + "required": [ + "method", + "url", + "queryString" + ] + } + ] + } + }, + "required": [ + "language", + "version", + "request" + ] + } + } + } + }, + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "snippet": { + "type": "string" + } + }, + "required": [ + "snippet" + ] + } + } + } + } + } + } + }, + "/v0/webhooks/postman/notifications": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "eventId": { + "type": "string" + }, + "eventKey": { + "type": "string" + }, + "payload": { + "type": "object", + "properties": { + "collectionId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "languages": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "node", + "typescript", + "python", + "go", + "java", + "kotlin", + "ruby", + "terraform", + "cli", + "php", + "csharp" + ] + } + } + }, + "required": [ + "collectionId", + "name", + "languages" + ] + } + }, + "required": [ + "eventId", + "eventKey", + "payload" + ] + } + } + } + }, + "responses": { + "200": { + "description": "success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": {} + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ProjectBranch": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "project_branch" + ] + }, + "branch": { + "type": "string" + }, + "org": { + "type": "string" + }, + "project": { + "type": "string" + }, + "config_commit": { + "type": "object", + "properties": { + "sha": { + "type": "string" + }, + "repo": { + "type": "object", + "properties": { + "owner": { + "type": "string" + }, + "name": { + "type": "string" + }, + "branch": { + "type": "string" + } + }, + "required": [ + "owner", + "name", + "branch" + ] + } + }, + "required": [ + "sha", + "repo" + ] + }, + "latest_build": { + "oneOf": [ + { + "$ref": "#/components/schemas/BuildObject" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "object", + "branch", + "org", + "project", + "config_commit", + "latest_build" + ] + }, + "ProjectBranchMinimal": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "project_branch" + ] + }, + "branch": { + "type": "string" + }, + "org": { + "type": "string" + }, + "project": { + "type": "string" + }, + "config_commit": { + "type": "object", + "properties": { + "sha": { + "type": "string" + }, + "repo": { + "type": "object", + "properties": { + "owner": { + "type": "string" + }, + "name": { + "type": "string" + }, + "branch": { + "type": "string" + } + }, + "required": [ + "owner", + "name", + "branch" + ] + } + }, + "required": [ + "sha", + "repo" + ] + }, + "latest_build_id": { + "type": "string" + } + }, + "required": [ + "object", + "branch", + "org", + "project", + "config_commit", + "latest_build_id" + ] + }, + "BuildObject": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "build" + ] + }, + "id": { + "type": "string" + }, + "project": { + "type": "string" + }, + "org": { + "type": "string" + }, + "config_commit": { + "type": "string" + }, + "documented_spec": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "content" + ] + }, + "content": { + "type": "string" + } + }, + "required": [ + "type", + "content" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "url" + ] + }, + "url": { + "type": "string" + } + }, + "required": [ + "type", + "url" + ] + }, + { + "type": "null" + } + ] + }, + "targets": { + "type": "object", + "properties": { + "node": { + "$ref": "#/components/schemas/BuildTarget" + }, + "typescript": { + "$ref": "#/components/schemas/BuildTarget" + }, + "python": { + "$ref": "#/components/schemas/BuildTarget" + }, + "go": { + "$ref": "#/components/schemas/BuildTarget" + }, + "java": { + "$ref": "#/components/schemas/BuildTarget" + }, + "kotlin": { + "$ref": "#/components/schemas/BuildTarget" + }, + "ruby": { + "$ref": "#/components/schemas/BuildTarget" + }, + "terraform": { + "$ref": "#/components/schemas/BuildTarget" + }, + "cli": { + "$ref": "#/components/schemas/BuildTarget" + }, + "php": { + "$ref": "#/components/schemas/BuildTarget" + }, + "csharp": { + "$ref": "#/components/schemas/BuildTarget" + } + }, + "additionalProperties": false + } + }, + "required": [ + "object", + "id", + "project", + "org", + "config_commit", + "documented_spec", + "targets" + ] + }, + "BuildTarget": { + "type": "object", + "properties": { + "object": { + "type": "string", + "enum": [ + "build_target" + ] + }, + "status": { + "type": "string", + "enum": [ + "not_started", + "codegen", + "postgen", + "completed" + ] + }, + "commit": { + "oneOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "not_started" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "queued" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "in_progress" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "completed" + ] + }, + "completed": { + "type": "object", + "properties": { + "conclusion": { + "type": "string", + "enum": [ + "error", + "warning", + "note", + "success", + "merge_conflict", + "upstream_merge_conflict", + "fatal", + "payment_required", + "cancelled", + "timed_out", + "noop", + "version_bump" + ] + }, + "merge_conflict_pr": { + "type": [ + "object", + "null" + ], + "properties": { + "number": { + "type": "number" + }, + "repo": { + "type": "object", + "properties": { + "owner": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "owner", + "name" + ] + } + }, + "required": [ + "number", + "repo" + ] + }, + "commit": { + "type": [ + "object", + "null" + ], + "properties": { + "sha": { + "type": "string" + }, + "repo": { + "type": "object", + "properties": { + "owner": { + "type": "string" + }, + "name": { + "type": "string" + }, + "branch": { + "type": "string" + } + }, + "required": [ + "owner", + "name", + "branch" + ] + } + }, + "required": [ + "sha", + "repo" + ] + }, + "diagnostics": { + "type": "array", + "items": {} + } + }, + "required": [ + "conclusion", + "merge_conflict_pr", + "commit" + ] + } + }, + "required": [ + "status", + "completed" + ] + } + ] + }, + "lint": { + "oneOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "not_started" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "queued" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "in_progress" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "completed" + ] + }, + "completed": { + "type": "object", + "properties": { + "conclusion": { + "type": "string", + "enum": [ + "success", + "failure", + "skipped", + "cancelled", + "action_required", + "neutral", + "timed_out" + ] + } + }, + "required": [ + "conclusion" + ] + } + }, + "required": [ + "status", + "completed" + ] + } + ] + }, + "test": { + "oneOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "not_started" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "queued" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "in_progress" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "completed" + ] + }, + "completed": { + "type": "object", + "properties": { + "conclusion": { + "type": "string", + "enum": [ + "success", + "failure", + "skipped", + "cancelled", + "action_required", + "neutral", + "timed_out" + ] + } + }, + "required": [ + "conclusion" + ] + } + }, + "required": [ + "status", + "completed" + ] + } + ] + }, + "build": { + "oneOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "not_started" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "queued" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "in_progress" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "completed" + ] + }, + "completed": { + "type": "object", + "properties": { + "conclusion": { + "type": "string", + "enum": [ + "success", + "failure", + "skipped", + "cancelled", + "action_required", + "neutral", + "timed_out" + ] + } + }, + "required": [ + "conclusion" + ] + } + }, + "required": [ + "status", + "completed" + ] + } + ] + }, + "upload": { + "oneOf": [ + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "not_started" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "queued" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "in_progress" + ] + } + }, + "required": [ + "status" + ] + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "completed" + ] + }, + "completed": { + "type": "object", + "properties": { + "conclusion": { + "type": "string", + "enum": [ + "success", + "failure", + "skipped", + "cancelled", + "action_required", + "neutral", + "timed_out" + ] + } + }, + "required": [ + "conclusion" + ] + } + }, + "required": [ + "status", + "completed" + ] + } + ] + } + }, + "required": [ + "object", + "status", + "commit", + "lint", + "test" + ] + } + } + } +} \ No newline at end of file diff --git a/stainless-workspace.json b/stainless-workspace.json new file mode 100644 index 0000000..e69de29