feat(cli): add opt-in terminal scan output#24
Open
HEETMEHTA18 wants to merge 2 commits into
Open
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a new terminal output mode that consumes the existing NDJSON record stream and renders a human-readable terminal report at the end of a scan.
Changes:
- Introduces
internal/output/terminalsink.goto buffer NDJSON records and render a formatted report (tables, colors, limited findings). - Extends CLI sink selection to support
--output=terminaland adds--terminalconvenience flag. - Adds a stderr spinner and updates README docs for interactive terminal usage.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| internal/output/terminalsink.go | New sink implementation that parses NDJSON records and prints a formatted terminal report on close. |
| cmd/bumblebee/sink.go | Adds terminal as a supported --output destination. |
| cmd/bumblebee/main.go | Adds --terminal flag, suppresses NDJSON diagnostics in terminal mode, and runs a stderr spinner. |
| README.md | Documents terminal output mode behavior (summary + spinner). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+151
to
+158
| summary := t.summary | ||
| if summary == nil { | ||
| summary = &model.ScanSummary{ | ||
| Status: model.ScanStatusComplete, | ||
| PackageRecordsEmitted: t.totalPackages(), | ||
| FindingsEmitted: len(t.findings), | ||
| } | ||
| } |
| } | ||
|
|
||
| if len(t.findings) == 0 { | ||
| return t.writeLine("No findings matched the supplied exposure catalog.") |
Comment on lines
+135
to
+136
| t.severityCounts[finding.Severity]++ | ||
| t.findings = append(t.findings, finding) |
Comment on lines
+230
to
+232
| if o.outputDest == "terminal" { | ||
| diagW = io.Discard | ||
| } |
| spinnerDone := make(chan struct{}) | ||
| spinnerFinished := make(chan struct{}) | ||
| if o.outputDest == "terminal" { | ||
| go runSpinner(os.Stderr, spinnerDone, spinnerFinished) |
Comment on lines
+277
to
+278
| spinnerFinished := make(chan struct{}) | ||
| if o.outputDest == "terminal" && isInteractiveTerminal(os.Stderr) { |
Comment on lines
+296
to
+299
| if o.outputDest == "terminal" { | ||
| close(spinnerDone) | ||
| <-spinnerFinished | ||
| } |
| } | ||
|
|
||
| if err := t.renderKeyValueTable("Run Summary", []kvRow{ | ||
| {"Status", t.styleStatus(summary.Status)}, |
Comment on lines
+136
to
+157
| t.severityCounts[finding.Severity]++ | ||
| t.findingCount++ | ||
| t.findings = append(t.findings, finding) | ||
| sort.SliceStable(t.findings, func(i, j int) bool { | ||
| li, lj := severityRank(t.findings[i].Severity), severityRank(t.findings[j].Severity) | ||
| if li != lj { | ||
| return li < lj | ||
| } | ||
| if t.findings[i].PackageName != t.findings[j].PackageName { | ||
| return t.findings[i].PackageName < t.findings[j].PackageName | ||
| } | ||
| if t.findings[i].Version != t.findings[j].Version { | ||
| return t.findings[i].Version < t.findings[j].Version | ||
| } | ||
| if t.findings[i].SourceFile != t.findings[j].SourceFile { | ||
| return t.findings[i].SourceFile < t.findings[j].SourceFile | ||
| } | ||
| return t.findings[i].CatalogID < t.findings[j].CatalogID | ||
| }) | ||
| if len(t.findings) > terminalFindingLimit { | ||
| t.findings = append([]model.Finding(nil), t.findings[:terminalFindingLimit]...) | ||
| } |
Comment on lines
+386
to
+390
| type terminalDiagWriter struct { | ||
| out io.Writer | ||
| mu sync.Mutex | ||
| pending []byte | ||
| } |
Comment on lines
+399
to
+424
| w.pending = append(w.pending, p...) | ||
| for { | ||
| idx := bytes.IndexByte(w.pending, '\n') | ||
| if idx < 0 { | ||
| break | ||
| } | ||
| line := bytes.TrimSpace(w.pending[:idx]) | ||
| w.pending = w.pending[idx+1:] | ||
| if len(line) == 0 { | ||
| continue | ||
| } | ||
| var d model.Diagnostic | ||
| if err := json.Unmarshal(line, &d); err != nil { | ||
| _, _ = fmt.Fprintln(w.out, string(line)) | ||
| continue | ||
| } | ||
| msg := d.Message | ||
| if d.Path != "" { | ||
| msg = d.Path + ": " + msg | ||
| } | ||
| if d.Level != "" { | ||
| _, _ = fmt.Fprintf(w.out, "%s: %s\n", strings.ToUpper(d.Level), msg) | ||
| } else { | ||
| _, _ = fmt.Fprintln(w.out, msg) | ||
| } | ||
| } |
Comment on lines
+232
to
+235
| diagW := io.Writer(os.Stderr) | ||
| if o.outputDest == "terminal" { | ||
| diagW = newTerminalDiagWriter(os.Stderr) | ||
| } |
| return 0 | ||
| } | ||
|
|
||
| func runSpinner(w io.Writer, done <-chan struct{}, finished chan<- struct{}) { |
Comment on lines
+473
to
+488
| frames := []string{"|", "/", "-", "\\"} | ||
| ticker := time.NewTicker(120 * time.Millisecond) | ||
| defer ticker.Stop() | ||
| idx := 0 | ||
| _, _ = fmt.Fprint(w, "scanning ") | ||
| for { | ||
| select { | ||
| case <-done: | ||
| _, _ = fmt.Fprint(w, "\r") | ||
| _, _ = fmt.Fprintln(w, "scanning complete") | ||
| return | ||
| case <-ticker.C: | ||
| _, _ = fmt.Fprintf(w, "\rscanning %s", frames[idx%len(frames)]) | ||
| idx++ | ||
| } | ||
| } |
| _, _ = fmt.Fprintln(w, "scanning complete") | ||
| return | ||
| case <-ticker.C: | ||
| _, _ = fmt.Fprintf(w, "\rscanning %s", frames[idx%len(frames)]) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fixes #22
Adds an opt-in terminal mode for
bumblebee scanthat renders a human-readable summary, compact tables, and live scan feedback while keeping the default NDJSON output unchanged.Before / After
--terminalto get a readable summary and tables in the integrated terminal without affecting the default automation-friendly output.Validation
PATH="$HOME/.local/go1.25.4/bin:$PATH" go test ./...PATH="$HOME/.local/go1.25.4/bin:$PATH" go run ./cmd/bumblebee scan --profile baseline --terminalNotes