refactor(leaf): declare params on LeafSpec; keep Call as execution
Lift business flags/const params out of Call/PostMount, add typed flag defaults and policy gates, and realign the RFC acceptance bar to "no Execute/Call body exists only to assemble params". Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -76,7 +76,7 @@ type FlagSpec struct {
|
||||
Name string // flag name (kebab-case)
|
||||
Usage string // registration usage text
|
||||
Kind FlagKind // value type, defaults to KindString
|
||||
Default string // registration default (only KindString uses it at registration; as a fallback-chain tail it applies to all kinds without shadowing aliases/env)
|
||||
Default string // registration default for every Kind; also the fallback-chain tail when aliases/env are empty
|
||||
|
||||
// Required, when true, validates a non-empty effective value in RunE. Plain
|
||||
// Required flags aggregate into a cmdutil.ValidateRequiredFlags-compatible
|
||||
@@ -91,8 +91,10 @@ type FlagSpec struct {
|
||||
Aliases []string // hidden aliases, registered with the main flag's Kind; used in order when the main flag is not explicitly provided
|
||||
EnvVar string // environment variable consulted when the effective value is empty (an integer flag's env value must be parseable)
|
||||
// ArgDefault covers the case where the registration default is empty but
|
||||
// toolArgs still needs a fallback; used as the arg value when the effective
|
||||
// value is empty.
|
||||
// toolArgs still needs a fallback. For KindString it is used when the
|
||||
// effective value is empty. For KindInt it is also the floor: when the
|
||||
// resolved integer is < 1, ArgDefault is emitted instead (cursor page-size
|
||||
// semantics).
|
||||
ArgDefault string
|
||||
// Bind is the toolArgs key; empty uses Name.
|
||||
Bind string
|
||||
@@ -181,12 +183,16 @@ type CommandSpec struct {
|
||||
Flags []FlagSpec
|
||||
Constraints []Constraint
|
||||
Risk Risk
|
||||
// ConstParams are fixed toolArgs merged after flag assembly (e.g. precheckOnly).
|
||||
// They are not flags and never satisfy Required.
|
||||
ConstParams map[string]any
|
||||
|
||||
// Validate is the cross-flag validation hook, run after required/constraint
|
||||
// checks and before args assembly; nil skips it.
|
||||
Validate func(cmd *cobra.Command, args []string) error
|
||||
// PostMount adjusts the built command after flag registration and before
|
||||
// RunE is set (Args/DisableAutoGenTag/annotate/…); always runs.
|
||||
// RunE is set (Args/DisableAutoGenTag/annotate/…); always runs. Business
|
||||
// flags belong in Flags, not here.
|
||||
PostMount func(cmd *cobra.Command)
|
||||
// RunE fully replaces the generated body (escape hatch).
|
||||
RunE func(cmd *cobra.Command, args []string) error
|
||||
@@ -326,6 +332,9 @@ func NewCommand(spec CommandSpec) *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for key, value := range spec.ConstParams {
|
||||
toolArgs[key] = value
|
||||
}
|
||||
if !ConfirmRisk(cmd, spec.Risk) {
|
||||
return apperrors.NewValidation("用户取消了操作")
|
||||
}
|
||||
@@ -373,15 +382,20 @@ func RegisterFlags(cmd *cobra.Command, flags []FlagSpec) {
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterFlag registers one flag by Kind; the registration default is only used
|
||||
// by KindString (other kinds treat Default purely as a fallback-chain tail,
|
||||
// matching the existing KindInt behavior).
|
||||
// RegisterFlag registers one flag by Kind. Default is applied at registration
|
||||
// for every kind so --help DefValue matches the declared fallback.
|
||||
func RegisterFlag(cmd *cobra.Command, kind FlagKind, name, def, usage string) {
|
||||
switch kind {
|
||||
case KindInt:
|
||||
cmd.Flags().Int(name, 0, usage)
|
||||
defInt := 0
|
||||
if def != "" {
|
||||
if v, err := strconv.Atoi(def); err == nil {
|
||||
defInt = v
|
||||
}
|
||||
}
|
||||
cmd.Flags().Int(name, defInt, usage)
|
||||
case KindBool:
|
||||
cmd.Flags().Bool(name, false, usage)
|
||||
cmd.Flags().Bool(name, def == "true", usage)
|
||||
case KindStringSlice:
|
||||
cmd.Flags().StringSlice(name, nil, usage)
|
||||
default:
|
||||
@@ -478,6 +492,14 @@ func BuildArgs(cmd *cobra.Command, flags []FlagSpec) (map[string]any, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// ArgDefault floors values < 1 (cursor page-size: 0/-1 → default).
|
||||
if v < 1 && flag.ArgDefault != "" {
|
||||
parsed, err := strconv.ParseInt(flag.ArgDefault, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("flag --%s: invalid ArgDefault %q", flag.Name, flag.ArgDefault)
|
||||
}
|
||||
v = parsed
|
||||
}
|
||||
// Keep "non-zero only" (putInt semantics).
|
||||
if v != 0 {
|
||||
toolArgs[bind] = int(v)
|
||||
|
||||
@@ -1001,3 +1001,67 @@ func TestCrossPlatformCoverageNewCommandOrchestrateHonorsConfirmation(t *testing
|
||||
t.Fatal("confirmed orchestrate did not run")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterFlagTypedDefaults(t *testing.T) {
|
||||
cmd := newTestCommand()
|
||||
RegisterFlag(cmd, KindInt, "page-size", "20", "page")
|
||||
RegisterFlag(cmd, KindBool, "flag", "true", "bool")
|
||||
if def, _ := cmd.Flags().GetInt("page-size"); def != 20 {
|
||||
t.Fatalf("int default = %d, want 20", def)
|
||||
}
|
||||
if def, _ := cmd.Flags().GetBool("flag"); !def {
|
||||
t.Fatal("bool default = false, want true")
|
||||
}
|
||||
if got := cmd.Flags().Lookup("page-size").DefValue; got != "20" {
|
||||
t.Fatalf("page-size DefValue = %q, want 20", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildArgsIntArgDefaultFloor(t *testing.T) {
|
||||
flags := []FlagSpec{{
|
||||
Name: "page-size", Usage: "P", Kind: KindInt,
|
||||
Default: "20", ArgDefault: "20", Bind: "pageSize",
|
||||
}}
|
||||
cmd := newTestCommand()
|
||||
RegisterFlags(cmd, flags)
|
||||
|
||||
args, err := BuildArgs(cmd, flags)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if args["pageSize"] != 20 {
|
||||
t.Fatalf("unset pageSize = %#v, want 20", args["pageSize"])
|
||||
}
|
||||
|
||||
_ = cmd.Flags().Set("page-size", "0")
|
||||
args, err = BuildArgs(cmd, flags)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if args["pageSize"] != 20 {
|
||||
t.Fatalf("zero pageSize = %#v, want floor 20", args["pageSize"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCommandMergesConstParams(t *testing.T) {
|
||||
var got map[string]any
|
||||
cmd := NewCommand(CommandSpec{
|
||||
Use: "pub",
|
||||
Flags: []FlagSpec{{Name: "id", Usage: "ID", Bind: "versionId", Trim: true}},
|
||||
ConstParams: map[string]any{"precheckOnly": false},
|
||||
Invoke: func(_ *Ctx, toolArgs map[string]any) error {
|
||||
got = toolArgs
|
||||
return nil
|
||||
},
|
||||
})
|
||||
_ = cmd.Flags().Set("id", "V1")
|
||||
if err := cmd.RunE(cmd, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got["versionId"] != "V1" {
|
||||
t.Fatalf("versionId = %#v", got["versionId"])
|
||||
}
|
||||
if got["precheckOnly"] != false {
|
||||
t.Fatalf("precheckOnly = %#v, want false", got["precheckOnly"])
|
||||
}
|
||||
}
|
||||
|
||||
+69
-127
@@ -249,7 +249,7 @@ func newDevAppEventListCommand(runner executor.Runner) *cobra.Command {
|
||||
Use: "list",
|
||||
Short: "查询应用已订阅的事件列表",
|
||||
Example: " dws dev app event list --unified-app-id UNIFIED_APP_ID --page-size 20 --format json",
|
||||
Tool: devAppEventListTool,
|
||||
Tool: devAppEventListTool,
|
||||
Flags: []LeafFlag{
|
||||
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
|
||||
{Name: "keyword", Usage: "事件搜索关键词,支持按事件码或事件名称模糊匹配", Bind: "keyword", Trim: true, OmitEmpty: true},
|
||||
@@ -258,12 +258,8 @@ func newDevAppEventListCommand(runner executor.Runner) *cobra.Command {
|
||||
_, err := requiredDevAppUnifiedID(cmd)
|
||||
return err
|
||||
},
|
||||
// cursor/pageSize 由 devAppApplyCursorParams 注入(page-size 默认 20、floor 20)。
|
||||
Call: devAppCallCursor(runner),
|
||||
PostMount: func(cmd *cobra.Command) {
|
||||
devAppLeafMeta(cmd, devAppEventListTool)
|
||||
registerDevAppCursorFlags(cmd)
|
||||
},
|
||||
Call: devAppCallCursor(runner),
|
||||
PostMount: devAppMetaCursor(devAppEventListTool),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -275,6 +271,7 @@ func newDevAppEventSubscribeCommand(runner executor.Runner) *cobra.Command {
|
||||
Tool: devAppEventSubscribeTool,
|
||||
Flags: []LeafFlag{
|
||||
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
|
||||
{Name: "event-codes", Usage: "事件码,多个用逗号或分号分隔", Bind: "eventCodes", Trim: true, Transform: transformDevAppListParam},
|
||||
},
|
||||
Validate: func(cmd *cobra.Command, args []string) error {
|
||||
if err := devAppRequireWriteGuard(cmd, "event subscribe"); err != nil {
|
||||
@@ -288,15 +285,8 @@ func newDevAppEventSubscribeCommand(runner executor.Runner) *cobra.Command {
|
||||
}
|
||||
return nil
|
||||
},
|
||||
// eventCodes 数组由 Call 解析注入(与手写 params["eventCodes"] 等价)。
|
||||
Call: func(cmd *cobra.Command, tool string, params map[string]any) error {
|
||||
params["eventCodes"] = parseDevAppListFlag(cmd, "event-codes")
|
||||
return runDevAppTool(runner, cmd, tool, params)
|
||||
},
|
||||
PostMount: func(cmd *cobra.Command) {
|
||||
devAppLeafMeta(cmd, devAppEventSubscribeTool)
|
||||
cmd.Flags().String("event-codes", "", "事件码,多个用逗号或分号分隔")
|
||||
},
|
||||
Call: devAppCall(runner),
|
||||
PostMount: devAppMeta(devAppEventSubscribeTool),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -308,6 +298,7 @@ func newDevAppEventUnsubscribeCommand(runner executor.Runner) *cobra.Command {
|
||||
Tool: devAppEventUnsubscribeTool,
|
||||
Flags: []LeafFlag{
|
||||
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
|
||||
{Name: "event-codes", Usage: "事件码,多个用逗号或分号分隔", Bind: "eventCodes", Trim: true, Transform: transformDevAppListParam},
|
||||
},
|
||||
Validate: func(cmd *cobra.Command, args []string) error {
|
||||
if err := devAppRequireWriteGuard(cmd, "event unsubscribe"); err != nil {
|
||||
@@ -321,14 +312,8 @@ func newDevAppEventUnsubscribeCommand(runner executor.Runner) *cobra.Command {
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Call: func(cmd *cobra.Command, tool string, params map[string]any) error {
|
||||
params["eventCodes"] = parseDevAppListFlag(cmd, "event-codes")
|
||||
return runDevAppTool(runner, cmd, tool, params)
|
||||
},
|
||||
PostMount: func(cmd *cobra.Command) {
|
||||
devAppLeafMeta(cmd, devAppEventUnsubscribeTool)
|
||||
cmd.Flags().String("event-codes", "", "事件码,多个用逗号或分号分隔")
|
||||
},
|
||||
Call: devAppCall(runner),
|
||||
PostMount: devAppMeta(devAppEventUnsubscribeTool),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -337,7 +322,7 @@ func newDevAppListCommand(runner executor.Runner) *cobra.Command {
|
||||
Use: "list",
|
||||
Short: "查询开放平台企业内部应用列表",
|
||||
Example: " dws dev app list --name DemoApp --page-size 20 --format json",
|
||||
Tool: devAppListTool,
|
||||
Tool: devAppListTool,
|
||||
Flags: []LeafFlag{
|
||||
{Name: "name", Usage: "应用名称关键词", Bind: "name", Trim: true, OmitEmpty: true, Aliases: []string{"keyword"}},
|
||||
{Name: "app-key", Usage: "按 appKey/clientId 过滤", Bind: "appKey", Trim: true, OmitEmpty: true},
|
||||
@@ -349,13 +334,8 @@ func newDevAppListCommand(runner executor.Runner) *cobra.Command {
|
||||
{Name: "sort-type", Usage: "排序字段,如 gmt_modified", Bind: "sortType", Trim: true, OmitEmpty: true},
|
||||
{Name: "sort-order", Usage: "排序方向 asc 或 desc", Bind: "sortOrder", Trim: true, OmitEmpty: true},
|
||||
},
|
||||
// cursor/pageSize 由 devAppApplyCursorParams 注入(退役 devAppApplyCursorParams 之外,
|
||||
// 本命令同时退役 devAppPutString/devAppPutInt/devAppFlagOrFallback 全套手搓 helper)。
|
||||
Call: devAppCallCursor(runner),
|
||||
PostMount: func(cmd *cobra.Command) {
|
||||
devAppLeafMeta(cmd, devAppListTool)
|
||||
registerDevAppCursorFlags(cmd)
|
||||
},
|
||||
Call: devAppCallCursor(runner),
|
||||
PostMount: devAppMetaCursor(devAppListTool),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -629,7 +609,7 @@ func newDevAppPermissionListCommand(runner executor.Runner) *cobra.Command {
|
||||
Use: "list",
|
||||
Short: "查询开放平台应用权限列表",
|
||||
Example: " dws dev app permission list --unified-app-id UNIFIED_APP_ID --keyword 通讯录 --page-size 20 --format json",
|
||||
Tool: devAppPermissionListTool,
|
||||
Tool: devAppPermissionListTool,
|
||||
// 命令级别名 "search" 由 PostMount 设回(LeafSpec 无 Command Aliases 字段)。
|
||||
Flags: []LeafFlag{
|
||||
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
|
||||
@@ -646,8 +626,7 @@ func newDevAppPermissionListCommand(runner executor.Runner) *cobra.Command {
|
||||
Call: devAppCallCursor(runner),
|
||||
PostMount: func(cmd *cobra.Command) {
|
||||
cmd.Aliases = []string{"search"}
|
||||
devAppLeafMeta(cmd, devAppPermissionListTool)
|
||||
registerDevAppCursorFlags(cmd)
|
||||
devAppMetaCursor(devAppPermissionListTool)(cmd)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -660,6 +639,7 @@ func newDevAppPermissionAddCommand(runner executor.Runner) *cobra.Command {
|
||||
Tool: devAppPermissionAddTool,
|
||||
Flags: []LeafFlag{
|
||||
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
|
||||
{Name: "scope-values", Usage: "权限点 scopeValue,多个用逗号或分号分隔", Bind: "scopeValues", Trim: true, Transform: transformDevAppScopeValues},
|
||||
},
|
||||
Validate: func(cmd *cobra.Command, args []string) error {
|
||||
if err := devAppRequireWriteGuard(cmd, "permission add"); err != nil {
|
||||
@@ -673,15 +653,8 @@ func newDevAppPermissionAddCommand(runner executor.Runner) *cobra.Command {
|
||||
}
|
||||
return nil
|
||||
},
|
||||
// scope-values 经 devAppPermissionScopes 解析为 []string,由 Call 注入。
|
||||
Call: func(cmd *cobra.Command, tool string, params map[string]any) error {
|
||||
params["scopeValues"] = devAppPermissionScopes(cmd)
|
||||
return runDevAppTool(runner, cmd, tool, params)
|
||||
},
|
||||
PostMount: func(cmd *cobra.Command) {
|
||||
devAppLeafMeta(cmd, devAppPermissionAddTool)
|
||||
cmd.Flags().String("scope-values", "", "权限点 scopeValue,多个用逗号或分号分隔")
|
||||
},
|
||||
Call: devAppCall(runner),
|
||||
PostMount: devAppMeta(devAppPermissionAddTool),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -693,6 +666,7 @@ func newDevAppPermissionRemoveCommand(runner executor.Runner) *cobra.Command {
|
||||
Tool: devAppPermissionRmTool,
|
||||
Flags: []LeafFlag{
|
||||
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
|
||||
{Name: "scope-values", Usage: "待取消权限点 scopeValue,多个用逗号或分号分隔", Bind: "scopeValues", Trim: true, Transform: transformDevAppScopeValues},
|
||||
},
|
||||
Validate: func(cmd *cobra.Command, args []string) error {
|
||||
if err := devAppRequireWriteGuard(cmd, "permission remove"); err != nil {
|
||||
@@ -706,14 +680,8 @@ func newDevAppPermissionRemoveCommand(runner executor.Runner) *cobra.Command {
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Call: func(cmd *cobra.Command, tool string, params map[string]any) error {
|
||||
params["scopeValues"] = devAppPermissionScopes(cmd)
|
||||
return runDevAppTool(runner, cmd, tool, params)
|
||||
},
|
||||
PostMount: func(cmd *cobra.Command) {
|
||||
devAppLeafMeta(cmd, devAppPermissionRmTool)
|
||||
cmd.Flags().String("scope-values", "", "待取消权限点 scopeValue,多个用逗号或分号分隔")
|
||||
},
|
||||
Call: devAppCall(runner),
|
||||
PostMount: devAppMeta(devAppPermissionRmTool),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -745,6 +713,7 @@ func newDevAppMemberAddCommand(runner executor.Runner) *cobra.Command {
|
||||
Flags: []LeafFlag{
|
||||
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
|
||||
{Name: "member-type", Usage: "成员类型,如 DEVELOPER (必填)", Bind: "memberType", Trim: true},
|
||||
{Name: "user-ids", Usage: "成员 userId 列表,多个用逗号分隔 (必填)", Bind: "userIds", Trim: true, Aliases: []string{"member-user-ids"}, Transform: transformDevAppListParam},
|
||||
},
|
||||
Validate: func(cmd *cobra.Command, args []string) error {
|
||||
if err := devAppRequireWriteGuard(cmd, op); err != nil {
|
||||
@@ -761,18 +730,8 @@ func newDevAppMemberAddCommand(runner executor.Runner) *cobra.Command {
|
||||
}
|
||||
return nil
|
||||
},
|
||||
// userIds 由 Call 解析注入(user-ids / member-user-ids 别名回退)。
|
||||
Call: func(cmd *cobra.Command, tool string, params map[string]any) error {
|
||||
users, _ := requiredDevAppUsers(cmd)
|
||||
params["userIds"] = users
|
||||
return runDevAppTool(runner, cmd, tool, params)
|
||||
},
|
||||
PostMount: func(cmd *cobra.Command) {
|
||||
devAppLeafMeta(cmd, devAppMemberAddTool)
|
||||
cmd.Flags().String("user-ids", "", "成员 userId 列表,多个用逗号分隔 (必填)")
|
||||
cmd.Flags().String("member-user-ids", "", "成员 userId 列表,多个用逗号分隔 (兼容旧参数)")
|
||||
_ = cmd.Flags().MarkHidden("member-user-ids")
|
||||
},
|
||||
Call: devAppCall(runner),
|
||||
PostMount: devAppMeta(devAppMemberAddTool),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -786,6 +745,7 @@ func newDevAppMemberRemoveCommand(runner executor.Runner) *cobra.Command {
|
||||
Flags: []LeafFlag{
|
||||
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
|
||||
{Name: "member-type", Usage: "成员类型,如 DEVELOPER (必填)", Bind: "memberType", Trim: true},
|
||||
{Name: "user-ids", Usage: "成员 userId 列表,多个用逗号分隔 (必填)", Bind: "userIds", Trim: true, Aliases: []string{"member-user-ids"}, Transform: transformDevAppListParam},
|
||||
},
|
||||
Validate: func(cmd *cobra.Command, args []string) error {
|
||||
if err := devAppRequireWriteGuard(cmd, op); err != nil {
|
||||
@@ -802,17 +762,8 @@ func newDevAppMemberRemoveCommand(runner executor.Runner) *cobra.Command {
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Call: func(cmd *cobra.Command, tool string, params map[string]any) error {
|
||||
users, _ := requiredDevAppUsers(cmd)
|
||||
params["userIds"] = users
|
||||
return runDevAppTool(runner, cmd, tool, params)
|
||||
},
|
||||
PostMount: func(cmd *cobra.Command) {
|
||||
devAppLeafMeta(cmd, devAppMemberRemoveTool)
|
||||
cmd.Flags().String("user-ids", "", "成员 userId 列表,多个用逗号分隔 (必填)")
|
||||
cmd.Flags().String("member-user-ids", "", "成员 userId 列表,多个用逗号分隔 (兼容旧参数)")
|
||||
_ = cmd.Flags().MarkHidden("member-user-ids")
|
||||
},
|
||||
Call: devAppCall(runner),
|
||||
PostMount: devAppMeta(devAppMemberRemoveTool),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -826,6 +777,9 @@ func newDevAppSecurityConfigCommand(runner executor.Runner) *cobra.Command {
|
||||
Tool: devAppSecurityConfigTool,
|
||||
Flags: []LeafFlag{
|
||||
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
|
||||
{Name: "ip-whitelist", Usage: "出口 IP 白名单,多个用逗号或分号分隔(整组覆盖,非追加)", Bind: "ipWhitelist", Trim: true, Transform: transformDevAppListParam},
|
||||
{Name: "redirect-urls", Usage: "登录重定向 URL,多个用逗号或分号分隔(整组覆盖,非追加)", Bind: "redirectUrls", Trim: true, Transform: transformDevAppListParam},
|
||||
{Name: "sso-urls", Usage: "端内免登地址,多个用逗号或分号分隔(整组覆盖,非追加)", Bind: "ssoUrls", Trim: true, Transform: transformDevAppListParam},
|
||||
},
|
||||
Validate: func(cmd *cobra.Command, args []string) error {
|
||||
if err := devAppRequireWriteGuard(cmd, op); err != nil {
|
||||
@@ -842,25 +796,8 @@ func newDevAppSecurityConfigCommand(runner executor.Runner) *cobra.Command {
|
||||
}
|
||||
return nil
|
||||
},
|
||||
// 三个列表 flag 经 parseDevAppListFlag 解析,非空才入参(与手写一致)。
|
||||
Call: func(cmd *cobra.Command, tool string, params map[string]any) error {
|
||||
if v := parseDevAppListFlag(cmd, "ip-whitelist"); len(v) > 0 {
|
||||
params["ipWhitelist"] = v
|
||||
}
|
||||
if v := parseDevAppListFlag(cmd, "redirect-urls"); len(v) > 0 {
|
||||
params["redirectUrls"] = v
|
||||
}
|
||||
if v := parseDevAppListFlag(cmd, "sso-urls"); len(v) > 0 {
|
||||
params["ssoUrls"] = v
|
||||
}
|
||||
return runDevAppTool(runner, cmd, tool, params)
|
||||
},
|
||||
PostMount: func(cmd *cobra.Command) {
|
||||
devAppLeafMeta(cmd, devAppSecurityConfigTool)
|
||||
cmd.Flags().String("ip-whitelist", "", "出口 IP 白名单,多个用逗号或分号分隔(整组覆盖,非追加)")
|
||||
cmd.Flags().String("redirect-urls", "", "登录重定向 URL,多个用逗号或分号分隔(整组覆盖,非追加)")
|
||||
cmd.Flags().String("sso-urls", "", "端内免登地址,多个用逗号或分号分隔(整组覆盖,非追加)")
|
||||
},
|
||||
Call: devAppCall(runner),
|
||||
PostMount: devAppMeta(devAppSecurityConfigTool),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1173,7 +1110,7 @@ func newDevAppVersionListCommand(runner executor.Runner) *cobra.Command {
|
||||
Use: "list",
|
||||
Short: "分页查询应用版本列表",
|
||||
Example: " dws dev app version list --unified-app-id UNIFIED_APP_ID --page-size 20 --format json",
|
||||
Tool: devAppVersionListTool,
|
||||
Tool: devAppVersionListTool,
|
||||
Flags: []LeafFlag{
|
||||
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
|
||||
},
|
||||
@@ -1181,11 +1118,8 @@ func newDevAppVersionListCommand(runner executor.Runner) *cobra.Command {
|
||||
_, err := requiredDevAppUnifiedID(cmd)
|
||||
return err
|
||||
},
|
||||
Call: devAppCallCursor(runner),
|
||||
PostMount: func(cmd *cobra.Command) {
|
||||
devAppLeafMeta(cmd, devAppVersionListTool)
|
||||
registerDevAppCursorFlags(cmd)
|
||||
},
|
||||
Call: devAppCallCursor(runner),
|
||||
PostMount: devAppMetaCursor(devAppVersionListTool),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1218,15 +1152,12 @@ func newDevAppVersionCheckApprovalCommand(runner executor.Runner) *cobra.Command
|
||||
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
|
||||
{Name: "version-id", Usage: "版本 ID (必填)", Bind: "versionId", Trim: true},
|
||||
},
|
||||
ConstParams: map[string]any{"precheckOnly": true},
|
||||
Validate: func(cmd *cobra.Command, args []string) error {
|
||||
_, err := devAppVersionLocator(cmd)
|
||||
return err
|
||||
},
|
||||
// 复用 publish 工具的服务端预检模式:precheckOnly=true 只返回审批要求,不发布。
|
||||
Call: func(cmd *cobra.Command, tool string, params map[string]any) error {
|
||||
params["precheckOnly"] = true
|
||||
return runDevAppTool(runner, cmd, tool, params)
|
||||
},
|
||||
Call: devAppCall(runner),
|
||||
PostMount: devAppMeta(devAppVersionPublishTool),
|
||||
})
|
||||
}
|
||||
@@ -1241,7 +1172,9 @@ func newDevAppVersionPublishCommand(runner executor.Runner) *cobra.Command {
|
||||
{Name: "unified-app-id", Usage: "开放平台统一应用 ID(必填)", Bind: "unifiedAppId", Trim: true},
|
||||
{Name: "version-id", Usage: "版本 ID (必填)", Bind: "versionId", Trim: true},
|
||||
{Name: "approver-user-id", Usage: "灰度选人模式下指定审批人 userId", Bind: "approverUserId", Trim: true, OmitEmpty: true},
|
||||
{Name: "confirmed-sensitive", Usage: "确认发布包含高敏权限的版本", Kind: LeafBool, Bind: "confirmedSensitive"},
|
||||
},
|
||||
ConstParams: map[string]any{"precheckOnly": false},
|
||||
Validate: func(cmd *cobra.Command, args []string) error {
|
||||
if err := devAppRequireWriteGuard(cmd, "version publish"); err != nil {
|
||||
return err
|
||||
@@ -1251,20 +1184,8 @@ func newDevAppVersionPublishCommand(runner executor.Runner) *cobra.Command {
|
||||
}
|
||||
return nil
|
||||
},
|
||||
// precheckOnly=false(真发布)+ confirmed-sensitive 的 Changed() 语义由 Call 处理
|
||||
//(框架无 LeafBool/Changed 语义,Bool flag 在 PostMount 注册)。
|
||||
Call: func(cmd *cobra.Command, tool string, params map[string]any) error {
|
||||
params["precheckOnly"] = false
|
||||
if cmd.Flags().Changed("confirmed-sensitive") {
|
||||
value, _ := cmd.Flags().GetBool("confirmed-sensitive")
|
||||
params["confirmedSensitive"] = value
|
||||
}
|
||||
return runDevAppTool(runner, cmd, tool, params)
|
||||
},
|
||||
PostMount: func(cmd *cobra.Command) {
|
||||
devAppLeafMeta(cmd, devAppVersionPublishTool)
|
||||
cmd.Flags().Bool("confirmed-sensitive", false, "确认发布包含高敏权限的版本")
|
||||
},
|
||||
Call: devAppCall(runner),
|
||||
PostMount: devAppMeta(devAppVersionPublishTool),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1338,20 +1259,13 @@ func devAppLeafMeta(cmd *cobra.Command, tool string) {
|
||||
}
|
||||
|
||||
// devAppCall 返回统一派发闭包(替代各命令重复的 Call: runDevAppTool 透传)。
|
||||
// 参数装配由 Flags/ConstParams 完成;本闭包只负责执行与响应处理。
|
||||
func devAppCall(runner executor.Runner) func(*cobra.Command, string, map[string]any) error {
|
||||
return func(cmd *cobra.Command, tool string, params map[string]any) error {
|
||||
return runDevAppTool(runner, cmd, tool, params)
|
||||
}
|
||||
}
|
||||
|
||||
// devAppCallCursor 同上,但先经 devAppApplyCursorParams 注入 cursor/pageSize。
|
||||
func devAppCallCursor(runner executor.Runner) func(*cobra.Command, string, map[string]any) error {
|
||||
return func(cmd *cobra.Command, tool string, params map[string]any) error {
|
||||
devAppApplyCursorParams(cmd, params)
|
||||
return runDevAppTool(runner, cmd, tool, params)
|
||||
}
|
||||
}
|
||||
|
||||
// devAppMeta 返回纯收尾 PostMount 闭包(无额外 flag 的命令用)。
|
||||
func devAppMeta(tool string) func(*cobra.Command) {
|
||||
return func(cmd *cobra.Command) { devAppLeafMeta(cmd, tool) }
|
||||
@@ -1866,6 +1780,34 @@ func splitDevAppList(raw string) []string {
|
||||
return values
|
||||
}
|
||||
|
||||
// transformDevAppListParam splits a comma/semicolon list for LeafFlag.Transform.
|
||||
// Empty input returns nil so the key is omitted from toolArgs.
|
||||
func transformDevAppListParam(raw string) (any, error) {
|
||||
values := splitDevAppList(raw)
|
||||
if len(values) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// transformDevAppScopeValues preserves the double-split used by
|
||||
// devAppPermissionScopes (each comma-separated token may itself be a list).
|
||||
func transformDevAppScopeValues(raw string) (any, error) {
|
||||
values := splitDevAppList(raw)
|
||||
out := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
for _, part := range splitDevAppList(value) {
|
||||
if part != "" {
|
||||
out = append(out, part)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// 应用定位:写操作统一只用 --unified-app-id;dev app get 额外支持只读 --app-key。
|
||||
// --name 定位已下线(列表搜索的 --name/--app-key 是过滤参数、不在此列)。
|
||||
// 写操作与其它 app 作用域命令共用 addDevAppUnifiedIDFlag + requiredDevAppUnifiedID。
|
||||
|
||||
@@ -13,16 +13,20 @@
|
||||
|
||||
package helpers
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
// devapp_cursor wires cursor pagination as a PASS-THROUGH: the CLI forwards
|
||||
// --cursor and --page-size straight into the upstream params, and the upstream
|
||||
// returns nextCursor/hasMore in its response. No synthesis, no offset/page
|
||||
// translation, no encode/decode in the CLI — the upstream tools own the cursor
|
||||
// contract (see docs/cursor-pagination-design.md and docs/upstream-todo.md).
|
||||
// Until the upstream tools accept `cursor`/`pageSize` and return `nextCursor`,
|
||||
// paging is in the "pending integration" state, same as the precheckOnly
|
||||
// rename — no anti-corruption layer bridges the gap.
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
|
||||
)
|
||||
|
||||
// Cursor pagination is a devapp list tool, not a LeafSpec declaration.
|
||||
// List factories compose registerDevAppCursorFlags (PostMount) with
|
||||
// devAppCallCursor (Call). LeafSpec itself has no Cursor field and does not
|
||||
// list --cursor/--page-size in Flags.
|
||||
//
|
||||
// Semantics stay PASS-THROUGH: the CLI forwards cursor/pageSize into upstream
|
||||
// params and the upstream returns nextCursor/hasMore. No synthesis, no
|
||||
// offset/page translation — see docs/cursor-pagination-design.md.
|
||||
|
||||
// registerDevAppCursorFlags adds the two cursor flags every list/search command
|
||||
// exposes. pageSize defaults to 20.
|
||||
@@ -32,7 +36,7 @@ func registerDevAppCursorFlags(cmd *cobra.Command) {
|
||||
}
|
||||
|
||||
// devAppApplyCursorParams forwards cursor/pageSize into params as-is. cursor is
|
||||
// only set when non-empty (first page omits it).
|
||||
// only set when non-empty (first page omits it). size < 1 floors to 20.
|
||||
func devAppApplyCursorParams(cmd *cobra.Command, params map[string]any) {
|
||||
if cur := devAppStringFlag(cmd, "cursor"); cur != "" {
|
||||
params["cursor"] = cur
|
||||
@@ -43,3 +47,19 @@ func devAppApplyCursorParams(cmd *cobra.Command, params map[string]any) {
|
||||
}
|
||||
params["pageSize"] = size
|
||||
}
|
||||
|
||||
// devAppCallCursor is the list execution body: apply cursor params, then run.
|
||||
func devAppCallCursor(runner executor.Runner) func(*cobra.Command, string, map[string]any) error {
|
||||
return func(cmd *cobra.Command, tool string, params map[string]any) error {
|
||||
devAppApplyCursorParams(cmd, params)
|
||||
return runDevAppTool(runner, cmd, tool, params)
|
||||
}
|
||||
}
|
||||
|
||||
// devAppMetaCursor is PostMount for list leaves: surface meta + cursor flags.
|
||||
func devAppMetaCursor(tool string) func(*cobra.Command) {
|
||||
return func(cmd *cobra.Command) {
|
||||
devAppLeafMeta(cmd, tool)
|
||||
registerDevAppCursorFlags(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,7 +283,7 @@ func TestDevAppListLeaf(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDevAppEventListLeaf 验证 cursor/pageSize 经 devAppApplyCursorParams 注入。
|
||||
// TestDevAppEventListLeaf 验证 cursor/pageSize 经 devAppCallCursor 工具注入。
|
||||
func TestDevAppEventListLeaf(t *testing.T) {
|
||||
r := &fakeDevAppRunner{}
|
||||
cmd := newDevAppEventListCommand(r)
|
||||
@@ -294,9 +294,9 @@ func TestDevAppEventListLeaf(t *testing.T) {
|
||||
if v, _ := r.got.Params["unifiedAppId"].(string); v != "APP-E" {
|
||||
t.Fatalf("unifiedAppId = %q, want APP-E", v)
|
||||
}
|
||||
// page-size 默认 20,经 devAppApplyCursorParams 注入为 float64(20)。
|
||||
if _, present := r.got.Params["pageSize"]; !present {
|
||||
t.Fatal("pageSize missing, want injected by devAppApplyCursorParams")
|
||||
// page-size 默认 20,由 registerDevAppCursorFlags + CallCursor 注入。
|
||||
if v, present := r.got.Params["pageSize"]; !present || v != 20 {
|
||||
t.Fatalf("pageSize = %v, want 20 from cursor tool", v)
|
||||
}
|
||||
// cursor 默认空:不入参。
|
||||
if _, present := r.got.Params["cursor"]; present {
|
||||
|
||||
@@ -117,10 +117,14 @@ type LeafSpec struct {
|
||||
// 则中止且不派发。
|
||||
Risk LeafRisk
|
||||
|
||||
// Call 是可插拔派发函数,非空时替代默认的 callMCPTool/callMCPToolOnServer。
|
||||
// 供非 MCP 直连命令(如 devapp 走 executor.Runner)复用本框架:调用方用
|
||||
// 闭包捕获自己的 runner/派发器即可。签名与默认路径一致——收到框架装配好
|
||||
// 的 toolArgs,自行派发。
|
||||
// ConstParams 是与 flag 无关的固定载荷(如 precheckOnly),在 flag 装配
|
||||
// 之后并入 toolArgs。它们从不满足 Required。
|
||||
ConstParams map[string]any
|
||||
|
||||
// Call 是执行体切面:非空时替代默认的 callMCPTool/callMCPToolOnServer。
|
||||
// 供非 MCP 直连命令(如 devapp 走 executor.Runner)注入派发与响应处理。
|
||||
// 收到的 toolArgs 已由 Flags/ConstParams 装配完成;Call 不应再写业务参数。
|
||||
// 分页等横切能力由领域工具(如 devAppCallCursor)在执行侧处理,不进声明。
|
||||
Call func(cmd *cobra.Command, tool string, args map[string]any) error
|
||||
|
||||
// Validate 是跨 flag 校验钩子(如时间区间、互斥关系),在 required
|
||||
@@ -133,7 +137,8 @@ type LeafSpec struct {
|
||||
|
||||
// PostMount 在 flag 注册完成之后、RunE 设定之前对构建好的 cmd 做最终
|
||||
// 调整(设置 Args/DisableAutoGenTag、调用 annotate/preferLegacy 等)。
|
||||
// 无论是否使用 RunE 逃生舱都会执行。对标 lark shortcut 的 PostMount。
|
||||
// 业务 flag 必须写在 Flags 里;分页 flag 由领域工具在 PostMount 注入,
|
||||
// 不作为 LeafSpec 声明字段。
|
||||
PostMount func(cmd *cobra.Command)
|
||||
}
|
||||
|
||||
@@ -160,6 +165,7 @@ func FromLeafSpec(spec LeafSpec) cmdcore.CommandSpec {
|
||||
Flags: spec.Flags,
|
||||
Constraints: spec.Constraints,
|
||||
Risk: spec.Risk,
|
||||
ConstParams: spec.ConstParams,
|
||||
Validate: spec.Validate,
|
||||
PostMount: spec.PostMount,
|
||||
RunE: spec.RunE,
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
// 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 helpers
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestLeafSpecPostMountDoesNotRegisterBusinessFlags keeps business flags in
|
||||
// Flags. PostMount may set surface metadata and may call domain tools such as
|
||||
// registerDevAppCursorFlags (pagination is a tool, not a LeafSpec field).
|
||||
func TestLeafSpecPostMountDoesNotRegisterBusinessFlags(t *testing.T) {
|
||||
for _, call := range leafSpecCompositeLits(t) {
|
||||
post := fieldExpr(call, "PostMount")
|
||||
if post == nil {
|
||||
continue
|
||||
}
|
||||
ast.Inspect(post, func(n ast.Node) bool {
|
||||
expr, ok := n.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
fun, ok := expr.Fun.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
switch fun.Sel.Name {
|
||||
case "String", "Bool", "Int", "StringSlice", "Int64", "Float64":
|
||||
inner, ok := fun.X.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
innerSel, ok := inner.Fun.(*ast.SelectorExpr)
|
||||
if ok && innerSel.Sel.Name == "Flags" {
|
||||
t.Errorf("%s: PostMount registers business flag via Flags().%s; declare it in Flags instead (cursor tools use registerDevAppCursorFlags)",
|
||||
call.pos, fun.Sel.Name)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLeafSpecCallDoesNotAssembleBusinessParams keeps Call as an execution body.
|
||||
// Business binding belongs in Flags/ConstParams. Named helpers like
|
||||
// devAppCall / devAppCallCursor may apply domain tooling (including cursor).
|
||||
func TestLeafSpecCallDoesNotAssembleBusinessParams(t *testing.T) {
|
||||
for _, call := range leafSpecCompositeLits(t) {
|
||||
body := fieldExpr(call, "Call")
|
||||
if body == nil {
|
||||
continue
|
||||
}
|
||||
// Named helpers (devAppCall / devAppCallCursor) are execution tools.
|
||||
if _, ok := body.(*ast.CallExpr); ok {
|
||||
continue
|
||||
}
|
||||
if ident, ok := body.(*ast.Ident); ok {
|
||||
if strings.HasPrefix(ident.Name, "devAppCall") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
ast.Inspect(body, func(n ast.Node) bool {
|
||||
assign, ok := n.(*ast.AssignStmt)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
for _, lhs := range assign.Lhs {
|
||||
index, ok := lhs.(*ast.IndexExpr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ident, ok := index.X.(*ast.Ident); ok && ident.Name == "params" {
|
||||
t.Errorf("%s: Call assigns params[...]; move business binding to Flags/ConstParams", call.pos)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type leafSpecLit struct {
|
||||
pos string
|
||||
lit *ast.CompositeLit
|
||||
}
|
||||
|
||||
func leafSpecCompositeLits(t *testing.T) []leafSpecLit {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
dir := filepath.Dir(file)
|
||||
fset := token.NewFileSet()
|
||||
pkgs, err := parser.ParseDir(fset, dir, func(info os.FileInfo) bool {
|
||||
name := info.Name()
|
||||
return strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go")
|
||||
}, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("parse helpers: %v", err)
|
||||
}
|
||||
var out []leafSpecLit
|
||||
for _, pkg := range pkgs {
|
||||
for _, f := range pkg.Files {
|
||||
ast.Inspect(f, func(n ast.Node) bool {
|
||||
call, ok := n.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
fun, ok := call.Fun.(*ast.Ident)
|
||||
if !ok || fun.Name != "NewLeafCommand" || len(call.Args) != 1 {
|
||||
return true
|
||||
}
|
||||
lit, ok := call.Args[0].(*ast.CompositeLit)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if typ, ok := lit.Type.(*ast.Ident); !ok || typ.Name != "LeafSpec" {
|
||||
return true
|
||||
}
|
||||
out = append(out, leafSpecLit{
|
||||
pos: fset.Position(lit.Pos()).String(),
|
||||
lit: lit,
|
||||
})
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
t.Fatal("no NewLeafCommand(LeafSpec{...}) literals found")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func fieldExpr(call leafSpecLit, name string) ast.Expr {
|
||||
for _, elt := range call.lit.Elts {
|
||||
kv, ok := elt.(*ast.KeyValueExpr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
key, ok := kv.Key.(*ast.Ident)
|
||||
if !ok || key.Name != name {
|
||||
continue
|
||||
}
|
||||
return kv.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user