@@ -2,11 +2,14 @@ package cmd
22
33import (
44 "context"
5- "encoding/json"
65 "fmt"
76 "os"
7+ "time"
88
9- "github.com/tidwall/gjson"
9+ "github.com/charmbracelet/bubbles/spinner"
10+ tea "github.com/charmbracelet/bubbletea"
11+ "github.com/charmbracelet/lipgloss"
12+ "github.com/stainless-api/stainless-api-go"
1013 "github.com/urfave/cli/v3"
1114)
1215
@@ -38,48 +41,218 @@ var lintCommand = cli.Command{
3841 Action : runLinter ,
3942}
4043
41- type GenerateSpecParams struct {
42- Project string `json:"project"`
43- Source struct {
44- Type string `json:"type"`
45- OpenAPISpec string `json:"openapi_spec"`
46- StainlessConfig string `json:"stainless_config"`
47- } `json:"source"`
44+ type lintModel struct {
45+ spinner spinner.Model
46+ diagnostics []stainless.BuildDiagnostic
47+ error error
48+ watching bool
49+ watchedFiles map [string ]time.Time
50+ ctx context.Context
51+ cmd * cli.Command
52+ cc * apiCommandContext
53+ stopPolling chan struct {}
54+ help helpModel
55+ }
56+
57+ type helpModel struct {
58+ width int
59+ height int
60+ }
61+
62+ type fileChangedEvent struct {}
63+
64+ func waitForFileChanges (m lintModel ) tea.Cmd {
65+ return func () tea.Msg {
66+ ticker := time .NewTicker (250 * time .Millisecond )
67+ defer ticker .Stop ()
68+
69+ for {
70+ select {
71+ case <- ticker .C :
72+ for file , lastModTime := range m .watchedFiles {
73+ stat , err := os .Stat (file )
74+ if err != nil {
75+ continue
76+ }
77+
78+ if stat .ModTime ().After (lastModTime ) {
79+ m .watchedFiles [file ] = stat .ModTime ()
80+ return fileChangedEvent {}
81+ }
82+ }
83+ case <- m .stopPolling :
84+ return nil
85+ }
86+ }
87+ }
88+ }
89+
90+ func (m lintModel ) Init () tea.Cmd {
91+ return m .spinner .Tick
92+ }
93+
94+ func (m lintModel ) Update (msg tea.Msg ) (tea.Model , tea.Cmd ) {
95+ switch msg := msg .(type ) {
96+ case tea.KeyMsg :
97+ if msg .String () == "ctrl+c" {
98+ return m , tea .Quit
99+ }
100+
101+ case diagnosticsMsg :
102+ m .diagnostics = msg .diagnostics
103+ m .error = msg .err
104+ m .ctx = msg .ctx
105+ m .cmd = msg .cmd
106+ m .cc = msg .cc
107+
108+ if m .watching {
109+ return m , waitForFileChanges (m )
110+ }
111+ return m , tea .Quit
112+
113+ case fileChangedEvent :
114+ m .diagnostics = nil // Clear diagnostics while linting
115+ return m , getDiagnosticsCmd (m .ctx , m .cmd , m .cc )
116+
117+ case spinner.TickMsg :
118+ var cmd tea.Cmd
119+ m .spinner , cmd = m .spinner .Update (msg )
120+ return m , cmd
121+
122+ case tea.WindowSizeMsg :
123+ m .help .width = msg .Width
124+ m .help .height = msg .Height
125+ return m , nil
126+ }
127+
128+ return m , nil
129+ }
130+
131+ func (m lintModel ) View () string {
132+ if m .error != nil {
133+ return fmt .Sprintf ("Error: %s\n " , m .error )
134+ }
135+
136+ var content string
137+ if m .diagnostics == nil {
138+ content = m .spinner .View () + " Linting"
139+ } else {
140+ content = ViewDiagnosticsPrint (m .diagnostics , - 1 )
141+ if m .watching {
142+ content += "\n " + m .spinner .View () + " Waiting for configuration changes"
143+ }
144+ }
145+
146+ // Add help menu
147+ helpStyle := lipgloss .NewStyle ().
148+ Foreground (lipgloss .Color ("241" )).
149+ Margin (1 , 0 , 0 , 0 )
150+
151+ helpText := helpStyle .Render ("Press Ctrl+C to exit" )
152+
153+ return content + helpText
154+ }
155+
156+ type diagnosticsMsg struct {
157+ diagnostics []stainless.BuildDiagnostic
158+ err error
159+ ctx context.Context
160+ cmd * cli.Command
161+ cc * apiCommandContext
162+ }
163+
164+ func getDiagnosticsCmd (ctx context.Context , cmd * cli.Command , cc * apiCommandContext ) tea.Cmd {
165+ return func () tea.Msg {
166+ diagnostics , err := getDiagnostics (ctx , cmd , cc )
167+ return diagnosticsMsg {
168+ diagnostics : diagnostics ,
169+ err : err ,
170+ ctx : ctx ,
171+ cmd : cmd ,
172+ cc : cc ,
173+ }
174+ }
48175}
49176
50177func runLinter (ctx context.Context , cmd * cli.Command ) error {
51178 cc := getAPICommandContext (cmd )
52- for {
53- diagnostics , err := getDiagnostics (ctx , cmd , cc )
54- if err != nil {
179+
180+ s := spinner .New ()
181+ s .Spinner = spinner .MiniDot
182+ s .Style = lipgloss .NewStyle ().Foreground (lipgloss .Color ("208" ))
183+
184+ m := lintModel {
185+ spinner : s ,
186+ watching : cmd .Bool ("watch" ),
187+ ctx : ctx ,
188+ cmd : cmd ,
189+ cc : cc ,
190+ watchedFiles : make (map [string ]time.Time ),
191+ stopPolling : make (chan struct {}),
192+ help : helpModel {},
193+ }
194+
195+ if m .watching {
196+ if err := setupFileWatcher (& m , cmd , cc ); err != nil {
55197 return err
56198 }
199+ }
57200
58- if cmd .IsSet ("format" ) {
59- rawJson , err := json .Marshal (diagnostics )
60- if err != nil {
61- return err
62- }
63- jsonObj := gjson .Parse (string (rawJson ))
64- if err := ShowJSON ("Diagnostics" , jsonObj , cmd .String ("format" ), "" ); err != nil {
65- return err
66- }
67- } else {
68- fmt .Println (ViewDiagnosticsPrint (diagnostics , - 1 ))
69- }
201+ p := tea .NewProgram (m , tea .WithContext (ctx ))
70202
71- if cmd .Bool ("watch" ) {
72- fmt .Println ("\n Diagnostic checks will re-run once you edit your configuration files..." )
73- if err := waitTillConfigChanges (ctx , cmd , cc ); err != nil {
74- return err
75- }
76- } else {
77- if hasBlockingDiagnostic (diagnostics ) {
78- os .Exit (1 )
79- }
80- break
81- }
203+ // Start the diagnostics process
204+ go func () {
205+ time .Sleep (100 * time .Millisecond ) // Small delay to let the UI initialize
206+ p .Send (getDiagnosticsCmd (ctx , cmd , cc )())
207+ }()
208+
209+ model , err := p .Run ()
210+ if err != nil {
211+ return err
82212 }
83213
214+ finalModel := model .(lintModel )
215+ if finalModel .stopPolling != nil {
216+ close (finalModel .stopPolling )
217+ }
218+
219+ if finalModel .error != nil {
220+ return finalModel .error
221+ }
222+
223+ // If not in watch mode and we have blocking diagnostics, exit with error code
224+ if ! finalModel .watching && hasBlockingDiagnostic (finalModel .diagnostics ) {
225+ os .Exit (1 )
226+ }
227+
228+ return nil
229+ }
230+
231+ func setupFileWatcher (m * lintModel , cmd * cli.Command , cc * apiCommandContext ) error {
232+ // Watch OpenAPI spec file
233+ openapiSpecPath := cc .workspaceConfig .OpenAPISpec
234+ if cmd .IsSet ("openapi-spec" ) {
235+ openapiSpecPath = cmd .String ("openapi-spec" )
236+ }
237+
238+ if err := addFileToWatch (m , openapiSpecPath ); err != nil {
239+ return err
240+ }
241+
242+ // Watch Stainless config file
243+ stainlessConfigPath := cc .workspaceConfig .StainlessConfig
244+ if cmd .IsSet ("stainless-config" ) {
245+ stainlessConfigPath = cmd .String ("stainless-config" )
246+ }
247+
248+ return addFileToWatch (m , stainlessConfigPath )
249+ }
250+
251+ func addFileToWatch (m * lintModel , path string ) error {
252+ stat , err := os .Stat (path )
253+ if err != nil {
254+ return fmt .Errorf ("failed to get file info for %s: %v" , path , err )
255+ }
256+ m .watchedFiles [path ] = stat .ModTime ()
84257 return nil
85258}
0 commit comments