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 .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.1.0-alpha.87"
".": "0.1.0-alpha.88"
}
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
# Changelog

## 0.1.0-alpha.88 (2026-04-09)

Full Changelog: [v0.1.0-alpha.87...v0.1.0-alpha.88](https://github.com/stainless-api/stainless-api-cli/compare/v0.1.0-alpha.87...v0.1.0-alpha.88)

### Features

* add console.IsInteractive() helper ([d2bd149](https://github.com/stainless-api/stainless-api-cli/commit/d2bd149865971de55adf17296f685f5c4c78b072))
* **components/dev:** forward error from build ([a4d78c6](https://github.com/stainless-api/stainless-api-cli/commit/a4d78c6b86282ca1f68d02f4c1bacc06a6502820))
* **dev:** require project parameter ([ee8b94d](https://github.com/stainless-api/stainless-api-cli/commit/ee8b94d20030127b3f0893917d8e793fa6ed5965))
* **init/builds create:** use dev component instead ([6e06b1e](https://github.com/stainless-api/stainless-api-cli/commit/6e06b1e0e180989faaf497bcd1ba074eafcd0428))
* **init:** don't overwrite config files by default ([96f9d11](https://github.com/stainless-api/stainless-api-cli/commit/96f9d116a27385c6ab1366abe001026eed17158a))
* **init:** support non-interactive mode for stl init ([7fbcaeb](https://github.com/stainless-api/stainless-api-cli/commit/7fbcaeb617ade079c002bac50cd4ebb935df349b))
* **init:** upload --stainless-config when creating a new project ([d96e641](https://github.com/stainless-api/stainless-api-cli/commit/d96e6416d2001f753540007235b7d17a77a03095))


### Bug Fixes

* handle openapi target in BuildTarget lookup ([d09eede](https://github.com/stainless-api/stainless-api-cli/commit/d09eede6ca8f6f4ea7d9e42d77e39a4d2400228b))
* **init:** require --openapi-spec for non-interactive project creation ([1b338ad](https://github.com/stainless-api/stainless-api-cli/commit/1b338ad116591b18eee735bdd3354d82e82b6a4b))
* **init:** respect --targets flag for existing projects ([c6444a0](https://github.com/stainless-api/stainless-api-cli/commit/c6444a098b86d9072ebbb551e868550f5e0df216))


### Refactors

* **init:** extract askSelectTargets from init flow ([62c6102](https://github.com/stainless-api/stainless-api-cli/commit/62c610247ee9bd411836a9b683fbf74c01772954))

## 0.1.0-alpha.87 (2026-03-30)

Full Changelog: [v0.1.0-alpha.86...v0.1.0-alpha.87](https://github.com/stainless-api/stainless-api-cli/compare/v0.1.0-alpha.86...v0.1.0-alpha.87)
Expand Down
48 changes: 45 additions & 3 deletions internal/mockstainless/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,50 @@ func newServeMux(m *Mock) http.Handler {
writeJSON(w, http.StatusOK, Page(m.Projects))
})

mux.HandleFunc("POST /v0/projects", func(w http.ResponseWriter, r *http.Request) {
body := mustReadBody(r)
slug := gjson.GetBytes(body, "slug").String()
displayName := gjson.GetBytes(body, "display_name").String()
org := gjson.GetBytes(body, "org").String()

if slug == "" {
writeJSON(w, http.StatusBadRequest, M{"error": "slug is required"})
return
}
if displayName == "" {
displayName = slug
}

var targets []any
gjson.GetBytes(body, "targets").ForEach(func(_, v gjson.Result) bool {
targets = append(targets, v.String())
return true
})
if len(targets) == 0 {
targets = []any{"typescript", "python", "go"}
}

project := M{
"slug": slug,
"display_name": displayName,
"object": "project",
"org": org,
"config_repo": fmt.Sprintf("https://github.com/%s/%s", org, slug),
"targets": targets,
}

m.mu.Lock()
m.Projects = append(m.Projects, project)
m.mu.Unlock()

// Create a build so the post-creation build-wait step succeeds.
if len(m.Builds) > 0 {
m.CreateBuildFromTemplate(m.Builds[0])
}

writeJSON(w, http.StatusOK, project)
})

mux.HandleFunc("GET /v0/projects/{project}", func(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("project")
for _, p := range m.Projects {
Expand All @@ -84,9 +128,7 @@ func newServeMux(m *Mock) http.Handler {
return
}
}
if len(m.Projects) > 0 {
writeJSON(w, http.StatusOK, m.Projects[0])
}
writeJSON(w, http.StatusNotFound, M{"error": "project not found"})
})

mux.HandleFunc("PATCH /v0/projects/{project}", func(w http.ResponseWriter, r *http.Request) {
Expand Down
106 changes: 22 additions & 84 deletions pkg/cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import (
"path"
"strings"

tea "github.com/charmbracelet/bubbletea"
"github.com/stainless-api/stainless-api-cli/internal/apiquery"
"github.com/stainless-api/stainless-api-cli/internal/requestflag"
cbuild "github.com/stainless-api/stainless-api-cli/pkg/components/build"
cdev "github.com/stainless-api/stainless-api-cli/pkg/components/dev"
"github.com/stainless-api/stainless-api-cli/pkg/console"
"github.com/stainless-api/stainless-api-cli/pkg/stainlessutils"
"github.com/stainless-api/stainless-api-cli/pkg/workspace"
Expand All @@ -25,24 +25,15 @@ import (
"github.com/urfave/cli/v3"
)

// WaitMode represents the level of waiting for build completion
type WaitMode int

const (
WaitNone WaitMode = iota // Don't wait
WaitCommit // Wait for commit only
WaitAll // Wait for everything including workflows
)

// parseWaitMode converts the --wait flag string to a WaitMode
func parseWaitMode(wait string) (WaitMode, error) {
func parseWaitMode(wait string) (cdev.WaitMode, error) {
switch wait {
case "none", "false": // Accept both "none" and "false" for backwards compatibility
return WaitNone, nil
return cdev.WaitNone, nil
case "commit":
return WaitCommit, nil
return cdev.WaitCommit, nil
case "all":
return WaitAll, nil
return cdev.WaitAll, nil
default:
return 0, fmt.Errorf("invalid --wait value: %q (must be 'none', 'commit', or 'all')", wait)
}
Expand Down Expand Up @@ -429,20 +420,26 @@ func handleBuildsCreate(ctx context.Context, cmd *cli.Command) error {

buildGroup.Property("build_id", build.ID)

if waitMode > WaitNone {
if waitMode > cdev.WaitNone {
console.Spacer()
buildModel := cbuild.NewModel(client, ctx, *build, cmd.String("branch"), downloadPaths)
buildModel.CommitOnly = waitMode == WaitCommit
model := tea.Model(buildCompletionModel{
Build: buildModel,
WaitMode: waitMode,
devModel := cdev.NewModel(cdev.ModelConfig{
Client: client,
Ctx: ctx,
Branch: cmd.String("branch"),
Start: func() (*stainless.Build, error) { return build, nil },
DownloadPaths: downloadPaths,
Label: "BUILD",
WaitMode: waitMode,
Indent: " ",
})
model, err = console.NewProgram(model).Run()
devModel.Build.CommitOnly = waitMode == cdev.WaitCommit
model, err := console.NewProgram(devModel).Run()
if err != nil {
console.Warn("%s", err.Error())
}
b := model.(buildCompletionModel).Build
build = &b.Build
if m, ok := model.(cdev.Model); ok {
build = &m.Build.Build
}
console.Spacer()
}

Expand All @@ -454,7 +451,7 @@ func handleBuildsCreate(ctx context.Context, cmd *cli.Command) error {
}

// Only check for failures if we waited for the build
if waitMode == WaitNone {
if waitMode == cdev.WaitNone {
return nil
}

Expand All @@ -475,7 +472,7 @@ func handleBuildsCreate(ctx context.Context, cmd *cli.Command) error {
}

// Only check workflow failures if we waited for them
if waitMode >= WaitAll {
if waitMode >= cdev.WaitAll {
if bt.Lint.Conclusion == "failure" || bt.Test.Conclusion == "failure" || bt.Build.Conclusion == "failure" {
failures = append(failures, fmt.Errorf("%s workflow failed", target))
}
Expand All @@ -489,65 +486,6 @@ func handleBuildsCreate(ctx context.Context, cmd *cli.Command) error {
return nil
}

type buildCompletionModel struct {
Build cbuild.Model
WaitMode WaitMode
}

func (c buildCompletionModel) Init() tea.Cmd {
return c.Build.Init()
}

func (c buildCompletionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
c.Build, cmd = c.Build.Update(msg)

if c.IsCompleted() {
return c, tea.Sequence(
cmd,
tea.Quit,
)
}

return c, cmd
}

func (c buildCompletionModel) IsCompleted() bool {
b := stainlessutils.NewBuild(c.Build.Build)
for _, target := range b.Languages() {
buildTarget := b.BuildTarget(target)

if buildTarget == nil {
return false
}

// Check if download is completed (if applicable)
downloadIsCompleted := true
if buildTarget.IsCommitCompleted() && buildTarget.IsGoodCommitConclusion() {
if download, ok := c.Build.Downloads[target]; ok {
downloadIsCompleted = download.Status == "completed"
}
}

// Check if target is done based on wait mode
done := buildTarget.IsCommitCompleted()
if c.WaitMode >= WaitAll {
done = buildTarget.IsCompleted()
}

if !done || !downloadIsCompleted {
return false
}
}

return true
}

func (c buildCompletionModel) View() string {
c.Build.CommitOnly = c.WaitMode == WaitCommit
return c.Build.View()
}

func handleBuildsRetrieve(ctx context.Context, cmd *cli.Command) error {
client := stainless.NewClient(getDefaultRequestOptions(cmd)...)
unusedArgs := cmd.Args().Slice()
Expand Down
3 changes: 3 additions & 0 deletions pkg/cmd/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ func gitRepoRoot(dir string) string {

func runDevBuild(ctx context.Context, client stainless.Client, wc workspace.Config, cmd *cli.Command) error {
projectName := cmd.String("project")
if projectName == "" {
return fmt.Errorf("project is required: use --project or set it in .stainless/workspace.json")
}
oasPath := cmd.String("openapi-spec")
configPath := cmd.String("stainless-config")

Expand Down
Loading
Loading