Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Changed

- `--output json` is now a pure serialisation flag; it no longer forces non-interactive mode. Interactive pickers and prompts (e.g. `grant request get -o json` with no ID, `grant request submit -o json` without `--target`/`--role`) work in a TTY, writing prompts to stderr and JSON to stdout.

### Added

- `grant request` command group for managing access requests through the approval workflow
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ Custom `SCAAccessService` follows SDK conventions:

## JSON Output
- `--output` / `-o` persistent flag on root command: `text` (default) or `json`
- Validated in `PersistentPreRunE`; JSON mode forces `IsTerminalFunc` to return false (non-interactive)
- Validated in `PersistentPreRunE`. `--output json` is a pure serialisation flag; it does not affect interactivity — interactive prompts still run in a TTY and write to stderr while JSON goes to stdout
- `cmd/output.go` — `outputFormat` var, `isJSONOutput()`, `writeJSON(w, data)`
- `cmd/output_types.go` — JSON structs: `cloudElevationOutput`, `groupElevationJSON`, `sessionOutput`, `statusOutput`, `revocationOutput`, `favoriteOutput`, `awsCredentialOutput`, `accessRequestOutput`, `accessRequestListOutput`
- All commands support JSON: root elevation, `env`, `status`, `revoke`, `favorites list`, `request list`, `request get`, `request submit`, `request cancel`, `request approve`, `request reject`
Expand Down
3 changes: 0 additions & 3 deletions cmd/request_picker.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ var resolveRequestIDFn = resolveRequestIDInteractive
// shows the interactive picker, returning the chosen request ID.
func resolveRequestIDInteractive(ctx context.Context, svc accessRequestService, scope pickerScope) (string, error) {
if !ui.IsInteractive() {
if isJSONOutput() {
return "", errors.New("request ID is required with --output json; run `grant request list --output json` to find it")
}
return "", fmt.Errorf("%w; pass the request ID as a positional argument (run `grant request list` to find it)", ui.ErrNotInteractive)
}

Expand Down
22 changes: 0 additions & 22 deletions cmd/request_picker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,28 +45,6 @@ func TestResolveRequestIDInteractive_NonInteractive(t *testing.T) {
}
}

func TestResolveRequestIDInteractive_JSONMode(t *testing.T) {
withInteractiveTTY(t, false)
orig := outputFormat
outputFormat = "json"
t.Cleanup(func() { outputFormat = orig })

svc := &capturingMockAccessRequestService{}
_, err := resolveRequestIDInteractive(t.Context(), svc, pickerScope{emptyMsg: "access requests"})
if err == nil {
t.Fatal("expected error")
}
if errors.Is(err, ui.ErrNotInteractive) {
t.Errorf("JSON mode error should not wrap ErrNotInteractive")
}
if !strings.Contains(err.Error(), "--output json") {
t.Errorf("expected --output json hint, got %v", err)
}
if strings.Contains(err.Error(), "requires a terminal") {
t.Errorf("JSON mode error should not mention terminal: %v", err)
}
}

func TestResolveRequestIDInteractive_EmptyList(t *testing.T) {
withInteractiveTTY(t, true)

Expand Down
3 changes: 0 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,6 @@ Examples:
if outputFormat != "text" && outputFormat != "json" {
return fmt.Errorf("invalid output format %q: must be one of: text, json", outputFormat)
}
if isJSONOutput() {
ui.IsTerminalFunc = func(fd uintptr) bool { return false }
}
return nil
Comment on lines 101 to 104
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

Now that --output json no longer forces non-interactive mode, some commands can enter interactive flows while still expecting machine-readable output. At least cmd/revoke.go prints human text to stdout on the “no active sessions” and “revocation canceled” paths, which will break consumers expecting JSON when -o json is set. Consider auditing interactive commands so that in JSON mode either (a) non-JSON messages go to stderr and stdout remains valid JSON, or (b) cancellation/empty states return a JSON payload (or a non-zero error) instead of plain text.

Copilot uses AI. Check for mistakes.
},
RunE: runFn,
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

Behavior around --output json affecting interactivity is a cross-cutting contract change, but there isn’t a test guarding it anymore (the JSON-mode request picker test was removed). Add a regression test that executes a trivial command with --output json and asserts ui.IsTerminalFunc is not mutated / interactive detection remains unchanged, so future refactors don’t reintroduce global side effects.

Suggested change
RunE: runFn,
RunE: func(cmd *cobra.Command, args []string) error {
originalIsTerminalFunc := ui.IsTerminalFunc
defer func() {
ui.IsTerminalFunc = originalIsTerminalFunc
}()
return runFn(cmd, args)
},

Copilot uses AI. Check for mistakes.
Expand Down
Loading