Skip to content

Commit 7f6d000

Browse files
author
Bruce Hill
committed
Set up linting
1 parent 5e89b57 commit 7f6d000

File tree

4 files changed

+328
-9
lines changed

4 files changed

+328
-9
lines changed

cmd/stl-docs/main.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2+
3+
package main
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"log"
9+
"os"
10+
11+
"github.com/stainless-api/stainless-api-cli/pkg/cmd"
12+
docs "github.com/urfave/cli-docs/v3"
13+
14+
"github.com/urfave/cli/v3"
15+
)
16+
17+
func main() {
18+
app := &cli.Command{
19+
Name: "stl-docs",
20+
Usage: "Generate STL documentation in manpage and/or markdown formats",
21+
Flags: []cli.Flag{
22+
&cli.StringFlag{
23+
Name: "manpage",
24+
Usage: "generate manpage documentation `FILE`",
25+
Value: "stl.1",
26+
},
27+
&cli.StringFlag{
28+
Name: "markdown",
29+
Usage: "generate markdown documentation `FILE`",
30+
Value: "usage.md",
31+
},
32+
},
33+
Action: generateDocs,
34+
}
35+
36+
if err := app.Run(context.Background(), os.Args); err != nil {
37+
log.Fatal(err)
38+
}
39+
}
40+
41+
func generateDocs(ctx context.Context, c *cli.Command) error {
42+
manpageRequested := c.IsSet("manpage")
43+
markdownRequested := c.IsSet("markdown")
44+
45+
if !manpageRequested && !markdownRequested {
46+
return cli.ShowAppHelp(c)
47+
}
48+
49+
if manpageRequested {
50+
if err := generateManpage(c.String("manpage")); err != nil {
51+
return fmt.Errorf("failed to generate manpage: %w", err)
52+
}
53+
}
54+
55+
if markdownRequested {
56+
if err := generateMarkdown(c.String("markdown")); err != nil {
57+
return fmt.Errorf("failed to generate markdown: %w", err)
58+
}
59+
}
60+
61+
return nil
62+
}
63+
64+
func generateManpage(filename string) error {
65+
fmt.Printf("Generating manpage: %s\n", filename)
66+
67+
app := cmd.Command
68+
69+
manpage, err := docs.ToMan(&app)
70+
if err != nil {
71+
return err
72+
}
73+
74+
file, err := os.Create(filename)
75+
if err != nil {
76+
return err
77+
}
78+
defer file.Close()
79+
if _, err := file.WriteString(manpage); err != nil {
80+
panic(err)
81+
}
82+
return nil
83+
}
84+
85+
func generateMarkdown(filename string) error {
86+
fmt.Printf("Generating markdown: %s\n", filename)
87+
88+
app := cmd.Command
89+
90+
md, err := docs.ToMarkdown(&app)
91+
if err != nil {
92+
return err
93+
}
94+
95+
file, err := os.Create(filename)
96+
if err != nil {
97+
return err
98+
}
99+
defer file.Close()
100+
if _, err := file.WriteString("# Stainless CLI\n\n" + md); err != nil {
101+
return err
102+
}
103+
return nil
104+
}

pkg/cmd/cmd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ stl builds create --branch <branch>`,
162162

163163
&devCommand,
164164

165+
&lintCommand,
166+
165167
{
166168
Name: "@manpages",
167169
Usage: "Generate documentation for 'man'",

pkg/cmd/dev.go

Lines changed: 155 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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
4447
type fetchDiagnosticsMsg []stainless.BuildDiagnostic
4548
type errorMsg error
4649
type downloadMsg stainless.Target
47-
type triggerNewBuildMsg struct{}
4850

4951
func 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

240242
var 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

356462
func 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

Comments
 (0)