256 lines
8.4 KiB
Go
256 lines
8.4 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package pipeline
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Phase represents a named stage in the CLI execution pipeline.
|
|
// Handlers are grouped by phase and executed in chain order within
|
|
// each phase. Phases themselves execute in a fixed order defined
|
|
// by the engine.
|
|
type Phase int
|
|
|
|
const (
|
|
// Register runs at CLI startup when the Cobra command tree is
|
|
// being built. Handlers can add, remove, or modify commands.
|
|
Register Phase = iota
|
|
|
|
// PreParse runs before Cobra parses the raw argv. Handlers
|
|
// receive the raw argument slice and can rewrite it — for
|
|
// example to fix flag-name typos or split glued values.
|
|
PreParse
|
|
|
|
// PostParse runs after Cobra has successfully parsed flags and
|
|
// args. Handlers receive structured parameters plus the tool
|
|
// schema, enabling value-level corrections such as date format
|
|
// normalisation.
|
|
PostParse
|
|
|
|
// PreRequest runs after validation, just before the JSON-RPC
|
|
// call is dispatched. Handlers can inspect or mutate the final
|
|
// payload.
|
|
PreRequest
|
|
|
|
// PostResponse runs after the transport returns a result and
|
|
// before the output is written to stdout.
|
|
PostResponse
|
|
)
|
|
|
|
// String returns the human-readable name of the phase.
|
|
func (p Phase) String() string {
|
|
switch p {
|
|
case Register:
|
|
return "register"
|
|
case PreParse:
|
|
return "pre-parse"
|
|
case PostParse:
|
|
return "post-parse"
|
|
case PreRequest:
|
|
return "pre-request"
|
|
case PostResponse:
|
|
return "post-response"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// phases returns all phases in execution order.
|
|
func phases() []Phase {
|
|
return []Phase{Register, PreParse, PostParse, PreRequest, PostResponse}
|
|
}
|
|
|
|
// Context carries mutable state through the handler chain. Each phase
|
|
// populates additional fields; earlier-phase fields remain available in
|
|
// later phases so that handlers can correlate raw input with structured
|
|
// parameters.
|
|
type Context struct {
|
|
// Args is the raw argv slice (available from PreParse onward).
|
|
// PreParse handlers may rewrite this in place.
|
|
Args []string
|
|
|
|
// Command identifies the resolved command. RunPreParse fills it with
|
|
// Cobra's raw CommandPath() (e.g. "dws chat message send-by-bot") so
|
|
// PreParse handlers can key per-command tables; the PostParse pipeline
|
|
// fills it with the resolved product.tool canonical path. The two phases
|
|
// use independent Context instances, so the differing forms never mix.
|
|
Command string
|
|
|
|
// Params holds structured key→value parameters after Cobra
|
|
// parsing (available from PostParse onward). Handlers may
|
|
// mutate values or add/remove keys.
|
|
Params map[string]any
|
|
|
|
// Schema is the JSON Schema for the resolved tool's input
|
|
// (available from PostParse onward). Handlers must treat this
|
|
// as read-only.
|
|
Schema map[string]any
|
|
|
|
// Payload is the merged, validated payload ready to be sent
|
|
// over the wire (available from PreRequest onward).
|
|
Payload map[string]any
|
|
|
|
// Response is the JSON-RPC result returned by the server
|
|
// (available from PostResponse onward). Handlers may mutate
|
|
// the response before it is written to stdout.
|
|
Response map[string]any
|
|
|
|
// FlagSpecs provides the list of known flag names for the
|
|
// current tool, derived from the input schema. PreParse
|
|
// handlers use this to match against raw argv tokens.
|
|
FlagSpecs []FlagInfo
|
|
|
|
// ProtectedFlags carries reviewed semantic guard decisions across the
|
|
// complete PreParse chain. Keys are morphed flag names. Sticky and fuzzy
|
|
// handlers must not reinterpret a name classified as blocked or ambiguous
|
|
// by the semantic alias table.
|
|
ProtectedFlags map[string]FlagProtection
|
|
|
|
// Corrections records every correction applied by handlers,
|
|
// enabling downstream logging and debugging.
|
|
Corrections []Correction
|
|
}
|
|
|
|
// FlagProtection identifies why an emitted flag name must not be automatically
|
|
// rewritten.
|
|
type FlagProtection string
|
|
|
|
const (
|
|
FlagProtectionBlocked FlagProtection = "blocked"
|
|
FlagProtectionAmbiguous FlagProtection = "ambiguous"
|
|
)
|
|
|
|
// ProtectFlag records a reviewed no-touch decision for the remainder of the
|
|
// current pipeline context.
|
|
func (c *Context) ProtectFlag(morphed string, protection FlagProtection) {
|
|
if c == nil || morphed == "" {
|
|
return
|
|
}
|
|
if c.ProtectedFlags == nil {
|
|
c.ProtectedFlags = make(map[string]FlagProtection)
|
|
}
|
|
c.ProtectedFlags[morphed] = protection
|
|
}
|
|
|
|
// IsFlagProtected reports whether a morphed flag name is guarded from further
|
|
// automatic interpretation.
|
|
func (c *Context) IsFlagProtected(morphed string) bool {
|
|
if c == nil {
|
|
return false
|
|
}
|
|
_, ok := c.ProtectedFlags[morphed]
|
|
return ok
|
|
}
|
|
|
|
// FlagConflictError is returned when multiple distinct spellings that reduce
|
|
// to one scalar canonical flag are present in the same argv. Rejecting the
|
|
// command makes the outcome independent of argument order.
|
|
type FlagConflictError struct {
|
|
Command string
|
|
Canonical string
|
|
Spellings []string
|
|
}
|
|
|
|
func (e *FlagConflictError) Error() string {
|
|
spellings := append([]string(nil), e.Spellings...)
|
|
sort.Strings(spellings)
|
|
for i := range spellings {
|
|
spellings[i] = "--" + strings.TrimPrefix(spellings[i], "--")
|
|
}
|
|
return fmt.Sprintf("conflicting parameter spellings for --%s on %q: %s; pass exactly one spelling", e.Canonical, e.Command, strings.Join(spellings, ", "))
|
|
}
|
|
|
|
// BoolValueConflictError is returned when one canonical boolean flag receives
|
|
// both true and false in the same argv. Rejecting contradictory values keeps
|
|
// the outcome independent of argument order while allowing repeated identical
|
|
// spellings to retain Cobra's native behaviour.
|
|
type BoolValueConflictError struct {
|
|
Command string
|
|
Flag string
|
|
Values []string
|
|
}
|
|
|
|
func (e *BoolValueConflictError) Error() string {
|
|
values := append([]string(nil), e.Values...)
|
|
sort.Strings(values)
|
|
return fmt.Sprintf("conflicting boolean values for --%s on %q: %s; pass exactly one value", strings.TrimPrefix(e.Flag, "--"), e.Command, strings.Join(values, ", "))
|
|
}
|
|
|
|
// FlagInfo describes a single CLI flag derived from a tool's input
|
|
// schema. PreParse handlers use this to recognise valid flag names
|
|
// when performing fuzzy matching or alias resolution.
|
|
type FlagInfo struct {
|
|
// Name is the canonical kebab-case flag name (e.g. "user-id").
|
|
Name string
|
|
|
|
// Shorthand is the optional single-character pflag shorthand (e.g. "y"
|
|
// for --yes). PreParse uses exact shorthand tokens when normalising
|
|
// explicit boolean values; shorthand clusters retain native pflag syntax.
|
|
Shorthand string
|
|
|
|
// PropertyName is the original schema property key (e.g. "userId").
|
|
PropertyName string
|
|
|
|
// Type is the JSON Schema / pflag type ("string", "integer",
|
|
// "bool", "stringSlice", "duration", etc.).
|
|
Type string
|
|
|
|
// Format carries the JSON Schema "format" hint when present —
|
|
// e.g. "date", "date-time", "duration", "email", "uri", "ipv4".
|
|
// PreParse handlers use this to decide whether a suffix in a
|
|
// glued token (e.g. "--starttime1") looks like a plausible value.
|
|
Format string
|
|
|
|
// Enum carries the JSON Schema "enum" string values when present.
|
|
// PreParse handlers use this for sticky-split validation: a glued
|
|
// suffix is accepted only if it matches one of the enum entries.
|
|
Enum []string
|
|
}
|
|
|
|
// Correction records a single input correction applied by a handler.
|
|
type Correction struct {
|
|
// Handler is the name of the handler that applied the correction.
|
|
Handler string
|
|
|
|
// Phase is the pipeline phase in which the correction occurred.
|
|
Phase Phase
|
|
|
|
// Field identifies the affected flag or parameter name.
|
|
Field string
|
|
|
|
// Original is the value before correction.
|
|
Original string
|
|
|
|
// Corrected is the value after correction.
|
|
Corrected string
|
|
|
|
// Kind classifies the correction (e.g. "alias", "sticky", "case").
|
|
Kind string
|
|
}
|
|
|
|
// AddCorrection appends a correction record to the context.
|
|
func (c *Context) AddCorrection(handler string, phase Phase, field, original, corrected, kind string) {
|
|
c.Corrections = append(c.Corrections, Correction{
|
|
Handler: handler,
|
|
Phase: phase,
|
|
Field: field,
|
|
Original: original,
|
|
Corrected: corrected,
|
|
Kind: kind,
|
|
})
|
|
}
|