Skip to content

feat(cli): add opt-in terminal scan output#24

Open
HEETMEHTA18 wants to merge 2 commits into
perplexityai:mainfrom
HEETMEHTA18:feat/terminal-cli-output
Open

feat(cli): add opt-in terminal scan output#24
HEETMEHTA18 wants to merge 2 commits into
perplexityai:mainfrom
HEETMEHTA18:feat/terminal-cli-output

Conversation

@HEETMEHTA18
Copy link
Copy Markdown

@HEETMEHTA18 HEETMEHTA18 commented May 26, 2026

fixes #22
Adds an opt-in terminal mode for bumblebee scan that renders a human-readable summary, compact tables, and live scan feedback while keeping the default NDJSON output unchanged.

Before / After

  • Before: scans were optimized for machine consumption only, which made interactive runs hard to read.
  • After: users can run --terminal to 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 ./...
  • Verified locally with PATH="$HOME/.local/go1.25.4/bin:$PATH" go run ./cmd/bumblebee scan --profile baseline --terminal

Notes

  • Default stdout behavior is unchanged.
  • The change is intentionally opt-in so it does not break existing NDJSON consumers.
9acc4c43-bbc5-4e80-8e0d-1cb7048749ba

Copilot AI review requested due to automatic review settings May 26, 2026 10:22
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.go to buffer NDJSON records and render a formatted report (tables, colors, limited findings).
  • Extends CLI sink selection to support --output=terminal and adds --terminal convenience 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),
}
}
Comment thread internal/output/terminalsink.go Outdated
}

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 thread cmd/bumblebee/main.go
Comment on lines +230 to +232
if o.outputDest == "terminal" {
diagW = io.Discard
}
Comment thread cmd/bumblebee/main.go
spinnerDone := make(chan struct{})
spinnerFinished := make(chan struct{})
if o.outputDest == "terminal" {
go runSpinner(os.Stderr, spinnerDone, spinnerFinished)
@HEETMEHTA18 HEETMEHTA18 requested a review from Copilot May 26, 2026 10:30
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 12 comments.

Comment thread cmd/bumblebee/main.go
Comment on lines +277 to +278
spinnerFinished := make(chan struct{})
if o.outputDest == "terminal" && isInteractiveTerminal(os.Stderr) {
Comment thread cmd/bumblebee/main.go
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 thread cmd/bumblebee/main.go
Comment on lines +386 to +390
type terminalDiagWriter struct {
out io.Writer
mu sync.Mutex
pending []byte
}
Comment thread cmd/bumblebee/main.go
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 thread cmd/bumblebee/main.go
Comment on lines +232 to +235
diagW := io.Writer(os.Stderr)
if o.outputDest == "terminal" {
diagW = newTerminalDiagWriter(os.Stderr)
}
Comment thread cmd/bumblebee/main.go
return 0
}

func runSpinner(w io.Writer, done <-chan struct{}, finished chan<- struct{}) {
Comment thread cmd/bumblebee/main.go
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++
}
}
Comment thread cmd/bumblebee/main.go
_, _ = fmt.Fprintln(w, "scanning complete")
return
case <-ticker.C:
_, _ = fmt.Fprintf(w, "\rscanning %s", frames[idx%len(frames)])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cli): add optional human-readable scan output

2 participants