// Copyright 2026 Alibaba Group // Licensed under the Apache License, Version 2.0 (the "License"); package output import ( "context" "fmt" "reflect" "strings" "sync" "github.com/spf13/cobra" ) // CommandResult is the immutable framework result consumed by CLI and MCP // adapters. The unexported envelope method prevents product packages from // implementing ad-hoc result shapes. type CommandResult interface { Outcome() Outcome ExitCode() int envelope() *Envelope } type commandResult struct { env Envelope exitCode int exitCodeOverride bool } func (r *commandResult) Outcome() Outcome { return r.env.Outcome } func (r *commandResult) ExitCode() int { return r.exitCode } func (r *commandResult) envelope() *Envelope { copy := cloneEnvelope(r.env) return © } // ResultOption enriches a result without exposing mutable framework fields. type ResultOption struct{ apply func(*Envelope) } func WithIdentity(identity string) ResultOption { return ResultOption{apply: func(env *Envelope) { env.Identity = identity }} } func WithDryRun() ResultOption { return ResultOption{apply: func(env *Envelope) { env.DryRun = true }} } func WithMeta(meta *Meta) ResultOption { return ResultOption{apply: func(env *Envelope) { env.Meta = cloneMeta(meta) }} } // Success constructs an immutable success result. func Success(data any, opts ...ResultOption) CommandResult { return newCommandResult(OutcomeSuccess, data, nil, opts...) } // Pending constructs an immutable accepted-but-not-terminal result. func Pending(data any, operation *OperationInfo, opts ...ResultOption) CommandResult { opts = append(opts, ResultOption{apply: func(env *Envelope) { if env.Meta == nil { env.Meta = &Meta{} } env.Meta.Operation = operation }}) return newCommandResult(OutcomePending, data, nil, opts...) } // Partial constructs an immutable multi-status result with exit code 7. func Partial(data *PartialData, opts ...ResultOption) CommandResult { return newCommandResult(OutcomePartialFailure, data, nil, opts...) } // Failure constructs an immutable typed failure result. func Failure(info *ErrorInfo, opts ...ResultOption) CommandResult { if info != nil { info = cloneErrorInfo(info) // Product code does not own process status. The framework derives it // from type/subtype so wire and process status cannot drift. info.ExitCode = 0 } return newCommandResult(OutcomeFailure, nil, info, opts...) } // FailureWithExitCode adapts an existing repository error whose historical // process status is part of a compatibility contract (for example SIGINT=130). // Product handlers must use Failure; only the root legacy-error adapter should // call this function. func FailureWithExitCode(info *ErrorInfo, exitCode int, opts ...ResultOption) CommandResult { if info != nil { info = cloneErrorInfo(info) info.ExitCode = 0 } return newCommandResultWithExitCode(OutcomeFailure, nil, info, exitCode, true, opts...) } func newCommandResult(outcome Outcome, data any, info *ErrorInfo, opts ...ResultOption) CommandResult { return newCommandResultWithExitCode(outcome, data, info, 0, false, opts...) } func newCommandResultWithExitCode(outcome Outcome, data any, info *ErrorInfo, override int, hasOverride bool, opts ...ResultOption) CommandResult { env := Envelope{Outcome: outcome, Data: data, Error: info} env.OK = outcome == OutcomeSuccess || outcome == OutcomePending for _, opt := range opts { if opt.apply != nil { opt.apply(&env) } } exitCode := ExitCodeForEnvelope(&env) if hasOverride && override > 0 { exitCode = override } if env.Error != nil { env.Error.ExitCode = exitCode } env = cloneEnvelope(env) return &commandResult{env: env, exitCode: exitCode, exitCodeOverride: hasOverride} } func cloneEnvelope(source Envelope) Envelope { out := source out.Meta = cloneMeta(source.Meta) out.Error = cloneErrorInfo(source.Error) out.Data = cloneResultData(source.Data) out.Notice = cloneResultData(source.Notice) if partial, ok := source.Data.(*PartialData); ok && partial != nil { copyPartial := &PartialData{ Total: partial.Total, Succeeded: make([]any, len(partial.Succeeded)), Failed: make([]PartialFailedEntry, len(partial.Failed)), Unknown: make([]PartialUnknownEntry, len(partial.Unknown)), } copy(copyPartial.Failed, partial.Failed) copy(copyPartial.Unknown, partial.Unknown) for i := range partial.Succeeded { copyPartial.Succeeded[i] = cloneResultData(partial.Succeeded[i]) } for i := range copyPartial.Failed { copyPartial.Failed[i].Error = cloneErrorInfo(copyPartial.Failed[i].Error) } out.Data = copyPartial } return out } func cloneMeta(source *Meta) *Meta { if source == nil { return nil } meta := *source if source.Count != nil { count := *source.Count meta.Count = &count } if source.Operation != nil { op := *source.Operation meta.Operation = &op } if source.Pagination != nil { pagination := *source.Pagination meta.Pagination = &pagination } return &meta } func cloneResultData(value any) any { if value == nil { return nil } return cloneReflectValue(reflect.ValueOf(value), make(map[cloneVisit]reflect.Value)).Interface() } type cloneVisit struct { typ reflect.Type ptr uintptr len int cap int } func cloneReflectValue(value reflect.Value, seen map[cloneVisit]reflect.Value) reflect.Value { if !value.IsValid() { return value } switch value.Kind() { case reflect.Interface: if value.IsNil() { return reflect.Zero(value.Type()) } cloned := cloneReflectValue(value.Elem(), seen) out := reflect.New(value.Type()).Elem() out.Set(cloned) return out case reflect.Pointer: if value.IsNil() { return reflect.Zero(value.Type()) } visit := cloneVisit{typ: value.Type(), ptr: value.Pointer()} if cloned, ok := seen[visit]; ok { return cloned } out := reflect.New(value.Type().Elem()) seen[visit] = out out.Elem().Set(cloneReflectValue(value.Elem(), seen)) return out case reflect.Map: if value.IsNil() { return reflect.Zero(value.Type()) } visit := cloneVisit{typ: value.Type(), ptr: value.Pointer()} if cloned, ok := seen[visit]; ok { return cloned } out := reflect.MakeMapWithSize(value.Type(), value.Len()) seen[visit] = out iter := value.MapRange() for iter.Next() { out.SetMapIndex(cloneReflectValue(iter.Key(), seen), cloneReflectValue(iter.Value(), seen)) } return out case reflect.Slice: if value.IsNil() { return reflect.Zero(value.Type()) } visit := cloneVisit{typ: value.Type(), ptr: value.Pointer(), len: value.Len(), cap: value.Cap()} if cloned, ok := seen[visit]; ok { return cloned } out := reflect.MakeSlice(value.Type(), value.Len(), value.Len()) seen[visit] = out for i := 0; i < value.Len(); i++ { out.Index(i).Set(cloneReflectValue(value.Index(i), seen)) } return out case reflect.Array: out := reflect.New(value.Type()).Elem() for i := 0; i < value.Len(); i++ { out.Index(i).Set(cloneReflectValue(value.Index(i), seen)) } return out case reflect.Struct: out := reflect.New(value.Type()).Elem() out.Set(value) for i := 0; i < value.NumField(); i++ { if value.Type().Field(i).PkgPath == "" && out.Field(i).CanSet() { out.Field(i).Set(cloneReflectValue(value.Field(i), seen)) } } return out default: return value } } func cloneErrorInfo(source *ErrorInfo) *ErrorInfo { if source == nil { return nil } out := *source out.UpstreamCode = cloneResultData(source.UpstreamCode) out.Params = append([]string(nil), source.Params...) out.Actions = append([]string(nil), source.Actions...) out.AvailableFlags = append([]string(nil), source.AvailableFlags...) if source.ExecutionStarted != nil { started := *source.ExecutionStarted out.ExecutionStarted = &started } if source.Details != nil { out.Details = cloneResultData(source.Details).(map[string]any) } out.RPCData = cloneResultData(source.RPCData) if source.RetryAfterSeconds != nil { seconds := *source.RetryAfterSeconds out.RetryAfterSeconds = &seconds } return &out } // ValidateResult is the mandatory pre-emission policy boundary. func ValidateResult(result CommandResult) error { if result == nil { return fmt.Errorf("output: nil command result") } env := result.envelope() if err := env.Validate(); err != nil { return err } if env.Outcome == OutcomePending { if env.Meta == nil || env.Meta.Operation == nil { return fmt.Errorf("output: pending result requires meta.operation") } op := env.Meta.Operation if strings.TrimSpace(op.ID) == "" { return fmt.Errorf("output: pending result requires operation.id") } if strings.TrimSpace(op.State) == "" { return fmt.Errorf("output: pending result requires operation.state") } if strings.TrimSpace(op.NextCommand) == "" { return fmt.Errorf("output: pending result requires operation.next_command") } } if env.Outcome == OutcomePartialFailure { if partial, ok := env.Data.(*PartialData); !ok || partial == nil { return fmt.Errorf("output: partial_failure result requires typed *PartialData") } } concrete, _ := result.(*commandResult) want := ExitCodeForEnvelope(env) if concrete == nil || !concrete.exitCodeOverride { if got := result.ExitCode(); got != want { return fmt.Errorf("output: result exit code=%d disagrees with outcome-derived code=%d", got, want) } } else if result.ExitCode() <= 0 { return fmt.Errorf("output: explicit compatibility exit code must be positive") } if env.Error != nil && env.Error.ExitCode != result.ExitCode() { return fmt.Errorf("output: error.exit_code=%d disagrees with process exit code=%d", env.Error.ExitCode, result.ExitCode()) } return nil } // ResultStore transfers a successful in-memory result through Cobra without // pretending it is an error. The root PersistentPostRunE emits the result // before closing output sinks, so normal cleanup always runs. type ResultStore struct { mu sync.Mutex result CommandResult emitAttempted bool bytesRisk bool emitted bool exitCode int } type resultStoreContextKey struct{} func WithResultStore(ctx context.Context) (context.Context, *ResultStore) { if ctx == nil { ctx = context.Background() } if store, ok := resultStoreFromContext(ctx); ok { return ctx, store } store := &ResultStore{} return context.WithValue(ctx, resultStoreContextKey{}, store), store } func resultStoreFromContext(ctx context.Context) (*ResultStore, bool) { if ctx == nil { return nil, false } store, ok := ctx.Value(resultStoreContextKey{}).(*ResultStore) return store, ok && store != nil } // ResetResultStore starts a new command execution on a reusable Cobra tree. // // NewRootCommand installs one store in the root context so process-level // signal and exit-code tracking can retain a stable pointer. Library callers, // however, may call ExecuteC more than once on that same root. Resetting the // execution fields before PersistentPreRunE prevents a previous result or // emission attempt from leaking into the next invocation while preserving the // store identity used by the process boundary. func ResetResultStore(ctx context.Context) error { store, ok := resultStoreFromContext(ctx) if !ok { return fmt.Errorf("output: command context has no result store; install output.WithResultStore at the root execution boundary before resetting it") } store.mu.Lock() defer store.mu.Unlock() store.result = nil store.emitAttempted = false store.bytesRisk = false store.emitted = false store.exitCode = 0 return nil } // StoreResult records exactly one terminal framework result for this command. func StoreResult(ctx context.Context, result CommandResult) error { if err := ValidateResult(result); err != nil { return err } store, ok := resultStoreFromContext(ctx) if !ok { return fmt.Errorf("output: command context has no result store; install output.WithResultStore at the root execution boundary before calling StoreResult") } store.mu.Lock() defer store.mu.Unlock() if store.result != nil { return fmt.Errorf("output: command produced more than one framework result") } store.result = result return nil } // EmitStoredResult is called from PersistentPostRunE before output cleanup. func EmitStoredResult(cmd *cobra.Command) (int, bool, error) { if cmd == nil { return 0, false, nil } store, ok := resultStoreFromContext(cmd.Context()) if !ok { return 0, false, nil } store.mu.Lock() defer store.mu.Unlock() if store.result == nil || store.emitAttempted { return store.exitCode, store.emitted, nil } if !UsesUnifiedResult(cmd) { return 0, false, fmt.Errorf("output: legacy command %q produced a unified framework result", cmd.CommandPath()) } store.emitAttempted = true store.exitCode = store.result.ExitCode() code, bytesRisk, err := emitResult(cmd, store.result) store.bytesRisk = bytesRisk if err != nil { store.exitCode = exitCodeInternal if !bytesRisk { // emitResult renders into memory before touching stdout. A rendering // failure therefore has no externally visible bytes and can safely be // replaced by one minimal typed failure. Write failures keep // bytesRisk=true and must never attempt a second primary result. fallback := Failure(&ErrorInfo{ Type: "internal", Subtype: "result_encoding_failed", Message: "failed to encode command result", TechnicalDetail: err.Error(), }) fallbackCode, fallbackBytesRisk, fallbackErr := emitResult(cmd, fallback) store.bytesRisk = fallbackBytesRisk if fallbackErr == nil { store.exitCode = fallbackCode store.emitted = true return fallbackCode, true, nil } return store.exitCode, false, fmt.Errorf("output: render command result: %v; emit fallback: %w", err, fallbackErr) } // The writer may have consumed bytes even when it returned an error. // Preserve the single-primary-result invariant and surface the write // failure only as a diagnostic plus the stored internal exit code. return store.exitCode, false, err } store.exitCode = code store.emitted = true return code, true, nil } // StoredEmissionState reports whether result emission was attempted. Once an // attempt starts, callers must not try another envelope because a writer may // have accepted bytes even when it returned an error. func StoredEmissionState(store *ResultStore) (exitCode int, attempted, emitted, bytesRisk bool) { if store == nil { return 0, false, false, false } store.mu.Lock() defer store.mu.Unlock() return store.exitCode, store.emitAttempted, store.emitted, store.bytesRisk } // StoredExitCode returns the code produced by PersistentPostRunE. func StoredExitCode(store *ResultStore) (int, bool) { if store == nil { return 0, false } store.mu.Lock() defer store.mu.Unlock() return store.exitCode, store.emitted } // EnvelopeFromResult returns a detached envelope copy for adapters/tests. func EnvelopeFromResult(result CommandResult) (*Envelope, error) { if err := ValidateResult(result); err != nil { return nil, err } return result.envelope(), nil }