Files
dingtalk-workspace-cli/internal/pipeline/phase.go
6707e56f9c feat(cli): schema-aware sticky flag splitting and structured unknown-flag recovery (#272)
* chore: update coverage badge [skip ci]

* chore: update coverage badge [skip ci]

* chore: update coverage badge [skip ci]

* chore: update coverage badge [skip ci]

* chore: update coverage badge [skip ci]

* 1.0.19 changelog

* stick opt

* fix(cli): utf-8 safe sticky suffix guard + changelog (#272)

SuffixLooksLikeValue used to read suffix[0] (a single byte) for both the
uuid format branch and the fallback "is the first rune a letter?" check.
For multi-byte UTF-8 leading runes — common in dws because value text is
often Chinese — this picked up only the first byte (0xE0..0xF4 lead),
which is not a letter and not a hex digit, so the function silently
returned true and let glued tokens like --name<CJK> get split into
--name <CJK>... This switches both branches to utf8.DecodeRuneInString
and adds a utf8.RuneError guard so invalid UTF-8 input is rejected too.

Also locks down the new behaviour in CHANGELOG ## [Unreleased]:
- Changed: schema-aware sticky flag splitting
- Added: available_flags field on unknown-flag errors

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 10:56:02 +08:00

171 lines
5.5 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
// 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 is the resolved product.tool canonical path
// (available from PostParse onward).
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
// Corrections records every correction applied by handlers,
// enabling downstream logging and debugging.
Corrections []Correction
}
// 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
// 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,
})
}