@@ -12,262 +12,17 @@ import (
1212 "strings"
1313 "time"
1414
15- "github.com/charmbracelet/bubbles/help"
16- "github.com/charmbracelet/bubbles/key"
1715 tea "github.com/charmbracelet/bubbletea"
1816 "github.com/charmbracelet/huh"
17+ "github.com/stainless-api/stainless-api-cli/pkg/components/build"
18+ "github.com/stainless-api/stainless-api-cli/pkg/components/dev"
1919 "github.com/stainless-api/stainless-api-cli/pkg/console"
20- "github.com/stainless-api/stainless-api-cli/pkg/stainlessutils"
21- "github.com/stainless-api/stainless-api-cli/pkg/stainlessviews"
2220 "github.com/stainless-api/stainless-api-go"
2321 "github.com/stainless-api/stainless-api-go/option"
2422 "github.com/tidwall/gjson"
2523 "github.com/urfave/cli/v3"
2624)
2725
28- var ErrUserCancelled = errors .New ("user cancelled" )
29-
30- // BuildModel represents the bubbletea model for build monitoring
31- type BuildModel struct {
32- start func () (* stainless.Build , error )
33- started time.Time
34- ended * time.Time
35- build * stainless.Build
36- branch string
37- help help.Model
38- diagnostics []stainless.BuildDiagnostic
39- downloads map [stainless.Target ]stainlessviews.DownloadStatus
40- view string
41-
42- cc * apiCommandContext
43- ctx context.Context
44- err error
45- isCompleted bool
46- }
47-
48- type tickMsg time.Time
49- type fetchBuildMsg * stainless.Build
50- type fetchDiagnosticsMsg []stainless.BuildDiagnostic
51- type errorMsg error
52- type downloadMsg stainless.Target
53- type fileChangeMsg struct {}
54-
55- func NewBuildModel (cc * apiCommandContext , ctx context.Context , branch string , fn func () (* stainless.Build , error )) BuildModel {
56- return BuildModel {
57- start : fn ,
58- started : time .Now (),
59- cc : cc ,
60- ctx : ctx ,
61- branch : branch ,
62- help : help .New (),
63- }
64- }
65-
66- func (m BuildModel ) Init () tea.Cmd {
67- return tea .Batch (
68- tea .Tick (time .Second , func (t time.Time ) tea.Msg {
69- return tickMsg (t )
70- }),
71- func () tea.Msg {
72- build , err := m .start ()
73- if err != nil {
74- return errorMsg (err )
75- }
76- return fetchBuildMsg (build )
77- },
78- )
79- }
80-
81- func (m BuildModel ) Update (msg tea.Msg ) (tea.Model , tea.Cmd ) {
82- cmds := []tea.Cmd {}
83- switch msg := msg .(type ) {
84- case tea.WindowSizeMsg :
85- m .help .Width = msg .Width
86- case tea.KeyMsg :
87- switch msg .String () {
88- case "ctrl+c" :
89- m .err = ErrUserCancelled
90- cmds = append (cmds , tea .Quit )
91- case "enter" :
92- if m .cc .cmd .Bool ("watch" ) {
93- cmds = append (cmds , tea .Quit )
94- }
95- }
96-
97- case downloadMsg :
98- download := m .downloads [stainless .Target (msg )]
99- download .Status = "completed"
100- m .downloads [stainless .Target (msg )] = download
101-
102- case tickMsg :
103- if m .build != nil {
104- cmds = append (cmds , m .fetchBuildStatus ())
105- }
106- m .getBuildDuration ()
107- cmds = append (cmds , tea .Tick (time .Second , func (t time.Time ) tea.Msg {
108- return tickMsg (t )
109- }))
110-
111- case fetchBuildMsg :
112- if m .build == nil {
113- m .build = msg
114- m .downloads = make (map [stainless.Target ]stainlessviews.DownloadStatus )
115- for targetName , targetConfig := range m .cc .workspaceConfig .Targets {
116- m .downloads [stainless .Target (targetName )] = stainlessviews.DownloadStatus {
117- Status : "not started" ,
118- Path : targetConfig .OutputPath ,
119- }
120- }
121- cmds = append (cmds , m .updateView ("header" ))
122- }
123-
124- m .build = msg
125- buildObj := stainlessutils .NewBuild (m .build )
126- if ! m .isCompleted {
127- // Check if all commit steps are completed
128- allCommitsCompleted := true
129- for _ , target := range buildObj .Languages () {
130- buildTarget := buildObj .BuildTarget (target )
131- if buildTarget != nil && ! buildTarget .IsCommitCompleted () {
132- allCommitsCompleted = false
133- break
134- }
135- }
136- if allCommitsCompleted {
137- m .isCompleted = true
138- cmds = append (cmds , m .fetchDiagnostics ())
139- }
140- }
141- languages := buildObj .Languages ()
142- for _ , target := range languages {
143- buildTarget := buildObj .BuildTarget (target )
144- if buildTarget == nil {
145- continue
146- }
147- status , _ , conclusion := buildTarget .StepInfo ("commit" )
148- if status == "completed" && conclusion != "fatal" {
149- if download , ok := m .downloads [target ]; ok && download .Status == "not started" {
150- download .Status = "started"
151- cmds = append (cmds , m .downloadTarget (target ))
152- m .downloads [target ] = download
153- }
154- }
155- }
156-
157- case fetchDiagnosticsMsg :
158- if m .diagnostics == nil {
159- m .diagnostics = msg
160- cmds = append (cmds , m .updateView ("diagnostics" ))
161- }
162-
163- case errorMsg :
164- m .err = msg
165- cmds = append (cmds , tea .Quit )
166-
167- case fileChangeMsg :
168- // File change detected, exit with success
169- cmds = append (cmds , tea .Quit )
170- }
171- return m , tea .Sequence (cmds ... )
172- }
173-
174- func (m BuildModel ) ShortHelp () []key.Binding {
175- if m .cc .cmd .Bool ("watch" ) {
176- return []key.Binding {
177- key .NewBinding (key .WithKeys ("ctrl+c" ), key .WithHelp ("ctrl-c" , "quit" )),
178- key .NewBinding (key .WithKeys ("enter" ), key .WithHelp ("enter" , "rebuild" )),
179- }
180- } else {
181- return []key.Binding {key .NewBinding (key .WithKeys ("ctrl+c" ), key .WithHelp ("ctrl-c" , "quit" ))}
182- }
183- }
184-
185- func (m BuildModel ) FullHelp () [][]key.Binding {
186- if m .cc .cmd .Bool ("watch" ) {
187- return [][]key.Binding {{
188- key .NewBinding (key .WithKeys ("ctrl+c" ), key .WithHelp ("ctrl-c" , "quit" )),
189- key .NewBinding (key .WithKeys ("enter" ), key .WithHelp ("enter" , "rebuild" )),
190- }}
191- } else {
192- return [][]key.Binding {{key .NewBinding (key .WithKeys ("ctrl+c" ), key .WithHelp ("ctrl-c" , "quit" ))}}
193- }
194- }
195-
196- func (m BuildModel ) downloadTarget (target stainless.Target ) tea.Cmd {
197- return func () tea.Msg {
198- if m .build == nil {
199- return errorMsg (fmt .Errorf ("no current build to download target from" ))
200- }
201- params := stainless.BuildTargetOutputGetParams {
202- BuildID : m .build .ID ,
203- Target : stainless .BuildTargetOutputGetParamsTarget (target ),
204- Type : "source" ,
205- Output : "git" ,
206- }
207- outputRes , err := m .cc .client .Builds .TargetOutputs .Get (
208- context .TODO (),
209- params ,
210- )
211- if err != nil {
212- return errorMsg (err )
213- }
214- err = pullOutput (outputRes .Output , outputRes .URL , outputRes .Ref , m .downloads [target ].Path , & console.Group {})
215- if err != nil {
216- return errorMsg (err )
217- }
218- return downloadMsg (target )
219- }
220- }
221-
222- func (m BuildModel ) fetchBuildStatus () tea.Cmd {
223- return func () tea.Msg {
224- if m .build == nil {
225- return errorMsg (fmt .Errorf ("no current build to fetch status for" ))
226- }
227- build , err := m .cc .client .Builds .Get (m .ctx , m .build .ID )
228- if err != nil {
229- return errorMsg (fmt .Errorf ("failed to get build status: %v" , err ))
230- }
231- return fetchBuildMsg (build )
232- }
233- }
234-
235- func (m BuildModel ) fetchDiagnostics () tea.Cmd {
236- return func () tea.Msg {
237- if m .build == nil {
238- return errorMsg (fmt .Errorf ("no current build to fetch diagnostics for" ))
239- }
240- diags := []stainless.BuildDiagnostic {}
241- diagnostics := m .cc .client .Builds .Diagnostics .ListAutoPaging (m .ctx , m .build .ID , stainless.BuildDiagnosticListParams {
242- Limit : stainless .Float (100 ),
243- })
244- for diagnostics .Next () {
245- diag := diagnostics .Current ()
246- if ! diag .Ignored {
247- diags = append (diags , diag )
248- }
249- }
250- return fetchDiagnosticsMsg (diags )
251- }
252- }
253-
254- func (m * BuildModel ) getBuildDuration () time.Duration {
255- if m .build == nil {
256- return time .Since (m .started )
257- }
258-
259- buildObj := stainlessutils .NewBuild (m .build )
260- if buildObj .IsCompleted () {
261- if m .ended == nil {
262- now := time .Now ()
263- m .ended = & now
264- }
265- return m .ended .Sub (m .started )
266- }
267-
268- return time .Since (m .started )
269- }
270-
27126var devCommand = cli.Command {
27227 Name : "preview" ,
27328 Aliases : []string {"dev" },
@@ -340,7 +95,7 @@ func runPreview(ctx context.Context, cmd *cli.Command) error {
34095 if cmd .IsSet ("target" ) {
34196 selectedTargets = cmd .StringSlice ("target" )
34297 for _ , target := range selectedTargets {
343- if ! isValidTarget (targetInfos , target ) {
98+ if ! isValidTarget (targetInfos , stainless . Target ( target ) ) {
34499 return fmt .Errorf ("invalid language target: %s" , target )
345100 }
346101 }
@@ -364,15 +119,15 @@ func runPreview(ctx context.Context, cmd *cli.Command) error {
364119 for {
365120 // Make the user get past linter errors
366121 if err := runLinter (ctx , cmd , true ); err != nil {
367- if errors .Is (err , ErrUserCancelled ) {
122+ if errors .Is (err , build . ErrUserCancelled ) {
368123 return nil
369124 }
370125 return err
371126 }
372127
373128 // Start the build process
374129 if err := runDevBuild (ctx , cc , cmd , selectedBranch , targets ); err != nil {
375- if errors .Is (err , ErrUserCancelled ) {
130+ if errors .Is (err , build . ErrUserCancelled ) {
376131 return nil
377132 }
378133 return err
@@ -464,22 +219,34 @@ func runDevBuild(ctx context.Context, cc *apiCommandContext, cmd *cli.Command, b
464219 AllowEmpty : stainless .Bool (true ),
465220 }
466221
467- model := NewBuildModel (cc , ctx , branch , func () (* stainless.Build , error ) {
468- build , err := cc .client .Builds .New (ctx , buildReq , option .WithMiddleware (cc .AsMiddleware ()))
469- if err != nil {
470- return nil , fmt .Errorf ("failed to create build: %v" , err )
471- }
472- return build , err
473- })
222+ downloads := make (map [stainless.Target ]string )
223+ for targetName , targetConfig := range cc .workspaceConfig .Targets {
224+ downloads [stainless .Target (targetName )] = targetConfig .OutputPath
225+ }
226+
227+ model := dev .NewModel (
228+ cc .client ,
229+ ctx ,
230+ branch ,
231+ func () (* stainless.Build , error ) {
232+ build , err := cc .client .Builds .New (ctx , buildReq , option .WithMiddleware (cc .AsMiddleware ()))
233+ if err != nil {
234+ return nil , fmt .Errorf ("failed to create build: %v" , err )
235+ }
236+ return build , err
237+ },
238+ downloads ,
239+ cmd .Bool ("watch" ),
240+ )
474241
475242 p := tea .NewProgram (model )
476243 finalModel , err := p .Run ()
477244
478245 if err != nil {
479246 return fmt .Errorf ("failed to run TUI: %v" , err )
480247 }
481- if buildModel , ok := finalModel .(BuildModel ); ok {
482- return buildModel .err
248+ if buildModel , ok := finalModel .(dev. Model ); ok {
249+ return buildModel .Err
483250 }
484251 return nil
485252}
0 commit comments