Skip to content

Commit bc06233

Browse files
committed
refactor: converge command pipelines onto a typed metadata model + catalog
Extract the metadata logic duplicated across schema/cmdutil/registry into two new single-purpose modules and thin the command layer to adapters over them — net -924 lines of behavior-preserving moves and de-duplication, plus a few agent-facing additions and fixes found along the way. Moved / de-duplicated - New internal/meta: typed model over meta_data.json. Absorbs field-value normalization and the access-token<->identity bijection previously split across schema.convertAccessTokens, cmdutil.AccessTokensToIdentities and registry.IdentityToAccessToken — three duplicate token interpreters collapsed into one (cmdutil identity/risk/fileupload now defer to it). - New internal/apicatalog: navigation over the typed model. Path resolution moved here (internal/schema/path.go -> internal/apicatalog/path.go); the resolve<->walk round-trip is closed for nested resources. - internal/schema is now render-only — dropped its cmdutil and registry imports; risk-level constants moved to the new internal/core/risk.go. - internal/registry keeps only source adapter + scope policy; its scope collectors and cmd/service registration now walk the catalog instead of hand-rolling traversal. cmd/service param-flag handling extracted into paramflags.go/flaggroups.go and no longer imports schema. New (agent-facing) - Typed flags for raw API commands; grouped --help by input kind with inline enum value=meaning and method affordance in Long help; envelope carries enumDescriptions (parallel to enum) for per-value meanings. Fixed (found during the convergence) - meta.Option.Value is tolerant `any` and already-typed literals normalize to the canonical type, so one bad option value can't blank the whole catalog and numeric enums sort correctly. - nested-resource descent extended to completion; auth-hint resolves the nested catalog path instead of a hard-coded depth. Removed - The byte-for-byte envelope golden: it snapshotted metadata fetched at build time, baking real API data into git with no way to desensitize (CI regenerates from fresh real data and compares exactly). Assembler logic is covered on synthetic data instead. Behavior preserved: public command tree/paths, request planning, pagination, dry-run, confirmation. Typed enum flags are documentation, not hard-validated. Change-Id: I2d6c5127a9e76955fb467a45557ae155299a2660
1 parent 99e314f commit bc06233

50 files changed

Lines changed: 3788 additions & 2864 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cmd/auth/login_interactive.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,11 @@ func buildDomainMeta(name, lang string) domainMeta {
9292
Description: desc,
9393
}
9494
}
95-
// Fallback: read from from_meta spec (legacy)
96-
meta := registry.LoadFromMeta(name)
95+
// Fallback: read from the typed service spec (legacy)
9796
dm := domainMeta{Name: name}
98-
if meta != nil {
99-
if t, ok := meta["title"].(string); ok {
100-
dm.Title = t
101-
}
102-
if d, ok := meta["description"].(string); ok {
103-
dm.Description = d
104-
}
97+
if svc, ok := registry.ServiceTyped(name); ok {
98+
dm.Title = svc.Title
99+
dm.Description = svc.Description
105100
}
106101
return dm
107102
}

cmd/command_catalog_path_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
2+
// SPDX-License-Identifier: MIT
3+
4+
package cmd
5+
6+
import (
7+
"reflect"
8+
"testing"
9+
10+
"github.com/spf13/cobra"
11+
)
12+
13+
// TestCommandCatalogPath pins that the auth-hint path reconstruction inverts the
14+
// service command tree for any depth — flat dotted resources AND genuinely
15+
// nested resources — so it round-trips through apicatalog.Resolve instead of
16+
// assuming a fixed root->service->resource->method shape.
17+
func TestCommandCatalogPath(t *testing.T) {
18+
chain := func(names ...string) *cobra.Command {
19+
var parent, leaf *cobra.Command
20+
for _, n := range names {
21+
c := &cobra.Command{Use: n}
22+
if parent != nil {
23+
parent.AddCommand(c)
24+
}
25+
parent = c
26+
leaf = c
27+
}
28+
return leaf
29+
}
30+
31+
tests := []struct {
32+
name string
33+
leaf *cobra.Command
34+
want []string
35+
}{
36+
{"flat dotted resource", chain("lark-cli", "im", "chat.members", "create"), []string{"im", "chat.members", "create"}},
37+
{"nested resources", chain("lark-cli", "im", "spaces", "items", "get"), []string{"im", "spaces", "items", "get"}},
38+
{"service level", chain("lark-cli", "im"), []string{"im"}},
39+
}
40+
for _, tt := range tests {
41+
t.Run(tt.name, func(t *testing.T) {
42+
if got := commandCatalogPath(tt.leaf); !reflect.DeepEqual(got, tt.want) {
43+
t.Errorf("commandCatalogPath = %v, want %v", got, tt.want)
44+
}
45+
})
46+
}
47+
48+
// The root command (no parent) has no catalog path.
49+
if got := commandCatalogPath(&cobra.Command{Use: "lark-cli"}); len(got) != 0 {
50+
t.Errorf("root path = %v, want empty", got)
51+
}
52+
}

cmd/error_auth_hint.go

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/spf13/cobra"
1212

1313
"github.com/larksuite/cli/errs"
14+
"github.com/larksuite/cli/internal/apicatalog"
1415
internalauth "github.com/larksuite/cli/internal/auth"
1516
"github.com/larksuite/cli/internal/cmdutil"
1617
"github.com/larksuite/cli/internal/core"
@@ -118,38 +119,37 @@ func resolveDeclaredShortcutScopes(cmd *cobra.Command, identity string) []string
118119
}
119120

120121
// resolveDeclaredServiceMethodScopes returns the scopes declared by a
121-
// service/resource/method command from the embedded from_meta registry.
122+
// service/resource/method command. It reconstructs the catalog path from the
123+
// command ancestry and resolves it through the same navigation Module the
124+
// command tree is built from (apicatalog), so it stays correct for nested
125+
// resources instead of hard-coding a root->service->resource->method depth.
126+
// Non-method commands (services, resources, shortcuts) resolve to a non-method
127+
// target and yield no scopes.
122128
func resolveDeclaredServiceMethodScopes(cmd *cobra.Command, identity string) []string {
123-
// Service-method scope lookup only applies to commands mounted as
124-
// root -> service -> resource -> method. Non-resource/method commands
125-
// intentionally return no scopes here so auth-hint enrichment does not
126-
// change runtime semantics for other command shapes.
127-
if cmd == nil || cmd.Parent() == nil || cmd.Parent().Parent() == nil || cmd.Parent().Parent().Parent() == nil {
129+
if cmd == nil || strings.HasPrefix(cmd.Name(), "+") {
128130
return nil
129131
}
130-
if strings.HasPrefix(cmd.Name(), "+") {
132+
path := commandCatalogPath(cmd)
133+
if len(path) == 0 {
131134
return nil
132135
}
133-
134-
service := cmd.Parent().Parent().Name()
135-
resource := cmd.Parent().Name()
136-
method := cmd.Name()
137-
138-
spec := registry.LoadFromMeta(service)
139-
if spec == nil {
140-
return nil
141-
}
142-
resources, _ := spec["resources"].(map[string]interface{})
143-
resMap, _ := resources[resource].(map[string]interface{})
144-
if resMap == nil {
136+
target, err := registry.RuntimeCatalog().Resolve(path)
137+
if err != nil || target.Kind != apicatalog.TargetMethod {
145138
return nil
146139
}
147-
methods, _ := resMap["methods"].(map[string]interface{})
148-
methodMap, _ := methods[method].(map[string]interface{})
149-
if methodMap == nil {
150-
return nil
151-
}
152-
return registry.DeclaredScopesForMethod(methodMap, identity)
140+
return registry.DeclaredScopesForMethod(target.Method.Method, identity)
141+
}
142+
143+
// commandCatalogPath reconstructs the catalog path [service, resource..., method]
144+
// from a command's ancestry, excluding the root command. It is the inverse of
145+
// the service command tree's construction, so any depth (flat or nested)
146+
// round-trips through apicatalog.Resolve.
147+
func commandCatalogPath(cmd *cobra.Command) []string {
148+
var path []string
149+
for c := cmd; c != nil && c.Parent() != nil; c = c.Parent() {
150+
path = append([]string{c.Name()}, path...)
151+
}
152+
return path
153153
}
154154

155155
// shortcutSupportsIdentity reports whether a shortcut supports the requested

cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const rootLong = `lark-cli — Lark/Feishu CLI tool.
3333
USAGE:
3434
lark-cli <command> [subcommand] [method] [options]
3535
lark-cli api <method> <path> [--params <json>] [--data <json>]
36-
lark-cli schema <service.resource.method> [--format pretty]
36+
lark-cli schema <service.resource.method>
3737
3838
EXAMPLES:
3939
# View upcoming events

0 commit comments

Comments
 (0)