diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18baad7..0396cb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,10 @@ on: - 'integrated/**' - 'stl-preview-head/**' - 'stl-preview-base/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' jobs: lint: diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fd0ccba..000572e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.12" + ".": "0.1.0-alpha.13" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 60175c3..8e8b1fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.1.0-alpha.13 (2025-06-17) + +Full Changelog: [v0.1.0-alpha.12...v0.1.0-alpha.13](https://github.com/stainless-api/stainless-api-cli/compare/v0.1.0-alpha.12...v0.1.0-alpha.13) + +### Chores + +* **ci:** enable for pull requests ([3539b82](https://github.com/stainless-api/stainless-api-cli/commit/3539b82e2606cc2252ed808bf4feff883cab21c8)) +* **internal:** codegen related update ([567a6e6](https://github.com/stainless-api/stainless-api-cli/commit/567a6e653a6ca70db50cf9cc80ac6deaa64be97a)) + ## 0.1.0-alpha.12 (2025-06-16) Full Changelog: [v0.1.0-alpha.11...v0.1.0-alpha.12](https://github.com/stainless-api/stainless-api-cli/compare/v0.1.0-alpha.11...v0.1.0-alpha.12) diff --git a/cmd/stl/main.go b/cmd/stl/main.go index c5ac3b2..2c96a42 100644 --- a/cmd/stl/main.go +++ b/cmd/stl/main.go @@ -4,15 +4,23 @@ package main import ( "context" - "log" + "errors" + "fmt" "os" "github.com/stainless-api/stainless-api-cli/pkg/cmd" + "github.com/stainless-api/stainless-api-go" ) func main() { app := cmd.Command if err := app.Run(context.Background(), os.Args); err != nil { - log.Fatal(err) + var apierr *stainlessv0.Error + if errors.As(err, &apierr) { + fmt.Printf("%s\n", cmd.ColorizeJSON(apierr.RawJSON(), os.Stderr)) + } else { + fmt.Printf("%s\n", err.Error()) + } + os.Exit(1) } } diff --git a/pkg/cmd/auth.go b/pkg/cmd/auth.go index 22002cd..d3ba0b4 100644 --- a/pkg/cmd/auth.go +++ b/pkg/cmd/auth.go @@ -305,7 +305,7 @@ func pollForToken(clientID, deviceCode string, interval, expiresIn int) (*AuthCo } // GetClientOptions returns the request options for API calls -func getClientOptions(ctx context.Context, cmd *cli.Command) []option.RequestOption { +func getClientOptions() []option.RequestOption { options := []option.RequestOption{} if apiKey := os.Getenv("STAINLESS_API_KEY"); apiKey != "" { diff --git a/pkg/cmd/build.go b/pkg/cmd/build.go index 3443d4a..5ddd078 100644 --- a/pkg/cmd/build.go +++ b/pkg/cmd/build.go @@ -398,8 +398,7 @@ func handleBuildsCreate(ctx context.Context, cmd *cli.Command) error { } } - // Print the actual JSON response to stdout for piping - fmt.Printf("%s\n", colorizeJSON(res.RawJSON(), os.Stdout)) + fmt.Printf("%s\n", ColorizeJSON(res.RawJSON(), os.Stdout)) return nil } @@ -414,7 +413,7 @@ func handleBuildsRetrieve(ctx context.Context, cmd *cli.Command) error { return err } - fmt.Printf("%s\n", colorizeJSON(res.RawJSON(), os.Stdout)) + fmt.Printf("%s\n", ColorizeJSON(res.RawJSON(), os.Stdout)) return nil } @@ -573,7 +572,7 @@ func handleBuildsList(ctx context.Context, cmd *cli.Command) error { return err } - fmt.Printf("%s\n", colorizeJSON(res.RawJSON(), os.Stdout)) + fmt.Printf("%s\n", ColorizeJSON(res.RawJSON(), os.Stdout)) return nil } @@ -589,7 +588,7 @@ func handleBuildsCompare(ctx context.Context, cmd *cli.Command) error { return err } - fmt.Printf("%s\n", colorizeJSON(res.RawJSON(), os.Stdout)) + fmt.Printf("%s\n", ColorizeJSON(res.RawJSON(), os.Stdout)) return nil } diff --git a/pkg/cmd/buildtargetoutput.go b/pkg/cmd/buildtargetoutput.go index 03da217..ed3d6e3 100644 --- a/pkg/cmd/buildtargetoutput.go +++ b/pkg/cmd/buildtargetoutput.go @@ -64,7 +64,7 @@ func handleBuildsTargetOutputsRetrieve(ctx context.Context, cmd *cli.Command) er return err } - fmt.Printf("%s\n", colorizeJSON(res.RawJSON(), os.Stdout)) + fmt.Printf("%s\n", ColorizeJSON(res.RawJSON(), os.Stdout)) if cmd.Bool("pull") { build, err := cc.client.Builds.Get(ctx, cmd.String("build-id")) @@ -74,5 +74,6 @@ func handleBuildsTargetOutputsRetrieve(ctx context.Context, cmd *cli.Command) er targetDir := fmt.Sprintf("%s-%s", build.Project, cmd.String("target")) return pullOutput(res.Output, res.URL, res.Ref, targetDir) } + return nil } diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 48f6d75..b9c208c 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -9,6 +9,12 @@ import ( var Command = cli.Command{ Name: "stl", Usage: "CLI for the stainless API", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + Usage: "Enable debug logging", + }, + }, Commands: []*cli.Command{ { Name: "auth", diff --git a/pkg/cmd/org.go b/pkg/cmd/org.go index 84d2fd7..d00b5a1 100644 --- a/pkg/cmd/org.go +++ b/pkg/cmd/org.go @@ -42,7 +42,7 @@ func handleOrgsRetrieve(ctx context.Context, cmd *cli.Command) error { return err } - fmt.Printf("%s\n", colorizeJSON(res.RawJSON(), os.Stdout)) + fmt.Printf("%s\n", ColorizeJSON(res.RawJSON(), os.Stdout)) return nil } @@ -53,6 +53,6 @@ func handleOrgsList(ctx context.Context, cmd *cli.Command) error { return err } - fmt.Printf("%s\n", colorizeJSON(res.RawJSON(), os.Stdout)) + fmt.Printf("%s\n", ColorizeJSON(res.RawJSON(), os.Stdout)) return nil } diff --git a/pkg/cmd/project.go b/pkg/cmd/project.go index 7dde655..9a5074e 100644 --- a/pkg/cmd/project.go +++ b/pkg/cmd/project.go @@ -125,13 +125,12 @@ func handleProjectsCreate(ctx context.Context, cmd *cli.Command) error { context.TODO(), params, option.WithMiddleware(cc.AsMiddleware()), - option.WithRequestBody("application/json", cc.body), ) if err != nil { return err } - fmt.Printf("%s\n", colorizeJSON(res.RawJSON(), os.Stdout)) + fmt.Printf("%s\n", ColorizeJSON(res.RawJSON(), os.Stdout)) return nil } @@ -150,7 +149,7 @@ func handleProjectsRetrieve(ctx context.Context, cmd *cli.Command) error { return err } - fmt.Printf("%s\n", colorizeJSON(res.RawJSON(), os.Stdout)) + fmt.Printf("%s\n", ColorizeJSON(res.RawJSON(), os.Stdout)) return nil } @@ -164,13 +163,12 @@ func handleProjectsUpdate(ctx context.Context, cmd *cli.Command) error { context.TODO(), params, option.WithMiddleware(cc.AsMiddleware()), - option.WithRequestBody("application/json", cc.body), ) if err != nil { return err } - fmt.Printf("%s\n", colorizeJSON(res.RawJSON(), os.Stdout)) + fmt.Printf("%s\n", ColorizeJSON(res.RawJSON(), os.Stdout)) return nil } @@ -186,6 +184,6 @@ func handleProjectsList(ctx context.Context, cmd *cli.Command) error { return err } - fmt.Printf("%s\n", colorizeJSON(res.RawJSON(), os.Stdout)) + fmt.Printf("%s\n", ColorizeJSON(res.RawJSON(), os.Stdout)) return nil } diff --git a/pkg/cmd/projectbranch.go b/pkg/cmd/projectbranch.go index f169554..fc74412 100644 --- a/pkg/cmd/projectbranch.go +++ b/pkg/cmd/projectbranch.go @@ -71,13 +71,12 @@ func handleProjectsBranchesCreate(ctx context.Context, cmd *cli.Command) error { context.TODO(), params, option.WithMiddleware(cc.AsMiddleware()), - option.WithRequestBody("application/json", cc.body), ) if err != nil { return err } - fmt.Printf("%s\n", colorizeJSON(res.RawJSON(), os.Stdout)) + fmt.Printf("%s\n", ColorizeJSON(res.RawJSON(), os.Stdout)) return nil } @@ -97,6 +96,6 @@ func handleProjectsBranchesRetrieve(ctx context.Context, cmd *cli.Command) error return err } - fmt.Printf("%s\n", colorizeJSON(res.RawJSON(), os.Stdout)) + fmt.Printf("%s\n", ColorizeJSON(res.RawJSON(), os.Stdout)) return nil } diff --git a/pkg/cmd/projectconfig.go b/pkg/cmd/projectconfig.go index b6efac9..9114a40 100644 --- a/pkg/cmd/projectconfig.go +++ b/pkg/cmd/projectconfig.go @@ -75,7 +75,7 @@ func handleProjectsConfigsRetrieve(ctx context.Context, cmd *cli.Command) error return err } - fmt.Printf("%s\n", colorizeJSON(string(res), os.Stdout)) + fmt.Printf("%s\n", ColorizeJSON(string(res), os.Stdout)) return nil } @@ -90,13 +90,12 @@ func handleProjectsConfigsGuess(ctx context.Context, cmd *cli.Command) error { context.TODO(), params, option.WithMiddleware(cc.AsMiddleware()), - option.WithRequestBody("application/json", cc.body), option.WithResponseBodyInto(&res), ) if err != nil { return err } - fmt.Printf("%s\n", colorizeJSON(string(res), os.Stdout)) + fmt.Printf("%s\n", ColorizeJSON(string(res), os.Stdout)) return nil } diff --git a/pkg/cmd/util.go b/pkg/cmd/util.go index efd65ed..c5fb3ad 100644 --- a/pkg/cmd/util.go +++ b/pkg/cmd/util.go @@ -1,10 +1,12 @@ package cmd import ( + "bytes" "fmt" "io" "log" "net/http" + "net/http/httputil" "net/url" "os" "strings" @@ -14,45 +16,61 @@ import ( "github.com/stainless-api/stainless-api-go" "github.com/stainless-api/stainless-api-go/option" - "github.com/tidwall/sjson" "github.com/tidwall/gjson" "github.com/tidwall/pretty" + "github.com/tidwall/sjson" "github.com/urfave/cli/v3" "golang.org/x/term" ) func getDefaultRequestOptions() []option.RequestOption { - return []option.RequestOption{ - option.WithHeader("X-Stainless-Lang", "cli"), - option.WithHeader("X-Stainless-Runtime", "cli"), - } + return append( + []option.RequestOption{ + option.WithHeader("X-Stainless-Lang", "cli"), + option.WithHeader("X-Stainless-Runtime", "cli"), + }, + getClientOptions()..., + ) } type apiCommandContext struct { client stainlessv0.Client - body []byte - query []byte - header []byte + cmd *cli.Command } func (c apiCommandContext) AsMiddleware() option.Middleware { + body := getStdInput() + if body == nil { + body = []byte("{}") + } + var query = []byte("{}") + var header = []byte("{}") + + // Apply JSON flag mutations + body, query, header, err := jsonflag.Apply(body, query, header) + if err != nil { + log.Fatal(err) + } + + debug := c.cmd.Bool("debug") + return func(r *http.Request, mn option.MiddlewareNext) (*http.Response, error) { q := r.URL.Query() - for key, values := range serializeQuery(c.query) { + for key, values := range serializeQuery(query) { for _, value := range values { q.Set(key, value) } } r.URL.RawQuery = q.Encode() - for key, values := range serializeHeader(c.header) { + for key, values := range serializeHeader(header) { for _, value := range values { - r.Header.Add(key, value) + r.Header.Set(key, value) } } // Handle request body merging if there's a body to process - if r.Body != nil && len(c.body) > 2 { // More than just "{}" + if r.Body != nil || len(body) > 2 { // More than just "{}" // Read the existing request body existingBody, err := io.ReadAll(r.Body) r.Body.Close() @@ -67,7 +85,7 @@ func (c apiCommandContext) AsMiddleware() option.Middleware { } // Parse command body and merge top-level keys - commandResult := gjson.ParseBytes(c.body) + commandResult := gjson.ParseBytes(body) if commandResult.IsObject() { commandResult.ForEach(func(key, value gjson.Result) bool { // Set each top-level key from command body, overwriting existing values @@ -82,31 +100,38 @@ func (c apiCommandContext) AsMiddleware() option.Middleware { } // Set the new body - r.Body = io.NopCloser(strings.NewReader(string(mergedBody))) + r.Body = io.NopCloser(bytes.NewBuffer(mergedBody)) r.ContentLength = int64(len(mergedBody)) r.Header.Set("Content-Type", "application/json") } + // Add debug logging if the --debug flag is set + if debug { + logger := log.Default() + + if reqBytes, err := httputil.DumpRequest(r, true); err == nil { + logger.Printf("Request Content:\n%s\n", reqBytes) + } + + resp, err := mn(r) + if err != nil { + return resp, err + } + + if respBytes, err := httputil.DumpResponse(resp, true); err == nil { + logger.Printf("Response Content:\n%s\n", respBytes) + } + + return resp, err + } + return mn(r) } } func getAPICommandContext(cmd *cli.Command) *apiCommandContext { client := stainlessv0.NewClient(getDefaultRequestOptions()...) - body := getStdInput() - if body == nil { - body = []byte("{}") - } - var query = []byte("{}") - var header = []byte("{}") - - // Apply JSON flag mutations - body, query, header, err := jsonflag.Apply(body, query, header) - if err != nil { - log.Fatal(err) - } - - return &apiCommandContext{client, body, query, header} + return &apiCommandContext{client, cmd} } func serializeQuery(params []byte) url.Values { @@ -224,7 +249,7 @@ func shouldUseColors(w io.Writer) bool { return false } -func colorizeJSON(input string, w io.Writer) string { +func ColorizeJSON(input string, w io.Writer) string { if !shouldUseColors(w) { return input } diff --git a/pkg/cmd/workspace.go b/pkg/cmd/workspace.go index 2150db9..98b6833 100644 --- a/pkg/cmd/workspace.go +++ b/pkg/cmd/workspace.go @@ -131,10 +131,10 @@ type projectInfo struct { // fetchUserProjects retrieves the list of projects the user has access to func fetchUserProjects(ctx context.Context) map[string]projectInfo { - client := stainlessv0.NewClient(getClientOptions(ctx, nil)...) + client := stainlessv0.NewClient(getClientOptions()...) params := stainlessv0.ProjectListParams{} - res, err := client.Projects.List(context.TODO(), params) + res, err := client.Projects.List(ctx, params) if err != nil { // Return empty map if we can't fetch projects return map[string]projectInfo{}