@@ -4,16 +4,19 @@ import (
44 "context"
55 "crypto/rand"
66 "encoding/base64"
7+ "encoding/json"
78 "errors"
89 "fmt"
10+ "os"
911 "os/exec"
1012 "strings"
1113 "time"
1214
13- "github.com/charmbracelet/bubbletea"
15+ tea "github.com/charmbracelet/bubbletea"
1416 "github.com/charmbracelet/huh"
1517 "github.com/stainless-api/stainless-api-go"
1618 "github.com/stainless-api/stainless-api-go/option"
19+ "github.com/tidwall/gjson"
1720 "github.com/urfave/cli/v3"
1821)
1922
@@ -44,7 +47,6 @@ type fetchBuildMsg *stainless.Build
4447type fetchDiagnosticsMsg []stainless.BuildDiagnostic
4548type errorMsg error
4649type downloadMsg stainless.Target
47- type triggerNewBuildMsg struct {}
4850
4951func NewBuildModel (cc * apiCommandContext , ctx context.Context , branch string , fn func () (* stainless.Build , error )) BuildModel {
5052 return BuildModel {
@@ -238,8 +240,9 @@ func (m *BuildModel) getBuildDuration() time.Duration {
238240}
239241
240242var devCommand = cli.Command {
241- Name : "dev" ,
242- Usage : "Development mode with interactive build monitoring" ,
243+ Name : "preview" ,
244+ Aliases : []string {"dev" },
245+ Usage : "Development mode with interactive build monitoring" ,
243246 Flags : []cli.Flag {
244247 & cli.StringFlag {
245248 Name : "project" ,
@@ -256,15 +259,40 @@ var devCommand = cli.Command{
256259 Aliases : []string {"config" },
257260 Usage : "Path to Stainless config file" ,
258261 },
262+ & cli.BoolFlag {
263+ Name : "watch" ,
264+ Aliases : []string {"w" },
265+ Value : false ,
266+ Usage : "Run in 'watch' mode to loop and rebuild when files change." ,
267+ },
268+ & cli.BoolFlag {
269+ Name : "lint" ,
270+ Aliases : []string {"l" },
271+ Value : false ,
272+ Usage : "Only check for diagnostic errors without running a full build." ,
273+ },
274+ & cli.StringFlag {
275+ Name : "lint-format" ,
276+ Value : "pretty" ,
277+ Usage : "The format for the linter output." ,
278+ },
279+ & cli.BoolFlag {
280+ Name : "lint-first" ,
281+ Value : false ,
282+ Usage : "Run linting before starting the build process." ,
283+ },
259284 },
260- Action : func (ctx context.Context , cmd * cli.Command ) error {
261- return runDevMode (ctx , cmd )
262- },
285+ Action : runPreview ,
263286}
264287
265- func runDevMode (ctx context.Context , cmd * cli.Command ) error {
288+ func runPreview (ctx context.Context , cmd * cli.Command ) error {
266289 cc := getAPICommandContext (cmd )
267290
291+ // If only linting is requested, run in lint-only mode
292+ if cmd .Bool ("lint" ) {
293+ return runLintLoop (ctx , cmd )
294+ }
295+
268296 gitUser , err := getGitUsername ()
269297 if err != nil {
270298 Warn ("Couldn't get a git user: %s" , err )
@@ -343,14 +371,92 @@ func runDevMode(ctx context.Context, cmd *cli.Command) error {
343371
344372 // Phase 3: Start build and monitor progress in a loop
345373 for {
346- err := runDevBuild (ctx , cc , cmd , selectedBranch , targets )
374+ // Check if we should run linting before building
375+ if cmd .Bool ("lint-first" ) {
376+ diagnostics , err := getPreviewDiagnosticsJSON (ctx , cmd )
377+ if err != nil {
378+ if errors .Is (err , ErrUserCancelled ) {
379+ return nil
380+ }
381+ return err
382+ }
383+ ShowJSON ("Linting Results" , diagnostics .Raw , cmd .String ("lint-format" ))
384+ }
385+
386+ // Start the build process
387+ if err := runDevBuild (ctx , cc , cmd , selectedBranch , targets ); err != nil {
388+ if errors .Is (err , ErrUserCancelled ) {
389+ return nil
390+ }
391+ return err
392+ }
393+
394+ if ! cmd .Bool ("watch" ) {
395+ break
396+ }
397+ }
398+ return nil
399+ }
400+
401+ // runLintLoop handles linting in a loop for watch mode
402+ func runLintLoop (ctx context.Context , cmd * cli.Command ) error {
403+ // Get the initial file modification times
404+ openapiSpecPath := cmd .String ("openapi-spec" )
405+ stainlessConfigPath := cmd .String ("stainless-config" )
406+
407+ var openapiSpecLastModified , stainlessConfigLastModified time.Time
408+
409+ if openapiSpecStat , err := os .Stat (openapiSpecPath ); err == nil {
410+ openapiSpecLastModified = openapiSpecStat .ModTime ()
411+ }
412+
413+ if stainlessConfigStat , err := os .Stat (stainlessConfigPath ); err == nil {
414+ stainlessConfigLastModified = stainlessConfigStat .ModTime ()
415+ }
416+
417+ for {
418+ diagnostics , err := getPreviewDiagnosticsJSON (ctx , cmd )
347419 if err != nil {
348420 if errors .Is (err , ErrUserCancelled ) {
349421 return nil
350422 }
351423 return err
352424 }
425+ ShowJSON ("Diagnostics" , diagnostics .Raw , cmd .String ("lint-format" ))
426+
427+ if ! cmd .Bool ("watch" ) {
428+ break
429+ }
430+
431+ // Watch for file changes instead of sleeping
432+ for {
433+ time .Sleep (500 * time .Millisecond ) // Check every 500ms
434+
435+ // Check OpenAPI spec file
436+ if openapiSpecStat , err := os .Stat (openapiSpecPath ); err == nil {
437+ if openapiSpecStat .ModTime ().After (openapiSpecLastModified ) {
438+ openapiSpecLastModified = openapiSpecStat .ModTime ()
439+ break // File changed, run linting again
440+ }
441+ }
442+
443+ // Check Stainless config file
444+ if stainlessConfigStat , err := os .Stat (stainlessConfigPath ); err == nil {
445+ if stainlessConfigStat .ModTime ().After (stainlessConfigLastModified ) {
446+ stainlessConfigLastModified = stainlessConfigStat .ModTime ()
447+ break // File changed, run linting again
448+ }
449+ }
450+
451+ // Check for cancellation
452+ select {
453+ case <- ctx .Done ():
454+ return ctx .Err ()
455+ default :
456+ }
457+ }
353458 }
459+ return nil
354460}
355461
356462func runDevBuild (ctx context.Context , cc * apiCommandContext , cmd * cli.Command , branch string , languages []stainless.Target ) error {
@@ -420,3 +526,43 @@ func getCurrentGitBranch() (string, error) {
420526
421527 return branch , nil
422528}
529+
530+ func getPreviewDiagnosticsJSON (ctx context.Context , cmd * cli.Command ) (* gjson.Result , error ) {
531+ cc := getAPICommandContext (cmd )
532+ var specParams GenerateSpecParams
533+ specParams .Project = cmd .String ("project" )
534+ specParams .Source .Type = "upload"
535+
536+ stainlessConfig , err := os .ReadFile (cmd .String ("stainless-config" ))
537+ if err != nil {
538+ return nil , err
539+ }
540+ specParams .Source .StainlessConfig = string (stainlessConfig )
541+
542+ openAPISpec , err := os .ReadFile (cmd .String ("openapi-spec" ))
543+ if err != nil {
544+ return nil , err
545+ }
546+ specParams .Source .OpenAPISpec = string (openAPISpec )
547+
548+ var result []byte
549+ err = cc .client .Post (ctx , "api/generate/spec" , specParams , & result , option .WithMiddleware (cc .AsMiddleware ()))
550+ if err != nil {
551+ return nil , err
552+ }
553+ json := gjson .ParseBytes (result )
554+ diagnostics := json .Get ("spec.diagnostics.@values.@flatten.#.{code,level,ignored,endpoint,message,more,language,location,stainlessPath,oasRef,configRef}" )
555+ return & diagnostics , nil
556+ }
557+
558+ func getPreviewDiagnostics (ctx context.Context , cmd * cli.Command ) ([]stainless.BuildDiagnostic , error ) {
559+ jsonDiagnostics , err := getPreviewDiagnosticsJSON (ctx , cmd )
560+ if err != nil {
561+ return nil , err
562+ }
563+ var diagnostics []stainless.BuildDiagnostic
564+ if err := json .Unmarshal ([]byte (jsonDiagnostics .Raw ), & diagnostics ); err != nil {
565+ return nil , err
566+ }
567+ return diagnostics , err
568+ }
0 commit comments