Skip to content
Open
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: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -493,3 +493,5 @@ Test cases should cover:
4. **Performance**: Parallel execution speeds up test runs
5. **Debugging**: testify assertions and clear structure provide helpful failure messages
6. **Reliability**: Using `t.Context()` ensures proper test lifecycle management

IMPORTANT: when user requests to "use granary", run `granary` command before performing any other task.
110 changes: 110 additions & 0 deletions cmd/openapi/commands/openapi/analyze.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package openapi

import (
"fmt"
"io"
"os"

tea "github.com/charmbracelet/bubbletea"
"github.com/speakeasy-api/openapi/cmd/openapi/internal/analyze"
"github.com/speakeasy-api/openapi/cmd/openapi/internal/analyze/tui"
"github.com/spf13/cobra"
"golang.org/x/term"
)

var analyzeCmd = &cobra.Command{
Use: "analyze <file>",
Short: "Analyze schema complexity, cyclicality, and codegen difficulty",
Long: `Analyze an OpenAPI specification to understand schema complexity.

This command examines schema references to identify:
- Cycles and strongly connected components (SCCs)
- Per-schema complexity metrics (fan-in, fan-out, nesting)
- Code generation difficulty tiers (green/yellow/red)
- Actionable refactoring suggestions

Output formats:
tui - Interactive terminal UI with progressive disclosure (default)
json - Machine-readable JSON report for CI/CD pipelines
text - Human-readable text summary
dot - Graphviz DOT format for graph visualization
mermaid - Mermaid diagram syntax

The TUI format auto-falls back to text when stdout is not a terminal.

Stdin is supported — pipe data or use '-':
cat spec.yaml | openapi spec analyze
cat spec.yaml | openapi spec analyze - --format json`,
Args: stdinOrFileArgs(1, 1),
RunE: runAnalyze,
}

func init() {
analyzeCmd.Flags().StringP("format", "f", "tui", "output format: tui, json, text, dot, mermaid")
analyzeCmd.Flags().StringP("output", "o", "", "write output to file instead of stdout")
}

func runAnalyze(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
inputFile := inputFileFromArgs(args)
format, _ := cmd.Flags().GetString("format")
outputFile, _ := cmd.Flags().GetString("output")

// Load the document
doc, err := loadOpenAPIDocument(ctx, inputFile)
if err != nil {
return err
}

// Run analysis
report := analyze.Analyze(ctx, doc)

// Auto-fallback: if format is TUI but stdout is not a terminal, use text
if format == "tui" && outputFile == "" && !term.IsTerminal(int(os.Stdout.Fd())) {
Comment on lines +62 to +63
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The auto-fallback for --format tui only checks whether stdout is a terminal. When the spec is read from stdin (e.g. cat spec.yaml | openapi spec analyze), stdin is not a TTY, so Bubble Tea won't receive keyboard input (stdin will be consumed/EOF) even though stdout is a TTY. To match the documented behavior, also detect non-TTY stdin / IsStdin(inputFile) and fall back to text (or explicitly open /dev/tty and pass it via tea.WithInput).

Suggested change
// Auto-fallback: if format is TUI but stdout is not a terminal, use text
if format == "tui" && outputFile == "" && !term.IsTerminal(int(os.Stdout.Fd())) {
// Auto-fallback: if format is TUI but stdout or stdin is not a terminal, use text
if format == "tui" && outputFile == "" &&
(!term.IsTerminal(int(os.Stdout.Fd())) || !term.IsTerminal(int(os.Stdin.Fd()))) {

Copilot uses AI. Check for mistakes.
format = "text"
}

// TUI is incompatible with --output; suggest text instead
if format == "tui" && outputFile != "" {
return fmt.Errorf("--output is not compatible with --format tui; use --format text, json, or dot instead")
}

// Open output writer
var w io.Writer = os.Stdout
if outputFile != "" {
f, err := os.Create(outputFile)
if err != nil {
return fmt.Errorf("failed to create output file: %w", err)
}
defer f.Close()
w = f
}

switch format {
case "tui":
m := tui.NewModel(report)
p := tea.NewProgram(m, tea.WithAltScreen())
if _, err := p.Run(); err != nil {
return fmt.Errorf("error running analyzer TUI: %w", err)
}
return nil

case "json":
return analyze.WriteJSON(w, report)

case "text":
analyze.WriteText(w, report)
return nil

case "dot":
analyze.WriteDOT(w, report)
return nil

case "mermaid":
analyze.WriteMermaid(w, report)
return nil

default:
return fmt.Errorf("unknown format: %s (expected tui, json, text, dot, or mermaid)", format)
}
}
1 change: 1 addition & 0 deletions cmd/openapi/commands/openapi/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ func Apply(rootCmd *cobra.Command) {
rootCmd.AddCommand(optimizeCmd)
rootCmd.AddCommand(localizeCmd)
rootCmd.AddCommand(exploreCmd)
rootCmd.AddCommand(analyzeCmd)
rootCmd.AddCommand(snipCmd)
}
3 changes: 2 additions & 1 deletion cmd/openapi/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/speakeasy-api/openapi/openapi/linter/customrules v0.0.0-20260217225223-7d484a30828f
github.com/spf13/cobra v1.10.1
github.com/stretchr/testify v1.11.1
golang.org/x/term v0.40.0
gopkg.in/yaml.v3 v3.0.1
)

Expand Down Expand Up @@ -44,6 +45,6 @@ require (
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
)
6 changes: 4 additions & 2 deletions cmd/openapi/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,10 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
Expand Down
Loading
Loading