mirror of
https://github.com/larksuite/cli.git
synced 2026-08-03 08:32:46 +08:00
Compare commits
65 Commits
feat/event
...
refactor/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cbaa7ff21 | ||
|
|
a72297b026 | ||
|
|
7512f017d6 | ||
|
|
d0b26821df | ||
|
|
7faf9da3a4 | ||
|
|
ae56d30ce3 | ||
|
|
7e34eccf3a | ||
|
|
4c22015464 | ||
|
|
1698ac1ff3 | ||
|
|
8f3e9630a1 | ||
|
|
aa93b3f3a6 | ||
|
|
0885ec2eae | ||
|
|
b39258c169 | ||
|
|
2a252d2a80 | ||
|
|
1ba4d1fd77 | ||
|
|
bb7342c3cc | ||
|
|
f25ef0ae75 | ||
|
|
5bae5bbbc2 | ||
|
|
d7cf797bdd | ||
|
|
0266a7e9a1 | ||
|
|
939fc1eeb8 | ||
|
|
c69a61c672 | ||
|
|
ee27d0cdc2 | ||
|
|
3732c6bcce | ||
|
|
9d8e93c682 | ||
|
|
38312d3a9c | ||
|
|
342d1a247d | ||
|
|
770c23035c | ||
|
|
2634092ff2 | ||
|
|
9c50045f14 | ||
|
|
7b6962a726 | ||
|
|
1af34e6649 | ||
|
|
c12ab91349 | ||
|
|
57bf8ccd1e | ||
|
|
97e397cf0c | ||
|
|
c138f29972 | ||
|
|
2662729cd6 | ||
|
|
475f04a8dd | ||
|
|
52a1187c20 | ||
|
|
bf56e903ba | ||
|
|
b028c33e8f | ||
|
|
4073e75def | ||
|
|
72ea02875c | ||
|
|
aec9c4677d | ||
|
|
e5b2e96df4 | ||
|
|
602f6719dc | ||
|
|
85490cb3da | ||
|
|
2d1341aff6 | ||
|
|
aa50bec07e | ||
|
|
5157c3a00e | ||
|
|
5e23bbeddb | ||
|
|
d62f8dcbe8 | ||
|
|
8ba2431192 | ||
|
|
217f4e5567 | ||
|
|
820305536c | ||
|
|
d48c218d0d | ||
|
|
abe0d09d4b | ||
|
|
7c2ca4e465 | ||
|
|
cbe0fb12df | ||
|
|
59b6393250 | ||
|
|
f1ce88b48e | ||
|
|
c09b0d5dd3 | ||
|
|
1772afe22d | ||
|
|
acd50f25fa | ||
|
|
e488cf4cd3 |
13
.github/workflows/arch-audit.yml
vendored
13
.github/workflows/arch-audit.yml
vendored
@@ -62,19 +62,6 @@ jobs:
|
||||
go list -m -u all 2>/dev/null | grep '\[' >> report.md || echo "All dependencies up to date" >> report.md
|
||||
echo '```' >> report.md
|
||||
|
||||
- name: Circular dependency check
|
||||
run: |
|
||||
echo "## Circular Dependencies" >> report.md
|
||||
go list -f '{{.ImportPath}} {{join .Imports " "}}' ./... | \
|
||||
go run golang.org/x/tools/cmd/digraph@v0.31.0 scc 2>&1 | tee cycles.txt
|
||||
if [ -s cycles.txt ]; then
|
||||
echo '```' >> report.md
|
||||
cat cycles.txt >> report.md
|
||||
echo '```' >> report.md
|
||||
else
|
||||
echo "No circular dependencies detected." >> report.md
|
||||
fi
|
||||
|
||||
- name: E2E coverage gaps
|
||||
run: |
|
||||
echo "## E2E Coverage Gaps" >> report.md
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -119,6 +119,8 @@ jobs:
|
||||
env:
|
||||
QUALITY_GATE_CHANGED_FROM: ${{ github.event.pull_request.base.sha || github.event.before || 'origin/main' }}
|
||||
run: echo "QUALITY_GATE_CHANGED_FROM=$(bash scripts/resolve-changed-from.sh)" >> "$GITHUB_ENV"
|
||||
- name: Enforce layering ratchet
|
||||
run: bash scripts/check-layering-ratchet.sh "$QUALITY_GATE_CHANGED_FROM"
|
||||
- name: Run golangci-lint
|
||||
run: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 run --new-from-rev="$QUALITY_GATE_CHANGED_FROM"
|
||||
- name: Run source-contract lint guards (lintcheck)
|
||||
|
||||
13
AGENTS.md
13
AGENTS.md
@@ -62,10 +62,21 @@ Both notices recommend the same fix command: `lark-cli update`. The skills notic
|
||||
| `internal/credential/` | Credential provider chain (extension → default) |
|
||||
| `extension/credential/` | Plugin-facing credential interfaces and env provider |
|
||||
| `internal/client/client.go` | APIClient: DoSDKRequest, DoStream |
|
||||
| `internal/core/config.go` | Multi-profile config loading/saving |
|
||||
| `brand/` | Brand (feishu/lark) and its endpoint hosts — repo root, so `extension/` may import it |
|
||||
| `internal/workspace/` | Workspace detection plus the config and runtime directory paths |
|
||||
| `internal/identity/` | The `--as` identity (user/bot) and the strict-mode policy |
|
||||
| `internal/config/config.go` | Multi-profile config loading/saving |
|
||||
| `internal/vfs/` | Filesystem abstraction (use `vfs.*` instead of `os.*`) |
|
||||
| `internal/validate/path.go` | Path safety validation |
|
||||
|
||||
`internal/core` is gone. Besides the four packages above it also became
|
||||
`internal/secret` (app secret storage and resolution) and `internal/risk` (the
|
||||
read / write / high-risk-write vocabulary). Import the narrowest one you need:
|
||||
`brand`, `internal/workspace`, `internal/identity`, `internal/secret` and
|
||||
`internal/risk` do not import each other — only `internal/config` sits on top of
|
||||
them — so asking for a config directory no longer drags in keychain, i18n and
|
||||
validate.
|
||||
|
||||
## Who Uses This CLI
|
||||
|
||||
This CLI's primary consumers include AI agents (Claude Code, Cursor, Gemini CLI). Your code is read by machines — error messages, output format, and flag design all directly affect agent success rates.
|
||||
|
||||
1
Makefile
1
Makefile
@@ -49,6 +49,7 @@ fmt-check:
|
||||
|
||||
script-test:
|
||||
bash scripts/resolve-changed-from.test.sh
|
||||
bash scripts/check-layering-ratchet.test.sh
|
||||
bash scripts/ci-workflow.test.sh
|
||||
bash scripts/semantic-review-workflow.test.sh
|
||||
$(NODE) --test scripts/e2e_domains.test.js scripts/fetch_e2e_tat.test.js scripts/install.test.js scripts/release-preflight.test.js scripts/semantic-review-verify-artifact.test.js scripts/pr-quality-summary.test.js scripts/semantic-review-publish.test.js scripts/ci-quality-summary-publish.test.js
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package core
|
||||
package brand
|
||||
|
||||
import "strings"
|
||||
|
||||
// LarkBrand represents the Lark platform brand.
|
||||
// Brand represents the Lark platform brand.
|
||||
// "feishu" targets China-mainland, "lark" targets international.
|
||||
// ParseBrand and ResolveEndpoints map unrecognized values to BrandFeishu.
|
||||
type LarkBrand string
|
||||
// ParseBrand and ResolveEndpoints map unrecognized values to Feishu.
|
||||
type Brand string
|
||||
|
||||
const (
|
||||
BrandFeishu LarkBrand = "feishu"
|
||||
BrandLark LarkBrand = "lark"
|
||||
Feishu Brand = "feishu"
|
||||
Lark Brand = "lark"
|
||||
)
|
||||
|
||||
// ParseBrand normalizes a brand string (case-insensitive, whitespace-tolerant);
|
||||
// anything other than "lark" normalizes to BrandFeishu.
|
||||
func ParseBrand(value string) LarkBrand {
|
||||
// anything other than "lark" normalizes to Feishu.
|
||||
func ParseBrand(value string) Brand {
|
||||
if strings.ToLower(strings.TrimSpace(value)) == "lark" {
|
||||
return BrandLark
|
||||
return Lark
|
||||
}
|
||||
return BrandFeishu
|
||||
return Feishu
|
||||
}
|
||||
|
||||
// OAuthTokenV3Path is the unified OAuth 2.0 Token Endpoint path on the accounts
|
||||
@@ -40,9 +40,9 @@ type Endpoints struct {
|
||||
|
||||
// ResolveEndpoints resolves endpoint URLs for the brand, normalizing its
|
||||
// input so stored values with unusual casing still resolve correctly.
|
||||
func ResolveEndpoints(brand LarkBrand) Endpoints {
|
||||
func ResolveEndpoints(brand Brand) Endpoints {
|
||||
switch ParseBrand(string(brand)) {
|
||||
case BrandLark:
|
||||
case Lark:
|
||||
return Endpoints{
|
||||
Open: "https://open.larksuite.com",
|
||||
Accounts: "https://accounts.larksuite.com",
|
||||
@@ -60,6 +60,6 @@ func ResolveEndpoints(brand LarkBrand) Endpoints {
|
||||
}
|
||||
|
||||
// ResolveOpenBaseURL returns the Open API base URL for the given brand.
|
||||
func ResolveOpenBaseURL(brand LarkBrand) string {
|
||||
func ResolveOpenBaseURL(brand Brand) string {
|
||||
return ResolveEndpoints(brand).Open
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package core
|
||||
package brand
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestResolveEndpoints_Feishu(t *testing.T) {
|
||||
ep := ResolveEndpoints(BrandFeishu)
|
||||
ep := ResolveEndpoints(Feishu)
|
||||
if ep.Open != "https://open.feishu.cn" {
|
||||
t.Errorf("Open = %q, want feishu.cn", ep.Open)
|
||||
}
|
||||
@@ -22,7 +22,7 @@ func TestResolveEndpoints_Feishu(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveEndpoints_Lark(t *testing.T) {
|
||||
ep := ResolveEndpoints(BrandLark)
|
||||
ep := ResolveEndpoints(Lark)
|
||||
if ep.Open != "https://open.larksuite.com" {
|
||||
t.Errorf("Open = %q, want larksuite.com", ep.Open)
|
||||
}
|
||||
@@ -50,10 +50,10 @@ func TestResolveEndpoints_EmptyDefaultsToFeishu(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResolveOpenBaseURL(t *testing.T) {
|
||||
if got := ResolveOpenBaseURL(BrandFeishu); got != "https://open.feishu.cn" {
|
||||
if got := ResolveOpenBaseURL(Feishu); got != "https://open.feishu.cn" {
|
||||
t.Errorf("ResolveOpenBaseURL(feishu) = %q", got)
|
||||
}
|
||||
if got := ResolveOpenBaseURL(BrandLark); got != "https://open.larksuite.com" {
|
||||
if got := ResolveOpenBaseURL(Lark); got != "https://open.larksuite.com" {
|
||||
t.Errorf("ResolveOpenBaseURL(lark) = %q", got)
|
||||
}
|
||||
}
|
||||
@@ -61,15 +61,15 @@ func TestResolveOpenBaseURL(t *testing.T) {
|
||||
func TestParseBrand(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
want LarkBrand
|
||||
want Brand
|
||||
}{
|
||||
{"", BrandFeishu},
|
||||
{"feishu", BrandFeishu},
|
||||
{"lark", BrandLark},
|
||||
{"LARK", BrandLark},
|
||||
{" lark ", BrandLark},
|
||||
{"Lark", BrandLark},
|
||||
{"xyz", BrandFeishu},
|
||||
{"", Feishu},
|
||||
{"feishu", Feishu},
|
||||
{"lark", Lark},
|
||||
{"LARK", Lark},
|
||||
{" lark ", Lark},
|
||||
{"Lark", Lark},
|
||||
{"xyz", Feishu},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := ParseBrand(c.in); got != c.want {
|
||||
@@ -83,11 +83,11 @@ func TestParseBrand(t *testing.T) {
|
||||
// unusual casing or whitespace still resolve to their intended endpoints.
|
||||
func TestResolveEndpoints_NormalizesBrand(t *testing.T) {
|
||||
for _, raw := range []string{"LARK", " lark ", "Lark"} {
|
||||
if got := ResolveEndpoints(LarkBrand(raw)).Open; got != "https://open.larksuite.com" {
|
||||
if got := ResolveEndpoints(Brand(raw)).Open; got != "https://open.larksuite.com" {
|
||||
t.Errorf("ResolveEndpoints(%q).Open = %q, want the lark endpoint", raw, got)
|
||||
}
|
||||
}
|
||||
if got := ResolveEndpoints(LarkBrand("unexpected")).Open; got != "https://open.feishu.cn" {
|
||||
if got := ResolveEndpoints(Brand("unexpected")).Open; got != "https://open.feishu.cn" {
|
||||
t.Errorf("ResolveEndpoints(unexpected).Open = %q, want the feishu default", got)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,8 @@ import (
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/client"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/identity"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
||||
@@ -33,7 +34,7 @@ type APIOptions struct {
|
||||
// Flags
|
||||
Params string
|
||||
Data string
|
||||
As core.Identity
|
||||
As identity.Identity
|
||||
Output string
|
||||
PageAll bool
|
||||
PageSize int
|
||||
@@ -87,7 +88,7 @@ Examples:
|
||||
opts.Path = args[1]
|
||||
opts.Cmd = cmd
|
||||
opts.Ctx = cmd.Context()
|
||||
opts.As = core.Identity(asStr)
|
||||
opts.As = identity.Identity(asStr)
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
@@ -304,7 +305,7 @@ func apiRun(opts *APIOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiDryRun(f *cmdutil.Factory, request client.RawApiRequest, config *core.CliConfig, opts *APIOptions) error {
|
||||
func apiDryRun(f *cmdutil.Factory, request client.RawApiRequest, config *configpkg.CliConfig, opts *APIOptions) error {
|
||||
return cmdutil.PrintDryRun(request, config, dryRunOutputOptions(f, opts))
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,13 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/client"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/httpmock"
|
||||
"github.com/larksuite/cli/internal/identity"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
)
|
||||
|
||||
@@ -44,10 +46,10 @@ func newAPIPaginateTestHarness(t *testing.T) (*client.APIClient, *bytes.Buffer,
|
||||
output.PendingNotice = nil
|
||||
t.Cleanup(func() { output.PendingNotice = previousNotice })
|
||||
|
||||
config := &core.CliConfig{
|
||||
config := &configpkg.CliConfig{
|
||||
AppID: "test-app",
|
||||
AppSecret: "test-secret",
|
||||
Brand: core.BrandFeishu,
|
||||
Brand: brand.Feishu,
|
||||
}
|
||||
f, out, errOut, reg := cmdutil.TestFactory(t, config)
|
||||
ac, err := f.NewAPIClientWithConfig(config)
|
||||
@@ -62,7 +64,7 @@ func apiPaginateRequest() client.RawApiRequest {
|
||||
return client.RawApiRequest{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/test/v1/items",
|
||||
As: core.AsBot,
|
||||
As: identity.AsBot,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,13 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
extcs "github.com/larksuite/cli/extension/contentsafety"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/httpmock"
|
||||
"github.com/larksuite/cli/internal/identity"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -40,8 +42,8 @@ func newTestRootCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
func TestApiCmd_FlagParsing(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
var gotOpts *APIOptions
|
||||
@@ -60,7 +62,7 @@ func TestApiCmd_FlagParsing(t *testing.T) {
|
||||
if gotOpts.Path != "/open-apis/test" {
|
||||
t.Errorf("expected path /open-apis/test, got %s", gotOpts.Path)
|
||||
}
|
||||
if gotOpts.As != core.AsBot {
|
||||
if gotOpts.As != identity.AsBot {
|
||||
t.Errorf("expected as=bot, got %s", gotOpts.As)
|
||||
}
|
||||
if !gotOpts.DryRun {
|
||||
@@ -69,8 +71,8 @@ func TestApiCmd_FlagParsing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_DryRun(t *testing.T) {
|
||||
f, stdout, stderr, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, stdout, stderr, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
@@ -104,8 +106,8 @@ func TestApiCmd_DryRun(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_DryRunWithJq(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
@@ -122,8 +124,8 @@ func TestApiCmd_DryRunWithJq(t *testing.T) {
|
||||
// not panic. Symmetric to the typed-flag overlay path in cmd/service — both
|
||||
// write into the map ParseJSONMap returns.
|
||||
func TestApiCmd_NullParamsWithPageSize(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
@@ -137,8 +139,8 @@ func TestApiCmd_NullParamsWithPageSize(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_BotMode(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
// Register API endpoint stub
|
||||
@@ -170,8 +172,8 @@ func TestApiCmd_BotMode(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_MissingArgs(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
@@ -183,8 +185,8 @@ func TestApiCmd_MissingArgs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_EmptyMethodRejected(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
@@ -199,8 +201,8 @@ func TestApiCmd_EmptyMethodRejected(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_InvalidParamsJSON(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
@@ -212,8 +214,8 @@ func TestApiCmd_InvalidParamsJSON(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiValidArgsFunction(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
@@ -278,8 +280,8 @@ func TestApiValidArgsFunction(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewCmdApi_StrictModeHidesAsFlag(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu, SupportedIdentities: 2,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu, SupportedIdentities: 2,
|
||||
})
|
||||
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
@@ -296,8 +298,8 @@ func TestNewCmdApi_StrictModeHidesAsFlag(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_PageLimitDefault(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
var gotOpts *APIOptions
|
||||
@@ -316,8 +318,8 @@ func TestApiCmd_PageLimitDefault(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_ParamsAndDataBothStdinConflict(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
@@ -332,8 +334,8 @@ func TestApiCmd_ParamsAndDataBothStdinConflict(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_OutputAndPageAllConflict(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
var gotOpts *APIOptions
|
||||
@@ -355,8 +357,8 @@ func TestApiCmd_BinaryResponse_AutoSave(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cmdutil.TestChdir(t, dir)
|
||||
|
||||
f, stdout, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app-bin", AppSecret: "test-secret-bin", Brand: core.BrandFeishu,
|
||||
f, stdout, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app-bin", AppSecret: "test-secret-bin", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -405,8 +407,8 @@ func TestApiCmd_BinaryResponse_AutoSave(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_PageAll_NonBatchAPI_FallbackToJSON(t *testing.T) {
|
||||
f, stdout, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app-pageall1", AppSecret: "test-secret-pageall1", Brand: core.BrandFeishu,
|
||||
f, stdout, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app-pageall1", AppSecret: "test-secret-pageall1", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
// Register a non-batch API that returns scalar data (no array field)
|
||||
@@ -449,8 +451,8 @@ func TestApiCmd_PageAll_NonBatchAPI_FallbackToJSON(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_PageAll_NonBatchAPI_ErrorStillOutputsJSON(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app-pageall-err", AppSecret: "test-secret-pageall-err", Brand: core.BrandFeishu,
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app-pageall-err", AppSecret: "test-secret-pageall-err", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
// Non-batch API that returns a business error (code != 0)
|
||||
@@ -486,8 +488,8 @@ func TestApiCmd_PageAll_NonBatchAPI_ErrorStillOutputsJSON(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_PageAll_BatchAPI_StreamsItems(t *testing.T) {
|
||||
f, stdout, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app-pageall2", AppSecret: "test-secret-pageall2", Brand: core.BrandFeishu,
|
||||
f, stdout, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app-pageall2", AppSecret: "test-secret-pageall2", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
// Register a batch API that returns an array field
|
||||
@@ -519,8 +521,8 @@ func TestApiCmd_PageAll_BatchAPI_StreamsItems(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_PageAll_StreamBusinessErrorDoesNotDumpJSON(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app-pageall-stream-err", AppSecret: "test-secret-pageall-stream-err", Brand: core.BrandFeishu,
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app-pageall-stream-err", AppSecret: "test-secret-pageall-stream-err", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -561,8 +563,8 @@ func TestApiCmd_PageAll_StreamBusinessErrorDoesNotDumpJSON(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_PageAll_BatchAPI_DefaultJSONEnvelope(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app-pageall-json", AppSecret: "test-secret-pageall-json", Brand: core.BrandFeishu,
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app-pageall-json", AppSecret: "test-secret-pageall-json", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -627,8 +629,8 @@ func TestApiCmd_PageAll_DefaultJSONRunsContentSafety(t *testing.T) {
|
||||
extcs.Register(provider)
|
||||
t.Cleanup(func() { extcs.Register(nil) })
|
||||
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app-pageall-safety", AppSecret: "test-secret-pageall-safety", Brand: core.BrandFeishu,
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app-pageall-safety", AppSecret: "test-secret-pageall-safety", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -678,8 +680,8 @@ func TestApiCmd_PageAll_StreamFormatRunsContentSafety(t *testing.T) {
|
||||
extcs.Register(provider)
|
||||
t.Cleanup(func() { extcs.Register(nil) })
|
||||
|
||||
f, stdout, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app-pageall-stream-safety", AppSecret: "test-secret-pageall-stream-safety", Brand: core.BrandFeishu,
|
||||
f, stdout, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app-pageall-stream-safety", AppSecret: "test-secret-pageall-stream-safety", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -723,8 +725,8 @@ func TestApiCmd_PageAll_StreamFormatBlockSkipsBlockedPage(t *testing.T) {
|
||||
extcs.Register(provider)
|
||||
t.Cleanup(func() { extcs.Register(nil) })
|
||||
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app-pageall-stream-block", AppSecret: "test-secret-pageall-stream-block", Brand: core.BrandFeishu,
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app-pageall-stream-block", AppSecret: "test-secret-pageall-stream-block", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -809,8 +811,8 @@ func TestNormalisePath_StripsQueryAndFragment(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_JqFlag_Parsing(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
var gotOpts *APIOptions
|
||||
@@ -829,8 +831,8 @@ func TestApiCmd_JqFlag_Parsing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_JqFlag_ShortForm(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
var gotOpts *APIOptions
|
||||
@@ -849,8 +851,8 @@ func TestApiCmd_JqFlag_ShortForm(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_JqAndOutputConflict(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
@@ -867,8 +869,8 @@ func TestApiCmd_JqAndOutputConflict(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_JqFilter_AppliesExpression(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app-jq", AppSecret: "test-secret-jq", Brand: core.BrandFeishu,
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app-jq", AppSecret: "test-secret-jq", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -901,8 +903,8 @@ func TestApiCmd_JqFilter_AppliesExpression(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_JqAndFormatConflict(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
@@ -919,8 +921,8 @@ func TestApiCmd_JqAndFormatConflict(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_JqInvalidExpression(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
@@ -937,8 +939,8 @@ func TestApiCmd_JqInvalidExpression(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_PageAll_WithJq(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app-pjq", AppSecret: "test-secret-pjq", Brand: core.BrandFeishu,
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app-pjq", AppSecret: "test-secret-pjq", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -968,8 +970,8 @@ func TestApiCmd_PageAll_WithJq(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_MethodUppercase(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
var gotOpts *APIOptions
|
||||
@@ -988,8 +990,8 @@ func TestApiCmd_MethodUppercase(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_FileFlagParsing(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
var gotOpts *APIOptions
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
@@ -1007,8 +1009,8 @@ func TestApiCmd_FileFlagParsing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_FileAndOutputConflict(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
return apiRun(opts)
|
||||
@@ -1024,8 +1026,8 @@ func TestApiCmd_FileAndOutputConflict(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_FileWithGET(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
return apiRun(opts)
|
||||
@@ -1041,8 +1043,8 @@ func TestApiCmd_FileWithGET(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_FileStdinConflictWithData(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
|
||||
return apiRun(opts)
|
||||
@@ -1064,8 +1066,8 @@ func TestApiCmd_DryRunWithFile(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
cmd := newTestApiCmd(f, nil)
|
||||
cmd.SetArgs([]string{"POST", "/open-apis/im/v1/images", "--file", "image=" + tmpFile, "--data", `{"image_type":"message"}`, "--dry-run", "--as", "bot"})
|
||||
@@ -1102,8 +1104,8 @@ func TestApiCmd_DryRunWithFile(t *testing.T) {
|
||||
// — there is no raw-payload passthrough; new Lark diagnostic fields require
|
||||
// a CLI release.
|
||||
func TestApiCmd_PermissionError_DerivesFirstClassFields(t *testing.T) {
|
||||
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "cli_test_perm", AppSecret: "secret", Brand: core.BrandFeishu,
|
||||
f, _, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "cli_test_perm", AppSecret: "secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -1141,8 +1143,8 @@ func TestApiCmd_PermissionError_DerivesFirstClassFields(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_JsonFlag_Accepted(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
var gotOpts *APIOptions
|
||||
@@ -1194,8 +1196,8 @@ func parseMultipartFilenames(t *testing.T, stub *httpmock.Stub) (map[string]stri
|
||||
}
|
||||
|
||||
func TestApiCmd_FileUpload_PreservesFilename(t *testing.T) {
|
||||
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
dir := t.TempDir()
|
||||
@@ -1223,8 +1225,8 @@ func TestApiCmd_FileUpload_PreservesFilename(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_FileUpload_FieldPrefixKeepsBasename(t *testing.T) {
|
||||
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
dir := t.TempDir()
|
||||
@@ -1258,8 +1260,8 @@ func TestApiCmd_FileUpload_FieldPrefixKeepsBasename(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_FileUpload_WithDataFields(t *testing.T) {
|
||||
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
dir := t.TempDir()
|
||||
@@ -1291,8 +1293,8 @@ func TestApiCmd_FileUpload_WithDataFields(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApiCmd_FileUpload_StdinFallsBackToUnknown(t *testing.T) {
|
||||
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
f.IOStreams.In = bytes.NewReader([]byte("stdin-bytes"))
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
|
||||
larkauth "github.com/larksuite/cli/internal/auth"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/errclass"
|
||||
"github.com/larksuite/cli/internal/identity"
|
||||
)
|
||||
|
||||
// NewCmdAuth creates the auth command with subcommands.
|
||||
@@ -130,7 +130,7 @@ func getAppInfo(ctx context.Context, f *cmdutil.Factory, appId string) (*appInfo
|
||||
HttpMethod: http.MethodGet,
|
||||
ApiPath: larkauth.ApplicationInfoPath(appId),
|
||||
QueryParams: queryParams,
|
||||
}, core.AsBot)
|
||||
}, identity.AsBot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -170,7 +170,7 @@ func classifyAppInfoErr(rawBody []byte, code int, msg string, f *cmdutil.Factory
|
||||
}
|
||||
raw["code"] = code
|
||||
raw["msg"] = msg
|
||||
cc := errclass.ClassifyContext{Identity: string(core.AsBot)}
|
||||
cc := errclass.ClassifyContext{Identity: string(identity.AsBot)}
|
||||
if cfg, _ := f.Config(); cfg != nil {
|
||||
cc.Brand = string(cfg.Brand)
|
||||
cc.AppID = appId
|
||||
|
||||
@@ -12,10 +12,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
extcred "github.com/larksuite/cli/extension/credential"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/credential"
|
||||
"github.com/larksuite/cli/internal/httpmock"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
@@ -23,8 +24,8 @@ import (
|
||||
)
|
||||
|
||||
func TestAuthLoginCmd_FlagParsing(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
var gotOpts *LoginOptions
|
||||
@@ -46,8 +47,8 @@ func TestAuthLoginCmd_FlagParsing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthLoginCmd_HelpGuidesNonStreamingAgentsToSplitFlow(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
cmd := NewCmdAuthLogin(f, func(opts *LoginOptions) error { return nil })
|
||||
@@ -72,8 +73,8 @@ func TestAuthLoginCmd_HelpGuidesNonStreamingAgentsToSplitFlow(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthCheckCmd_FlagParsing(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
var gotOpts *CheckOptions
|
||||
@@ -92,8 +93,8 @@ func TestAuthCheckCmd_FlagParsing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthCheckCmd_AcceptsJSONFlag(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
var gotOpts *CheckOptions
|
||||
@@ -192,8 +193,8 @@ func TestAuthListCmd_AcceptsJSONFlag(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthStatusCmd_FlagParsing(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
var gotOpts *StatusOptions
|
||||
@@ -211,8 +212,8 @@ func TestAuthStatusCmd_FlagParsing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthStatusCmd_AcceptsJSONFlag(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
var gotOpts *StatusOptions
|
||||
@@ -234,8 +235,8 @@ func TestAuthStatusCmd_AcceptsJSONFlag(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthStatusCmd_VerifyFlag(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
var gotOpts *StatusOptions
|
||||
@@ -336,8 +337,8 @@ func TestDomainFlagCompletion(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthScopesCmd_FlagParsing(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
var gotOpts *ScopesOptions
|
||||
@@ -356,8 +357,8 @@ func TestAuthScopesCmd_FlagParsing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthScopesCmd_JSONFlagForcesJSONFormat(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
var gotOpts *ScopesOptions
|
||||
@@ -382,8 +383,8 @@ func TestAuthScopesCmd_JSONFlagForcesJSONFormat(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthScopesRun_UsesTenantAccessTokenFromCredentialProvider(t *testing.T) {
|
||||
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "", Brand: core.BrandFeishu,
|
||||
f, _, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "", Brand: brand.Feishu,
|
||||
})
|
||||
tokenResolver := &authScopesTokenResolver{}
|
||||
f.Credential = credential.NewCredentialProvider(nil, nil, tokenResolver, nil)
|
||||
@@ -438,8 +439,8 @@ func TestAuthScopesRun_UsesTenantAccessTokenFromCredentialProvider(t *testing.T)
|
||||
// getAppInfo classifies it as *errs.PermissionError carrying the server-
|
||||
// supplied MissingScopes — not a bare error wrapped as InternalError.
|
||||
func TestAuthScopesRun_LarkPermissionError_TypedAsPermissionError(t *testing.T) {
|
||||
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
tokenResolver := &authScopesTokenResolver{}
|
||||
f.Credential = credential.NewCredentialProvider(nil, nil, tokenResolver, nil)
|
||||
|
||||
@@ -9,9 +9,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
larkauth "github.com/larksuite/cli/internal/auth"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
@@ -23,8 +24,8 @@ import (
|
||||
// branch. These tests pin that contract end-to-end through the dispatcher.
|
||||
|
||||
func TestAuthCheckRun_NotLoggedIn_ExitOneWithStdoutOnly(t *testing.T) {
|
||||
f, stdout, stderr, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, stdout, stderr, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
// UserOpenId left empty: triggers the not_logged_in branch.
|
||||
})
|
||||
|
||||
@@ -55,8 +56,8 @@ func TestAuthCheckRun_NotLoggedIn_ExitOneWithStdoutOnly(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthCheckRun_NoStoredToken_ExitOneWithStdoutOnly(t *testing.T) {
|
||||
f, stdout, stderr, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, stdout, stderr, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
UserOpenId: "ou_user", UserName: "tester",
|
||||
})
|
||||
|
||||
@@ -92,10 +93,10 @@ func TestAuthCheckRun_ScopedTokenPresent_ExitZero(t *testing.T) {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
t.Setenv("LARKSUITE_CLI_DATA_DIR", t.TempDir())
|
||||
|
||||
cfg := &core.CliConfig{
|
||||
cfg := &configpkg.CliConfig{
|
||||
AppID: "test-app",
|
||||
AppSecret: "test-secret",
|
||||
Brand: core.BrandFeishu,
|
||||
Brand: brand.Feishu,
|
||||
UserOpenId: "ou_user",
|
||||
UserName: "tester",
|
||||
}
|
||||
@@ -150,8 +151,8 @@ func TestAuthCheckRun_EmptyScopeIsValidationError(t *testing.T) {
|
||||
// Scope validation is a real input error, not a predicate negative
|
||||
// answer — it must surface as a typed ValidationError with the normal
|
||||
// stderr envelope, distinct from the silent ErrBare predicate path.
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
err := authCheckRun(&CheckOptions{Factory: f, Scope: " "})
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/larksuite/cli/errs"
|
||||
larkauth "github.com/larksuite/cli/internal/auth"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
)
|
||||
|
||||
@@ -45,7 +45,7 @@ func NewCmdAuthList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Co
|
||||
func authListRun(opts *ListOptions) error {
|
||||
f := opts.Factory
|
||||
|
||||
multi, _ := core.LoadMultiAppConfig()
|
||||
multi, _ := configpkg.LoadMultiAppConfig()
|
||||
if multi == nil || len(multi.Apps) == 0 {
|
||||
if opts.JSON {
|
||||
output.PrintJson(f.IOStreams.Out, map[string]interface{}{
|
||||
@@ -61,7 +61,7 @@ func authListRun(opts *ListOptions) error {
|
||||
// workspace-aware, so we pull the message+hint out of
|
||||
// NotConfiguredError() instead of hard-coding it.
|
||||
var cfgErr *errs.ConfigError
|
||||
if errors.As(core.NotConfiguredError(), &cfgErr) {
|
||||
if errors.As(configpkg.NotConfiguredError(), &cfgErr) {
|
||||
fmt.Fprintln(f.IOStreams.ErrOut, cfgErr.Message)
|
||||
if cfgErr.Hint != "" {
|
||||
fmt.Fprintln(f.IOStreams.ErrOut, " hint: "+cfgErr.Hint)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/workspace"
|
||||
)
|
||||
|
||||
// TestAuthListRun_NotConfigured_ReturnsExitZero pins the contract that
|
||||
@@ -69,9 +69,9 @@ func TestAuthListRun_JSONMode_NotConfigured_WritesStdoutOnly(t *testing.T) {
|
||||
func TestAuthListRun_NotConfigured_AgentWorkspace_RoutesToBindHelp(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
|
||||
prev := core.CurrentWorkspace()
|
||||
t.Cleanup(func() { core.SetCurrentWorkspace(prev) })
|
||||
core.SetCurrentWorkspace(core.WorkspaceOpenClaw)
|
||||
prev := workspace.CurrentWorkspace()
|
||||
t.Cleanup(func() { workspace.SetCurrentWorkspace(prev) })
|
||||
workspace.SetCurrentWorkspace(workspace.WorkspaceOpenClaw)
|
||||
|
||||
f, _, stderr, _ := cmdutil.TestFactory(t, nil)
|
||||
if err := authListRun(&ListOptions{Factory: f}); err != nil {
|
||||
|
||||
@@ -13,12 +13,14 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
brandpkg "github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
|
||||
larkauth "github.com/larksuite/cli/internal/auth"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/i18n"
|
||||
"github.com/larksuite/cli/internal/identity"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/registry"
|
||||
"github.com/larksuite/cli/shortcuts"
|
||||
@@ -55,7 +57,7 @@ send the verification URL (or QR code) to the user as your final message, end th
|
||||
run --device-code in a later step after the user confirms authorization. Use 'lark-cli auth qrcode'
|
||||
to generate QR codes (supports ASCII and PNG formats).`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if mode := f.ResolveStrictMode(cmd.Context()); mode == core.StrictModeBot {
|
||||
if mode := f.ResolveStrictMode(cmd.Context()); mode == identity.StrictModeBot {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument,
|
||||
"strict mode is %q, user login is disabled in this profile", mode).
|
||||
WithHint("if the user explicitly wants to switch to user identity, see `lark-cli config strict-mode --help` (confirm with the user before switching; switching does NOT require re-bind)")
|
||||
@@ -72,7 +74,7 @@ to generate QR codes (supports ASCII and PNG formats).`,
|
||||
|
||||
cmd.Flags().StringVar(&opts.Scope, "scope", "", "scopes to request (space- or comma-separated). Combines additively with --domain/--recommend")
|
||||
cmd.Flags().BoolVar(&opts.Recommend, "recommend", false, "request only recommended (auto-approve) scopes")
|
||||
var helpBrand core.LarkBrand
|
||||
var helpBrand brandpkg.Brand
|
||||
if f != nil && f.Config != nil {
|
||||
if cfg, err := f.Config(); err == nil && cfg != nil {
|
||||
helpBrand = cfg.Brand
|
||||
@@ -125,7 +127,7 @@ func authLoginRun(opts *LoginOptions) error {
|
||||
|
||||
// Determine UI language from saved config
|
||||
var lang i18n.Lang
|
||||
if multi, _ := core.LoadMultiAppConfig(); multi != nil {
|
||||
if multi, _ := configpkg.LoadMultiAppConfig(); multi != nil {
|
||||
if app := multi.FindApp(config.ProfileName); app != nil {
|
||||
lang = app.Lang
|
||||
}
|
||||
@@ -391,7 +393,7 @@ func authLoginRun(opts *LoginOptions) error {
|
||||
|
||||
// authLoginPollDeviceCode resumes the device flow by polling with a device code
|
||||
// obtained from a previous --no-wait call.
|
||||
func authLoginPollDeviceCode(opts *LoginOptions, config *core.CliConfig, msg *loginMsg, log func(string, ...interface{})) error {
|
||||
func authLoginPollDeviceCode(opts *LoginOptions, config *configpkg.CliConfig, msg *loginMsg, log func(string, ...interface{})) error {
|
||||
f := opts.Factory
|
||||
|
||||
httpClient, err := f.HttpClient()
|
||||
@@ -474,7 +476,7 @@ func authLoginPollDeviceCode(opts *LoginOptions, config *core.CliConfig, msg *lo
|
||||
|
||||
// syncLoginUserToProfile persists the logged-in user info into the named profile.
|
||||
func syncLoginUserToProfile(profileName, appID, openID, userName string) error {
|
||||
multi, err := core.LoadMultiAppConfig()
|
||||
multi, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
return errs.NewInternalError(errs.SubtypeStorage, "load config: %v", err).WithCause(err)
|
||||
}
|
||||
@@ -484,9 +486,9 @@ func syncLoginUserToProfile(profileName, appID, openID, userName string) error {
|
||||
return errs.NewConfigError(errs.SubtypeNotConfigured, "profile %q not found in config", profileName)
|
||||
}
|
||||
|
||||
oldUsers := append([]core.AppUser(nil), app.Users...)
|
||||
app.Users = []core.AppUser{{UserOpenId: openID, UserName: userName}}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
oldUsers := append([]configpkg.AppUser(nil), app.Users...)
|
||||
app.Users = []configpkg.AppUser{{UserOpenId: openID, UserName: userName}}
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
return errs.NewInternalError(errs.SubtypeStorage, "save config: %v", err).WithCause(err)
|
||||
}
|
||||
|
||||
@@ -499,7 +501,7 @@ func syncLoginUserToProfile(profileName, appID, openID, userName string) error {
|
||||
}
|
||||
|
||||
// findProfileByName returns the AppConfig matching profileName, or nil.
|
||||
func findProfileByName(multi *core.MultiAppConfig, profileName string) *core.AppConfig {
|
||||
func findProfileByName(multi *configpkg.MultiAppConfig, profileName string) *configpkg.AppConfig {
|
||||
for i := range multi.Apps {
|
||||
if multi.Apps[i].ProfileName() == profileName {
|
||||
return &multi.Apps[i]
|
||||
@@ -512,7 +514,7 @@ func findProfileByName(multi *core.MultiAppConfig, profileName string) *core.App
|
||||
// shortcut scopes for the given domain names.
|
||||
// Domains with auth_domain children are automatically expanded to include
|
||||
// their children's scopes.
|
||||
func collectScopesForDomains(domains []string, identity string, brand core.LarkBrand) []string {
|
||||
func collectScopesForDomains(domains []string, identity string, brand brandpkg.Brand) []string {
|
||||
scopeSet := make(map[string]bool)
|
||||
|
||||
// 1. API scopes from from_meta projects
|
||||
@@ -553,7 +555,7 @@ func collectScopesForDomains(domains []string, identity string, brand core.LarkB
|
||||
// allKnownDomains returns all valid auth domain names (from_meta projects +
|
||||
// shortcut services), excluding domains that have auth_domain set (they are
|
||||
// folded into their parent domain).
|
||||
func allKnownDomains(brand core.LarkBrand) map[string]bool {
|
||||
func allKnownDomains(brand brandpkg.Brand) map[string]bool {
|
||||
domains := make(map[string]bool)
|
||||
for _, p := range registry.ListFromMetaProjects() {
|
||||
if !registry.HasAuthDomain(p) {
|
||||
@@ -572,7 +574,7 @@ func allKnownDomains(brand core.LarkBrand) map[string]bool {
|
||||
}
|
||||
|
||||
// sortedKnownDomains returns all valid domain names sorted alphabetically.
|
||||
func sortedKnownDomains(brand core.LarkBrand) []string {
|
||||
func sortedKnownDomains(brand brandpkg.Brand) []string {
|
||||
m := allKnownDomains(brand)
|
||||
domains := make([]string, 0, len(m))
|
||||
for d := range m {
|
||||
|
||||
@@ -6,26 +6,26 @@ package auth
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/brand"
|
||||
)
|
||||
|
||||
func TestBrandFilter_AppsExcludedOnLark(t *testing.T) {
|
||||
feishuDomains := allKnownDomains(core.BrandFeishu)
|
||||
feishuDomains := allKnownDomains(brand.Feishu)
|
||||
if !feishuDomains["apps"] {
|
||||
t.Errorf("expected apps domain to be known on Feishu brand")
|
||||
}
|
||||
|
||||
larkDomains := allKnownDomains(core.BrandLark)
|
||||
larkDomains := allKnownDomains(brand.Lark)
|
||||
if larkDomains["apps"] {
|
||||
t.Errorf("expected apps domain to be EXCLUDED on Lark brand")
|
||||
}
|
||||
|
||||
feishuScopes := collectScopesForDomains([]string{"apps"}, "user", core.BrandFeishu)
|
||||
feishuScopes := collectScopesForDomains([]string{"apps"}, "user", brand.Feishu)
|
||||
if len(feishuScopes) == 0 {
|
||||
t.Errorf("expected non-empty scopes for apps on Feishu brand, got %d", len(feishuScopes))
|
||||
}
|
||||
|
||||
larkScopes := collectScopesForDomains([]string{"apps"}, "user", core.BrandLark)
|
||||
larkScopes := collectScopesForDomains([]string{"apps"}, "user", brand.Lark)
|
||||
if len(larkScopes) != 0 {
|
||||
t.Errorf("expected empty scopes for apps on Lark brand, got %d: %v", len(larkScopes), larkScopes)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
)
|
||||
|
||||
func setupLoginConfigDir(t *testing.T) {
|
||||
@@ -17,22 +17,22 @@ func setupLoginConfigDir(t *testing.T) {
|
||||
|
||||
func TestSyncLoginUserToProfile_UpdatesOnlyTargetProfile(t *testing.T) {
|
||||
setupLoginConfigDir(t)
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "target",
|
||||
Apps: []core.AppConfig{
|
||||
Apps: []configpkg.AppConfig{
|
||||
{
|
||||
Name: "target",
|
||||
AppId: "app-target",
|
||||
Users: []core.AppUser{{UserOpenId: "ou_old", UserName: "old"}},
|
||||
Users: []configpkg.AppUser{{UserOpenId: "ou_old", UserName: "old"}},
|
||||
},
|
||||
{
|
||||
Name: "other",
|
||||
AppId: "app-other",
|
||||
Users: []core.AppUser{{UserOpenId: "ou_other", UserName: "other"}},
|
||||
Users: []configpkg.AppUser{{UserOpenId: "ou_other", UserName: "other"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func TestSyncLoginUserToProfile_UpdatesOnlyTargetProfile(t *testing.T) {
|
||||
t.Fatalf("syncLoginUserToProfile() error = %v", err)
|
||||
}
|
||||
|
||||
saved, err := core.LoadMultiAppConfig()
|
||||
saved, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMultiAppConfig() error = %v", err)
|
||||
}
|
||||
@@ -54,13 +54,13 @@ func TestSyncLoginUserToProfile_UpdatesOnlyTargetProfile(t *testing.T) {
|
||||
|
||||
func TestSyncLoginUserToProfile_ProfileNotFoundReturnsError(t *testing.T) {
|
||||
setupLoginConfigDir(t)
|
||||
multi := &core.MultiAppConfig{
|
||||
Apps: []core.AppConfig{{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
Apps: []configpkg.AppConfig{{
|
||||
Name: "default",
|
||||
AppId: "app-default",
|
||||
}},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
|
||||
brandpkg "github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/registry"
|
||||
"github.com/larksuite/cli/shortcuts"
|
||||
@@ -102,7 +102,7 @@ func buildDomainMeta(name, lang string) domainMeta {
|
||||
}
|
||||
|
||||
// runInteractiveLogin shows an interactive TUI form for domain and permission selection.
|
||||
func runInteractiveLogin(ios *cmdutil.IOStreams, lang string, msg *loginMsg, brand core.LarkBrand) (*interactiveResult, error) {
|
||||
func runInteractiveLogin(ios *cmdutil.IOStreams, lang string, msg *loginMsg, brand brandpkg.Brand) (*interactiveResult, error) {
|
||||
allDomains := getDomainMetadata(lang)
|
||||
|
||||
// Build multi-select options
|
||||
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
"regexp"
|
||||
|
||||
larkauth "github.com/larksuite/cli/internal/auth"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
"github.com/larksuite/cli/internal/vfs"
|
||||
"github.com/larksuite/cli/internal/workspace"
|
||||
)
|
||||
|
||||
var loginScopeCacheSafeChars = regexp.MustCompile(`[^a-zA-Z0-9._-]`)
|
||||
@@ -25,7 +25,7 @@ type loginScopeCacheRecord struct {
|
||||
// loginScopeCacheDir returns the directory used to persist auth login --no-wait
|
||||
// requested scopes keyed by device_code.
|
||||
func loginScopeCacheDir() string {
|
||||
return filepath.Join(core.GetConfigDir(), "cache", "auth_login_scopes")
|
||||
return filepath.Join(workspace.GetConfigDir(), "cache", "auth_login_scopes")
|
||||
}
|
||||
|
||||
// loginScopeCachePath returns the cache file path for a given device_code.
|
||||
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
|
||||
extcred "github.com/larksuite/cli/extension/credential"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
)
|
||||
|
||||
func TestAuthLogin_StrictModeBot_Blocked(t *testing.T) {
|
||||
cfg := &core.CliConfig{
|
||||
cfg := &configpkg.CliConfig{
|
||||
AppID: "a", AppSecret: "s",
|
||||
SupportedIdentities: uint8(extcred.SupportsBot),
|
||||
}
|
||||
@@ -39,7 +39,7 @@ func TestAuthLogin_StrictModeBot_Blocked(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthLogin_StrictModeUser_Allowed(t *testing.T) {
|
||||
cfg := &core.CliConfig{
|
||||
cfg := &configpkg.CliConfig{
|
||||
AppID: "a", AppSecret: "s",
|
||||
SupportedIdentities: uint8(extcred.SupportsUser),
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func TestAuthLogin_StrictModeUser_Allowed(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthLogin_StrictModeOff_Allowed(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "a", AppSecret: "s"})
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "a", AppSecret: "s"})
|
||||
|
||||
var called bool
|
||||
cmd := NewCmdAuthLogin(f, func(opts *LoginOptions) error {
|
||||
|
||||
@@ -14,9 +14,10 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
brandpkg "github.com/larksuite/cli/brand"
|
||||
larkauth "github.com/larksuite/cli/internal/auth"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/httpmock"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/registry"
|
||||
@@ -308,8 +309,8 @@ func TestGetDomainMetadata_HasTitleAndDescription(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthLoginRun_NonTerminal_NoFlags_RejectsWithHint(t *testing.T) {
|
||||
f, _, stderr, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "cli_test", AppSecret: "secret", Brand: core.BrandFeishu,
|
||||
f, _, stderr, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "cli_test", AppSecret: "secret", Brand: brandpkg.Feishu,
|
||||
})
|
||||
// TestFactory has IsTerminal=false by default
|
||||
opts := &LoginOptions{Factory: f, Ctx: context.Background()}
|
||||
@@ -600,21 +601,21 @@ func TestAuthLoginRun_MissingRequestedScopeAlignsWithLoginSuccess(t *testing.T)
|
||||
setupLoginConfigDir(t)
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "default",
|
||||
Apps: []core.AppConfig{
|
||||
Apps: []configpkg.AppConfig{
|
||||
{Name: "default", AppId: "cli_test"},
|
||||
},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
|
||||
f, _, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
f, _, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
ProfileName: "default",
|
||||
AppID: "cli_test",
|
||||
AppSecret: "secret",
|
||||
Brand: core.BrandFeishu,
|
||||
Brand: brandpkg.Feishu,
|
||||
})
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -696,7 +697,7 @@ func TestAuthLoginRun_MissingRequestedScopeAlignsWithLoginSuccess(t *testing.T)
|
||||
if stored.Scope != "offline_access" {
|
||||
t.Fatalf("stored scope = %q", stored.Scope)
|
||||
}
|
||||
cfg, err := core.LoadMultiAppConfig()
|
||||
cfg, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMultiAppConfig() error = %v", err)
|
||||
}
|
||||
@@ -716,21 +717,21 @@ func TestAuthLoginRun_DeviceCodeUsesCachedRequestedScopes(t *testing.T) {
|
||||
setupLoginConfigDir(t)
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "default",
|
||||
Apps: []core.AppConfig{
|
||||
Apps: []configpkg.AppConfig{
|
||||
{Name: "default", AppId: "cli_test"},
|
||||
},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
|
||||
f, stdout, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
f, stdout, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
ProfileName: "default",
|
||||
AppID: "cli_test",
|
||||
AppSecret: "secret",
|
||||
Brand: core.BrandFeishu,
|
||||
Brand: brandpkg.Feishu,
|
||||
})
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -847,15 +848,15 @@ func TestAuthLoginRun_DeviceCodeTokenNilCleansScopeCache(t *testing.T) {
|
||||
|
||||
original := pollDeviceToken
|
||||
t.Cleanup(func() { pollDeviceToken = original })
|
||||
pollDeviceToken = func(ctx context.Context, httpClient *http.Client, appId, appSecret string, brand core.LarkBrand, deviceCode string, interval, expiresIn int, errOut io.Writer) *larkauth.DeviceFlowResult {
|
||||
pollDeviceToken = func(ctx context.Context, httpClient *http.Client, appId, appSecret string, brand brandpkg.Brand, deviceCode string, interval, expiresIn int, errOut io.Writer) *larkauth.DeviceFlowResult {
|
||||
return &larkauth.DeviceFlowResult{OK: true, Token: nil}
|
||||
}
|
||||
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
ProfileName: "default",
|
||||
AppID: "cli_test",
|
||||
AppSecret: "secret",
|
||||
Brand: core.BrandFeishu,
|
||||
Brand: brandpkg.Feishu,
|
||||
})
|
||||
|
||||
err := authLoginRun(&LoginOptions{
|
||||
@@ -886,15 +887,15 @@ func TestAuthLoginRun_JSONAbort_StdoutEventOnly_StderrEmpty(t *testing.T) {
|
||||
|
||||
original := pollDeviceToken
|
||||
t.Cleanup(func() { pollDeviceToken = original })
|
||||
pollDeviceToken = func(ctx context.Context, httpClient *http.Client, appId, appSecret string, brand core.LarkBrand, deviceCode string, interval, expiresIn int, errOut io.Writer) *larkauth.DeviceFlowResult {
|
||||
pollDeviceToken = func(ctx context.Context, httpClient *http.Client, appId, appSecret string, brand brandpkg.Brand, deviceCode string, interval, expiresIn int, errOut io.Writer) *larkauth.DeviceFlowResult {
|
||||
return &larkauth.DeviceFlowResult{OK: false, Message: "user denied"}
|
||||
}
|
||||
|
||||
f, stdout, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
f, stdout, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
ProfileName: "default",
|
||||
AppID: "cli_test",
|
||||
AppSecret: "secret",
|
||||
Brand: core.BrandFeishu,
|
||||
Brand: brandpkg.Feishu,
|
||||
})
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -956,11 +957,11 @@ func TestAuthLoginRun_JSONAbort_StdoutEventOnly_StderrEmpty(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthLoginRun_JSONWriteFailure_NoWaitReturnsWriterError(t *testing.T) {
|
||||
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
f, _, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
ProfileName: "default",
|
||||
AppID: "cli_test",
|
||||
AppSecret: "secret",
|
||||
Brand: core.BrandFeishu,
|
||||
Brand: brandpkg.Feishu,
|
||||
})
|
||||
f.IOStreams.Out = failWriter{}
|
||||
|
||||
@@ -993,11 +994,11 @@ func TestAuthLoginRun_JSONWriteFailure_NoWaitReturnsWriterError(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthLoginRun_NoWaitJSONHintIncludesRawURLGuidance(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
ProfileName: "default",
|
||||
AppID: "cli_test",
|
||||
AppSecret: "secret",
|
||||
Brand: core.BrandFeishu,
|
||||
Brand: brandpkg.Feishu,
|
||||
})
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -1067,11 +1068,11 @@ func TestAuthLoginRun_NoWaitJSONHintIncludesRawURLGuidance(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthLoginRun_JSONWriteFailure_DeviceAuthorizationReturnsWriterError(t *testing.T) {
|
||||
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
f, _, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
ProfileName: "default",
|
||||
AppID: "cli_test",
|
||||
AppSecret: "secret",
|
||||
Brand: core.BrandFeishu,
|
||||
Brand: brandpkg.Feishu,
|
||||
})
|
||||
f.IOStreams.Out = failWriter{}
|
||||
|
||||
@@ -1105,11 +1106,11 @@ func TestAuthLoginRun_JSONWriteFailure_DeviceAuthorizationReturnsWriterError(t *
|
||||
}
|
||||
|
||||
func TestAuthLoginRun_JSONDeviceAuthorizationAgentHintIncludesRawURLGuidance(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
ProfileName: "default",
|
||||
AppID: "cli_test",
|
||||
AppSecret: "secret",
|
||||
Brand: core.BrandFeishu,
|
||||
Brand: brandpkg.Feishu,
|
||||
})
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
|
||||
@@ -11,8 +11,9 @@ import (
|
||||
"github.com/larksuite/cli/errs"
|
||||
larkauth "github.com/larksuite/cli/internal/auth"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/secret"
|
||||
)
|
||||
|
||||
// LogoutOptions holds all inputs for auth logout.
|
||||
@@ -44,7 +45,7 @@ func NewCmdAuthLogout(f *cmdutil.Factory, runF func(*LogoutOptions) error) *cobr
|
||||
func authLogoutRun(opts *LogoutOptions) error {
|
||||
f := opts.Factory
|
||||
|
||||
multi, _ := core.LoadMultiAppConfig()
|
||||
multi, _ := configpkg.LoadMultiAppConfig()
|
||||
if multi == nil || len(multi.Apps) == 0 {
|
||||
if opts.JSON {
|
||||
output.PrintJson(f.IOStreams.Out, map[string]interface{}{
|
||||
@@ -73,7 +74,7 @@ func authLogoutRun(opts *LogoutOptions) error {
|
||||
}
|
||||
|
||||
httpClient, httpErr := f.HttpClient()
|
||||
appSecret, secretErr := core.ResolveSecretInput(app.AppSecret, f.Keychain)
|
||||
appSecret, secretErr := secret.ResolveSecretInput(app.AppSecret, f.Keychain)
|
||||
|
||||
for _, user := range app.Users {
|
||||
if httpErr == nil && secretErr == nil {
|
||||
@@ -94,8 +95,8 @@ func authLogoutRun(opts *LogoutOptions) error {
|
||||
}
|
||||
}
|
||||
|
||||
app.Users = []core.AppUser{}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
app.Users = []configpkg.AppUser{}
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
return errs.NewInternalError(errs.SubtypeStorage, "failed to save config: %v", err).WithCause(err)
|
||||
}
|
||||
if opts.JSON {
|
||||
|
||||
@@ -9,22 +9,24 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
larkauth "github.com/larksuite/cli/internal/auth"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/httpmock"
|
||||
"github.com/larksuite/cli/internal/secret"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
func writeLogoutConfig(t *testing.T, users []core.AppUser) {
|
||||
func writeLogoutConfig(t *testing.T, users []configpkg.AppUser) {
|
||||
t.Helper()
|
||||
if err := core.SaveMultiAppConfig(&core.MultiAppConfig{
|
||||
if err := configpkg.SaveMultiAppConfig(&configpkg.MultiAppConfig{
|
||||
CurrentApp: "test-app",
|
||||
Apps: []core.AppConfig{
|
||||
Apps: []configpkg.AppConfig{
|
||||
{
|
||||
AppId: "test-app",
|
||||
AppSecret: core.PlainSecret("test-secret"),
|
||||
Brand: core.BrandFeishu,
|
||||
AppSecret: secret.PlainSecret("test-secret"),
|
||||
Brand: brand.Feishu,
|
||||
Users: users,
|
||||
},
|
||||
},
|
||||
@@ -91,7 +93,7 @@ func TestAuthLogoutRun_JSONMode_Success_WritesStdoutOnly(t *testing.T) {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
t.Setenv("LARKSUITE_CLI_DATA_DIR", t.TempDir())
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
writeLogoutConfig(t, []core.AppUser{{UserOpenId: "ou_user", UserName: "tester"}})
|
||||
writeLogoutConfig(t, []configpkg.AppUser{{UserOpenId: "ou_user", UserName: "tester"}})
|
||||
if err := larkauth.SetStoredToken(&larkauth.StoredUAToken{
|
||||
AppId: "test-app",
|
||||
UserOpenId: "ou_user",
|
||||
@@ -127,7 +129,7 @@ func TestAuthLogoutRun_DefaultMode_KeepsTextOutput(t *testing.T) {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
t.Setenv("LARKSUITE_CLI_DATA_DIR", t.TempDir())
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
writeLogoutConfig(t, []core.AppUser{{UserOpenId: "ou_user", UserName: "tester"}})
|
||||
writeLogoutConfig(t, []configpkg.AppUser{{UserOpenId: "ou_user", UserName: "tester"}})
|
||||
if err := larkauth.SetStoredToken(&larkauth.StoredUAToken{
|
||||
AppId: "test-app",
|
||||
UserOpenId: "ou_user",
|
||||
@@ -153,19 +155,19 @@ func TestAuthLogoutRun_RevokesTokenAndClearsLocalState(t *testing.T) {
|
||||
setupLoginConfigDir(t)
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "default",
|
||||
Apps: []core.AppConfig{
|
||||
Apps: []configpkg.AppConfig{
|
||||
{
|
||||
Name: "default",
|
||||
AppId: "cli_test",
|
||||
AppSecret: core.PlainSecret("secret"),
|
||||
Brand: core.BrandFeishu,
|
||||
Users: []core.AppUser{{UserOpenId: "ou_user", UserName: "tester"}},
|
||||
AppSecret: secret.PlainSecret("secret"),
|
||||
Brand: brand.Feishu,
|
||||
Users: []configpkg.AppUser{{UserOpenId: "ou_user", UserName: "tester"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
if err := larkauth.SetStoredToken(&larkauth.StoredUAToken{
|
||||
@@ -177,11 +179,11 @@ func TestAuthLogoutRun_RevokesTokenAndClearsLocalState(t *testing.T) {
|
||||
t.Fatalf("SetStoredToken() error = %v", err)
|
||||
}
|
||||
|
||||
f, _, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
f, _, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
ProfileName: "default",
|
||||
AppID: "cli_test",
|
||||
AppSecret: "secret",
|
||||
Brand: core.BrandFeishu,
|
||||
Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -210,7 +212,7 @@ func TestAuthLogoutRun_RevokesTokenAndClearsLocalState(t *testing.T) {
|
||||
if got := larkauth.GetStoredToken("cli_test", "ou_user"); got != nil {
|
||||
t.Fatalf("expected stored token removed, got %#v", got)
|
||||
}
|
||||
saved, err := core.LoadMultiAppConfig()
|
||||
saved, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMultiAppConfig() error = %v", err)
|
||||
}
|
||||
@@ -224,19 +226,19 @@ func TestAuthLogoutRun_FallsBackToAccessTokenWhenRefreshTokenMissing(t *testing.
|
||||
setupLoginConfigDir(t)
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "default",
|
||||
Apps: []core.AppConfig{
|
||||
Apps: []configpkg.AppConfig{
|
||||
{
|
||||
Name: "default",
|
||||
AppId: "cli_test",
|
||||
AppSecret: core.PlainSecret("secret"),
|
||||
Brand: core.BrandFeishu,
|
||||
Users: []core.AppUser{{UserOpenId: "ou_user", UserName: "tester"}},
|
||||
AppSecret: secret.PlainSecret("secret"),
|
||||
Brand: brand.Feishu,
|
||||
Users: []configpkg.AppUser{{UserOpenId: "ou_user", UserName: "tester"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
if err := larkauth.SetStoredToken(&larkauth.StoredUAToken{
|
||||
@@ -247,11 +249,11 @@ func TestAuthLogoutRun_FallsBackToAccessTokenWhenRefreshTokenMissing(t *testing.
|
||||
t.Fatalf("SetStoredToken() error = %v", err)
|
||||
}
|
||||
|
||||
f, _, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
f, _, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
ProfileName: "default",
|
||||
AppID: "cli_test",
|
||||
AppSecret: "secret",
|
||||
Brand: core.BrandFeishu,
|
||||
Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -280,7 +282,7 @@ func TestAuthLogoutRun_FallsBackToAccessTokenWhenRefreshTokenMissing(t *testing.
|
||||
if got := larkauth.GetStoredToken("cli_test", "ou_user"); got != nil {
|
||||
t.Fatalf("expected stored token removed, got %#v", got)
|
||||
}
|
||||
saved, err := core.LoadMultiAppConfig()
|
||||
saved, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMultiAppConfig() error = %v", err)
|
||||
}
|
||||
@@ -294,19 +296,19 @@ func TestAuthLogoutRun_RevokeFailureStillClearsLocalState(t *testing.T) {
|
||||
setupLoginConfigDir(t)
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "default",
|
||||
Apps: []core.AppConfig{
|
||||
Apps: []configpkg.AppConfig{
|
||||
{
|
||||
Name: "default",
|
||||
AppId: "cli_test",
|
||||
AppSecret: core.PlainSecret("secret"),
|
||||
Brand: core.BrandFeishu,
|
||||
Users: []core.AppUser{{UserOpenId: "ou_user", UserName: "tester"}},
|
||||
AppSecret: secret.PlainSecret("secret"),
|
||||
Brand: brand.Feishu,
|
||||
Users: []configpkg.AppUser{{UserOpenId: "ou_user", UserName: "tester"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
if err := larkauth.SetStoredToken(&larkauth.StoredUAToken{
|
||||
@@ -318,11 +320,11 @@ func TestAuthLogoutRun_RevokeFailureStillClearsLocalState(t *testing.T) {
|
||||
t.Fatalf("SetStoredToken() error = %v", err)
|
||||
}
|
||||
|
||||
f, _, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
f, _, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
ProfileName: "default",
|
||||
AppID: "cli_test",
|
||||
AppSecret: "secret",
|
||||
Brand: core.BrandFeishu,
|
||||
Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
@@ -346,7 +348,7 @@ func TestAuthLogoutRun_RevokeFailureStillClearsLocalState(t *testing.T) {
|
||||
if got := larkauth.GetStoredToken("cli_test", "ou_user"); got != nil {
|
||||
t.Fatalf("expected stored token removed, got %#v", got)
|
||||
}
|
||||
saved, err := core.LoadMultiAppConfig()
|
||||
saved, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMultiAppConfig() error = %v", err)
|
||||
}
|
||||
|
||||
@@ -11,14 +11,15 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
)
|
||||
|
||||
func TestNewCmdAuthQRCode_FlagParsing(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
var gotOpts *QRCodeOptions
|
||||
@@ -45,8 +46,8 @@ func TestNewCmdAuthQRCode_FlagParsing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewCmdAuthQRCode_ASCIIFlag(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
var gotOpts *QRCodeOptions
|
||||
|
||||
@@ -9,9 +9,10 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
)
|
||||
|
||||
// stubGetAppInfoErr swaps getAppInfoFn for the duration of t so authScopesRun
|
||||
@@ -31,10 +32,10 @@ func stubGetAppInfoErr(t *testing.T, errToReturn error) {
|
||||
// and reach the getAppInfoFn call.
|
||||
func scopesTestFactory(t *testing.T) *ScopesOptions {
|
||||
t.Helper()
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app",
|
||||
AppSecret: "test-secret",
|
||||
Brand: core.BrandFeishu,
|
||||
Brand: brand.Feishu,
|
||||
})
|
||||
return &ScopesOptions{
|
||||
Factory: f,
|
||||
|
||||
@@ -8,14 +8,15 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/httpmock"
|
||||
)
|
||||
|
||||
func TestAuthStatusRun_SplitsBotAndUserIdentity(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "secret", Brand: core.BrandFeishu,
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
if err := authStatusRun(&StatusOptions{Factory: f}); err != nil {
|
||||
@@ -38,8 +39,8 @@ func TestAuthStatusRun_SplitsBotAndUserIdentity(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthStatusRun_VerifyReportsBotIdentity(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "secret", Brand: core.BrandFeishu,
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "secret", Brand: brand.Feishu,
|
||||
})
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: http.MethodGet,
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"io/fs"
|
||||
|
||||
brandpkg "github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/cmd/api"
|
||||
"github.com/larksuite/cli/cmd/auth"
|
||||
"github.com/larksuite/cli/cmd/completion"
|
||||
@@ -20,11 +21,11 @@ import (
|
||||
"github.com/larksuite/cli/cmd/skill"
|
||||
cmdupdate "github.com/larksuite/cli/cmd/update"
|
||||
"github.com/larksuite/cli/cmd/whoami"
|
||||
_ "github.com/larksuite/cli/events"
|
||||
"github.com/larksuite/cli/internal/apicatalog"
|
||||
"github.com/larksuite/cli/internal/build"
|
||||
"github.com/larksuite/cli/internal/cmdpolicy"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/hook"
|
||||
"github.com/larksuite/cli/internal/keychain"
|
||||
"github.com/larksuite/cli/internal/registry"
|
||||
@@ -43,7 +44,7 @@ type buildConfig struct {
|
||||
skipStrictMode bool
|
||||
skipService bool
|
||||
serviceCatalog *apicatalog.Catalog
|
||||
startupBrand core.LarkBrand
|
||||
startupBrand brandpkg.Brand
|
||||
}
|
||||
|
||||
// WithStartupBrand initializes the API registry with the given brand before
|
||||
@@ -51,7 +52,7 @@ type buildConfig struct {
|
||||
// registry's sync.Once locks onto the Feishu default at first catalog access,
|
||||
// long before the lazily-resolved config brand is known — see
|
||||
// ResolveStartupBrand for the caller-side resolution.
|
||||
func WithStartupBrand(brand core.LarkBrand) BuildOption {
|
||||
func WithStartupBrand(brand brandpkg.Brand) BuildOption {
|
||||
return func(c *buildConfig) {
|
||||
c.startupBrand = brand
|
||||
}
|
||||
|
||||
@@ -14,12 +14,15 @@ import (
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/i18n"
|
||||
"github.com/larksuite/cli/internal/identity"
|
||||
"github.com/larksuite/cli/internal/keychain"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/secret"
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
"github.com/larksuite/cli/internal/vfs"
|
||||
"github.com/larksuite/cli/internal/workspace"
|
||||
)
|
||||
|
||||
// BindOptions holds all inputs for config bind.
|
||||
@@ -128,8 +131,8 @@ func configBindRun(opts *BindOptions) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
core.SetCurrentWorkspace(core.Workspace(source))
|
||||
targetConfigPath := core.GetConfigPath()
|
||||
workspace.SetCurrentWorkspace(workspace.Workspace(source))
|
||||
targetConfigPath := workspace.GetConfigPath()
|
||||
|
||||
existing, err := reconcileExistingBinding(opts, source, targetConfigPath)
|
||||
if err != nil {
|
||||
@@ -186,12 +189,12 @@ func finalizeSource(opts *BindOptions) (string, error) {
|
||||
}
|
||||
|
||||
var detected string
|
||||
switch core.DetectWorkspaceFromEnv(os.Getenv) {
|
||||
case core.WorkspaceOpenClaw:
|
||||
switch workspace.DetectWorkspaceFromEnv(os.Getenv) {
|
||||
case workspace.WorkspaceOpenClaw:
|
||||
detected = "openclaw"
|
||||
case core.WorkspaceHermes:
|
||||
case workspace.WorkspaceHermes:
|
||||
detected = "hermes"
|
||||
case core.WorkspaceLarkChannel:
|
||||
case workspace.WorkspaceLarkChannel:
|
||||
detected = "lark-channel"
|
||||
}
|
||||
|
||||
@@ -264,7 +267,7 @@ func reconcileExistingBinding(opts *BindOptions, source, configPath string) (exi
|
||||
// enumerate candidates, pick one via the shared decision layer, and build a
|
||||
// ready-to-persist AppConfig. Adding a new bind source only requires
|
||||
// implementing SourceBinder — none of the logic below needs to change.
|
||||
func resolveAccount(opts *BindOptions, source string) (*core.AppConfig, error) {
|
||||
func resolveAccount(opts *BindOptions, source string) (*configpkg.AppConfig, error) {
|
||||
binder, err := newBinder(source, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -307,12 +310,12 @@ func resolveIdentity(opts *BindOptions) error {
|
||||
// the bind flow treats a corrupt previous config (commitBinding will
|
||||
// overwrite it cleanly).
|
||||
func hasStrictBotLock(data []byte) bool {
|
||||
var multi core.MultiAppConfig
|
||||
var multi configpkg.MultiAppConfig
|
||||
if err := json.Unmarshal(data, &multi); err != nil {
|
||||
return false
|
||||
}
|
||||
for _, app := range multi.Apps {
|
||||
if app.StrictMode != nil && *app.StrictMode == core.StrictModeBot {
|
||||
if app.StrictMode != nil && *app.StrictMode == identity.StrictModeBot {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -369,16 +372,16 @@ func preferredLang(requested, prior i18n.Lang) i18n.Lang {
|
||||
return prior
|
||||
}
|
||||
|
||||
func applyPreferences(appConfig *core.AppConfig, opts *BindOptions, prior i18n.Lang) {
|
||||
func applyPreferences(appConfig *configpkg.AppConfig, opts *BindOptions, prior i18n.Lang) {
|
||||
switch opts.Identity {
|
||||
case "bot-only":
|
||||
sm := core.StrictModeBot
|
||||
sm := identity.StrictModeBot
|
||||
appConfig.StrictMode = &sm
|
||||
appConfig.DefaultAs = core.AsBot
|
||||
appConfig.DefaultAs = identity.AsBot
|
||||
case "user-default":
|
||||
sm := core.StrictModeOff
|
||||
sm := identity.StrictModeOff
|
||||
appConfig.StrictMode = &sm
|
||||
appConfig.DefaultAs = core.AsUser
|
||||
appConfig.DefaultAs = identity.AsUser
|
||||
}
|
||||
appConfig.Lang = preferredLang(i18n.Lang(opts.Lang), prior)
|
||||
}
|
||||
@@ -389,7 +392,7 @@ func applyPreferences(appConfig *core.AppConfig, opts *BindOptions, prior i18n.L
|
||||
// wrong profile's preference into a re-bind when the workspace holds multiple
|
||||
// named profiles and the active one disagrees with Apps[0].
|
||||
func priorLang(previousConfigBytes []byte) i18n.Lang {
|
||||
var multi core.MultiAppConfig
|
||||
var multi configpkg.MultiAppConfig
|
||||
if json.Unmarshal(previousConfigBytes, &multi) != nil {
|
||||
return ""
|
||||
}
|
||||
@@ -404,10 +407,10 @@ func priorLang(previousConfigBytes []byte) i18n.Lang {
|
||||
// any), and a JSON success envelope. Cleanup runs only after the new config
|
||||
// is durably written — if anything fails earlier, the old workspace stays
|
||||
// usable.
|
||||
func commitBinding(opts *BindOptions, appConfig *core.AppConfig, previousConfigBytes []byte, source, configPath string) error {
|
||||
multi := &core.MultiAppConfig{Apps: []core.AppConfig{*appConfig}}
|
||||
func commitBinding(opts *BindOptions, appConfig *configpkg.AppConfig, previousConfigBytes []byte, source, configPath string) error {
|
||||
multi := &configpkg.MultiAppConfig{Apps: []configpkg.AppConfig{*appConfig}}
|
||||
|
||||
if err := vfs.MkdirAll(core.GetConfigDir(), 0700); err != nil {
|
||||
if err := vfs.MkdirAll(workspace.GetConfigDir(), 0700); err != nil {
|
||||
return errs.NewInternalError(errs.SubtypeFileIO, "failed to create workspace directory: %v", err).WithCause(err)
|
||||
}
|
||||
data, err := json.MarshalIndent(multi, "", " ")
|
||||
@@ -476,8 +479,8 @@ func commitBinding(opts *BindOptions, appConfig *core.AppConfig, previousConfigB
|
||||
// the secret that ForStorage just wrote (old and new secret share the same
|
||||
// keychain key, derived from appId). Best-effort: errors are silently
|
||||
// ignored (same contract as config init's cleanup).
|
||||
func cleanupKeychainFromData(kc keychain.KeychainAccess, data []byte, keep *core.AppConfig) {
|
||||
var multi core.MultiAppConfig
|
||||
func cleanupKeychainFromData(kc keychain.KeychainAccess, data []byte, keep *configpkg.AppConfig) {
|
||||
var multi configpkg.MultiAppConfig
|
||||
if err := json.Unmarshal(data, &multi); err != nil {
|
||||
return
|
||||
}
|
||||
@@ -489,7 +492,7 @@ func cleanupKeychainFromData(kc keychain.KeychainAccess, data []byte, keep *core
|
||||
if keepID != "" && app.AppSecret.Ref != nil && app.AppSecret.Ref.Source == "keychain" && app.AppSecret.Ref.ID == keepID {
|
||||
continue
|
||||
}
|
||||
core.RemoveSecretStore(app.AppSecret, kc)
|
||||
secret.RemoveSecretStore(app.AppSecret, kc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,13 +506,13 @@ func tuiSelectSource(opts *BindOptions) (string, error) {
|
||||
var source string
|
||||
|
||||
// Pre-select based on detected env signals
|
||||
detected := core.DetectWorkspaceFromEnv(os.Getenv)
|
||||
detected := workspace.DetectWorkspaceFromEnv(os.Getenv)
|
||||
switch detected {
|
||||
case core.WorkspaceOpenClaw:
|
||||
case workspace.WorkspaceOpenClaw:
|
||||
source = "openclaw"
|
||||
case core.WorkspaceHermes:
|
||||
case workspace.WorkspaceHermes:
|
||||
source = "hermes"
|
||||
case core.WorkspaceLarkChannel:
|
||||
case workspace.WorkspaceLarkChannel:
|
||||
source = "lark-channel"
|
||||
default:
|
||||
source = "openclaw" // default first option
|
||||
@@ -582,7 +585,7 @@ func tuiConflictPrompt(opts *BindOptions, source, configPath string) (string, er
|
||||
// Build existing binding summary
|
||||
existingSummary := fmt.Sprintf(msg.ConflictDesc, source, "?", "?", configPath)
|
||||
if data, err := vfs.ReadFile(configPath); err == nil {
|
||||
var multi core.MultiAppConfig
|
||||
var multi configpkg.MultiAppConfig
|
||||
if json.Unmarshal(data, &multi) == nil && len(multi.Apps) > 0 {
|
||||
app := multi.Apps[0]
|
||||
existingSummary = fmt.Sprintf(msg.ConflictDesc,
|
||||
|
||||
@@ -13,11 +13,15 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/i18n"
|
||||
"github.com/larksuite/cli/internal/identity"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/secret"
|
||||
"github.com/larksuite/cli/internal/workspace"
|
||||
)
|
||||
|
||||
// wantErrDetail is the normalized comparison shape for a typed error's wire
|
||||
@@ -80,8 +84,8 @@ func assertEnvelope(t *testing.T, stdout []byte, want map[string]any) {
|
||||
// Must be called at the start of any test that may trigger configBindRun (which sets workspace).
|
||||
func saveWorkspace(t *testing.T) {
|
||||
t.Helper()
|
||||
orig := core.CurrentWorkspace()
|
||||
t.Cleanup(func() { core.SetCurrentWorkspace(orig) })
|
||||
orig := workspace.CurrentWorkspace()
|
||||
t.Cleanup(func() { workspace.SetCurrentWorkspace(orig) })
|
||||
}
|
||||
|
||||
// ── Command flag parsing tests (aligned with config_test.go pattern) ──
|
||||
@@ -229,7 +233,7 @@ func TestConfigBindRun_EmptyLangIsNoOp(t *testing.T) {
|
||||
t.Fatalf("configBindRun(--lang %q) = %v, want nil", tc.lang, err)
|
||||
}
|
||||
|
||||
multi, err := core.LoadMultiAppConfig()
|
||||
multi, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMultiAppConfig: %v", err)
|
||||
}
|
||||
@@ -265,7 +269,7 @@ func TestConfigBindRun_OmitLangPreservesPrior(t *testing.T) {
|
||||
t.Fatalf("re-bind (no --lang): %v", err)
|
||||
}
|
||||
|
||||
multi, err := core.LoadMultiAppConfig()
|
||||
multi, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMultiAppConfig: %v", err)
|
||||
}
|
||||
@@ -279,9 +283,9 @@ func TestConfigBindRun_OmitLangPreservesPrior(t *testing.T) {
|
||||
// workspace (set up via `profile add` before a re-bind), the active profile's
|
||||
// Lang must win over a sibling profile that happens to sit earlier in the slice.
|
||||
func TestPriorLang_RespectsCurrentApp(t *testing.T) {
|
||||
multi := core.MultiAppConfig{
|
||||
multi := configpkg.MultiAppConfig{
|
||||
CurrentApp: "active",
|
||||
Apps: []core.AppConfig{
|
||||
Apps: []configpkg.AppConfig{
|
||||
{Name: "stale", AppId: "cli_stale", Lang: i18n.LangJaJP},
|
||||
{Name: "active", AppId: "cli_active", Lang: i18n.LangEnUS},
|
||||
},
|
||||
@@ -300,8 +304,8 @@ func TestPriorLang_RespectsCurrentApp(t *testing.T) {
|
||||
// so a bind-written config (which always has exactly one app and no
|
||||
// CurrentApp field) still inherits its Lang.
|
||||
func TestPriorLang_FallsBackToFirstAppWhenCurrentUnset(t *testing.T) {
|
||||
multi := core.MultiAppConfig{
|
||||
Apps: []core.AppConfig{
|
||||
multi := configpkg.MultiAppConfig{
|
||||
Apps: []configpkg.AppConfig{
|
||||
{AppId: "cli_only", Lang: i18n.LangJaJP},
|
||||
},
|
||||
}
|
||||
@@ -639,8 +643,8 @@ func TestConfigBindRun_LarkChannel_Success(t *testing.T) {
|
||||
// Brand is not in the stdout envelope — read it back from the persisted
|
||||
// workspace config to verify accounts.app.tenant flowed through to the
|
||||
// stored AppConfig.Brand field.
|
||||
core.SetCurrentWorkspace(core.WorkspaceLarkChannel)
|
||||
multi, err := core.LoadMultiAppConfig()
|
||||
workspace.SetCurrentWorkspace(workspace.WorkspaceLarkChannel)
|
||||
multi, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("load workspace config: %v", err)
|
||||
}
|
||||
@@ -686,8 +690,8 @@ func TestConfigBindRun_LarkChannel_LarkTenant(t *testing.T) {
|
||||
if err := configBindRun(&BindOptions{Factory: f, Source: "lark-channel"}); err != nil {
|
||||
t.Fatalf("expected success, got error: %v", err)
|
||||
}
|
||||
core.SetCurrentWorkspace(core.WorkspaceLarkChannel)
|
||||
multi, err := core.LoadMultiAppConfig()
|
||||
workspace.SetCurrentWorkspace(workspace.WorkspaceLarkChannel)
|
||||
multi, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("load workspace config: %v", err)
|
||||
}
|
||||
@@ -801,16 +805,16 @@ func TestConfigShowRun_WorkspaceField(t *testing.T) {
|
||||
configDir := t.TempDir()
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", configDir)
|
||||
|
||||
core.SetCurrentWorkspace(core.WorkspaceLocal)
|
||||
workspace.SetCurrentWorkspace(workspace.WorkspaceLocal)
|
||||
|
||||
multi := &core.MultiAppConfig{
|
||||
Apps: []core.AppConfig{{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
Apps: []configpkg.AppConfig{{
|
||||
AppId: "cli_local_test",
|
||||
AppSecret: core.PlainSecret("secret"),
|
||||
Brand: core.BrandFeishu,
|
||||
AppSecret: secret.PlainSecret("secret"),
|
||||
Brand: brand.Feishu,
|
||||
}},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("save: %v", err)
|
||||
}
|
||||
|
||||
@@ -827,7 +831,7 @@ func TestConfigShowRun_AgentWorkspaceNotBound(t *testing.T) {
|
||||
saveWorkspace(t)
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
|
||||
core.SetCurrentWorkspace(core.WorkspaceOpenClaw)
|
||||
workspace.SetCurrentWorkspace(workspace.WorkspaceOpenClaw)
|
||||
|
||||
f, _, _, _ := cmdutil.TestFactory(t, nil)
|
||||
err := configShowRun(&ConfigShowOptions{Factory: f})
|
||||
@@ -998,7 +1002,7 @@ func TestConfigBindRun_HermesSuccess(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("read config.json: %v", err)
|
||||
}
|
||||
var multi core.MultiAppConfig
|
||||
var multi configpkg.MultiAppConfig
|
||||
if err := json.Unmarshal(data, &multi); err != nil {
|
||||
t.Fatalf("unmarshal config.json: %v", err)
|
||||
}
|
||||
@@ -1008,8 +1012,8 @@ func TestConfigBindRun_HermesSuccess(t *testing.T) {
|
||||
if multi.Apps[0].AppId != "cli_hermes_abc" {
|
||||
t.Errorf("appId = %q, want %q", multi.Apps[0].AppId, "cli_hermes_abc")
|
||||
}
|
||||
if multi.Apps[0].Brand != core.BrandLark {
|
||||
t.Errorf("brand = %q, want %q", multi.Apps[0].Brand, core.BrandLark)
|
||||
if multi.Apps[0].Brand != brand.Lark {
|
||||
t.Errorf("brand = %q, want %q", multi.Apps[0].Brand, brand.Lark)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1275,7 +1279,7 @@ func TestConfigBindRun_Identity_BotOnly_Applied(t *testing.T) {
|
||||
"message": fmt.Sprintf(msg.MessageBotOnly, "cli_abc", "Hermes", brandDisplay("feishu", "en")),
|
||||
})
|
||||
assertPresetApplied(t, filepath.Join(configDir, "hermes", "config.json"),
|
||||
core.StrictModeBot, core.AsBot)
|
||||
identity.StrictModeBot, identity.AsBot)
|
||||
}
|
||||
|
||||
// TestConfigBindRun_FlagModeDefaultsToBotOnly verifies the flag-mode default
|
||||
@@ -1310,7 +1314,7 @@ func TestConfigBindRun_FlagModeDefaultsToBotOnly(t *testing.T) {
|
||||
"message": fmt.Sprintf(msg.MessageBotOnly, "cli_abc", "Hermes", brandDisplay("feishu", "")),
|
||||
})
|
||||
assertPresetApplied(t, filepath.Join(configDir, "hermes", "config.json"),
|
||||
core.StrictModeBot, core.AsBot)
|
||||
identity.StrictModeBot, identity.AsBot)
|
||||
}
|
||||
|
||||
// TestConfigBindRun_WarnsOnIdentityEscalationWithoutForce verifies the
|
||||
@@ -1406,7 +1410,7 @@ func TestConfigBindRun_IdentityEscalationWithForceAllowed(t *testing.T) {
|
||||
t.Fatalf("expected --force to allow the escalation, got: %v", err)
|
||||
}
|
||||
assertPresetApplied(t, filepath.Join(hermesDir, "config.json"),
|
||||
core.StrictModeOff, core.AsUser)
|
||||
identity.StrictModeOff, identity.AsUser)
|
||||
}
|
||||
|
||||
// TestConfigBindRun_AllowsRebindSameBotOnly verifies re-binding the same
|
||||
@@ -1442,7 +1446,7 @@ func TestConfigBindRun_AllowsRebindSameBotOnly(t *testing.T) {
|
||||
t.Fatalf("expected rebind to same bot-only identity to succeed, got: %v", err)
|
||||
}
|
||||
assertPresetApplied(t, filepath.Join(hermesDir, "config.json"),
|
||||
core.StrictModeBot, core.AsBot)
|
||||
identity.StrictModeBot, identity.AsBot)
|
||||
}
|
||||
|
||||
// TestConfigBindRun_AllowsUserDefaultOnUserDefaultConfig verifies that if the
|
||||
@@ -1479,18 +1483,18 @@ func TestConfigBindRun_AllowsUserDefaultOnUserDefaultConfig(t *testing.T) {
|
||||
t.Fatalf("expected user-default→user-default rebind to succeed, got: %v", err)
|
||||
}
|
||||
assertPresetApplied(t, filepath.Join(hermesDir, "config.json"),
|
||||
core.StrictModeOff, core.AsUser)
|
||||
identity.StrictModeOff, identity.AsUser)
|
||||
}
|
||||
|
||||
// assertPresetApplied verifies the on-disk config.json applied the identity
|
||||
// preset's StrictMode + DefaultAs expansion.
|
||||
func assertPresetApplied(t *testing.T, configPath string, wantStrict core.StrictMode, wantDefault core.Identity) {
|
||||
func assertPresetApplied(t *testing.T, configPath string, wantStrict identity.StrictMode, wantDefault identity.Identity) {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", configPath, err)
|
||||
}
|
||||
var multi core.MultiAppConfig
|
||||
var multi configpkg.MultiAppConfig
|
||||
if err := json.Unmarshal(data, &multi); err != nil {
|
||||
t.Fatalf("unmarshal %s: %v", configPath, err)
|
||||
}
|
||||
@@ -1787,10 +1791,10 @@ func TestCleanupKeychainFromData_KeepsSecretSharedWithNewApp(t *testing.T) {
|
||||
}
|
||||
|
||||
oldConfig := []byte(`{"apps":[{"appId":"cli_shared","appSecret":{"source":"keychain","id":"` + sharedID + `"}}]}`)
|
||||
newApp := &core.AppConfig{
|
||||
newApp := &configpkg.AppConfig{
|
||||
AppId: "cli_shared",
|
||||
AppSecret: core.SecretInput{
|
||||
Ref: &core.SecretRef{Source: "keychain", ID: sharedID},
|
||||
AppSecret: secret.SecretInput{
|
||||
Ref: &secret.SecretRef{Source: "keychain", ID: sharedID},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1817,10 +1821,10 @@ func TestCleanupKeychainFromData_RemovesStaleSecretWhenAppIDChanges(t *testing.T
|
||||
}
|
||||
|
||||
oldConfig := []byte(`{"apps":[{"appId":"cli_old","appSecret":{"source":"keychain","id":"` + oldID + `"}}]}`)
|
||||
newApp := &core.AppConfig{
|
||||
newApp := &configpkg.AppConfig{
|
||||
AppId: "cli_new",
|
||||
AppSecret: core.SecretInput{
|
||||
Ref: &core.SecretRef{Source: "keychain", ID: newID},
|
||||
AppSecret: secret.SecretInput{
|
||||
Ref: &secret.SecretRef{Source: "keychain", ID: newID},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,11 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/binding"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/openclawbind"
|
||||
secretpkg "github.com/larksuite/cli/internal/secret"
|
||||
"github.com/larksuite/cli/internal/vfs"
|
||||
)
|
||||
|
||||
@@ -36,7 +38,7 @@ type SourceBinder interface {
|
||||
ListCandidates() ([]Candidate, error)
|
||||
// Build resolves secrets, persists to keychain, and returns a ready AppConfig
|
||||
// for the chosen candidate AppID. Must be called after ListCandidates succeeds.
|
||||
Build(appID string) (*core.AppConfig, error)
|
||||
Build(appID string) (*configpkg.AppConfig, error)
|
||||
}
|
||||
|
||||
// newBinder constructs the SourceBinder for the given source name.
|
||||
@@ -138,15 +140,15 @@ type openclawBinder struct {
|
||||
path string
|
||||
|
||||
// Cached between ListCandidates and Build so we don't re-read / re-parse.
|
||||
cfg *binding.OpenClawRoot
|
||||
rawApps []binding.CandidateApp
|
||||
cfg *openclawbind.OpenClawRoot
|
||||
rawApps []openclawbind.CandidateApp
|
||||
}
|
||||
|
||||
func (b *openclawBinder) Name() string { return "openclaw" }
|
||||
func (b *openclawBinder) ConfigPath() string { return b.path }
|
||||
|
||||
func (b *openclawBinder) ListCandidates() ([]Candidate, error) {
|
||||
cfg, err := binding.ReadOpenClawConfig(b.path)
|
||||
cfg, err := openclawbind.ReadOpenClawConfig(b.path)
|
||||
if err != nil {
|
||||
return nil, errs.NewConfigError(errs.SubtypeInvalidConfig, "cannot read %s: %v", b.path, err).
|
||||
WithHint("verify OpenClaw is installed and configured").
|
||||
@@ -157,7 +159,7 @@ func (b *openclawBinder) ListCandidates() ([]Candidate, error) {
|
||||
WithHint("configure Feishu in OpenClaw first")
|
||||
}
|
||||
|
||||
raw := binding.ListCandidateApps(cfg.Channels.Feishu)
|
||||
raw := openclawbind.ListCandidateApps(cfg.Channels.Feishu)
|
||||
b.cfg = cfg
|
||||
b.rawApps = raw
|
||||
|
||||
@@ -168,12 +170,12 @@ func (b *openclawBinder) ListCandidates() ([]Candidate, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (b *openclawBinder) Build(appID string) (*core.AppConfig, error) {
|
||||
func (b *openclawBinder) Build(appID string) (*configpkg.AppConfig, error) {
|
||||
if b.cfg == nil {
|
||||
return nil, errs.NewInternalError(errs.SubtypeSDKError, "internal: Build called before ListCandidates")
|
||||
}
|
||||
|
||||
var selected *binding.CandidateApp
|
||||
var selected *openclawbind.CandidateApp
|
||||
for i := range b.rawApps {
|
||||
if b.rawApps[i].AppID == appID {
|
||||
selected = &b.rawApps[i]
|
||||
@@ -188,24 +190,24 @@ func (b *openclawBinder) Build(appID string) (*core.AppConfig, error) {
|
||||
return nil, errs.NewConfigError(errs.SubtypeInvalidClient, "appSecret is empty for app %s in %s", selected.AppID, b.path).
|
||||
WithHint("configure channels.feishu.appSecret in openclaw.json")
|
||||
}
|
||||
secret, err := binding.ResolveSecretInput(selected.AppSecret, b.cfg.Secrets, os.Getenv)
|
||||
secret, err := openclawbind.ResolveSecretInput(selected.AppSecret, b.cfg.Secrets, os.Getenv)
|
||||
if err != nil {
|
||||
return nil, errs.NewConfigError(errs.SubtypeInvalidClient, "failed to resolve appSecret for %s: %v", selected.AppID, err).
|
||||
WithHint("check appSecret configuration in %s", b.path).
|
||||
WithCause(err)
|
||||
}
|
||||
|
||||
stored, err := core.ForStorage(selected.AppID, core.PlainSecret(secret), b.opts.Factory.Keychain)
|
||||
stored, err := secretpkg.ForStorage(selected.AppID, secretpkg.PlainSecret(secret), b.opts.Factory.Keychain)
|
||||
if err != nil {
|
||||
return nil, errs.NewInternalError(errs.SubtypeStorage, "keychain unavailable: %v", err).
|
||||
WithHint("use file: reference in config to bypass keychain").
|
||||
WithCause(err)
|
||||
}
|
||||
|
||||
return &core.AppConfig{
|
||||
return &configpkg.AppConfig{
|
||||
AppId: selected.AppID,
|
||||
AppSecret: stored,
|
||||
Brand: core.ParseBrand(selected.Brand),
|
||||
Brand: brand.ParseBrand(selected.Brand),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -238,7 +240,7 @@ func (b *hermesBinder) ListCandidates() ([]Candidate, error) {
|
||||
return []Candidate{{AppID: appID, Label: "default"}}, nil
|
||||
}
|
||||
|
||||
func (b *hermesBinder) Build(appID string) (*core.AppConfig, error) {
|
||||
func (b *hermesBinder) Build(appID string) (*configpkg.AppConfig, error) {
|
||||
if b.envMap == nil {
|
||||
return nil, errs.NewInternalError(errs.SubtypeSDKError, "internal: Build called before ListCandidates")
|
||||
}
|
||||
@@ -251,17 +253,17 @@ func (b *hermesBinder) Build(appID string) (*core.AppConfig, error) {
|
||||
WithHint("run 'hermes setup' to configure Feishu credentials")
|
||||
}
|
||||
|
||||
stored, err := core.ForStorage(appID, core.PlainSecret(appSecret), b.opts.Factory.Keychain)
|
||||
stored, err := secretpkg.ForStorage(appID, secretpkg.PlainSecret(appSecret), b.opts.Factory.Keychain)
|
||||
if err != nil {
|
||||
return nil, errs.NewInternalError(errs.SubtypeStorage, "keychain unavailable: %v", err).
|
||||
WithHint("use file: reference in config to bypass keychain").
|
||||
WithCause(err)
|
||||
}
|
||||
|
||||
return &core.AppConfig{
|
||||
return &configpkg.AppConfig{
|
||||
AppId: appID,
|
||||
AppSecret: stored,
|
||||
Brand: core.ParseBrand(b.envMap["FEISHU_DOMAIN"]),
|
||||
Brand: brand.ParseBrand(b.envMap["FEISHU_DOMAIN"]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -274,14 +276,14 @@ type larkChannelBinder struct {
|
||||
path string
|
||||
|
||||
// Cached between ListCandidates and Build so we don't re-read the file.
|
||||
cfg *binding.LarkChannelRoot
|
||||
cfg *openclawbind.LarkChannelRoot
|
||||
}
|
||||
|
||||
func (b *larkChannelBinder) Name() string { return "lark-channel" }
|
||||
func (b *larkChannelBinder) ConfigPath() string { return b.path }
|
||||
|
||||
func (b *larkChannelBinder) ListCandidates() ([]Candidate, error) {
|
||||
cfg, err := binding.ReadLarkChannelConfig(b.path)
|
||||
cfg, err := openclawbind.ReadLarkChannelConfig(b.path)
|
||||
if err != nil {
|
||||
return nil, errs.NewConfigError(errs.SubtypeInvalidConfig, "cannot read %s: %v", b.path, err).
|
||||
WithHint("verify lark-channel-bridge is installed and configured").
|
||||
@@ -295,7 +297,7 @@ func (b *larkChannelBinder) ListCandidates() ([]Candidate, error) {
|
||||
return []Candidate{{AppID: cfg.Accounts.App.ID, Label: "default"}}, nil
|
||||
}
|
||||
|
||||
func (b *larkChannelBinder) Build(appID string) (*core.AppConfig, error) {
|
||||
func (b *larkChannelBinder) Build(appID string) (*configpkg.AppConfig, error) {
|
||||
if b.cfg == nil {
|
||||
return nil, errs.NewInternalError(errs.SubtypeSDKError, "internal: Build called before ListCandidates")
|
||||
}
|
||||
@@ -309,24 +311,24 @@ func (b *larkChannelBinder) Build(appID string) (*core.AppConfig, error) {
|
||||
|
||||
// Resolve through the same SecretInput pipeline openclaw uses, so
|
||||
// bridge configs can use ${VAR} / env / file / exec just like openclaw.
|
||||
secret, err := binding.ResolveSecretInput(b.cfg.Accounts.App.Secret, b.cfg.Secrets, os.Getenv)
|
||||
secret, err := openclawbind.ResolveSecretInput(b.cfg.Accounts.App.Secret, b.cfg.Secrets, os.Getenv)
|
||||
if err != nil {
|
||||
return nil, errs.NewConfigError(errs.SubtypeInvalidClient, "failed to resolve appSecret for %s: %v", appID, err).
|
||||
WithHint("check appSecret configuration in %s", b.path).
|
||||
WithCause(err)
|
||||
}
|
||||
|
||||
stored, err := core.ForStorage(appID, core.PlainSecret(secret), b.opts.Factory.Keychain)
|
||||
stored, err := secretpkg.ForStorage(appID, secretpkg.PlainSecret(secret), b.opts.Factory.Keychain)
|
||||
if err != nil {
|
||||
return nil, errs.NewInternalError(errs.SubtypeStorage, "keychain unavailable: %v", err).
|
||||
WithHint("use file: reference in config to bypass keychain").
|
||||
WithCause(err)
|
||||
}
|
||||
|
||||
return &core.AppConfig{
|
||||
return &configpkg.AppConfig{
|
||||
AppId: appID,
|
||||
AppSecret: stored,
|
||||
Brand: core.ParseBrand(b.cfg.Accounts.App.Tenant),
|
||||
Brand: brand.ParseBrand(b.cfg.Accounts.App.Tenant),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
)
|
||||
|
||||
@@ -20,10 +20,10 @@ type fakeBinder struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func (b *fakeBinder) Name() string { return b.name }
|
||||
func (b *fakeBinder) ConfigPath() string { return b.path }
|
||||
func (b *fakeBinder) ListCandidates() ([]Candidate, error) { return nil, nil }
|
||||
func (b *fakeBinder) Build(appID string) (*core.AppConfig, error) { return nil, nil }
|
||||
func (b *fakeBinder) Name() string { return b.name }
|
||||
func (b *fakeBinder) ConfigPath() string { return b.path }
|
||||
func (b *fakeBinder) ListCandidates() ([]Candidate, error) { return nil, nil }
|
||||
func (b *fakeBinder) Build(appID string) (*configpkg.AppConfig, error) { return nil, nil }
|
||||
|
||||
// tuiUnreachable is a tuiPrompt that fails the test if called. It's the
|
||||
// guardrail that proves the non-TUI decision paths really do stay out of the
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -38,6 +38,6 @@ func NewCmdConfig(f *cmdutil.Factory) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func parseBrand(value string) core.LarkBrand {
|
||||
return core.ParseBrand(value)
|
||||
func parseBrand(value string) brand.Brand {
|
||||
return brand.ParseBrand(value)
|
||||
}
|
||||
|
||||
@@ -12,14 +12,16 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
extcred "github.com/larksuite/cli/extension/credential"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/credential"
|
||||
"github.com/larksuite/cli/internal/i18n"
|
||||
"github.com/larksuite/cli/internal/keychain"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/secret"
|
||||
)
|
||||
|
||||
type noopConfigKeychain struct{}
|
||||
@@ -66,8 +68,8 @@ func TestConfigInitCmd_FlagParsing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigShowCmd_FlagParsing(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
var gotOpts *ConfigShowOptions
|
||||
@@ -108,16 +110,16 @@ func TestConfigShowRun_NotConfiguredReturnsStructuredError(t *testing.T) {
|
||||
|
||||
func TestConfigShowRun_NoActiveProfileReturnsStructuredError(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "missing",
|
||||
Apps: []core.AppConfig{{
|
||||
Apps: []configpkg.AppConfig{{
|
||||
Name: "default",
|
||||
AppId: "app-default",
|
||||
AppSecret: core.PlainSecret("secret-default"),
|
||||
Brand: core.BrandFeishu,
|
||||
AppSecret: secret.PlainSecret("secret-default"),
|
||||
Brand: brand.Feishu,
|
||||
}},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
|
||||
@@ -186,18 +188,18 @@ func TestSaveInitConfig_OmitLangPreservesPrior(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
f, _, _, _ := cmdutil.TestFactory(t, nil)
|
||||
|
||||
existing := &core.MultiAppConfig{Apps: []core.AppConfig{
|
||||
{AppId: "cli_x", AppSecret: core.PlainSecret("s"), Brand: core.BrandFeishu, Lang: i18n.LangJaJP},
|
||||
existing := &configpkg.MultiAppConfig{Apps: []configpkg.AppConfig{
|
||||
{AppId: "cli_x", AppSecret: secret.PlainSecret("s"), Brand: brand.Feishu, Lang: i18n.LangJaJP},
|
||||
}}
|
||||
if err := core.SaveMultiAppConfig(existing); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(existing); err != nil {
|
||||
t.Fatalf("seed config: %v", err)
|
||||
}
|
||||
|
||||
if err := saveInitConfig("", existing, f, "cli_x", core.PlainSecret("s2"), core.BrandFeishu, ""); err != nil {
|
||||
if err := saveInitConfig("", existing, f, "cli_x", secret.PlainSecret("s2"), brand.Feishu, ""); err != nil {
|
||||
t.Fatalf("saveInitConfig (no --lang): %v", err)
|
||||
}
|
||||
|
||||
got, err := core.LoadMultiAppConfig()
|
||||
got, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMultiAppConfig: %v", err)
|
||||
}
|
||||
@@ -318,17 +320,17 @@ func TestConfigRemoveRun_SaveFailurePreservesExistingConfigAndSecrets(t *testing
|
||||
configDir := t.TempDir()
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", configDir)
|
||||
|
||||
multi := &core.MultiAppConfig{
|
||||
Apps: []core.AppConfig{{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
Apps: []configpkg.AppConfig{{
|
||||
AppId: "app-test",
|
||||
AppSecret: core.SecretInput{
|
||||
Ref: &core.SecretRef{Source: "keychain", ID: "appsecret:app-test"},
|
||||
AppSecret: secret.SecretInput{
|
||||
Ref: &secret.SecretRef{Source: "keychain", ID: "appsecret:app-test"},
|
||||
},
|
||||
Brand: core.BrandFeishu,
|
||||
Users: []core.AppUser{{UserOpenId: "ou_1", UserName: "Tester"}},
|
||||
Brand: brand.Feishu,
|
||||
Users: []configpkg.AppUser{{UserOpenId: "ou_1", UserName: "Tester"}},
|
||||
}},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
|
||||
@@ -357,7 +359,7 @@ func TestConfigRemoveRun_SaveFailurePreservesExistingConfigAndSecrets(t *testing
|
||||
if err := os.Chmod(configDir, 0700); err != nil {
|
||||
t.Fatalf("restore Chmod(%s) error = %v", configDir, err)
|
||||
}
|
||||
saved, err := core.LoadMultiAppConfig()
|
||||
saved, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMultiAppConfig() error = %v", err)
|
||||
}
|
||||
@@ -377,18 +379,18 @@ func TestConfigRemoveRun_SaveFailurePreservesExistingConfigAndSecrets(t *testing
|
||||
func TestSaveAsProfile_RejectsProfileNameCollisionWithExistingAppID(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
|
||||
existing := &core.MultiAppConfig{
|
||||
Apps: []core.AppConfig{
|
||||
existing := &configpkg.MultiAppConfig{
|
||||
Apps: []configpkg.AppConfig{
|
||||
{
|
||||
Name: "prod",
|
||||
AppId: "cli_prod",
|
||||
AppSecret: core.PlainSecret("secret"),
|
||||
Brand: core.BrandFeishu,
|
||||
AppSecret: secret.PlainSecret("secret"),
|
||||
Brand: brand.Feishu,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := saveAsProfile(existing, keychain.KeychainAccess(&noopConfigKeychain{}), "cli_prod", "app-new", core.PlainSecret("new-secret"), core.BrandLark, "en")
|
||||
err := saveAsProfile(existing, keychain.KeychainAccess(&noopConfigKeychain{}), "cli_prod", "app-new", secret.PlainSecret("new-secret"), brand.Lark, "en")
|
||||
if err == nil {
|
||||
t.Fatal("expected conflict error")
|
||||
}
|
||||
@@ -428,21 +430,21 @@ func TestWrapSaveConfigError_PassesTypedValidationThrough(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdateExistingProfileWithoutSecret_RejectsAppIDChange(t *testing.T) {
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "prod",
|
||||
Apps: []core.AppConfig{
|
||||
Apps: []configpkg.AppConfig{
|
||||
{
|
||||
Name: "prod",
|
||||
AppId: "app-old",
|
||||
AppSecret: core.SecretInput{Ref: &core.SecretRef{Source: "keychain", ID: "appsecret:app-old"}},
|
||||
Brand: core.BrandFeishu,
|
||||
AppSecret: secret.SecretInput{Ref: &secret.SecretRef{Source: "keychain", ID: "appsecret:app-old"}},
|
||||
Brand: brand.Feishu,
|
||||
Lang: "zh",
|
||||
Users: []core.AppUser{{UserOpenId: "ou_1", UserName: "User"}},
|
||||
Users: []configpkg.AppUser{{UserOpenId: "ou_1", UserName: "User"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := updateExistingProfileWithoutSecret(multi, "", "app-new", core.BrandLark, "en")
|
||||
err := updateExistingProfileWithoutSecret(multi, "", "app-new", brand.Lark, "en")
|
||||
if err == nil {
|
||||
t.Fatal("expected error when changing app ID without a new secret")
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import (
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/identity"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -20,14 +21,14 @@ func NewCmdConfigDefaultAs(f *cmdutil.Factory) *cobra.Command {
|
||||
Long: "Without arguments, shows the current default identity. Pass user, bot, or auto to set a new default.",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
multi, err := core.LoadOrNotConfigured()
|
||||
multi, err := configpkg.LoadOrNotConfigured()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
app := multi.CurrentAppConfig(f.Invocation.Profile)
|
||||
if app == nil {
|
||||
return core.NoActiveProfileError()
|
||||
return configpkg.NoActiveProfileError()
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
@@ -44,8 +45,8 @@ func NewCmdConfigDefaultAs(f *cmdutil.Factory) *cobra.Command {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid identity type %q, valid values: user | bot | auto", value)
|
||||
}
|
||||
|
||||
app.DefaultAs = core.Identity(value)
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
app.DefaultAs = identity.Identity(value)
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
return errs.NewInternalError(errs.SubtypeStorage, "failed to save config: %v", err).WithCause(err)
|
||||
}
|
||||
fmt.Fprintf(f.IOStreams.ErrOut, "Default identity set to: %s\n", value)
|
||||
|
||||
@@ -13,13 +13,16 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
brandpkg "github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/auth"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/i18n"
|
||||
"github.com/larksuite/cli/internal/keychain"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
secretpkg "github.com/larksuite/cli/internal/secret"
|
||||
"github.com/larksuite/cli/internal/workspace"
|
||||
)
|
||||
|
||||
// ConfigInitOptions holds all inputs for config init.
|
||||
@@ -121,7 +124,7 @@ func guardAgentWorkspace(opts *ConfigInitOptions) error {
|
||||
if opts.ForceInit {
|
||||
return nil
|
||||
}
|
||||
ws := core.DetectWorkspaceFromEnv(os.Getenv)
|
||||
ws := workspace.DetectWorkspaceFromEnv(os.Getenv)
|
||||
if ws.IsLocal() {
|
||||
return nil
|
||||
}
|
||||
@@ -136,7 +139,7 @@ func (o *ConfigInitOptions) hasAnyNonInteractiveFlag() bool {
|
||||
}
|
||||
|
||||
// cleanupOldConfig clears keychain entries (AppSecret + UAT) for all apps in existing config except the app whose AppId equals skipAppID.
|
||||
func cleanupOldConfig(existing *core.MultiAppConfig, f *cmdutil.Factory, skipAppID string) {
|
||||
func cleanupOldConfig(existing *configpkg.MultiAppConfig, f *cmdutil.Factory, skipAppID string) {
|
||||
if existing == nil {
|
||||
return
|
||||
}
|
||||
@@ -144,7 +147,7 @@ func cleanupOldConfig(existing *core.MultiAppConfig, f *cmdutil.Factory, skipApp
|
||||
if app.AppId == skipAppID {
|
||||
continue
|
||||
}
|
||||
core.RemoveSecretStore(app.AppSecret, f.Keychain)
|
||||
secretpkg.RemoveSecretStore(app.AppSecret, f.Keychain)
|
||||
for _, user := range app.Users {
|
||||
auth.RemoveStoredToken(app.AppId, user.UserOpenId)
|
||||
}
|
||||
@@ -152,19 +155,19 @@ func cleanupOldConfig(existing *core.MultiAppConfig, f *cmdutil.Factory, skipApp
|
||||
}
|
||||
|
||||
// saveAsOnlyApp overwrites config.json with a single-app config.
|
||||
func saveAsOnlyApp(appId string, secret core.SecretInput, brand core.LarkBrand, lang string) error {
|
||||
config := &core.MultiAppConfig{
|
||||
Apps: []core.AppConfig{{
|
||||
AppId: appId, AppSecret: secret, Brand: brand, Lang: i18n.Lang(lang), Users: []core.AppUser{},
|
||||
func saveAsOnlyApp(appId string, secret secretpkg.SecretInput, brand brandpkg.Brand, lang string) error {
|
||||
config := &configpkg.MultiAppConfig{
|
||||
Apps: []configpkg.AppConfig{{
|
||||
AppId: appId, AppSecret: secret, Brand: brand, Lang: i18n.Lang(lang), Users: []configpkg.AppUser{},
|
||||
}},
|
||||
}
|
||||
return core.SaveMultiAppConfig(config)
|
||||
return configpkg.SaveMultiAppConfig(config)
|
||||
}
|
||||
|
||||
// saveInitConfig saves a new/updated app config, respecting --profile mode.
|
||||
// With profileName: appends or updates the named profile (preserves other profiles).
|
||||
// Without profileName: cleans up old config and saves as the only app.
|
||||
func saveInitConfig(profileName string, existing *core.MultiAppConfig, f *cmdutil.Factory, appId string, secret core.SecretInput, brand core.LarkBrand, lang string) error {
|
||||
func saveInitConfig(profileName string, existing *configpkg.MultiAppConfig, f *cmdutil.Factory, appId string, secret secretpkg.SecretInput, brand brandpkg.Brand, lang string) error {
|
||||
if profileName != "" {
|
||||
return saveAsProfile(existing, f.Keychain, profileName, appId, secret, brand, lang)
|
||||
}
|
||||
@@ -195,20 +198,20 @@ func wrapSaveConfigError(err error) error {
|
||||
// saveAsProfile appends or updates a named profile in the config.
|
||||
// If a profile with the same name exists, it updates it; otherwise appends.
|
||||
// When updating, cleans up old keychain secrets if AppId changed.
|
||||
func saveAsProfile(existing *core.MultiAppConfig, kc keychain.KeychainAccess, profileName, appId string, secret core.SecretInput, brand core.LarkBrand, lang string) error {
|
||||
func saveAsProfile(existing *configpkg.MultiAppConfig, kc keychain.KeychainAccess, profileName, appId string, secret secretpkg.SecretInput, brand brandpkg.Brand, lang string) error {
|
||||
multi := existing
|
||||
if multi == nil {
|
||||
multi = &core.MultiAppConfig{}
|
||||
multi = &configpkg.MultiAppConfig{}
|
||||
}
|
||||
|
||||
if idx := findProfileIndexByName(multi, profileName); idx >= 0 {
|
||||
// Clean up old keychain secret and user tokens if AppId changed
|
||||
if multi.Apps[idx].AppId != appId {
|
||||
core.RemoveSecretStore(multi.Apps[idx].AppSecret, kc)
|
||||
secretpkg.RemoveSecretStore(multi.Apps[idx].AppSecret, kc)
|
||||
for _, user := range multi.Apps[idx].Users {
|
||||
auth.RemoveStoredToken(multi.Apps[idx].AppId, user.UserOpenId)
|
||||
}
|
||||
multi.Apps[idx].Users = []core.AppUser{}
|
||||
multi.Apps[idx].Users = []configpkg.AppUser{}
|
||||
}
|
||||
multi.Apps[idx].AppId = appId
|
||||
multi.Apps[idx].AppSecret = secret
|
||||
@@ -221,19 +224,19 @@ func saveAsProfile(existing *core.MultiAppConfig, kc keychain.KeychainAccess, pr
|
||||
WithParam("--name")
|
||||
}
|
||||
// Append new profile
|
||||
multi.Apps = append(multi.Apps, core.AppConfig{
|
||||
multi.Apps = append(multi.Apps, configpkg.AppConfig{
|
||||
Name: profileName,
|
||||
AppId: appId,
|
||||
AppSecret: secret,
|
||||
Brand: brand,
|
||||
Lang: i18n.Lang(lang),
|
||||
Users: []core.AppUser{},
|
||||
Users: []configpkg.AppUser{},
|
||||
})
|
||||
}
|
||||
return core.SaveMultiAppConfig(multi)
|
||||
return configpkg.SaveMultiAppConfig(multi)
|
||||
}
|
||||
|
||||
func findProfileIndexByName(multi *core.MultiAppConfig, profileName string) int {
|
||||
func findProfileIndexByName(multi *configpkg.MultiAppConfig, profileName string) int {
|
||||
if multi == nil {
|
||||
return -1
|
||||
}
|
||||
@@ -245,7 +248,7 @@ func findProfileIndexByName(multi *core.MultiAppConfig, profileName string) int
|
||||
return -1
|
||||
}
|
||||
|
||||
func findAppIndexByAppID(multi *core.MultiAppConfig, appID string) int {
|
||||
func findAppIndexByAppID(multi *configpkg.MultiAppConfig, appID string) int {
|
||||
if multi == nil {
|
||||
return -1
|
||||
}
|
||||
@@ -272,13 +275,13 @@ func wrapUpdateExistingProfileErr(err error) error {
|
||||
return errs.NewInternalError(errs.SubtypeSDKError, "failed to save config: %v", err).WithCause(err)
|
||||
}
|
||||
|
||||
func updateExistingProfileWithoutSecret(existing *core.MultiAppConfig, profileName, appID string, brand core.LarkBrand, lang string) error {
|
||||
func updateExistingProfileWithoutSecret(existing *configpkg.MultiAppConfig, profileName, appID string, brand brandpkg.Brand, lang string) error {
|
||||
if existing == nil {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "App Secret cannot be empty for new configuration").
|
||||
WithParam("--app-secret")
|
||||
}
|
||||
|
||||
var app *core.AppConfig
|
||||
var app *configpkg.AppConfig
|
||||
if profileName != "" {
|
||||
if idx := findProfileIndexByName(existing, profileName); idx >= 0 {
|
||||
app = &existing.Apps[idx]
|
||||
@@ -302,7 +305,7 @@ func updateExistingProfileWithoutSecret(existing *core.MultiAppConfig, profileNa
|
||||
app.AppId = appID
|
||||
app.Brand = brand
|
||||
app.Lang = preferredLang(i18n.Lang(lang), app.Lang)
|
||||
return core.SaveMultiAppConfig(existing)
|
||||
return configpkg.SaveMultiAppConfig(existing)
|
||||
}
|
||||
|
||||
func configInitRun(opts *ConfigInitOptions) error {
|
||||
@@ -323,14 +326,14 @@ func configInitRun(opts *ConfigInitOptions) error {
|
||||
}
|
||||
}
|
||||
|
||||
existing, err := core.LoadMultiAppConfig()
|
||||
existing, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
existing = nil // treat as empty
|
||||
}
|
||||
|
||||
// Validate --profile name if set
|
||||
if opts.ProfileName != "" {
|
||||
if err := core.ValidateProfileName(opts.ProfileName); err != nil {
|
||||
if err := configpkg.ValidateProfileName(opts.ProfileName); err != nil {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%v", err).WithCause(err)
|
||||
}
|
||||
}
|
||||
@@ -338,14 +341,14 @@ func configInitRun(opts *ConfigInitOptions) error {
|
||||
// Mode 1: Non-interactive
|
||||
if opts.AppID != "" && opts.appSecret != "" {
|
||||
brand := parseBrand(opts.Brand)
|
||||
secret, err := core.ForStorage(opts.AppID, core.PlainSecret(opts.appSecret), f.Keychain)
|
||||
secret, err := secretpkg.ForStorage(opts.AppID, secretpkg.PlainSecret(opts.appSecret), f.Keychain)
|
||||
if err != nil {
|
||||
return errs.NewInternalError(errs.SubtypeSDKError, "%v", err).WithCause(err)
|
||||
}
|
||||
if err := saveInitConfig(opts.ProfileName, existing, f, opts.AppID, secret, brand, opts.Lang); err != nil {
|
||||
return wrapSaveConfigError(err)
|
||||
}
|
||||
output.PrintSuccess(f.IOStreams.ErrOut, fmt.Sprintf("Configuration saved to %s", core.GetConfigPath()))
|
||||
output.PrintSuccess(f.IOStreams.ErrOut, fmt.Sprintf("Configuration saved to %s", workspace.GetConfigPath()))
|
||||
printLangPreferenceConfirmation(opts)
|
||||
output.PrintJson(f.IOStreams.Out, map[string]interface{}{"appId": opts.AppID, "appSecret": "****", "brand": brand})
|
||||
if err := runProbe(opts.Ctx, f, opts.AppID, opts.appSecret, brand); err != nil {
|
||||
@@ -377,8 +380,8 @@ func configInitRun(opts *ConfigInitOptions) error {
|
||||
if result == nil {
|
||||
return errs.NewInternalError(errs.SubtypeSDKError, "app creation returned no result")
|
||||
}
|
||||
existing, _ := core.LoadMultiAppConfig()
|
||||
secret, err := core.ForStorage(result.AppID, core.PlainSecret(result.AppSecret), f.Keychain)
|
||||
existing, _ := configpkg.LoadMultiAppConfig()
|
||||
secret, err := secretpkg.ForStorage(result.AppID, secretpkg.PlainSecret(result.AppSecret), f.Keychain)
|
||||
if err != nil {
|
||||
return errs.NewInternalError(errs.SubtypeSDKError, "%v", err).WithCause(err)
|
||||
}
|
||||
@@ -404,11 +407,11 @@ func configInitRun(opts *ConfigInitOptions) error {
|
||||
WithParam("--app-id")
|
||||
}
|
||||
|
||||
existing, _ := core.LoadMultiAppConfig()
|
||||
existing, _ := configpkg.LoadMultiAppConfig()
|
||||
|
||||
if result.AppSecret != "" {
|
||||
// New secret provided (either from "create" or "existing" with input)
|
||||
secret, err := core.ForStorage(result.AppID, core.PlainSecret(result.AppSecret), f.Keychain)
|
||||
secret, err := secretpkg.ForStorage(result.AppID, secretpkg.PlainSecret(result.AppSecret), f.Keychain)
|
||||
if err != nil {
|
||||
return errs.NewInternalError(errs.SubtypeSDKError, "%v", err).WithCause(err)
|
||||
}
|
||||
@@ -443,7 +446,7 @@ func configInitRun(opts *ConfigInitOptions) error {
|
||||
}
|
||||
|
||||
// Mode 5: Legacy interactive (readline fallback)
|
||||
firstApp := (*core.AppConfig)(nil)
|
||||
firstApp := (*configpkg.AppConfig)(nil)
|
||||
if existing != nil {
|
||||
firstApp = existing.CurrentAppConfig("")
|
||||
}
|
||||
@@ -494,9 +497,9 @@ func configInitRun(opts *ConfigInitOptions) error {
|
||||
if resolvedAppId == "" && firstApp != nil {
|
||||
resolvedAppId = firstApp.AppId
|
||||
}
|
||||
var resolvedSecret core.SecretInput
|
||||
var resolvedSecret secretpkg.SecretInput
|
||||
if appSecretInput != "" {
|
||||
resolvedSecret = core.PlainSecret(appSecretInput)
|
||||
resolvedSecret = secretpkg.PlainSecret(appSecretInput)
|
||||
} else if firstApp != nil {
|
||||
resolvedSecret = firstApp.AppSecret
|
||||
}
|
||||
@@ -513,14 +516,14 @@ func configInitRun(opts *ConfigInitOptions) error {
|
||||
WithParam("--app-id")
|
||||
}
|
||||
|
||||
storedSecret, err := core.ForStorage(resolvedAppId, resolvedSecret, f.Keychain)
|
||||
storedSecret, err := secretpkg.ForStorage(resolvedAppId, resolvedSecret, f.Keychain)
|
||||
if err != nil {
|
||||
return errs.NewInternalError(errs.SubtypeSDKError, "%v", err).WithCause(err)
|
||||
}
|
||||
if err := saveInitConfig(opts.ProfileName, existing, f, resolvedAppId, storedSecret, parseBrand(resolvedBrand), opts.Lang); err != nil {
|
||||
return wrapSaveConfigError(err)
|
||||
}
|
||||
output.PrintSuccess(f.IOStreams.ErrOut, fmt.Sprintf("Configuration saved to %s", core.GetConfigPath()))
|
||||
output.PrintSuccess(f.IOStreams.ErrOut, fmt.Sprintf("Configuration saved to %s", workspace.GetConfigPath()))
|
||||
printLangPreferenceConfirmation(opts)
|
||||
if appSecretInput != "" {
|
||||
if err := runProbe(opts.Ctx, f, resolvedAppId, appSecretInput, parseBrand(resolvedBrand)); err != nil {
|
||||
|
||||
@@ -10,13 +10,14 @@ import (
|
||||
"net"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/internal/build"
|
||||
qrcode "github.com/skip2/go-qrcode"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
larkauth "github.com/larksuite/cli/internal/auth"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/transport"
|
||||
)
|
||||
@@ -24,7 +25,7 @@ import (
|
||||
// configInitResult holds the result of the interactive config init flow.
|
||||
type configInitResult struct {
|
||||
Mode string // "create" or "existing"
|
||||
Brand core.LarkBrand
|
||||
Brand brand.Brand
|
||||
AppID string
|
||||
AppSecret string
|
||||
}
|
||||
@@ -62,8 +63,8 @@ func runInteractiveConfigInit(ctx context.Context, f *cmdutil.Factory, msg *init
|
||||
// runExistingAppForm shows a huh form for manually entering App ID / App Secret / Brand.
|
||||
func runExistingAppForm(f *cmdutil.Factory, msg *initMsg) (*configInitResult, error) {
|
||||
// Load existing config for defaults
|
||||
existing, _ := core.LoadMultiAppConfig()
|
||||
var firstApp *core.AppConfig
|
||||
existing, _ := configpkg.LoadMultiAppConfig()
|
||||
var firstApp *configpkg.AppConfig
|
||||
if existing != nil {
|
||||
firstApp = existing.CurrentAppConfig("")
|
||||
}
|
||||
@@ -150,8 +151,8 @@ func runExistingAppForm(f *cmdutil.Factory, msg *initMsg) (*configInitResult, er
|
||||
|
||||
// runCreateAppFlow runs the "create new app" flow via OpenClaw device flow.
|
||||
// If brandOverride is non-empty, skip the interactive brand selection.
|
||||
func runCreateAppFlow(ctx context.Context, f *cmdutil.Factory, brandOverride core.LarkBrand, msg *initMsg) (*configInitResult, error) {
|
||||
var larkBrand core.LarkBrand
|
||||
func runCreateAppFlow(ctx context.Context, f *cmdutil.Factory, brandOverride brand.Brand, msg *initMsg) (*configInitResult, error) {
|
||||
var larkBrand brand.Brand
|
||||
if brandOverride != "" {
|
||||
larkBrand = brandOverride
|
||||
} else {
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
brandpkg "github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/build"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/credential"
|
||||
)
|
||||
|
||||
@@ -47,7 +47,7 @@ const probeTimeout = 3 * time.Second
|
||||
// 2. If TAT succeeded, a POST to the probe endpoint is fired. The outcome of
|
||||
// that call (success, server error, timeout, parse failure) is always
|
||||
// ignored — return nil regardless.
|
||||
func runProbe(parent context.Context, factory *cmdutil.Factory, appID, appSecret string, brand core.LarkBrand) error {
|
||||
func runProbe(parent context.Context, factory *cmdutil.Factory, appID, appSecret string, brand brandpkg.Brand) error {
|
||||
if factory == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func runProbe(parent context.Context, factory *cmdutil.Factory, appID, appSecret
|
||||
}
|
||||
|
||||
// TAT succeeded — fire the probe call. Any outcome is ignored.
|
||||
url := core.ResolveEndpoints(brand).Open + "/open-apis/application/v6/larksuite_cli_app/probe"
|
||||
url := brandpkg.ResolveEndpoints(brand).Open + "/open-apis/application/v6/larksuite_cli_app/probe"
|
||||
body := []byte(fmt.Sprintf(`{"from":"lark-cli/%s"}`, build.Version))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
|
||||
@@ -13,10 +13,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/build"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
)
|
||||
|
||||
// fakeRT routes requests to per-path handlers and records what it saw.
|
||||
@@ -132,7 +132,7 @@ func TestRunProbe_TATInvalidClient_ReturnsConfigError(t *testing.T) {
|
||||
}
|
||||
f, errBuf := fakeFactory(t, rt)
|
||||
|
||||
err := runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu)
|
||||
err := runProbe(context.Background(), f, "cli_x", "secret_y", brand.Feishu)
|
||||
|
||||
if rt.probeCalls != 0 {
|
||||
t.Error("probe endpoint must not be called when TAT fails")
|
||||
@@ -148,7 +148,7 @@ func TestRunProbe_TATUnauthorizedClient_ReturnsConfigError(t *testing.T) {
|
||||
},
|
||||
}
|
||||
f, errBuf := fakeFactory(t, rt)
|
||||
assertConfigRejection(t, runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu), errBuf)
|
||||
assertConfigRejection(t, runProbe(context.Background(), f, "cli_x", "secret_y", brand.Feishu), errBuf)
|
||||
}
|
||||
|
||||
// Any other deterministic client-side OAuth error (e.g. invalid_scope) falls
|
||||
@@ -161,7 +161,7 @@ func TestRunProbe_TATOtherClientError_Propagates(t *testing.T) {
|
||||
},
|
||||
}
|
||||
f, errBuf := fakeFactory(t, rt)
|
||||
err := runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu)
|
||||
err := runProbe(context.Background(), f, "cli_x", "secret_y", brand.Feishu)
|
||||
if err == nil || !errs.IsTyped(err) {
|
||||
t.Fatalf("expected a propagated typed error, got %T: %v", err, err)
|
||||
}
|
||||
@@ -180,7 +180,7 @@ func TestRunProbe_TATHTTPNon200_Silent(t *testing.T) {
|
||||
},
|
||||
}
|
||||
f, errBuf := fakeFactory(t, rt)
|
||||
assertSilent(t, runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu), errBuf)
|
||||
assertSilent(t, runProbe(context.Background(), f, "cli_x", "secret_y", brand.Feishu), errBuf)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ func TestRunProbe_TATTransportError_Silent(t *testing.T) {
|
||||
},
|
||||
}
|
||||
f, errBuf := fakeFactory(t, rt)
|
||||
assertSilent(t, runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu), errBuf)
|
||||
assertSilent(t, runProbe(context.Background(), f, "cli_x", "secret_y", brand.Feishu), errBuf)
|
||||
}
|
||||
|
||||
func TestRunProbe_TATSuccess_ProbeFails_Silent(t *testing.T) {
|
||||
@@ -201,7 +201,7 @@ func TestRunProbe_TATSuccess_ProbeFails_Silent(t *testing.T) {
|
||||
},
|
||||
}
|
||||
f, errBuf := fakeFactory(t, rt)
|
||||
err := runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu)
|
||||
err := runProbe(context.Background(), f, "cli_x", "secret_y", brand.Feishu)
|
||||
if rt.probeCalls != 1 {
|
||||
t.Errorf("probe should be called once, got %d", rt.probeCalls)
|
||||
}
|
||||
@@ -211,7 +211,7 @@ func TestRunProbe_TATSuccess_ProbeFails_Silent(t *testing.T) {
|
||||
func TestRunProbe_TATSuccess_ProbeOK_Silent(t *testing.T) {
|
||||
rt := &fakeRT{}
|
||||
f, errBuf := fakeFactory(t, rt)
|
||||
err := runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu)
|
||||
err := runProbe(context.Background(), f, "cli_x", "secret_y", brand.Feishu)
|
||||
if rt.tatCalls != 1 || rt.probeCalls != 1 {
|
||||
t.Errorf("expected 1/1 calls, got tat=%d probe=%d", rt.tatCalls, rt.probeCalls)
|
||||
}
|
||||
@@ -221,7 +221,7 @@ func TestRunProbe_TATSuccess_ProbeOK_Silent(t *testing.T) {
|
||||
func TestRunProbe_ProbeRequestShape(t *testing.T) {
|
||||
rt := &fakeRT{}
|
||||
f, _ := fakeFactory(t, rt)
|
||||
if err := runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu); err != nil {
|
||||
if err := runProbe(context.Background(), f, "cli_x", "secret_y", brand.Feishu); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ func TestRunProbe_ProbeRequestShape(t *testing.T) {
|
||||
func TestRunProbe_LarkBrand_HostRoutedCorrectly(t *testing.T) {
|
||||
rt := &fakeRT{}
|
||||
f, _ := fakeFactory(t, rt)
|
||||
if err := runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandLark); err != nil {
|
||||
if err := runProbe(context.Background(), f, "cli_x", "secret_y", brand.Lark); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if rt.probeReq == nil {
|
||||
@@ -262,7 +262,7 @@ func TestRunProbe_HTTPClientError_Silent(t *testing.T) {
|
||||
f.HttpClient = func() (*http.Client, error) {
|
||||
return nil, errors.New("client init failed")
|
||||
}
|
||||
assertSilent(t, runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu), errBuf)
|
||||
assertSilent(t, runProbe(context.Background(), f, "cli_x", "secret_y", brand.Feishu), errBuf)
|
||||
}
|
||||
|
||||
func TestRunProbe_TimeoutHonored(t *testing.T) {
|
||||
@@ -275,7 +275,7 @@ func TestRunProbe_TimeoutHonored(t *testing.T) {
|
||||
f, errBuf := fakeFactory(t, rt)
|
||||
|
||||
start := time.Now()
|
||||
err := runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu)
|
||||
err := runProbe(context.Background(), f, "cli_x", "secret_y", brand.Feishu)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
if elapsed > 4*time.Second {
|
||||
|
||||
@@ -8,9 +8,11 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/secret"
|
||||
)
|
||||
|
||||
// updateExistingProfileWithoutSecret guards four blank-input scenarios. Each
|
||||
@@ -19,47 +21,47 @@ import (
|
||||
// not for missing user input.
|
||||
|
||||
func TestUpdateExistingProfileWithoutSecret_NilConfig_EmitsValidationError(t *testing.T) {
|
||||
err := updateExistingProfileWithoutSecret(nil, "", "cli_test", core.BrandFeishu, "en")
|
||||
err := updateExistingProfileWithoutSecret(nil, "", "cli_test", brand.Feishu, "en")
|
||||
assertValidationParam(t, err, "--app-secret")
|
||||
}
|
||||
|
||||
func TestUpdateExistingProfileWithoutSecret_UnknownProfile_EmitsValidationError(t *testing.T) {
|
||||
existing := &core.MultiAppConfig{
|
||||
Apps: []core.AppConfig{{
|
||||
existing := &configpkg.MultiAppConfig{
|
||||
Apps: []configpkg.AppConfig{{
|
||||
Name: "default",
|
||||
AppId: "app-default",
|
||||
AppSecret: core.PlainSecret("secret-default"),
|
||||
Brand: core.BrandFeishu,
|
||||
AppSecret: secret.PlainSecret("secret-default"),
|
||||
Brand: brand.Feishu,
|
||||
}},
|
||||
}
|
||||
err := updateExistingProfileWithoutSecret(existing, "missing-profile", "cli_test", core.BrandFeishu, "en")
|
||||
err := updateExistingProfileWithoutSecret(existing, "missing-profile", "cli_test", brand.Feishu, "en")
|
||||
assertValidationParam(t, err, "--app-secret")
|
||||
}
|
||||
|
||||
func TestUpdateExistingProfileWithoutSecret_NoCurrentApp_EmitsValidationError(t *testing.T) {
|
||||
existing := &core.MultiAppConfig{
|
||||
existing := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "missing",
|
||||
Apps: []core.AppConfig{{
|
||||
Apps: []configpkg.AppConfig{{
|
||||
Name: "default",
|
||||
AppId: "app-default",
|
||||
AppSecret: core.PlainSecret("secret-default"),
|
||||
Brand: core.BrandFeishu,
|
||||
AppSecret: secret.PlainSecret("secret-default"),
|
||||
Brand: brand.Feishu,
|
||||
}},
|
||||
}
|
||||
err := updateExistingProfileWithoutSecret(existing, "", "cli_test", core.BrandFeishu, "en")
|
||||
err := updateExistingProfileWithoutSecret(existing, "", "cli_test", brand.Feishu, "en")
|
||||
assertValidationParam(t, err, "--app-secret")
|
||||
}
|
||||
|
||||
func TestUpdateExistingProfileWithoutSecret_AppIdMismatch_EmitsValidationError(t *testing.T) {
|
||||
existing := &core.MultiAppConfig{
|
||||
Apps: []core.AppConfig{{
|
||||
existing := &configpkg.MultiAppConfig{
|
||||
Apps: []configpkg.AppConfig{{
|
||||
Name: "default",
|
||||
AppId: "app-default",
|
||||
AppSecret: core.PlainSecret("secret-default"),
|
||||
Brand: core.BrandFeishu,
|
||||
AppSecret: secret.PlainSecret("secret-default"),
|
||||
Brand: brand.Feishu,
|
||||
}},
|
||||
}
|
||||
err := updateExistingProfileWithoutSecret(existing, "", "cli_different", core.BrandFeishu, "en")
|
||||
err := updateExistingProfileWithoutSecret(existing, "", "cli_different", brand.Feishu, "en")
|
||||
assertValidationParam(t, err, "--app-secret")
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,9 @@ import (
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/auth"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/secret"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -41,21 +42,21 @@ func NewCmdConfigRemove(f *cmdutil.Factory, runF func(*ConfigRemoveOptions) erro
|
||||
func configRemoveRun(opts *ConfigRemoveOptions) error {
|
||||
f := opts.Factory
|
||||
|
||||
config, err := core.LoadMultiAppConfig()
|
||||
config, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil || config == nil || len(config.Apps) == 0 {
|
||||
return errs.NewConfigError(errs.SubtypeNotConfigured, "not configured yet")
|
||||
}
|
||||
|
||||
// Save empty config first. If this fails, keep secrets and tokens intact so the
|
||||
// existing config can still be retried instead of ending up half-removed.
|
||||
empty := &core.MultiAppConfig{Apps: []core.AppConfig{}}
|
||||
if err := core.SaveMultiAppConfig(empty); err != nil {
|
||||
empty := &configpkg.MultiAppConfig{Apps: []configpkg.AppConfig{}}
|
||||
if err := configpkg.SaveMultiAppConfig(empty); err != nil {
|
||||
return errs.NewInternalError(errs.SubtypeStorage, "failed to save config: %v", err).WithCause(err)
|
||||
}
|
||||
|
||||
// Clean up keychain entries for all apps after config is cleared.
|
||||
for _, app := range config.Apps {
|
||||
core.RemoveSecretStore(app.AppSecret, f.Keychain)
|
||||
secret.RemoveSecretStore(app.AppSecret, f.Keychain)
|
||||
for _, user := range app.Users {
|
||||
_ = auth.RemoveStoredToken(app.AppId, user.UserOpenId)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
)
|
||||
|
||||
// NewCmdConfigRiskControl creates the workspace risk-control policy command.
|
||||
@@ -29,7 +29,7 @@ opt it back in explicitly, or default to remove the explicit preference.`,
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
config, err := core.LoadOrNotConfigured()
|
||||
config, err := configpkg.LoadOrNotConfigured()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -52,7 +52,7 @@ opt it back in explicitly, or default to remove the explicit preference.`,
|
||||
"invalid risk-control value %q, valid values: on | off | default", args[0])
|
||||
}
|
||||
|
||||
if err := core.SaveMultiAppConfig(config); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(config); err != nil {
|
||||
return errs.NewInternalError(errs.SubtypeStorage,
|
||||
"failed to save risk-control policy: %v", err).WithCause(err)
|
||||
}
|
||||
@@ -64,7 +64,7 @@ opt it back in explicitly, or default to remove the explicit preference.`,
|
||||
return cmd
|
||||
}
|
||||
|
||||
func printRiskControl(f *cmdutil.Factory, config *core.MultiAppConfig) {
|
||||
func printRiskControl(f *cmdutil.Factory, config *configpkg.MultiAppConfig) {
|
||||
source := "default"
|
||||
if config.RiskControl != nil {
|
||||
source = "workspace"
|
||||
|
||||
@@ -8,17 +8,19 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/secret"
|
||||
)
|
||||
|
||||
func TestRiskControlWorkspacePolicy(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
config := &core.MultiAppConfig{Apps: []core.AppConfig{{
|
||||
AppId: "cli_test", AppSecret: core.PlainSecret("secret"), Brand: core.BrandFeishu,
|
||||
config := &configpkg.MultiAppConfig{Apps: []configpkg.AppConfig{{
|
||||
AppId: "cli_test", AppSecret: secret.PlainSecret("secret"), Brand: brand.Feishu,
|
||||
}}}
|
||||
if err := core.SaveMultiAppConfig(config); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -28,7 +30,7 @@ func TestRiskControlWorkspacePolicy(t *testing.T) {
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("set off: %v", err)
|
||||
}
|
||||
loaded, err := core.LoadMultiAppConfig()
|
||||
loaded, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -53,7 +55,7 @@ func TestRiskControlWorkspacePolicy(t *testing.T) {
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("set on: %v", err)
|
||||
}
|
||||
loaded, err = core.LoadMultiAppConfig()
|
||||
loaded, err = configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -66,7 +68,7 @@ func TestRiskControlWorkspacePolicy(t *testing.T) {
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("reset default: %v", err)
|
||||
}
|
||||
loaded, err = core.LoadMultiAppConfig()
|
||||
loaded, err = configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -86,8 +88,8 @@ func TestRiskControlWorkspacePolicy(t *testing.T) {
|
||||
|
||||
func TestRiskControlWorkspacePolicyRejectsInvalidValue(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
if err := core.SaveMultiAppConfig(&core.MultiAppConfig{Apps: []core.AppConfig{{
|
||||
AppId: "cli_test", AppSecret: core.PlainSecret("secret"), Brand: core.BrandFeishu,
|
||||
if err := configpkg.SaveMultiAppConfig(&configpkg.MultiAppConfig{Apps: []configpkg.AppConfig{{
|
||||
AppId: "cli_test", AppSecret: secret.PlainSecret("secret"), Brand: brand.Feishu,
|
||||
}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -107,10 +109,10 @@ func TestRiskControlWorkspacePolicyRejectsInvalidValue(t *testing.T) {
|
||||
|
||||
func TestRiskControlWorkspacePolicyAllowedWithExternalCredentials(t *testing.T) {
|
||||
f := newConfigFactoryWithExternalProvider(t)
|
||||
config := &core.MultiAppConfig{Apps: []core.AppConfig{{
|
||||
AppId: "cli_test", AppSecret: core.PlainSecret("secret"), Brand: core.BrandFeishu,
|
||||
config := &configpkg.MultiAppConfig{Apps: []configpkg.AppConfig{{
|
||||
AppId: "cli_test", AppSecret: secret.PlainSecret("secret"), Brand: brand.Feishu,
|
||||
}}}
|
||||
if err := core.SaveMultiAppConfig(config); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -120,7 +122,7 @@ func TestRiskControlWorkspacePolicyAllowedWithExternalCredentials(t *testing.T)
|
||||
t.Fatalf("set off with external credentials: %v", err)
|
||||
}
|
||||
|
||||
loaded, err := core.LoadMultiAppConfig()
|
||||
loaded, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ import (
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/workspace"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -43,15 +44,15 @@ func NewCmdConfigShow(f *cmdutil.Factory, runF func(*ConfigShowOptions) error) *
|
||||
func configShowRun(opts *ConfigShowOptions) error {
|
||||
f := opts.Factory
|
||||
|
||||
config, err := core.LoadMultiAppConfig()
|
||||
config, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return core.NotConfiguredError()
|
||||
return configpkg.NotConfiguredError()
|
||||
}
|
||||
return errs.NewConfigError(errs.SubtypeInvalidConfig, "failed to load config: %v", err).WithCause(err)
|
||||
}
|
||||
if config == nil || len(config.Apps) == 0 {
|
||||
return core.NotConfiguredError()
|
||||
return configpkg.NotConfiguredError()
|
||||
}
|
||||
app := config.CurrentAppConfig(f.Invocation.Profile)
|
||||
if app == nil {
|
||||
@@ -66,7 +67,7 @@ func configShowRun(opts *ConfigShowOptions) error {
|
||||
users = strings.Join(userStrs, ", ")
|
||||
}
|
||||
output.PrintJson(f.IOStreams.Out, map[string]interface{}{
|
||||
"workspace": core.CurrentWorkspace().Display(),
|
||||
"workspace": workspace.CurrentWorkspace().Display(),
|
||||
"profile": app.ProfileName(),
|
||||
"appId": app.AppId,
|
||||
"appSecret": "****",
|
||||
@@ -74,6 +75,6 @@ func configShowRun(opts *ConfigShowOptions) error {
|
||||
"lang": app.Lang,
|
||||
"users": users,
|
||||
})
|
||||
fmt.Fprintf(f.IOStreams.ErrOut, "\nConfig file path: %s\n", core.GetConfigPath())
|
||||
fmt.Fprintf(f.IOStreams.ErrOut, "\nConfig file path: %s\n", workspace.GetConfigPath())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import (
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/identity"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -37,7 +38,7 @@ explicit user confirmation — never run on your own initiative.`,
|
||||
lark-cli config strict-mode --reset # clear profile override`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
multi, err := core.LoadOrNotConfigured()
|
||||
multi, err := configpkg.LoadOrNotConfigured()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -45,20 +46,20 @@ explicit user confirmation — never run on your own initiative.`,
|
||||
if reset {
|
||||
app := multi.CurrentAppConfig(f.Invocation.Profile)
|
||||
if app == nil {
|
||||
return core.NoActiveProfileError()
|
||||
return configpkg.NoActiveProfileError()
|
||||
}
|
||||
return resetStrictMode(f, multi, app, global, args)
|
||||
}
|
||||
if len(args) == 0 {
|
||||
app := multi.CurrentAppConfig(f.Invocation.Profile)
|
||||
if app == nil {
|
||||
return core.NoActiveProfileError()
|
||||
return configpkg.NoActiveProfileError()
|
||||
}
|
||||
return showStrictMode(cmd.Context(), f, multi, app)
|
||||
}
|
||||
app := multi.CurrentAppConfig(f.Invocation.Profile)
|
||||
if !global && app == nil {
|
||||
return core.NoActiveProfileError()
|
||||
return configpkg.NoActiveProfileError()
|
||||
}
|
||||
return setStrictMode(f, multi, app, args[0], global)
|
||||
},
|
||||
@@ -71,7 +72,7 @@ explicit user confirmation — never run on your own initiative.`,
|
||||
return cmd
|
||||
}
|
||||
|
||||
func resetStrictMode(f *cmdutil.Factory, multi *core.MultiAppConfig, app *core.AppConfig, global bool, args []string) error {
|
||||
func resetStrictMode(f *cmdutil.Factory, multi *configpkg.MultiAppConfig, app *configpkg.AppConfig, global bool, args []string) error {
|
||||
if global {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--reset cannot be used with --global").WithParam("--reset")
|
||||
}
|
||||
@@ -79,14 +80,14 @@ func resetStrictMode(f *cmdutil.Factory, multi *core.MultiAppConfig, app *core.A
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--reset cannot be used with a value argument").WithParam("--reset")
|
||||
}
|
||||
app.StrictMode = nil
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
return errs.NewInternalError(errs.SubtypeStorage, "failed to save config: %v", err).WithCause(err)
|
||||
}
|
||||
fmt.Fprintln(f.IOStreams.ErrOut, "Profile strict-mode reset (inherits global)")
|
||||
return nil
|
||||
}
|
||||
|
||||
func showStrictMode(ctx context.Context, f *cmdutil.Factory, multi *core.MultiAppConfig, app *core.AppConfig) error {
|
||||
func showStrictMode(ctx context.Context, f *cmdutil.Factory, multi *configpkg.MultiAppConfig, app *configpkg.AppConfig) error {
|
||||
// Runtime effective mode from credential provider chain is the source of truth.
|
||||
runtime := f.ResolveStrictMode(ctx)
|
||||
configMode, configSource := resolveStrictModeStatus(multi, app)
|
||||
@@ -99,10 +100,10 @@ func showStrictMode(ctx context.Context, f *cmdutil.Factory, multi *core.MultiAp
|
||||
return nil
|
||||
}
|
||||
|
||||
func setStrictMode(f *cmdutil.Factory, multi *core.MultiAppConfig, app *core.AppConfig, value string, global bool) error {
|
||||
mode := core.StrictMode(value)
|
||||
func setStrictMode(f *cmdutil.Factory, multi *configpkg.MultiAppConfig, app *configpkg.AppConfig, value string, global bool) error {
|
||||
mode := identity.StrictMode(value)
|
||||
switch mode {
|
||||
case core.StrictModeBot, core.StrictModeUser, core.StrictModeOff:
|
||||
case identity.StrictModeBot, identity.StrictModeUser, identity.StrictModeOff:
|
||||
default:
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid value %q, valid values: bot | user | off", value)
|
||||
}
|
||||
@@ -118,7 +119,7 @@ func setStrictMode(f *cmdutil.Factory, multi *core.MultiAppConfig, app *core.App
|
||||
// false-positived (--global change while current profile has an explicit
|
||||
// override) and false-negatived (--global broadening that doesn't affect
|
||||
// the current profile but does affect other inheriting profiles).
|
||||
var oldMode core.StrictMode
|
||||
var oldMode identity.StrictMode
|
||||
if global {
|
||||
oldMode = multi.StrictMode
|
||||
} else {
|
||||
@@ -138,16 +139,16 @@ func setStrictMode(f *cmdutil.Factory, multi *core.MultiAppConfig, app *core.App
|
||||
}
|
||||
} else {
|
||||
if app == nil {
|
||||
return core.NoActiveProfileError()
|
||||
return configpkg.NoActiveProfileError()
|
||||
}
|
||||
app.StrictMode = &mode
|
||||
}
|
||||
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
return errs.NewInternalError(errs.SubtypeStorage, "failed to save config: %v", err).WithCause(err)
|
||||
}
|
||||
|
||||
if oldMode == core.StrictModeBot && (mode == core.StrictModeUser || mode == core.StrictModeOff) {
|
||||
if oldMode == identity.StrictModeBot && (mode == identity.StrictModeUser || mode == identity.StrictModeOff) {
|
||||
fmt.Fprintln(f.IOStreams.ErrOut, "⚠️ "+strictModeRelaxLang(app).IdentityEscalationMessage)
|
||||
}
|
||||
|
||||
@@ -162,19 +163,19 @@ func setStrictMode(f *cmdutil.Factory, multi *core.MultiAppConfig, app *core.App
|
||||
// strictModeRelaxLang picks the bind-message bundle whose language matches the
|
||||
// active profile's Lang setting. Falls back to bindMsgZh when no profile is
|
||||
// available (global mutation with no current app).
|
||||
func strictModeRelaxLang(app *core.AppConfig) *bindMsg {
|
||||
func strictModeRelaxLang(app *configpkg.AppConfig) *bindMsg {
|
||||
if app != nil {
|
||||
return getBindMsg(app.Lang)
|
||||
}
|
||||
return getBindMsg("")
|
||||
}
|
||||
|
||||
func resolveStrictModeStatus(multi *core.MultiAppConfig, app *core.AppConfig) (core.StrictMode, string) {
|
||||
func resolveStrictModeStatus(multi *configpkg.MultiAppConfig, app *configpkg.AppConfig) (identity.StrictMode, string) {
|
||||
if app != nil && app.StrictMode != nil {
|
||||
return *app.StrictMode, fmt.Sprintf("profile %q", app.ProfileName())
|
||||
}
|
||||
if multi.StrictMode.IsActive() {
|
||||
return multi.StrictMode, "global"
|
||||
}
|
||||
return core.StrictModeOff, "global (default)"
|
||||
return identity.StrictModeOff, "global (default)"
|
||||
}
|
||||
|
||||
@@ -7,29 +7,32 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/identity"
|
||||
"github.com/larksuite/cli/internal/secret"
|
||||
)
|
||||
|
||||
func setupStrictModeTestConfig(t *testing.T) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
|
||||
multi := &core.MultiAppConfig{
|
||||
Apps: []core.AppConfig{{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
Apps: []configpkg.AppConfig{{
|
||||
AppId: "test-app",
|
||||
AppSecret: core.PlainSecret("secret"),
|
||||
Brand: core.BrandFeishu,
|
||||
AppSecret: secret.PlainSecret("secret"),
|
||||
Brand: brand.Feishu,
|
||||
}},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrictMode_Show_Default(t *testing.T) {
|
||||
setupStrictModeTestConfig(t)
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test-app", AppSecret: "secret"})
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test-app", AppSecret: "secret"})
|
||||
cmd := NewCmdConfigStrictMode(f)
|
||||
cmd.SetArgs([]string{})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
@@ -42,37 +45,37 @@ func TestStrictMode_Show_Default(t *testing.T) {
|
||||
|
||||
func TestStrictMode_SetBot_Profile(t *testing.T) {
|
||||
setupStrictModeTestConfig(t)
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test-app", AppSecret: "secret"})
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test-app", AppSecret: "secret"})
|
||||
cmd := NewCmdConfigStrictMode(f)
|
||||
cmd.SetArgs([]string{"bot"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
multi, _ := core.LoadMultiAppConfig()
|
||||
multi, _ := configpkg.LoadMultiAppConfig()
|
||||
app := multi.CurrentAppConfig("")
|
||||
if app.StrictMode == nil || *app.StrictMode != core.StrictModeBot {
|
||||
if app.StrictMode == nil || *app.StrictMode != identity.StrictModeBot {
|
||||
t.Error("expected StrictMode=bot on profile")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrictMode_SetUser_Profile(t *testing.T) {
|
||||
setupStrictModeTestConfig(t)
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test-app", AppSecret: "secret"})
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test-app", AppSecret: "secret"})
|
||||
cmd := NewCmdConfigStrictMode(f)
|
||||
cmd.SetArgs([]string{"user"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
multi, _ := core.LoadMultiAppConfig()
|
||||
multi, _ := configpkg.LoadMultiAppConfig()
|
||||
app := multi.CurrentAppConfig("")
|
||||
if app.StrictMode == nil || *app.StrictMode != core.StrictModeUser {
|
||||
if app.StrictMode == nil || *app.StrictMode != identity.StrictModeUser {
|
||||
t.Error("expected StrictMode=user on profile")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrictMode_SetOff_Profile(t *testing.T) {
|
||||
setupStrictModeTestConfig(t)
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test-app", AppSecret: "secret"})
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test-app", AppSecret: "secret"})
|
||||
cmd := NewCmdConfigStrictMode(f)
|
||||
cmd.SetArgs([]string{"bot"})
|
||||
cmd.Execute()
|
||||
@@ -81,23 +84,23 @@ func TestStrictMode_SetOff_Profile(t *testing.T) {
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
multi, _ := core.LoadMultiAppConfig()
|
||||
multi, _ := configpkg.LoadMultiAppConfig()
|
||||
app := multi.CurrentAppConfig("")
|
||||
if app.StrictMode == nil || *app.StrictMode != core.StrictModeOff {
|
||||
if app.StrictMode == nil || *app.StrictMode != identity.StrictModeOff {
|
||||
t.Error("expected StrictMode=off on profile")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrictMode_SetBot_Global(t *testing.T) {
|
||||
setupStrictModeTestConfig(t)
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test-app", AppSecret: "secret"})
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test-app", AppSecret: "secret"})
|
||||
cmd := NewCmdConfigStrictMode(f)
|
||||
cmd.SetArgs([]string{"bot", "--global"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
multi, _ := core.LoadMultiAppConfig()
|
||||
if multi.StrictMode != core.StrictModeBot {
|
||||
multi, _ := configpkg.LoadMultiAppConfig()
|
||||
if multi.StrictMode != identity.StrictModeBot {
|
||||
t.Error("expected global StrictMode=bot")
|
||||
}
|
||||
}
|
||||
@@ -105,38 +108,38 @@ func TestStrictMode_SetBot_Global(t *testing.T) {
|
||||
func TestStrictMode_SetGlobal_DoesNotRequireActiveProfile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "missing-profile",
|
||||
Apps: []core.AppConfig{{
|
||||
Apps: []configpkg.AppConfig{{
|
||||
Name: "default",
|
||||
AppId: "test-app",
|
||||
AppSecret: core.PlainSecret("secret"),
|
||||
Brand: core.BrandFeishu,
|
||||
AppSecret: secret.PlainSecret("secret"),
|
||||
Brand: brand.Feishu,
|
||||
}},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test-app", AppSecret: "secret"})
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test-app", AppSecret: "secret"})
|
||||
cmd := NewCmdConfigStrictMode(f)
|
||||
cmd.SetArgs([]string{"bot", "--global"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("Execute() error = %v", err)
|
||||
}
|
||||
|
||||
saved, err := core.LoadMultiAppConfig()
|
||||
saved, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMultiAppConfig() error = %v", err)
|
||||
}
|
||||
if saved.StrictMode != core.StrictModeBot {
|
||||
t.Fatalf("StrictMode = %q, want %q", saved.StrictMode, core.StrictModeBot)
|
||||
if saved.StrictMode != identity.StrictModeBot {
|
||||
t.Fatalf("StrictMode = %q, want %q", saved.StrictMode, identity.StrictModeBot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrictMode_Reset(t *testing.T) {
|
||||
setupStrictModeTestConfig(t)
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test-app", AppSecret: "secret"})
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test-app", AppSecret: "secret"})
|
||||
cmd := NewCmdConfigStrictMode(f)
|
||||
cmd.SetArgs([]string{"bot"})
|
||||
cmd.Execute()
|
||||
@@ -145,7 +148,7 @@ func TestStrictMode_Reset(t *testing.T) {
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
multi, _ := core.LoadMultiAppConfig()
|
||||
multi, _ := configpkg.LoadMultiAppConfig()
|
||||
app := multi.CurrentAppConfig("")
|
||||
if app.StrictMode != nil {
|
||||
t.Errorf("expected nil StrictMode after reset, got %v", *app.StrictMode)
|
||||
@@ -154,7 +157,7 @@ func TestStrictMode_Reset(t *testing.T) {
|
||||
|
||||
func TestStrictMode_InvalidValue(t *testing.T) {
|
||||
setupStrictModeTestConfig(t)
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test-app", AppSecret: "secret"})
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test-app", AppSecret: "secret"})
|
||||
cmd := NewCmdConfigStrictMode(f)
|
||||
cmd.SetArgs([]string{"on"})
|
||||
err := cmd.Execute()
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
)
|
||||
|
||||
// runStrictMode is a small helper that runs `config strict-mode <args...>` and
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
// new user-identity warning land.
|
||||
func runStrictMode(t *testing.T, args ...string) string {
|
||||
t.Helper()
|
||||
f, _, stderr, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test-app", AppSecret: "secret"})
|
||||
f, _, stderr, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test-app", AppSecret: "secret"})
|
||||
cmd := NewCmdConfigStrictMode(f)
|
||||
cmd.SetArgs(args)
|
||||
if err := cmd.Execute(); err != nil {
|
||||
|
||||
@@ -14,14 +14,16 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/build"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/identitydiag"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/transport"
|
||||
"github.com/larksuite/cli/internal/update"
|
||||
"github.com/larksuite/cli/internal/workspace"
|
||||
)
|
||||
|
||||
// DoctorOptions holds inputs for the doctor command.
|
||||
@@ -85,7 +87,7 @@ func doctorRun(opts *DoctorOptions) error {
|
||||
}
|
||||
|
||||
// ── 1. Config file ──
|
||||
_, err := core.LoadMultiAppConfig()
|
||||
_, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
// For "config not present" cases, prefer the workspace-aware
|
||||
// NotConfiguredError message + hint (e.g. "openclaw context
|
||||
@@ -96,7 +98,7 @@ func doctorRun(opts *DoctorOptions) error {
|
||||
msg, hint := err.Error(), ""
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
var cfgErr *errs.ConfigError
|
||||
if errors.As(core.NotConfiguredError(), &cfgErr) {
|
||||
if errors.As(configpkg.NotConfiguredError(), &cfgErr) {
|
||||
msg, hint = cfgErr.Message, cfgErr.Hint
|
||||
}
|
||||
}
|
||||
@@ -118,7 +120,7 @@ func doctorRun(opts *DoctorOptions) error {
|
||||
}
|
||||
checks = append(checks, pass("app_resolved", fmt.Sprintf("app: %s (%s)", cfg.AppID, cfg.Brand)))
|
||||
|
||||
ep := core.ResolveEndpoints(cfg.Brand)
|
||||
ep := brand.ResolveEndpoints(cfg.Brand)
|
||||
|
||||
// ── 3. Identity readiness ──
|
||||
diagnostics := identitydiag.Diagnose(opts.Ctx, f, cfg, !opts.Offline)
|
||||
@@ -149,7 +151,7 @@ func identityCheck(name string, id identitydiag.Identity) checkResult {
|
||||
}
|
||||
|
||||
// networkChecks probes Open API and MCP endpoints concurrently.
|
||||
func networkChecks(ctx context.Context, opts *DoctorOptions, ep core.Endpoints) []checkResult {
|
||||
func networkChecks(ctx context.Context, opts *DoctorOptions, ep brand.Endpoints) []checkResult {
|
||||
if opts.Offline {
|
||||
return []checkResult{
|
||||
skip("endpoint_open", "skipped (--offline)"),
|
||||
@@ -239,7 +241,7 @@ func finishDoctor(f *cmdutil.Factory, checks []checkResult) error {
|
||||
|
||||
result := map[string]interface{}{
|
||||
"ok": allOK,
|
||||
"workspace": core.CurrentWorkspace().Display(),
|
||||
"workspace": workspace.CurrentWorkspace().Display(),
|
||||
"checks": checks,
|
||||
}
|
||||
output.PrintJson(f.IOStreams.Out, result)
|
||||
|
||||
@@ -13,15 +13,17 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
extcred "github.com/larksuite/cli/extension/credential"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/credential"
|
||||
"github.com/larksuite/cli/internal/secret"
|
||||
)
|
||||
|
||||
func TestNewCmdDoctor_FlagParsing(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
|
||||
})
|
||||
|
||||
cmd := NewCmdDoctor(f)
|
||||
@@ -88,7 +90,7 @@ func TestFinishDoctor(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNetworkChecks_Offline(t *testing.T) {
|
||||
ep := core.Endpoints{Open: "https://open.feishu.cn", MCP: "https://mcp.feishu.cn"}
|
||||
ep := brand.Endpoints{Open: "https://open.feishu.cn", MCP: "https://mcp.feishu.cn"}
|
||||
opts := &DoctorOptions{Ctx: context.Background(), Offline: true}
|
||||
checks := networkChecks(opts.Ctx, opts, ep)
|
||||
if len(checks) != 2 {
|
||||
@@ -103,22 +105,22 @@ func TestNetworkChecks_Offline(t *testing.T) {
|
||||
|
||||
func TestDoctorRun_SplitsBotAndMissingUserIdentity(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
if err := core.SaveMultiAppConfig(&core.MultiAppConfig{
|
||||
if err := configpkg.SaveMultiAppConfig(&configpkg.MultiAppConfig{
|
||||
CurrentApp: "default",
|
||||
Apps: []core.AppConfig{
|
||||
Apps: []configpkg.AppConfig{
|
||||
{
|
||||
Name: "default",
|
||||
AppId: "test-app",
|
||||
AppSecret: core.PlainSecret("secret"),
|
||||
Brand: core.BrandFeishu,
|
||||
AppSecret: secret.PlainSecret("secret"),
|
||||
Brand: brand.Feishu,
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "test-app", AppSecret: "secret", Brand: core.BrandFeishu,
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "test-app", AppSecret: "secret", Brand: brand.Feishu,
|
||||
})
|
||||
err := doctorRun(&DoctorOptions{
|
||||
Factory: f,
|
||||
@@ -180,16 +182,16 @@ func (p *fakeExtProvider) ResolveToken(context.Context, extcred.TokenSpec) (*ext
|
||||
// per-identity checks already carry the source-appropriate escalation.
|
||||
func TestDoctor_ExternalProvider_IdentityReadyHintNotBlockedCommand(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
if err := core.SaveMultiAppConfig(&core.MultiAppConfig{
|
||||
if err := configpkg.SaveMultiAppConfig(&configpkg.MultiAppConfig{
|
||||
CurrentApp: "default",
|
||||
Apps: []core.AppConfig{{Name: "default", AppId: "cli_x", AppSecret: core.PlainSecret("secret"), Brand: core.BrandFeishu}},
|
||||
Apps: []configpkg.AppConfig{{Name: "default", AppId: "cli_x", AppSecret: secret.PlainSecret("secret"), Brand: brand.Feishu}},
|
||||
}); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
|
||||
// Provider serves neither identity: bot unsupported, user supported but not
|
||||
// signed in → both unavailable → identity_ready fails.
|
||||
cfg := &core.CliConfig{AppID: "cli_x", Brand: core.BrandFeishu, SupportedIdentities: uint8(extcred.SupportsUser)}
|
||||
cfg := &configpkg.CliConfig{AppID: "cli_x", Brand: brand.Feishu, SupportedIdentities: uint8(extcred.SupportsUser)}
|
||||
cred := credential.NewCredentialProvider(
|
||||
[]extcred.Provider{&fakeExtProvider{name: "corp-sso", account: &extcred.Account{AppID: "cli_x"}}},
|
||||
nil, nil,
|
||||
@@ -197,7 +199,7 @@ func TestDoctor_ExternalProvider_IdentityReadyHintNotBlockedCommand(t *testing.T
|
||||
)
|
||||
out := &bytes.Buffer{}
|
||||
f := &cmdutil.Factory{
|
||||
Config: func() (*core.CliConfig, error) { return cfg, nil },
|
||||
Config: func() (*configpkg.CliConfig, error) { return cfg, nil },
|
||||
Credential: cred,
|
||||
IOStreams: &cmdutil.IOStreams{Out: out, ErrOut: &bytes.Buffer{}},
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/larksuite/cli/internal/apicatalog"
|
||||
internalauth "github.com/larksuite/cli/internal/auth"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
identitypkg "github.com/larksuite/cli/internal/identity"
|
||||
"github.com/larksuite/cli/internal/registry"
|
||||
"github.com/larksuite/cli/shortcuts"
|
||||
shortcutcommon "github.com/larksuite/cli/shortcuts/common"
|
||||
@@ -58,9 +58,9 @@ func resolveDeclaredScopesForCurrentCommand(f *cmdutil.Factory) []string {
|
||||
|
||||
identity := string(f.ResolvedIdentity)
|
||||
if identity == "" {
|
||||
identity = string(core.AsUser)
|
||||
identity = string(identitypkg.AsUser)
|
||||
}
|
||||
if identity != string(core.AsUser) && identity != string(core.AsBot) {
|
||||
if identity != string(identitypkg.AsUser) && identity != string(identitypkg.AsBot) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ func commandCatalogPath(cmd *cobra.Command) []string {
|
||||
func shortcutSupportsIdentity(sc shortcutcommon.Shortcut, identity string) bool {
|
||||
authTypes := sc.AuthTypes
|
||||
if len(authTypes) == 0 {
|
||||
authTypes = []string{string(core.AsUser)}
|
||||
authTypes = []string{string(identitypkg.AsUser)}
|
||||
}
|
||||
for _, authType := range authTypes {
|
||||
if authType == identity {
|
||||
|
||||
@@ -14,16 +14,14 @@ import (
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/event"
|
||||
"github.com/larksuite/cli/internal/event/adapter/lark/websocket"
|
||||
"github.com/larksuite/cli/internal/event/adapter/localbus/transport"
|
||||
"github.com/larksuite/cli/internal/event/bus"
|
||||
"github.com/larksuite/cli/internal/event/catalog"
|
||||
"github.com/larksuite/cli/internal/event/transport"
|
||||
"github.com/larksuite/cli/internal/workspace"
|
||||
)
|
||||
|
||||
// NewCmdBus creates the hidden `event _bus` daemon subcommand, forked by the consume client; fork argv lives in consume/startup.go.
|
||||
func NewCmdBus(f *cmdutil.Factory, snap *catalog.Snapshot) *cobra.Command {
|
||||
func NewCmdBus(f *cmdutil.Factory) *cobra.Command {
|
||||
var domain string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@@ -37,7 +35,7 @@ func NewCmdBus(f *cmdutil.Factory, snap *catalog.Snapshot) *cobra.Command {
|
||||
}
|
||||
|
||||
// Sanitize AppID: an unsanitized value could escape events/ via ".." or separators.
|
||||
eventsDir := filepath.Join(core.GetConfigDir(), "events", event.SanitizeAppID(cfg.AppID))
|
||||
eventsDir := filepath.Join(workspace.GetConfigDir(), "events", event.SanitizeAppID(cfg.AppID))
|
||||
|
||||
logger, err := bus.SetupBusLogger(eventsDir)
|
||||
if err != nil {
|
||||
@@ -46,13 +44,7 @@ func NewCmdBus(f *cmdutil.Factory, snap *catalog.Snapshot) *cobra.Command {
|
||||
}
|
||||
|
||||
tr := transport.New()
|
||||
ingress := &websocket.FeishuSource{
|
||||
AppID: cfg.AppID,
|
||||
AppSecret: cfg.AppSecret,
|
||||
Domain: domain,
|
||||
Logger: logger,
|
||||
}
|
||||
b := bus.NewBus(cfg.AppID, cfg.AppSecret, domain, tr, logger, snap, ingress)
|
||||
b := bus.NewBus(cfg.AppID, cfg.AppSecret, domain, tr, logger)
|
||||
|
||||
ctx, cancel := context.WithCancel(cmd.Context())
|
||||
defer cancel()
|
||||
|
||||
@@ -8,9 +8,10 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
)
|
||||
|
||||
// The hidden `event _bus` daemon command must exit with a typed file_io error
|
||||
@@ -24,10 +25,10 @@ func TestBusCommandLoggerSetupFailureIsTypedFileIO(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
AppID: "cli_bus_test", AppSecret: "secret", Brand: core.BrandFeishu,
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
|
||||
AppID: "cli_bus_test", AppSecret: "secret", Brand: brand.Feishu,
|
||||
})
|
||||
cmd := NewCmdBus(f, compileCatalog())
|
||||
cmd := NewCmdBus(f)
|
||||
cmd.SetArgs([]string{})
|
||||
|
||||
err := cmd.Execute()
|
||||
|
||||
@@ -10,8 +10,9 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
brandpkg "github.com/larksuite/cli/brand"
|
||||
eventlib "github.com/larksuite/cli/internal/event"
|
||||
identitypkg "github.com/larksuite/cli/internal/identity"
|
||||
)
|
||||
|
||||
// Landing-page contract for the scan-to-enable deep link, verified against the
|
||||
@@ -67,23 +68,23 @@ func encodeAddons(a ManifestAddons) (string, error) {
|
||||
}
|
||||
|
||||
// consoleAddonsURL builds the scan-to-enable deep link carrying incremental scopes/events/callbacks.
|
||||
func consoleAddonsURL(brand core.LarkBrand, appID string, a ManifestAddons) (string, error) {
|
||||
func consoleAddonsURL(brand brandpkg.Brand, appID string, a ManifestAddons) (string, error) {
|
||||
encoded, err := encodeAddons(a)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
host := core.ResolveEndpoints(brand).Open
|
||||
host := brandpkg.ResolveEndpoints(brand).Open
|
||||
return fmt.Sprintf("%s%s?%s=%s&addons=%s", host, addonsLandingPath, addonsClientIDParam, appID, encoded), nil
|
||||
}
|
||||
|
||||
// consoleLandingURL is the bare landing page (no addons) — fallback when encoding fails.
|
||||
func consoleLandingURL(brand core.LarkBrand, appID string) string {
|
||||
host := core.ResolveEndpoints(brand).Open
|
||||
func consoleLandingURL(brand brandpkg.Brand, appID string) string {
|
||||
host := brandpkg.ResolveEndpoints(brand).Open
|
||||
return fmt.Sprintf("%s%s?%s=%s", host, addonsLandingPath, addonsClientIDParam, appID)
|
||||
}
|
||||
|
||||
// addonsHintURL returns the scan URL, degrading to the bare landing page on encode error.
|
||||
func addonsHintURL(brand core.LarkBrand, appID string, a ManifestAddons) string {
|
||||
func addonsHintURL(brand brandpkg.Brand, appID string, a ManifestAddons) string {
|
||||
url, err := consoleAddonsURL(brand, appID, a)
|
||||
if err != nil {
|
||||
return consoleLandingURL(brand, appID)
|
||||
@@ -94,7 +95,7 @@ func addonsHintURL(brand core.LarkBrand, appID string, a ManifestAddons) string
|
||||
// missingScopeAddons routes missing scopes into the identity-appropriate section.
|
||||
// The unused side is an empty (non-nil) slice so JSON encodes [] not null —
|
||||
// the addons spec treats a missing tenant/user as an empty array.
|
||||
func missingScopeAddons(identity core.Identity, missing []string) ManifestAddons {
|
||||
func missingScopeAddons(identity identitypkg.Identity, missing []string) ManifestAddons {
|
||||
s := &AddonsScopes{Tenant: []string{}, User: []string{}}
|
||||
if identity.IsBot() {
|
||||
s.Tenant = missing
|
||||
@@ -106,7 +107,7 @@ func missingScopeAddons(identity core.Identity, missing []string) ManifestAddons
|
||||
|
||||
// missingSubscriptionAddons routes missing events/callbacks into the right section.
|
||||
// Like missingScopeAddons, unused event sides stay [] (not null) per the addons spec.
|
||||
func missingSubscriptionAddons(subType eventlib.SubscriptionType, identity core.Identity, missing []string) ManifestAddons {
|
||||
func missingSubscriptionAddons(subType eventlib.SubscriptionType, identity identitypkg.Identity, missing []string) ManifestAddons {
|
||||
if subType == eventlib.SubTypeCallback {
|
||||
return ManifestAddons{Callbacks: &AddonsCallbacks{Items: missing}}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/brand"
|
||||
eventlib "github.com/larksuite/cli/internal/event"
|
||||
"github.com/larksuite/cli/internal/identity"
|
||||
)
|
||||
|
||||
func decodeAddons(t *testing.T, encoded string) ManifestAddons {
|
||||
@@ -55,11 +56,11 @@ func TestEncodeAddons_RoundTrip(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConsoleAddonsURL_FormatAndBrandHost(t *testing.T) {
|
||||
url, err := consoleAddonsURL(core.BrandFeishu, "cli_x", ManifestAddons{Callbacks: &AddonsCallbacks{Items: []string{"card.action.trigger"}}})
|
||||
url, err := consoleAddonsURL(brand.Feishu, "cli_x", ManifestAddons{Callbacks: &AddonsCallbacks{Items: []string{"card.action.trigger"}}})
|
||||
if err != nil {
|
||||
t.Fatalf("url: %v", err)
|
||||
}
|
||||
host := core.ResolveEndpoints(core.BrandFeishu).Open
|
||||
host := brand.ResolveEndpoints(brand.Feishu).Open
|
||||
prefix := host + "/page/launcher?clientID=cli_x&addons="
|
||||
if !strings.HasPrefix(url, prefix) {
|
||||
t.Errorf("url = %q, want prefix %q", url, prefix)
|
||||
@@ -71,22 +72,22 @@ func TestConsoleAddonsURL_FormatAndBrandHost(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMissingScopeAddons_ByIdentity(t *testing.T) {
|
||||
bot := missingScopeAddons(core.AsBot, []string{"im:message"})
|
||||
bot := missingScopeAddons(identity.AsBot, []string{"im:message"})
|
||||
if bot.Scopes == nil || len(bot.Scopes.Tenant) != 1 || len(bot.Scopes.User) != 0 {
|
||||
t.Errorf("bot scopes = %+v, want tenant-only", bot.Scopes)
|
||||
}
|
||||
user := missingScopeAddons(core.AsUser, []string{"im:message"})
|
||||
user := missingScopeAddons(identity.AsUser, []string{"im:message"})
|
||||
if user.Scopes == nil || len(user.Scopes.User) != 1 || len(user.Scopes.Tenant) != 0 {
|
||||
t.Errorf("user scopes = %+v, want user-only", user.Scopes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingSubscriptionAddons_EventVsCallback(t *testing.T) {
|
||||
ev := missingSubscriptionAddons(eventlib.SubTypeEvent, core.AsBot, []string{"im.message.receive_v1"})
|
||||
ev := missingSubscriptionAddons(eventlib.SubTypeEvent, identity.AsBot, []string{"im.message.receive_v1"})
|
||||
if ev.Events == nil || len(ev.Events.Items.Tenant) != 1 {
|
||||
t.Errorf("event addons = %+v, want events.items.tenant", ev.Events)
|
||||
}
|
||||
cb := missingSubscriptionAddons(eventlib.SubTypeCallback, core.AsBot, []string{"card.action.trigger"})
|
||||
cb := missingSubscriptionAddons(eventlib.SubTypeCallback, identity.AsBot, []string{"card.action.trigger"})
|
||||
if cb.Callbacks == nil || len(cb.Callbacks.Items) != 1 || cb.Events != nil {
|
||||
t.Errorf("callback addons = %+v, want callbacks.items only", cb)
|
||||
}
|
||||
@@ -96,9 +97,9 @@ func TestMissingAddons_EncodeEmptyArraysNotNull(t *testing.T) {
|
||||
// Unused identity sides must encode as [] (not null) so the launcher page's
|
||||
// shape validation treats them as "缺省 -> 空数组" per the addons spec.
|
||||
cases := []ManifestAddons{
|
||||
missingScopeAddons(core.AsBot, []string{"im:message"}),
|
||||
missingScopeAddons(core.AsUser, []string{"im:message"}),
|
||||
missingSubscriptionAddons(eventlib.SubTypeEvent, core.AsBot, []string{"im.message.receive_v1"}),
|
||||
missingScopeAddons(identity.AsBot, []string{"im:message"}),
|
||||
missingScopeAddons(identity.AsUser, []string{"im:message"}),
|
||||
missingSubscriptionAddons(eventlib.SubTypeEvent, identity.AsBot, []string{"im.message.receive_v1"}),
|
||||
}
|
||||
for i, a := range cases {
|
||||
raw, err := json.Marshal(a)
|
||||
|
||||
@@ -16,18 +16,16 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/larksuite/cli/cmd/event/render"
|
||||
brandpkg "github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/appmeta"
|
||||
"github.com/larksuite/cli/internal/auth"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/credential"
|
||||
eventlib "github.com/larksuite/cli/internal/event"
|
||||
"github.com/larksuite/cli/internal/event/adapter/localbus/transport"
|
||||
appconsume "github.com/larksuite/cli/internal/event/application/consume"
|
||||
"github.com/larksuite/cli/internal/event/catalog"
|
||||
"github.com/larksuite/cli/internal/event/consume"
|
||||
"github.com/larksuite/cli/internal/event/transport"
|
||||
identitypkg "github.com/larksuite/cli/internal/identity"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
)
|
||||
@@ -40,10 +38,9 @@ type consumeCmdOpts struct {
|
||||
|
||||
maxEvents int
|
||||
timeout time.Duration
|
||||
dryRun bool
|
||||
}
|
||||
|
||||
func NewCmdConsume(f *cmdutil.Factory, snap *catalog.Snapshot) *cobra.Command {
|
||||
func NewCmdConsume(f *cmdutil.Factory) *cobra.Command {
|
||||
var o consumeCmdOpts
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@@ -61,7 +58,7 @@ Use 'event list' to see all available EventKeys.
|
||||
Use 'event schema <EventKey>' for parameter details.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runConsume(cmd, f, snap, args[0], o)
|
||||
return runConsume(cmd, f, args[0], o)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -70,7 +67,6 @@ Use 'event schema <EventKey>' for parameter details.`,
|
||||
cmd.Flags().BoolVar(&o.quiet, "quiet", false, "Suppress informational messages on stderr")
|
||||
cmd.Flags().StringVar(&o.outputDir, "output-dir", "", "Write each event as a file in this directory (relative paths only; absolute paths and ~ are rejected to prevent path traversal)")
|
||||
cmd.Flags().IntVar(&o.maxEvents, "max-events", 0, "Exit after N successful emits (0 = unlimited). Multi-worker EventKeys may emit up to workers-1 past N before all workers stop. Bounded runs ignore stdin EOF.")
|
||||
cmd.Flags().BoolVar(&o.dryRun, "dry-run", false, "Decide and preview the consume (identity, preconditions, side effects) without performing any of them, then exit")
|
||||
cmd.Flags().DurationVar(&o.timeout, "timeout", 0, "Exit after DURATION (e.g. 30s, 2m). 0 = no timeout. Timeout is a normal exit (code 0; stderr 'reason: timeout'). Bounded runs ignore stdin EOF.")
|
||||
cmd.Flags().String("as", "auto", "identity type: user | bot | auto (must match EventKey's declared AuthTypes)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("as", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
@@ -81,7 +77,7 @@ Use 'event schema <EventKey>' for parameter details.`,
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runConsume(cmd *cobra.Command, f *cmdutil.Factory, snap *catalog.Snapshot, eventKey string, o consumeCmdOpts) error {
|
||||
func runConsume(cmd *cobra.Command, f *cmdutil.Factory, eventKey string, o consumeCmdOpts) error {
|
||||
// Pipe-close (e.g. `... | head -n 1`) must reach the EPIPE error path in the loop, not SIGPIPE-kill.
|
||||
ignoreBrokenPipe()
|
||||
|
||||
@@ -95,11 +91,10 @@ func runConsume(cmd *cobra.Command, f *cmdutil.Factory, snap *catalog.Snapshot,
|
||||
return err
|
||||
}
|
||||
|
||||
entry, ok := snap.Resolve(eventKey)
|
||||
keyDef, ok := eventlib.Lookup(eventKey)
|
||||
if !ok {
|
||||
return unknownEventKeyErr(snap, eventKey)
|
||||
return unknownEventKeyErr(eventKey)
|
||||
}
|
||||
keyDef := entry.Definition()
|
||||
|
||||
identity, err := resolveIdentity(cmd, f, keyDef)
|
||||
if err != nil {
|
||||
@@ -124,18 +119,11 @@ func runConsume(cmd *cobra.Command, f *cmdutil.Factory, snap *catalog.Snapshot,
|
||||
outputDir = safePath
|
||||
}
|
||||
|
||||
domain := core.ResolveEndpoints(cfg.Brand).Open
|
||||
domain := brandpkg.ResolveEndpoints(cfg.Brand).Open
|
||||
|
||||
// Surface auth errors before forking the bus daemon. A dry run instead
|
||||
// reports the unusable credential as a blocked precondition: the caller
|
||||
// asked what would happen, and "a real run would refuse to authenticate"
|
||||
// is a legitimate part of that answer.
|
||||
var tokenErr error
|
||||
// Surface auth errors before forking the bus daemon.
|
||||
if _, err := resolveTenantToken(cmd.Context(), f, cfg.AppID); err != nil {
|
||||
if !o.dryRun {
|
||||
return err
|
||||
}
|
||||
tokenErr = err
|
||||
return err
|
||||
}
|
||||
|
||||
apiClient, err := f.NewAPIClient()
|
||||
@@ -144,7 +132,7 @@ func runConsume(cmd *cobra.Command, f *cmdutil.Factory, snap *catalog.Snapshot,
|
||||
}
|
||||
runtime := &consumeRuntime{client: apiClient, accessIdentity: identity}
|
||||
// botRuntime pins AsBot: /app_versions rejects UAT (99991668) and /connection is app-level.
|
||||
botRuntime := &consumeRuntime{client: apiClient, accessIdentity: core.AsBot}
|
||||
botRuntime := &consumeRuntime{client: apiClient, accessIdentity: identitypkg.AsBot}
|
||||
|
||||
// Weak-dependency fetch: failures leave appVer==nil and downgrade preflight to a no-op.
|
||||
preflightErrOut := f.IOStreams.ErrOut
|
||||
@@ -182,31 +170,11 @@ func runConsume(cmd *cobra.Command, f *cmdutil.Factory, snap *catalog.Snapshot,
|
||||
appVer: appVer,
|
||||
subscribedCallbacks: subscribedCallbacks,
|
||||
}
|
||||
|
||||
svc := &appconsume.Service{
|
||||
Strategies: consumeStrategies,
|
||||
Identity: identityResolverFunc(func(context.Context, *catalog.Entry) (string, error) { return string(identity), nil }),
|
||||
Preflight: preflightReaderFunc(func(ctx context.Context, _ *catalog.Entry, _ string) ([]appconsume.Precondition, error) {
|
||||
return readPreconditions(ctx, pf, appVerErr, tokenErr), nil
|
||||
}),
|
||||
}
|
||||
req := appconsume.Request{
|
||||
EventKey: eventKey,
|
||||
Params: paramMap,
|
||||
JQExpr: o.jqExpr,
|
||||
OutputDir: outputDir,
|
||||
DryRun: o.dryRun,
|
||||
MaxEvents: o.maxEvents,
|
||||
Timeout: o.timeout,
|
||||
IsTTY: f.IOStreams.IsTerminal,
|
||||
}
|
||||
decision, err := svc.Decide(cmd.Context(), entry, req, appconsume.ExecutionContext{API: runtime})
|
||||
if err != nil {
|
||||
if err := preflightEventTypes(pf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.dryRun {
|
||||
return render.WriteDecisionJSON(f.IOStreams.Out, f.IOStreams.ErrOut, string(identity), decision.View())
|
||||
if err := preflightScopes(cmd.Context(), pf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(cmd.Context())
|
||||
@@ -237,31 +205,28 @@ func runConsume(cmd *cobra.Command, f *cmdutil.Factory, snap *catalog.Snapshot,
|
||||
watchStdinEOF(os.Stdin, cancel, errOut)
|
||||
}
|
||||
|
||||
runner := streamRunnerFunc(func(ctx context.Context, prepare appconsume.PrepareFunc) error {
|
||||
return consume.Run(ctx, transport.New(), cfg.AppID, cfg.ProfileName, domain, consume.Options{
|
||||
EventKey: eventKey,
|
||||
Def: keyDef,
|
||||
Params: decision.NormalizedParams(),
|
||||
ParamsNormalized: true,
|
||||
JQExpr: o.jqExpr,
|
||||
Quiet: o.quiet,
|
||||
OutputDir: outputDir,
|
||||
Runtime: runtime,
|
||||
Out: f.IOStreams.Out,
|
||||
ErrOut: errOut,
|
||||
RemoteAPIClient: botRuntime,
|
||||
MaxEvents: o.maxEvents,
|
||||
Timeout: o.timeout,
|
||||
IsTTY: f.IOStreams.IsTerminal,
|
||||
Prepare: prepare,
|
||||
})
|
||||
})
|
||||
return svc.Execute(ctx, entry, decision, runner, appconsume.ExecutionContext{API: runtime})
|
||||
if err := consume.Run(ctx, transport.New(), cfg.AppID, cfg.ProfileName, domain, consume.Options{
|
||||
EventKey: eventKey,
|
||||
Params: paramMap,
|
||||
JQExpr: o.jqExpr,
|
||||
Quiet: o.quiet,
|
||||
OutputDir: outputDir,
|
||||
Runtime: runtime,
|
||||
Out: f.IOStreams.Out,
|
||||
ErrOut: errOut,
|
||||
RemoteAPIClient: botRuntime,
|
||||
MaxEvents: o.maxEvents,
|
||||
Timeout: o.timeout,
|
||||
IsTTY: f.IOStreams.IsTerminal,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveIdentity resolves the session identity and enforces keyDef.AuthTypes as a whitelist.
|
||||
func resolveIdentity(cmd *cobra.Command, f *cmdutil.Factory, keyDef *eventlib.KeyDefinition) (core.Identity, error) {
|
||||
flagAs := core.Identity(cmd.Flag("as").Value.String())
|
||||
func resolveIdentity(cmd *cobra.Command, f *cmdutil.Factory, keyDef *eventlib.KeyDefinition) (identitypkg.Identity, error) {
|
||||
flagAs := identitypkg.Identity(cmd.Flag("as").Value.String())
|
||||
identity := f.ResolveAs(cmd.Context(), cmd, flagAs)
|
||||
if len(keyDef.AuthTypes) > 0 {
|
||||
if err := f.CheckIdentity(identity, keyDef.AuthTypes); err != nil {
|
||||
@@ -274,9 +239,9 @@ func resolveIdentity(cmd *cobra.Command, f *cmdutil.Factory, keyDef *eventlib.Ke
|
||||
type preflightCtx struct {
|
||||
factory *cmdutil.Factory
|
||||
appID string
|
||||
brand core.LarkBrand
|
||||
brand brandpkg.Brand
|
||||
eventKey string
|
||||
identity core.Identity
|
||||
identity identitypkg.Identity
|
||||
keyDef *eventlib.KeyDefinition
|
||||
appVer *appmeta.AppVersion
|
||||
// subscribedCallbacks is the application/get 底账 for callback-type EventKeys;
|
||||
@@ -284,14 +249,10 @@ type preflightCtx struct {
|
||||
subscribedCallbacks []string
|
||||
}
|
||||
|
||||
// preflightScopes compares required scopes against session-available scopes
|
||||
// (user: UAT stored; bot: appVer.TenantScopes). checked reports whether a
|
||||
// comparison actually happened: "the ledger was unavailable" and "the check
|
||||
// passed" are different answers, and only the caller can decide how loudly to
|
||||
// say the first one.
|
||||
func preflightScopes(ctx context.Context, pf *preflightCtx) (checked bool, err error) {
|
||||
// preflightScopes compares required scopes against session-available scopes (user: UAT stored; bot: appVer.TenantScopes).
|
||||
func preflightScopes(ctx context.Context, pf *preflightCtx) error {
|
||||
if len(pf.keyDef.Scopes) == 0 || pf.identity == "" {
|
||||
return true, nil
|
||||
return nil
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
@@ -301,24 +262,24 @@ func preflightScopes(ctx context.Context, pf *preflightCtx) (checked bool, err e
|
||||
switch {
|
||||
case pf.identity.IsBot():
|
||||
if pf.appVer == nil {
|
||||
return false, nil
|
||||
return nil
|
||||
}
|
||||
storedScopes = strings.Join(pf.appVer.TenantScopes, " ")
|
||||
case pf.identity == core.AsUser:
|
||||
case pf.identity == identitypkg.AsUser:
|
||||
result, err := pf.factory.Credential.ResolveToken(ctx, credential.NewTokenSpec(pf.identity, pf.appID))
|
||||
if err != nil || result == nil || result.Scopes == "" {
|
||||
return false, nil //nolint:nilerr // best-effort: the bus handshake surfaces the real auth error
|
||||
return nil //nolint:nilerr // best-effort: bus handshake will surface real auth error
|
||||
}
|
||||
storedScopes = result.Scopes
|
||||
default:
|
||||
return false, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
missing := auth.MissingScopes(storedScopes, pf.keyDef.Scopes)
|
||||
if len(missing) == 0 {
|
||||
return true, nil
|
||||
return nil
|
||||
}
|
||||
return true, errs.NewPermissionError(errs.SubtypeMissingScope,
|
||||
return errs.NewPermissionError(errs.SubtypeMissingScope,
|
||||
"missing required scopes for EventKey %s (as %s): %s",
|
||||
pf.eventKey, pf.identity, strings.Join(missing, ", ")).
|
||||
WithIdentity(string(pf.identity)).
|
||||
@@ -331,7 +292,7 @@ func preflightScopes(ctx context.Context, pf *preflightCtx) (checked bool, err e
|
||||
// the tenant token carries them. User: the scan link only updates the app
|
||||
// manifest — the user's own token still lacks the scopes until it is
|
||||
// re-authorized — so direct the user to re-login instead.
|
||||
func scopeRemediationHint(brand core.LarkBrand, appID string, identity core.Identity, missing []string) string {
|
||||
func scopeRemediationHint(brand brandpkg.Brand, appID string, identity identitypkg.Identity, missing []string) string {
|
||||
if identity.IsBot() {
|
||||
return fmt.Sprintf("grant these scopes by scanning: %s",
|
||||
addonsHintURL(brand, appID, missingScopeAddons(identity, missing)))
|
||||
@@ -408,7 +369,7 @@ func resolveTenantToken(ctx context.Context, f *cmdutil.Factory, appID string) (
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
result, err := f.Credential.ResolveToken(ctx, credential.NewTokenSpec(core.AsBot, appID))
|
||||
result, err := f.Credential.ResolveToken(ctx, credential.NewTokenSpec(identitypkg.AsBot, appID))
|
||||
if err != nil {
|
||||
if _, ok := errs.ProblemOf(err); ok {
|
||||
return "", err
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
)
|
||||
|
||||
// A dry run in a degraded environment (the test factory has no reachable
|
||||
// platform, so every weak read-only check comes back unanswered) still exits
|
||||
// zero with a structured decision that honestly says "unknown" — and performs
|
||||
// none of its declared write effects.
|
||||
func TestDryRun_DegradedEnvironmentStaysHonestAndSideEffectFree(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "cli_test"})
|
||||
snap := compileCatalog()
|
||||
|
||||
tmp := t.TempDir()
|
||||
prevWD, _ := os.Getwd()
|
||||
if err := os.Chdir(tmp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.Chdir(prevWD) })
|
||||
|
||||
cmd := NewCmdConsume(f, snap)
|
||||
cmd.SetArgs([]string{"im.message.receive_v1", "--as", "bot", "--dry-run", "--output-dir", "events-out"})
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SilenceErrors = true
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("dry-run must not fail on unusable credentials, got: %v", err)
|
||||
}
|
||||
|
||||
var envelope struct {
|
||||
OK bool `json:"ok"`
|
||||
DryRun bool `json:"dry_run"`
|
||||
Data struct {
|
||||
Decision struct {
|
||||
Status string `json:"status"`
|
||||
Preconditions []struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
} `json:"preconditions"`
|
||||
WouldWrite []string `json:"would_write"`
|
||||
} `json:"decision"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil {
|
||||
t.Fatalf("stdout is not a decision envelope: %v\n%s", err, stdout.String())
|
||||
}
|
||||
if !envelope.OK || !envelope.DryRun {
|
||||
t.Errorf("want ok=true dry_run=true, got: %s", stdout.String())
|
||||
}
|
||||
if envelope.Data.Decision.Status != "unknown" {
|
||||
t.Errorf("unanswerable weak checks must render unknown, not fake readiness; got status %q", envelope.Data.Decision.Status)
|
||||
}
|
||||
names := map[string]string{}
|
||||
for _, p := range envelope.Data.Decision.Preconditions {
|
||||
names[p.Name] = p.Status
|
||||
}
|
||||
if names["credentials_available"] == "" || names["console_event_published"] == "" || names["scopes_granted"] == "" {
|
||||
t.Errorf("preconditions must name every check, got: %v", names)
|
||||
}
|
||||
|
||||
// The declared write side effects must stay declarations: the requested
|
||||
// output dir must not exist after a dry run.
|
||||
if _, err := os.Stat(filepath.Join(tmp, "events-out")); !os.IsNotExist(err) {
|
||||
t.Error("dry-run created the output directory; the preview performed a side effect")
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,12 @@ func NewCmdEvents(f *cmdutil.Factory) *cobra.Command {
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
snap := compileCatalog()
|
||||
cmd.AddCommand(NewCmdConsume(f, snap))
|
||||
cmd.AddCommand(NewCmdList(f, snap))
|
||||
cmd.AddCommand(NewCmdSchema(f, snap))
|
||||
cmd.AddCommand(NewCmdConsume(f))
|
||||
cmd.AddCommand(NewCmdList(f))
|
||||
cmd.AddCommand(NewCmdSchema(f))
|
||||
cmd.AddCommand(NewCmdStatus(f))
|
||||
cmd.AddCommand(NewCmdStop(f))
|
||||
cmd.AddCommand(NewCmdBus(f, snap))
|
||||
cmd.AddCommand(NewCmdBus(f))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/event/adapter/localbus/protocol"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/event/protocol"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
)
|
||||
|
||||
@@ -287,11 +287,10 @@ func errorAs(err error, target interface{}) bool {
|
||||
}
|
||||
|
||||
func TestNewCmdFactories_WireFlags(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "cli_XXXXXXXXXXXXXXXX"})
|
||||
snap := compileCatalog()
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "cli_XXXXXXXXXXXXXXXX"})
|
||||
|
||||
t.Run("consume", func(t *testing.T) {
|
||||
cmd := NewCmdConsume(f, snap)
|
||||
cmd := NewCmdConsume(f)
|
||||
for _, flag := range []string{"param", "jq", "quiet", "output-dir", "max-events", "timeout", "as"} {
|
||||
if cmd.Flags().Lookup(flag) == nil {
|
||||
t.Errorf("consume missing --%s flag", flag)
|
||||
@@ -321,22 +320,14 @@ func TestNewCmdFactories_WireFlags(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("list", func(t *testing.T) {
|
||||
cmd := NewCmdList(f, snap)
|
||||
cmd := NewCmdList(f)
|
||||
if cmd.Flags().Lookup("json") == nil {
|
||||
t.Error("list missing --json flag")
|
||||
}
|
||||
domainFlag := cmd.Flags().Lookup("domain")
|
||||
if domainFlag == nil {
|
||||
t.Fatal("list missing --domain flag")
|
||||
}
|
||||
wantUsage := "Only list EventKeys of this domain. Valid domains: " + strings.Join(snap.Domains(), ", ")
|
||||
if domainFlag.Usage != wantUsage {
|
||||
t.Errorf("--domain usage = %q, want %q", domainFlag.Usage, wantUsage)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("bus", func(t *testing.T) {
|
||||
cmd := NewCmdBus(f, snap)
|
||||
cmd := NewCmdBus(f)
|
||||
if !cmd.Hidden {
|
||||
t.Error("bus should be hidden (internal daemon entrypoint)")
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
)
|
||||
|
||||
var updateGolden = flag.Bool("update", false, "rewrite golden files instead of comparing")
|
||||
|
||||
// goldenSchemaKeys picks one key per rendering path so every branch of the
|
||||
// list/schema output stays pinned: a processed key with a flat custom schema,
|
||||
// a native key with field overrides, a callback key with a single consumer,
|
||||
// and a key with a required parameter plus a pre-consume hook.
|
||||
var goldenSchemaKeys = map[string]string{
|
||||
"schema_im_message_receive": "im.message.receive_v1",
|
||||
"schema_im_chat_updated": "im.chat.updated_v1",
|
||||
"schema_card_action_trigger": "card.action.trigger",
|
||||
"schema_board_whiteboard": "board.whiteboard.updated_v1",
|
||||
}
|
||||
|
||||
// The golden files pin stdout byte-for-byte. The output is deterministic:
|
||||
// the snapshot keeps keys sorted, encoding/json sorts object keys, and nothing
|
||||
// on the rendering path reads the clock or randomness. Regenerate with:
|
||||
//
|
||||
// go test ./cmd/event/ -run TestGolden -update
|
||||
func TestGolden_ListOutput(t *testing.T) {
|
||||
snap := compileCatalog()
|
||||
for name, asJSON := range map[string]bool{"list_text": false, "list_json": true} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
if err := runList(f, snap, "", asJSON); err != nil {
|
||||
t.Fatalf("runList: %v", err)
|
||||
}
|
||||
assertGolden(t, name, stdout.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGolden_SchemaOutput(t *testing.T) {
|
||||
snap := compileCatalog()
|
||||
for name, key := range goldenSchemaKeys {
|
||||
for suffix, asJSON := range map[string]bool{"_text": false, "_json": true} {
|
||||
t.Run(name+suffix, func(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
if err := runSchema(f, snap, key, asJSON); err != nil {
|
||||
t.Fatalf("runSchema(%s): %v", key, err)
|
||||
}
|
||||
assertGolden(t, name+suffix, stdout.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertGolden(t *testing.T, name, got string) {
|
||||
t.Helper()
|
||||
path := filepath.Join("testdata", "golden", name+".golden")
|
||||
if *updateGolden {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(got), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
want, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("missing golden %s (regenerate with -update): %v", name, err)
|
||||
}
|
||||
if string(want) != got {
|
||||
t.Errorf("output drifted from golden %s\n--- want\n%s\n--- got\n%s", name, want, got)
|
||||
}
|
||||
}
|
||||
@@ -10,44 +10,31 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
eventlib "github.com/larksuite/cli/internal/event"
|
||||
"github.com/larksuite/cli/internal/event/catalog"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
)
|
||||
|
||||
func NewCmdList(f *cmdutil.Factory, snap *catalog.Snapshot) *cobra.Command {
|
||||
func NewCmdList(f *cmdutil.Factory) *cobra.Command {
|
||||
var asJSON bool
|
||||
var domain string
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all available EventKeys",
|
||||
Long: "Show all registered EventKeys grouped by domain (first segment of the key). Use --domain to keep one domain only, --json for machine-readable output.",
|
||||
Long: "Show all registered EventKeys grouped by domain (first segment of the key). Use --json for machine-readable output.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runList(f, snap, domain, asJSON)
|
||||
return runList(f, asJSON)
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&asJSON, "json", false, "Emit the full EventKey list as JSON (for AI / scripts)")
|
||||
cmd.Flags().StringVar(&domain, "domain", "", fmt.Sprintf(
|
||||
"Only list EventKeys of this domain. Valid domains: %s",
|
||||
strings.Join(snap.Domains(), ", "),
|
||||
))
|
||||
cmdutil.SetRisk(cmd, "read")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runList(f *cmdutil.Factory, snap *catalog.Snapshot, domain string, asJSON bool) error {
|
||||
entries, err := entriesForDomain(snap, domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func runList(f *cmdutil.Factory, asJSON bool) error {
|
||||
all := eventlib.ListAll()
|
||||
|
||||
if asJSON {
|
||||
return writeListJSON(f, entries)
|
||||
}
|
||||
all := make([]*eventlib.KeyDefinition, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
all = append(all, entry.Definition())
|
||||
return writeListJSON(f, all)
|
||||
}
|
||||
|
||||
if len(all) == 0 {
|
||||
@@ -117,43 +104,18 @@ func runList(f *cmdutil.Factory, snap *catalog.Snapshot, domain string, asJSON b
|
||||
return nil
|
||||
}
|
||||
|
||||
// listRow is the JSON shape of one `event list --json` row. It is a named
|
||||
// type (not a function-local literal) so the render contract test can walk
|
||||
// its fields and reject accidental additions to the public output.
|
||||
type listRow struct {
|
||||
*eventlib.KeyDefinition
|
||||
ResolvedSchema json.RawMessage `json:"resolved_output_schema,omitempty"`
|
||||
}
|
||||
|
||||
// entriesForDomain filters at the snapshot query layer: without a domain the
|
||||
// full catalog comes back untouched; with one, rows are only removed, never
|
||||
// reshaped. An unknown domain is rejected with the valid set spelled out.
|
||||
func entriesForDomain(snap *catalog.Snapshot, domain string) ([]*catalog.Entry, error) {
|
||||
if domain == "" {
|
||||
return snap.Entries(), nil
|
||||
func writeListJSON(f *cmdutil.Factory, all []*eventlib.KeyDefinition) error {
|
||||
type row struct {
|
||||
*eventlib.KeyDefinition
|
||||
ResolvedSchema json.RawMessage `json:"resolved_output_schema,omitempty"`
|
||||
}
|
||||
var filtered []*catalog.Entry
|
||||
for _, entry := range snap.Entries() {
|
||||
if entry.Descriptor().Domain == domain {
|
||||
filtered = append(filtered, entry)
|
||||
}
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument,
|
||||
"unknown domain: %s", domain).
|
||||
WithParam("--domain").
|
||||
WithHint("valid domains: %s", strings.Join(snap.Domains(), ", "))
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func writeListJSON(f *cmdutil.Factory, entries []*catalog.Entry) error {
|
||||
rows := make([]listRow, len(entries))
|
||||
for i, entry := range entries {
|
||||
rows[i] = listRow{
|
||||
KeyDefinition: entry.Definition(),
|
||||
ResolvedSchema: entry.Output().SchemaJSON,
|
||||
rows := make([]row, len(all))
|
||||
for i, def := range all {
|
||||
resolved, _, err := resolveSchemaJSON(def)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows[i] = row{KeyDefinition: def, ResolvedSchema: resolved}
|
||||
}
|
||||
output.PrintJson(f.IOStreams.Out, rows)
|
||||
return nil
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
)
|
||||
|
||||
// Filtering only removes rows: the vc selection must be exactly the catalog's
|
||||
// vc keys, and every remaining row keeps the unfiltered field set.
|
||||
func TestListDomain_FilterKeepsExactlyTheRequestedDomain(t *testing.T) {
|
||||
snap := compileCatalog()
|
||||
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
if err := runList(f, snap, "vc", true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var rows []map[string]json.RawMessage
|
||||
if err := json.Unmarshal(stdout.Bytes(), &rows); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := map[string]bool{}
|
||||
for _, key := range snap.Keys() {
|
||||
if strings.HasPrefix(key, "vc.") {
|
||||
want[key] = true
|
||||
}
|
||||
}
|
||||
if len(want) == 0 {
|
||||
t.Fatal("the catalog has no vc keys; the filter test proves nothing")
|
||||
}
|
||||
got := map[string]bool{}
|
||||
for _, row := range rows {
|
||||
var key string
|
||||
_ = json.Unmarshal(row["key"], &key)
|
||||
got[key] = true
|
||||
for _, field := range []string{"event_type", "schema", "resolved_output_schema"} {
|
||||
if _, ok := row[field]; !ok {
|
||||
t.Errorf("%s: filtering must not reshape rows; %q is missing", key, field)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("filtered rows = %v, want the exact vc set %v", got, want)
|
||||
}
|
||||
for key := range want {
|
||||
if !got[key] {
|
||||
t.Errorf("vc key missing from the filtered list: %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListDomain_TextFilter(t *testing.T) {
|
||||
snap := compileCatalog()
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
if err := runList(f, snap, "im", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := stdout.String()
|
||||
if !strings.Contains(out, "im.message.receive_v1") {
|
||||
t.Error("im keys must be listed")
|
||||
}
|
||||
for _, foreign := range []string{"vc.", "minutes.", "board.", "approval."} {
|
||||
if strings.Contains(out, foreign) {
|
||||
t.Errorf("foreign domain %q leaked into the filtered text output", foreign)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListDomain_UnknownDomainIsRejectedWithTheValidSet(t *testing.T) {
|
||||
snap := compileCatalog()
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
err := runList(f, snap, "definitely-bogus", true)
|
||||
if err == nil {
|
||||
t.Fatal("an unknown domain must be rejected")
|
||||
}
|
||||
problem, ok := errs.ProblemOf(err)
|
||||
if !ok || problem.Subtype != errs.SubtypeInvalidArgument {
|
||||
t.Fatalf("want invalid_argument, got %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unknown domain: definitely-bogus") {
|
||||
t.Errorf("error must name the rejected value, got %v", err)
|
||||
}
|
||||
for _, domain := range []string{"application", "approval", "board", "card", "im", "minutes", "task", "vc"} {
|
||||
if !strings.Contains(problem.Hint, domain) {
|
||||
t.Errorf("hint must list valid domain %q, got %q", domain, problem.Hint)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,27 +9,29 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
eventlib "github.com/larksuite/cli/internal/event"
|
||||
|
||||
_ "github.com/larksuite/cli/events"
|
||||
)
|
||||
|
||||
func TestEventLookup_VCMeetingLifecycleKeys(t *testing.T) {
|
||||
snap := compileCatalog()
|
||||
for _, key := range []string{
|
||||
"approval.instance.status_changed_v4",
|
||||
"approval.task.status_changed_v4",
|
||||
"vc.meeting.participant_meeting_started_v1",
|
||||
"vc.meeting.participant_meeting_joined_v1",
|
||||
} {
|
||||
if _, ok := snap.Resolve(key); !ok {
|
||||
t.Fatalf("snap.Resolve(%q) should succeed", key)
|
||||
if _, ok := eventlib.Lookup(key); !ok {
|
||||
t.Fatalf("event.Lookup(%q) should succeed", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunList_TextOutput(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
|
||||
|
||||
if err := runList(f, compileCatalog(), "", false); err != nil {
|
||||
if err := runList(f, false); err != nil {
|
||||
t.Fatalf("runList: %v", err)
|
||||
}
|
||||
|
||||
@@ -51,9 +53,9 @@ func TestRunList_TextOutput(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunList_JSONOutput(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
|
||||
|
||||
if err := runList(f, compileCatalog(), "", true); err != nil {
|
||||
if err := runList(f, true); err != nil {
|
||||
t.Fatalf("runList json: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
eventlib "github.com/larksuite/cli/internal/event"
|
||||
appconsume "github.com/larksuite/cli/internal/event/application/consume"
|
||||
)
|
||||
|
||||
func preconditionByName(list []appconsume.Precondition, name string) *appconsume.Precondition {
|
||||
for i := range list {
|
||||
if list[i].Name == name {
|
||||
return &list[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// An unusable credential blocks the decision and carries the exact error a
|
||||
// real run would have returned, so both paths refuse for the same reason.
|
||||
func TestReadPreconditions_TokenErrorBlocksWithTheSameError(t *testing.T) {
|
||||
tokenErr := errors.New("no tenant token available")
|
||||
pf := &preflightCtx{
|
||||
appID: "cli_test",
|
||||
identity: core.AsBot,
|
||||
keyDef: &eventlib.KeyDefinition{Key: "demo.thing.updated_v1"},
|
||||
}
|
||||
got := readPreconditions(context.Background(), pf, nil, tokenErr)
|
||||
|
||||
cred := preconditionByName(got, "credentials_available")
|
||||
if cred == nil {
|
||||
t.Fatal("credentials_available precondition missing")
|
||||
}
|
||||
if cred.Status != appconsume.PreconditionBlocked || !errors.Is(cred.BlockErr, tokenErr) {
|
||||
t.Errorf("token failure must block with the original error, got %+v", cred)
|
||||
}
|
||||
}
|
||||
|
||||
// A scope ledger nobody could read is reported as unknown — never as ok.
|
||||
func TestReadPreconditions_UnreadableScopesAreUnknown(t *testing.T) {
|
||||
pf := &preflightCtx{
|
||||
appID: "cli_test",
|
||||
identity: core.AsBot,
|
||||
keyDef: &eventlib.KeyDefinition{
|
||||
Key: "demo.thing.updated_v1",
|
||||
Scopes: []string{"demo:read"},
|
||||
},
|
||||
appVer: nil, // no published version: the bot scope ledger is unreadable
|
||||
}
|
||||
got := readPreconditions(context.Background(), pf, nil, nil)
|
||||
|
||||
scopes := preconditionByName(got, "scopes_granted")
|
||||
if scopes == nil {
|
||||
t.Fatal("scopes_granted precondition missing")
|
||||
}
|
||||
if scopes.Status != appconsume.PreconditionUnknown {
|
||||
t.Errorf("an unreadable ledger must report unknown, got %q", scopes.Status)
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,14 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
brandpkg "github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/appmeta"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
eventlib "github.com/larksuite/cli/internal/event"
|
||||
identitypkg "github.com/larksuite/cli/internal/identity"
|
||||
)
|
||||
|
||||
func newPreflightCtx(appID string, brand core.LarkBrand, identity core.Identity, keyDef *eventlib.KeyDefinition, appVer *appmeta.AppVersion) *preflightCtx {
|
||||
func newPreflightCtx(appID string, brand brandpkg.Brand, identity identitypkg.Identity, keyDef *eventlib.KeyDefinition, appVer *appmeta.AppVersion) *preflightCtx {
|
||||
key := ""
|
||||
if keyDef != nil {
|
||||
key = keyDef.Key
|
||||
@@ -108,7 +109,7 @@ func TestPreflightScopes_Bot_NoAppVer_SkipsCheck(t *testing.T) {
|
||||
Key: "im.message.text",
|
||||
Scopes: []string{"im:message", "im:message.group_at_msg"},
|
||||
}
|
||||
_, err := preflightScopes(nil, newPreflightCtx("cli_x", "feishu", core.AsBot, def, nil))
|
||||
err := preflightScopes(nil, newPreflightCtx("cli_x", "feishu", identitypkg.AsBot, def, nil))
|
||||
if err != nil {
|
||||
t.Fatalf("bot + nil appVer should skip, got: %v", err)
|
||||
}
|
||||
@@ -124,7 +125,7 @@ func TestPreflightScopes_Bot_AllGranted_Passes(t *testing.T) {
|
||||
"im:message.group_at_msg",
|
||||
"contact:user:readonly",
|
||||
}}
|
||||
_, err := preflightScopes(nil, newPreflightCtx("cli_x", "feishu", core.AsBot, def, appVer))
|
||||
err := preflightScopes(nil, newPreflightCtx("cli_x", "feishu", identitypkg.AsBot, def, appVer))
|
||||
if err != nil {
|
||||
t.Fatalf("all scopes granted, unexpected error: %v", err)
|
||||
}
|
||||
@@ -136,7 +137,7 @@ func TestPreflightScopes_Bot_MissingBlocks(t *testing.T) {
|
||||
Scopes: []string{"im:message", "im:message.group_at_msg"},
|
||||
}
|
||||
appVer := &appmeta.AppVersion{TenantScopes: []string{"im:message"}}
|
||||
_, err := preflightScopes(nil, newPreflightCtx("cli_x", "feishu", core.AsBot, def, appVer))
|
||||
err := preflightScopes(nil, newPreflightCtx("cli_x", "feishu", identitypkg.AsBot, def, appVer))
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing scope")
|
||||
}
|
||||
@@ -169,7 +170,7 @@ func TestPreflightScopes_Bot_MissingBlocks(t *testing.T) {
|
||||
|
||||
func TestPreflightScopes_NoRequiredScopes_SkipsCheck(t *testing.T) {
|
||||
def := &eventlib.KeyDefinition{Key: "x"}
|
||||
if _, err := preflightScopes(nil, newPreflightCtx("cli_x", "feishu", core.AsBot, def, nil)); err != nil {
|
||||
if err := preflightScopes(nil, newPreflightCtx("cli_x", "feishu", identitypkg.AsBot, def, nil)); err != nil {
|
||||
t.Fatalf("no required scopes means nothing to verify, got: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -177,9 +178,9 @@ func TestPreflightScopes_NoRequiredScopes_SkipsCheck(t *testing.T) {
|
||||
func TestPreflightEventTypes_CallbackMissing(t *testing.T) {
|
||||
pf := &preflightCtx{
|
||||
appID: "cli_x",
|
||||
brand: core.BrandFeishu,
|
||||
brand: brandpkg.Feishu,
|
||||
eventKey: "test.cb",
|
||||
identity: core.AsBot,
|
||||
identity: identitypkg.AsBot,
|
||||
subscribedCallbacks: []string{"profile.view.get"},
|
||||
keyDef: &eventlib.KeyDefinition{
|
||||
Key: "test.cb",
|
||||
@@ -206,9 +207,9 @@ func TestPreflightEventTypes_CallbackMissing(t *testing.T) {
|
||||
func TestPreflightEventTypes_CallbackSkippedWhenNil(t *testing.T) {
|
||||
pf := &preflightCtx{
|
||||
appID: "cli_x",
|
||||
brand: core.BrandFeishu,
|
||||
brand: brandpkg.Feishu,
|
||||
eventKey: "test.cb",
|
||||
identity: core.AsBot,
|
||||
identity: identitypkg.AsBot,
|
||||
subscribedCallbacks: nil, // fetch 失败/拿不到 -> 弱依赖跳过
|
||||
keyDef: &eventlib.KeyDefinition{
|
||||
Key: "test.cb",
|
||||
@@ -227,9 +228,9 @@ func TestPreflightEventTypes_CallbackEmptyReportsMissing(t *testing.T) {
|
||||
// not skipped as a weak dependency.
|
||||
pf := &preflightCtx{
|
||||
appID: "cli_x",
|
||||
brand: core.BrandFeishu,
|
||||
brand: brandpkg.Feishu,
|
||||
eventKey: "test.cb",
|
||||
identity: core.AsBot,
|
||||
identity: identitypkg.AsBot,
|
||||
subscribedCallbacks: []string{}, // fetched, none subscribed
|
||||
keyDef: &eventlib.KeyDefinition{
|
||||
Key: "test.cb",
|
||||
@@ -249,9 +250,9 @@ func TestPreflightEventTypes_CallbackEmptyReportsMissing(t *testing.T) {
|
||||
func TestPreflightEventTypes_CallbackAllSubscribed_Passes(t *testing.T) {
|
||||
pf := &preflightCtx{
|
||||
appID: "cli_x",
|
||||
brand: core.BrandFeishu,
|
||||
brand: brandpkg.Feishu,
|
||||
eventKey: "test.cb",
|
||||
identity: core.AsBot,
|
||||
identity: identitypkg.AsBot,
|
||||
subscribedCallbacks: []string{"card.action.trigger", "profile.view.get"},
|
||||
keyDef: &eventlib.KeyDefinition{
|
||||
Key: "test.cb",
|
||||
@@ -266,12 +267,12 @@ func TestPreflightEventTypes_CallbackAllSubscribed_Passes(t *testing.T) {
|
||||
|
||||
func TestScopeRemediationHint_ByIdentity(t *testing.T) {
|
||||
// bot: scan-to-enable link (adds scopes to app manifest)
|
||||
bot := scopeRemediationHint(core.BrandFeishu, "cli_x", core.AsBot, []string{"im:message"})
|
||||
bot := scopeRemediationHint(brandpkg.Feishu, "cli_x", identitypkg.AsBot, []string{"im:message"})
|
||||
if !strings.Contains(bot, "/page/launcher?clientID=cli_x&addons=") {
|
||||
t.Errorf("bot hint should give the scan link, got: %s", bot)
|
||||
}
|
||||
// user: re-login (scan link cannot grant scopes to the user's own token)
|
||||
user := scopeRemediationHint(core.BrandFeishu, "cli_x", core.AsUser, []string{"im:message"})
|
||||
user := scopeRemediationHint(brandpkg.Feishu, "cli_x", identitypkg.AsUser, []string{"im:message"})
|
||||
if !strings.Contains(user, "auth login --scope") {
|
||||
t.Errorf("user hint should direct to auth login, got: %s", user)
|
||||
}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package render turns consume decisions into user-facing output. It is the
|
||||
// only place a decision becomes JSON; the application layer never formats
|
||||
// anything itself.
|
||||
package render
|
||||
|
||||
import (
|
||||
"io"
|
||||
"regexp"
|
||||
|
||||
appconsume "github.com/larksuite/cli/internal/event/application/consume"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
)
|
||||
|
||||
// sensitiveParamName matches parameter names whose values must never be
|
||||
// echoed back in a rendered decision. Names are matched, not values: a
|
||||
// credential-bearing parameter is identifiable by its declaration, and
|
||||
// guessing at value shapes would miss more than it catches.
|
||||
var sensitiveParamName = regexp.MustCompile(`(?i)(token|secret|password|credential|cookie)`)
|
||||
|
||||
func redactParams(params map[string]string) map[string]string {
|
||||
out := make(map[string]string, len(params))
|
||||
for name, value := range params {
|
||||
if sensitiveParamName.MatchString(name) {
|
||||
out[name] = "[redacted]"
|
||||
continue
|
||||
}
|
||||
out[name] = value
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// decisionPayload is the JSON shape under data.decision — snake_case, stable,
|
||||
// documented in the event skill. Field additions must be additive.
|
||||
type decisionPayload struct {
|
||||
EventKey string `json:"event_key"`
|
||||
Domain string `json:"domain"`
|
||||
Identity string `json:"identity"`
|
||||
Status string `json:"status"`
|
||||
Params map[string]string `json:"params"`
|
||||
Scope string `json:"scope"`
|
||||
Preconditions []preconditionView `json:"preconditions"`
|
||||
Preparation *preparationView `json:"preparation,omitempty"`
|
||||
WouldRead []string `json:"would_read"`
|
||||
WouldWrite []string `json:"would_write"`
|
||||
}
|
||||
|
||||
type preconditionView struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
type preparationView struct {
|
||||
Strategy string `json:"strategy"`
|
||||
Condition string `json:"condition"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
// WriteDecisionJSON emits the decision inside the standard success envelope
|
||||
// with the envelope's own top-level dry_run marker set.
|
||||
func WriteDecisionJSON(out, errOut io.Writer, identity string, v appconsume.DecisionView) error {
|
||||
return output.WriteSuccessEnvelope(map[string]any{
|
||||
"decision": toPayload(v),
|
||||
}, output.SuccessEnvelopeOptions{
|
||||
CommandPath: "event consume",
|
||||
Identity: identity,
|
||||
DryRun: true,
|
||||
Out: out,
|
||||
ErrOut: errOut,
|
||||
})
|
||||
}
|
||||
|
||||
func toPayload(v appconsume.DecisionView) decisionPayload {
|
||||
p := decisionPayload{
|
||||
EventKey: v.EventKey,
|
||||
Domain: v.Domain,
|
||||
Identity: v.Identity,
|
||||
Status: v.Status,
|
||||
Params: redactParams(v.Params),
|
||||
Scope: v.Scope,
|
||||
WouldRead: v.WouldRead,
|
||||
WouldWrite: v.WouldWrite,
|
||||
}
|
||||
p.Preconditions = make([]preconditionView, 0, len(v.Preconditions))
|
||||
for _, pc := range v.Preconditions {
|
||||
p.Preconditions = append(p.Preconditions, preconditionView(pc))
|
||||
}
|
||||
if v.Preparation != nil {
|
||||
pv := preparationView(*v.Preparation)
|
||||
p.Preparation = &pv
|
||||
}
|
||||
return p
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package render
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
appconsume "github.com/larksuite/cli/internal/event/application/consume"
|
||||
)
|
||||
|
||||
func sampleView() appconsume.DecisionView {
|
||||
return appconsume.DecisionView{
|
||||
EventKey: "vc.note.generated_v1",
|
||||
Domain: "vc",
|
||||
Identity: "user",
|
||||
Status: "ready",
|
||||
Params: map[string]string{"whiteboard_id": "wb-1", "access_token": "sk-SENSITIVE-VALUE"},
|
||||
Scope: "vc.note.generated_v1",
|
||||
Preconditions: []appconsume.PreconditionView{
|
||||
{Name: "console_event_published", Status: "ok"},
|
||||
{Name: "scopes_granted", Status: "ok"},
|
||||
},
|
||||
Preparation: &appconsume.PreparationView{
|
||||
Strategy: "legacy_preconsume", Condition: "first_consumer_for_scope", Action: "register_event_delivery",
|
||||
},
|
||||
WouldRead: []string{"local_bus_probe", "app_metadata_preflight"},
|
||||
WouldWrite: []string{"start_or_reuse_local_bus", "register_consumer", "run_preparation_when_first", "open_event_stream"},
|
||||
}
|
||||
}
|
||||
|
||||
// The JSON contract: dry_run is the envelope's own top-level marker (never a
|
||||
// data field), and the decision sits under data.decision with its documented
|
||||
// members.
|
||||
func TestWriteDecisionJSON_EnvelopeContract(t *testing.T) {
|
||||
var out, errOut bytes.Buffer
|
||||
if err := WriteDecisionJSON(&out, &errOut, "user", sampleView()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var envelope map[string]json.RawMessage
|
||||
if err := json.Unmarshal(out.Bytes(), &envelope); err != nil {
|
||||
t.Fatalf("stdout is not a JSON envelope: %v\n%s", err, out.String())
|
||||
}
|
||||
if string(envelope["ok"]) != "true" || string(envelope["dry_run"]) != "true" {
|
||||
t.Errorf("envelope must carry top-level ok=true and dry_run=true, got %s", out.String())
|
||||
}
|
||||
if _, misplaced := envelope["decision"]; misplaced {
|
||||
t.Error("decision must live under data, not at the envelope top level")
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Decision struct {
|
||||
EventKey string `json:"event_key"`
|
||||
Domain string `json:"domain"`
|
||||
Identity string `json:"identity"`
|
||||
Status string `json:"status"`
|
||||
Params map[string]string `json:"params"`
|
||||
Scope string `json:"scope"`
|
||||
Preparation *struct {
|
||||
Strategy string `json:"strategy"`
|
||||
Condition string `json:"condition"`
|
||||
Action string `json:"action"`
|
||||
} `json:"preparation"`
|
||||
WouldRead []string `json:"would_read"`
|
||||
WouldWrite []string `json:"would_write"`
|
||||
DryRun *bool `json:"dry_run"`
|
||||
} `json:"decision"`
|
||||
}
|
||||
if err := json.Unmarshal(envelope["data"], &data); err != nil {
|
||||
t.Fatalf("data.decision does not match the documented shape: %v", err)
|
||||
}
|
||||
d := data.Decision
|
||||
if d.EventKey != "vc.note.generated_v1" || d.Domain != "vc" || d.Identity != "user" || d.Status != "ready" {
|
||||
t.Errorf("identity facts drifted: %+v", d)
|
||||
}
|
||||
if d.Preparation == nil || d.Preparation.Condition != "first_consumer_for_scope" {
|
||||
t.Errorf("conditional preparation must be stated: %+v", d.Preparation)
|
||||
}
|
||||
if len(d.WouldRead) == 0 || len(d.WouldWrite) == 0 {
|
||||
t.Error("would_read / would_write must be present")
|
||||
}
|
||||
if d.DryRun != nil {
|
||||
t.Error("dry_run inside data.decision would duplicate the envelope marker")
|
||||
}
|
||||
}
|
||||
|
||||
// Sensitive parameter values never reach the rendered output. The control
|
||||
// assertion first proves the sentinel would be visible if leaked.
|
||||
func TestWriteDecision_RedactsSensitiveParams(t *testing.T) {
|
||||
const sentinel = "sk-SENSITIVE-VALUE"
|
||||
view := sampleView()
|
||||
if !strings.Contains(view.Params["access_token"], sentinel) {
|
||||
t.Fatal("control failed: the sentinel is not in the input, the test cannot prove redaction")
|
||||
}
|
||||
|
||||
var jsonOut, jsonErr bytes.Buffer
|
||||
if err := WriteDecisionJSON(&jsonOut, &jsonErr, "user", view); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(jsonOut.String(), sentinel) {
|
||||
t.Errorf("JSON output leaks a sensitive param value: %s", jsonOut.String())
|
||||
}
|
||||
compact := strings.ReplaceAll(strings.ReplaceAll(jsonOut.String(), "\n", ""), " ", "")
|
||||
if !strings.Contains(compact, `"access_token":"[redacted]"`) {
|
||||
t.Errorf("sensitive param must render as redacted, got: %s", jsonOut.String())
|
||||
}
|
||||
if !strings.Contains(compact, `"whiteboard_id":"wb-1"`) {
|
||||
t.Errorf("non-sensitive params must render verbatim, got: %s", jsonOut.String())
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package render
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/events"
|
||||
"github.com/larksuite/cli/internal/event/catalog"
|
||||
)
|
||||
|
||||
// This file is a guard, not a contract: it does not pin what the redaction
|
||||
// regex matches, it hunts for declared parameter names that smell like
|
||||
// credentials yet would render verbatim. The detector wordlist is therefore
|
||||
// deliberately wider than the production sensitiveParamName pattern — a hit
|
||||
// here means either the parameter should be renamed or the production
|
||||
// pattern must grow, decided by a human, never by loosening this list.
|
||||
|
||||
// credentialWords are matched against whole '_'/'-'/'.'-separated segments of
|
||||
// a parameter name, so chat_key or tokenizer_mode cannot trip them. The bare
|
||||
// word "key" is intentionally absent (identifier names like whiteboard_id or
|
||||
// a hypothetical chat_key are not credentials); the api/key pairing is what
|
||||
// carries credential semantics and is detected as a pair below.
|
||||
var credentialWords = map[string]bool{
|
||||
"token": true,
|
||||
"secret": true,
|
||||
"password": true,
|
||||
"credential": true,
|
||||
"credentials": true,
|
||||
"cookie": true,
|
||||
"auth": true,
|
||||
"signature": true,
|
||||
"bearer": true,
|
||||
"apikey": true,
|
||||
}
|
||||
|
||||
// smellsLikeCredential reports whether a parameter name carries credential
|
||||
// semantics per the guard wordlist: any single segment in credentialWords,
|
||||
// or the adjacent segment pair api+key.
|
||||
func smellsLikeCredential(name string) bool {
|
||||
segments := strings.FieldsFunc(strings.ToLower(name), func(r rune) bool {
|
||||
return r == '_' || r == '-' || r == '.'
|
||||
})
|
||||
for i, seg := range segments {
|
||||
if credentialWords[seg] {
|
||||
return true
|
||||
}
|
||||
if seg == "api" && i+1 < len(segments) && segments[i+1] == "key" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// unredactedCredentialParams returns the names that smell like credentials
|
||||
// but are NOT matched by the production redaction pattern — every such name
|
||||
// would render its value verbatim in a dry-run decision.
|
||||
func unredactedCredentialParams(names []string) []string {
|
||||
var findings []string
|
||||
for _, name := range names {
|
||||
if smellsLikeCredential(name) && !sensitiveParamName.MatchString(name) {
|
||||
findings = append(findings, name)
|
||||
}
|
||||
}
|
||||
return findings
|
||||
}
|
||||
|
||||
// The detector itself must bite before the live scan means anything: known
|
||||
// credential-shaped names that the production pattern misses must be caught,
|
||||
// and ordinary identifier names must pass.
|
||||
func TestRedactionGuardDetector_SelfCheck(t *testing.T) {
|
||||
// Credential-shaped and covered by the production pattern: no finding.
|
||||
for _, name := range []string{"access_token", "client_secret", "user_password", "session_cookie", "sso_credential"} {
|
||||
if got := unredactedCredentialParams([]string{name}); len(got) != 0 {
|
||||
t.Errorf("%q is redacted by the production pattern, the guard must not flag it, got %v", name, got)
|
||||
}
|
||||
}
|
||||
// Credential-shaped but NOT covered by the production pattern today: the
|
||||
// guard must flag these, otherwise it can never catch a real gap.
|
||||
for _, name := range []string{"api_key", "auth_code", "request_signature", "bearer_value"} {
|
||||
if got := unredactedCredentialParams([]string{name}); len(got) != 1 {
|
||||
t.Errorf("%q smells like a credential and is not redacted; the guard must flag it, got %v", name, got)
|
||||
}
|
||||
}
|
||||
// Ordinary identifiers, including the wide-false-positive shapes the
|
||||
// wordlist is segment-matched to avoid: no finding.
|
||||
for _, name := range []string{"whiteboard_id", "chat_key", "tokenizer_mode", "author", "meeting_no"} {
|
||||
if got := unredactedCredentialParams([]string{name}); len(got) != 0 {
|
||||
t.Errorf("%q is an ordinary identifier, the guard must not flag it, got %v", name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every declared parameter of every compiled EventKey either carries no
|
||||
// credential semantics or is caught by the production redaction pattern.
|
||||
func TestRedactionGuard_CatalogParamsHaveNoUnredactedCredentials(t *testing.T) {
|
||||
snap, err := catalog.Compile(events.All(), catalog.StrategyRefs{
|
||||
catalog.StrategyNone,
|
||||
catalog.StrategyLegacyPreConsume,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("compile catalog: %v", err)
|
||||
}
|
||||
|
||||
var names []string
|
||||
for _, entry := range snap.Entries() {
|
||||
desc := entry.Descriptor()
|
||||
for _, p := range desc.Params {
|
||||
names = append(names, desc.Key+": "+p.Name)
|
||||
if findings := unredactedCredentialParams([]string{p.Name}); len(findings) != 0 {
|
||||
t.Errorf("EventKey %s declares param %q which smells like a credential but is not matched by the redaction pattern; rename the param or extend sensitiveParamName deliberately", desc.Key, p.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
// A scan that visited no parameters proves nothing.
|
||||
if len(names) == 0 {
|
||||
t.Fatal("the compiled catalog declares no parameters at all; the guard scanned nothing")
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// renderedDeclarationFields lists every JSON field the list/schema commands
|
||||
// are allowed to expose, each with the reason it belongs to the public
|
||||
// contract. Golden files pin today's bytes; this gate protects tomorrow: a
|
||||
// field added to the rendered structs (promoted through the embedded
|
||||
// definition or nested anywhere under it) must either appear here
|
||||
// deliberately or be tagged `json:"-"`. The set is flat — an entry admits its
|
||||
// rendered name at any nesting level, which is the same latitude
|
||||
// encoding/json gives a name.
|
||||
var renderedDeclarationFields = map[string]string{
|
||||
"key": "stable identifier agents subscribe by",
|
||||
"domain": "declared domain override; empty for every shipped key (filtering reads the derived descriptor value), so legacy output is byte-identical",
|
||||
"display_name": "human-readable name for pickers",
|
||||
"description": "what the event means (KeyDefinition) / what the parameter does (ParamDef)",
|
||||
"event_type": "upstream event type behind this key",
|
||||
"subscription_type": "which console ledger the precheck reads",
|
||||
"params": "declared consume parameters",
|
||||
"schema": "declared schema source (native/custom markers)",
|
||||
"scopes": "OAuth scopes required to consume",
|
||||
"auth_types": "identities the key accepts",
|
||||
"required_console_events": "console switches that must be enabled",
|
||||
"buffer_size": "delivery buffer size after normalization",
|
||||
"workers": "worker count after normalization",
|
||||
"single_consumer": "whether a second consumer is rejected",
|
||||
"resolved_output_schema": "fully resolved JSON schema of stdout events",
|
||||
"jq_root_path": "schema command only: jq root for consuming stdout",
|
||||
|
||||
// Nested under params (ParamDef): everything an agent needs to pass the
|
||||
// parameter correctly.
|
||||
"name": "parameter name as passed via --param",
|
||||
"type": "parameter value type (string/enum/multi/bool/int)",
|
||||
"required": "whether the parameter must be provided",
|
||||
"default": "value applied when the parameter is omitted",
|
||||
"values": "allowed values for enum/multi parameters",
|
||||
"subscription_key": "whether the parameter is part of the subscription identity",
|
||||
|
||||
// Nested under params.values (ParamValue).
|
||||
"value": "one allowed parameter value",
|
||||
"desc": "what choosing this value means",
|
||||
|
||||
// Nested under schema (SchemaDef / SchemaSpec): declaration markers only;
|
||||
// the resolved schema is the sibling resolved_output_schema.
|
||||
"native": "marker for keys delivering the raw V2 envelope",
|
||||
"custom": "marker for keys delivering processed output",
|
||||
"field_overrides": "per-field annotations overriding the reflected schema",
|
||||
"raw": "raw declared schema bytes; empty for reflected types",
|
||||
|
||||
// Nested under schema.field_overrides (schemas.FieldMeta). The type has
|
||||
// no json tags, so encoding/json renders the Go field names — pinned
|
||||
// as-is because retagging them would change the public bytes.
|
||||
"Description": "override for the field's schema description",
|
||||
"Enum": "override for the field's allowed values",
|
||||
"Kind": "override rendered as the field's schema format",
|
||||
}
|
||||
|
||||
// TestRenderContract_NoRuntimeFieldLeaksIntoJSON walks both rendered shapes,
|
||||
// following embedded struct promotion and recursing into every named type
|
||||
// reachable through the rendered fields, and fails on any exported member
|
||||
// that is neither allowlisted nor explicitly excluded from JSON.
|
||||
func TestRenderContract_NoRuntimeFieldLeaksIntoJSON(t *testing.T) {
|
||||
emitted := map[string]bool{}
|
||||
for _, typ := range []reflect.Type{
|
||||
reflect.TypeFor[listRow](),
|
||||
reflect.TypeFor[schemaPayload](),
|
||||
} {
|
||||
walkRenderedFields(t, typ, emitted, map[reflect.Type]bool{})
|
||||
}
|
||||
|
||||
if len(emitted) == 0 {
|
||||
t.Fatal("no rendered fields were visited; the gate scanned nothing")
|
||||
}
|
||||
// The embedded definition is where leaks would hide: prove promotion was
|
||||
// actually followed by requiring fields that only exist on it. The nested
|
||||
// sentinels prove each recursion path is really taken: subscription_key
|
||||
// (slice-of-struct: ParamDef), desc (slice inside a nested struct:
|
||||
// ParamValue), raw (pointer-to-struct: SchemaSpec), Enum (map value:
|
||||
// FieldMeta, rendered under its Go name because the type is untagged).
|
||||
for _, sentinel := range []string{
|
||||
"key", "event_type", "resolved_output_schema",
|
||||
"subscription_key", "desc", "raw", "Enum",
|
||||
} {
|
||||
if !emitted[sentinel] {
|
||||
t.Fatalf("field %q was not visited; the walker no longer reaches every rendered shape", sentinel)
|
||||
}
|
||||
}
|
||||
for name := range renderedDeclarationFields {
|
||||
if !emitted[name] {
|
||||
t.Errorf("allowlist entry %q is stale: no rendered struct emits it", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// walkRenderedFields records every JSON field name typ can render: embedded
|
||||
// structs promote into the parent object, and any struct reachable through a
|
||||
// field's type — behind pointers, slice/array elements, or map values — is
|
||||
// walked in turn, so a field added to a nested type like ParamDef cannot
|
||||
// escape the gate. visited breaks cycles; a type already recorded in this
|
||||
// walk contributes nothing new.
|
||||
func walkRenderedFields(t *testing.T, typ reflect.Type, emitted map[string]bool, visited map[reflect.Type]bool) {
|
||||
t.Helper()
|
||||
typ = nestedStructType(typ)
|
||||
if typ == nil || visited[typ] {
|
||||
return
|
||||
}
|
||||
visited[typ] = true
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
tag := field.Tag.Get("json")
|
||||
if tag == "-" {
|
||||
continue
|
||||
}
|
||||
if field.Anonymous && tag == "" {
|
||||
if ft := nestedStructType(field.Type); ft != nil {
|
||||
// Embedded struct without a tag: fields promote into the
|
||||
// parent JSON object.
|
||||
walkRenderedFields(t, ft, emitted, visited)
|
||||
continue
|
||||
}
|
||||
}
|
||||
name, _, _ := strings.Cut(tag, ",")
|
||||
if name == "" {
|
||||
// encoding/json renders an untagged exported field under its Go
|
||||
// name (schemas.FieldMeta does this today); the rendered name is
|
||||
// what the contract governs, so it is what must be declared.
|
||||
name = field.Name
|
||||
}
|
||||
if _, ok := renderedDeclarationFields[name]; !ok {
|
||||
t.Errorf("%s.%s renders JSON field %q that is not in the declared output contract; add it deliberately or exclude it with json:\"-\"", typ.Name(), field.Name, name)
|
||||
}
|
||||
emitted[name] = true
|
||||
walkRenderedFields(t, field.Type, emitted, visited)
|
||||
}
|
||||
}
|
||||
|
||||
// nestedStructType unwraps pointers, slice/array elements, and map values
|
||||
// until it reaches the struct that would render as a JSON object; nil means
|
||||
// the type renders as a leaf (scalar, string, raw bytes) and holds no fields
|
||||
// to govern.
|
||||
func nestedStructType(typ reflect.Type) reflect.Type {
|
||||
for {
|
||||
switch typ.Kind() {
|
||||
case reflect.Pointer, reflect.Slice, reflect.Array, reflect.Map:
|
||||
typ = typ.Elem()
|
||||
case reflect.Struct:
|
||||
return typ
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,13 @@ import (
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/client"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/identity"
|
||||
)
|
||||
|
||||
// consumeRuntime routes event.APIClient calls through the shared client.APIClient with a pinned identity.
|
||||
type consumeRuntime struct {
|
||||
client *client.APIClient
|
||||
accessIdentity core.Identity
|
||||
accessIdentity identity.Identity
|
||||
}
|
||||
|
||||
func (r *consumeRuntime) CallAPI(ctx context.Context, method, path string, body interface{}) (json.RawMessage, error) {
|
||||
|
||||
@@ -14,10 +14,12 @@ import (
|
||||
lark "github.com/larksuite/oapi-sdk-go/v3"
|
||||
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/client"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/credential"
|
||||
"github.com/larksuite/cli/internal/identity"
|
||||
)
|
||||
|
||||
// staticTokenResolver always returns a fixed token without any HTTP calls.
|
||||
@@ -45,9 +47,9 @@ func newTestConsumeRuntime(rt http.RoundTripper) *consumeRuntime {
|
||||
SDK: sdk,
|
||||
ErrOut: io.Discard,
|
||||
Credential: credential.NewCredentialProvider(nil, nil, &staticTokenResolver{}, nil),
|
||||
Config: &core.CliConfig{AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu},
|
||||
Config: &configpkg.CliConfig{AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu},
|
||||
},
|
||||
accessIdentity: core.AsBot,
|
||||
accessIdentity: identity.AsBot,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,13 +11,75 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
eventlib "github.com/larksuite/cli/internal/event"
|
||||
"github.com/larksuite/cli/internal/event/catalog"
|
||||
"github.com/larksuite/cli/internal/event/schemas"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
)
|
||||
|
||||
func NewCmdSchema(f *cmdutil.Factory, snap *catalog.Snapshot) *cobra.Command {
|
||||
// resolveSchemaJSON returns the final JSON Schema for an EventKey (reflected base, V2-wrapped for Native, overlay applied); orphans lists unresolved FieldOverrides pointers.
|
||||
func resolveSchemaJSON(def *eventlib.KeyDefinition) (json.RawMessage, []string, error) {
|
||||
spec, isNative := pickSpec(def.Schema)
|
||||
if spec == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
base, err := renderSpec(spec)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if base == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
if isNative {
|
||||
base = schemas.WrapV2Envelope(base)
|
||||
}
|
||||
|
||||
if len(def.Schema.FieldOverrides) > 0 {
|
||||
var parsed map[string]interface{}
|
||||
if err := json.Unmarshal(base, &parsed); err != nil {
|
||||
return nil, nil, errs.NewInternalError(errs.SubtypeUnknown,
|
||||
"parse base schema for field overrides: %s", err).WithCause(err)
|
||||
}
|
||||
orphans := schemas.ApplyFieldOverrides(parsed, def.Schema.FieldOverrides)
|
||||
out, err := json.Marshal(parsed)
|
||||
if err != nil {
|
||||
return nil, nil, errs.NewInternalError(errs.SubtypeUnknown,
|
||||
"serialize schema with field overrides: %s", err).WithCause(err)
|
||||
}
|
||||
return out, orphans, nil
|
||||
}
|
||||
|
||||
return base, nil, nil
|
||||
}
|
||||
|
||||
// pickSpec returns the non-nil spec and whether it is Native (requires V2 envelope wrap).
|
||||
func pickSpec(s eventlib.SchemaDef) (*eventlib.SchemaSpec, bool) {
|
||||
if s.Native != nil {
|
||||
return s.Native, true
|
||||
}
|
||||
if s.Custom != nil {
|
||||
return s.Custom, false
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// renderSpec produces a JSON Schema from Type (reflected) or Raw (copied).
|
||||
func renderSpec(s *eventlib.SchemaSpec) (json.RawMessage, error) {
|
||||
if s.Type != nil {
|
||||
return schemas.FromType(s.Type), nil
|
||||
}
|
||||
if len(s.Raw) > 0 {
|
||||
buf := make(json.RawMessage, len(s.Raw))
|
||||
copy(buf, s.Raw)
|
||||
return buf, nil
|
||||
}
|
||||
return nil, errs.NewInternalError(errs.SubtypeUnknown, "schemaSpec has neither Type nor Raw")
|
||||
}
|
||||
|
||||
func NewCmdSchema(f *cmdutil.Factory) *cobra.Command {
|
||||
var asJSON bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "schema <EventKey>",
|
||||
@@ -25,7 +87,7 @@ func NewCmdSchema(f *cmdutil.Factory, snap *catalog.Snapshot) *cobra.Command {
|
||||
Long: "Display detailed information about an EventKey including type, events, parameters, and response schema. Use --json for machine-readable output.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runSchema(f, snap, args[0], asJSON)
|
||||
return runSchema(f, args[0], asJSON)
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&asJSON, "json", false, "Emit the EventKey definition + resolved schema as JSON (for AI / scripts)")
|
||||
@@ -33,15 +95,14 @@ func NewCmdSchema(f *cmdutil.Factory, snap *catalog.Snapshot) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runSchema(f *cmdutil.Factory, snap *catalog.Snapshot, key string, asJSON bool) error {
|
||||
entry, ok := snap.Resolve(key)
|
||||
func runSchema(f *cmdutil.Factory, key string, asJSON bool) error {
|
||||
def, ok := eventlib.Lookup(key)
|
||||
if !ok {
|
||||
return unknownEventKeyErr(snap, key)
|
||||
return unknownEventKeyErr(key)
|
||||
}
|
||||
def := entry.Definition()
|
||||
|
||||
if asJSON {
|
||||
return writeSchemaJSON(f, entry)
|
||||
return writeSchemaJSON(f, def)
|
||||
}
|
||||
|
||||
out := f.IOStreams.Out
|
||||
@@ -109,7 +170,10 @@ func runSchema(f *cmdutil.Factory, snap *catalog.Snapshot, key string, asJSON bo
|
||||
}
|
||||
}
|
||||
|
||||
resolved := entry.Output().SchemaJSON
|
||||
resolved, _, err := resolveSchemaJSON(def)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resolved != nil {
|
||||
fmt.Fprintf(out, "\nOutput Schema:\n")
|
||||
printIndentedJSON(out, resolved)
|
||||
@@ -138,22 +202,30 @@ func printIndentedJSON(out io.Writer, raw json.RawMessage) {
|
||||
fmt.Fprintf(out, " %s\n", string(formatted))
|
||||
}
|
||||
|
||||
// schemaPayload is the JSON shape of `event schema --json`. It is a named
|
||||
// type (not a function-local literal) so the render contract test can walk
|
||||
// its fields and reject accidental additions to the public output.
|
||||
type schemaPayload struct {
|
||||
*eventlib.KeyDefinition
|
||||
ResolvedSchema json.RawMessage `json:"resolved_output_schema,omitempty"`
|
||||
JQRootPath string `json:"jq_root_path,omitempty"`
|
||||
}
|
||||
|
||||
// writeSchemaJSON emits the EventKey definition plus resolved schema; jq_root_path tells callers whether fields live at `.` or `.event`.
|
||||
func writeSchemaJSON(f *cmdutil.Factory, entry *catalog.Entry) error {
|
||||
contract := entry.Output()
|
||||
output.PrintJson(f.IOStreams.Out, schemaPayload{
|
||||
KeyDefinition: entry.Definition(),
|
||||
ResolvedSchema: contract.SchemaJSON,
|
||||
JQRootPath: contract.JQRootPath,
|
||||
func writeSchemaJSON(f *cmdutil.Factory, def *eventlib.KeyDefinition) error {
|
||||
type payload struct {
|
||||
*eventlib.KeyDefinition
|
||||
ResolvedSchema json.RawMessage `json:"resolved_output_schema,omitempty"`
|
||||
JQRootPath string `json:"jq_root_path,omitempty"`
|
||||
}
|
||||
resolved, _, err := resolveSchemaJSON(def)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var jqRootPath string
|
||||
if resolved != nil {
|
||||
// Native → V2 envelope ⇒ `.event.xxx`; Custom → flat ⇒ `.`.
|
||||
_, isNative := pickSpec(def.Schema)
|
||||
jqRootPath = "."
|
||||
if isNative {
|
||||
jqRootPath = ".event"
|
||||
}
|
||||
}
|
||||
output.PrintJson(f.IOStreams.Out, payload{
|
||||
KeyDefinition: def,
|
||||
ResolvedSchema: resolved,
|
||||
JQRootPath: jqRootPath,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -10,26 +10,14 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
eventlib "github.com/larksuite/cli/internal/event"
|
||||
"github.com/larksuite/cli/internal/event/catalog"
|
||||
"github.com/larksuite/cli/internal/event/schemas"
|
||||
)
|
||||
|
||||
// compileTestSnapshot compiles synthetic declarations into a snapshot using
|
||||
// the same strategy set the production wiring provides.
|
||||
func compileTestSnapshot(t *testing.T, defs ...eventlib.KeyDefinition) *catalog.Snapshot {
|
||||
t.Helper()
|
||||
snap, err := catalog.Compile(defs, catalog.StrategyRefs{
|
||||
catalog.StrategyNone,
|
||||
catalog.StrategyLegacyPreConsume,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("compile test catalog: %v", err)
|
||||
}
|
||||
return snap
|
||||
}
|
||||
_ "github.com/larksuite/cli/events"
|
||||
)
|
||||
|
||||
type approvalSchemaJSONPayload struct {
|
||||
JQRootPath string `json:"jq_root_path"`
|
||||
@@ -55,9 +43,9 @@ type approvalSchemaJSONProperty struct {
|
||||
}
|
||||
|
||||
func TestRunSchema_ProcessedKey_Text(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
|
||||
|
||||
if err := runSchema(f, compileCatalog(), "im.message.receive_v1", false); err != nil {
|
||||
if err := runSchema(f, "im.message.receive_v1", false); err != nil {
|
||||
t.Fatalf("runSchema: %v", err)
|
||||
}
|
||||
|
||||
@@ -75,9 +63,9 @@ func TestRunSchema_ProcessedKey_Text(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunSchema_NativeKey_WrapsEnvelope(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
|
||||
|
||||
if err := runSchema(f, compileCatalog(), "im.message.message_read_v1", false); err != nil {
|
||||
if err := runSchema(f, "im.message.message_read_v1", false); err != nil {
|
||||
t.Fatalf("runSchema: %v", err)
|
||||
}
|
||||
|
||||
@@ -95,9 +83,9 @@ func TestRunSchema_NativeKey_WrapsEnvelope(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunSchema_UnknownKey_SuggestsAlternatives(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
|
||||
|
||||
err := runSchema(f, compileCatalog(), "im.message.recieve_v1", false)
|
||||
err := runSchema(f, "im.message.recieve_v1", false)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unknown key")
|
||||
}
|
||||
@@ -111,9 +99,9 @@ func TestRunSchema_UnknownKey_SuggestsAlternatives(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunSchema_JSONOutput(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
|
||||
|
||||
if err := runSchema(f, compileCatalog(), "im.message.receive_v1", true); err != nil {
|
||||
if err := runSchema(f, "im.message.receive_v1", true); err != nil {
|
||||
t.Fatalf("runSchema json: %v", err)
|
||||
}
|
||||
|
||||
@@ -132,9 +120,9 @@ func TestRunSchema_JSONOutput(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunSchema_ReceiveMessageAgentFieldsJSON(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
|
||||
|
||||
if err := runSchema(f, compileCatalog(), "im.message.receive_v1", true); err != nil {
|
||||
if err := runSchema(f, "im.message.receive_v1", true); err != nil {
|
||||
t.Fatalf("runSchema json: %v", err)
|
||||
}
|
||||
|
||||
@@ -166,9 +154,9 @@ func TestRunSchema_ReceiveMessageAgentFieldsJSON(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunSchema_TaskUpdateUserAccessJSON(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
|
||||
|
||||
if err := runSchema(f, compileCatalog(), "task.task.update_user_access_v2", true); err != nil {
|
||||
if err := runSchema(f, "task.task.update_user_access_v2", true); err != nil {
|
||||
t.Fatalf("runSchema json: %v", err)
|
||||
}
|
||||
|
||||
@@ -205,9 +193,9 @@ func TestRunSchema_ApprovalStatusChangedJSON(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.key, func(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
|
||||
|
||||
if err := runSchema(f, compileCatalog(), tc.key, true); err != nil {
|
||||
if err := runSchema(f, tc.key, true); err != nil {
|
||||
t.Fatalf("runSchema json: %v", err)
|
||||
}
|
||||
|
||||
@@ -253,9 +241,9 @@ func TestRunSchema_JSONOutput_VCMeetingLifecycleKeys(t *testing.T) {
|
||||
"vc.meeting.participant_meeting_joined_v1",
|
||||
} {
|
||||
t.Run(key, func(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
|
||||
|
||||
if err := runSchema(f, compileCatalog(), key, true); err != nil {
|
||||
if err := runSchema(f, key, true); err != nil {
|
||||
t.Fatalf("runSchema json: %v", err)
|
||||
}
|
||||
|
||||
@@ -288,8 +276,9 @@ func TestRunSchema_JSONOutput_VCMeetingLifecycleKeys(t *testing.T) {
|
||||
|
||||
func TestSchema_RendersSubscriptionKeyMarker(t *testing.T) {
|
||||
const syntheticKey = "test.evt_sub"
|
||||
t.Cleanup(func() { eventlib.UnregisterKeyForTest(syntheticKey) })
|
||||
|
||||
snap := compileTestSnapshot(t, eventlib.KeyDefinition{
|
||||
eventlib.RegisterKey(eventlib.KeyDefinition{
|
||||
Key: syntheticKey,
|
||||
EventType: syntheticKey,
|
||||
Params: []eventlib.ParamDef{
|
||||
@@ -299,8 +288,8 @@ func TestSchema_RendersSubscriptionKeyMarker(t *testing.T) {
|
||||
Schema: eventlib.SchemaDef{Native: &eventlib.SchemaSpec{Type: reflect.TypeOf(struct{ X string }{})}},
|
||||
})
|
||||
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
if err := runSchema(f, snap, syntheticKey, false); err != nil {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
|
||||
if err := runSchema(f, syntheticKey, false); err != nil {
|
||||
t.Fatalf("runSchema: %v", err)
|
||||
}
|
||||
|
||||
@@ -336,16 +325,17 @@ func TestSchema_RendersSubscriptionKeyMarker(t *testing.T) {
|
||||
|
||||
func TestSchema_JSON_IncludesSubscriptionKey(t *testing.T) {
|
||||
const syntheticKey = "test.evt_json"
|
||||
t.Cleanup(func() { eventlib.UnregisterKeyForTest(syntheticKey) })
|
||||
|
||||
snap := compileTestSnapshot(t, eventlib.KeyDefinition{
|
||||
eventlib.RegisterKey(eventlib.KeyDefinition{
|
||||
Key: syntheticKey,
|
||||
EventType: syntheticKey,
|
||||
Params: []eventlib.ParamDef{{Name: "mailbox", SubscriptionKey: true}},
|
||||
Schema: eventlib.SchemaDef{Native: &eventlib.SchemaSpec{Type: reflect.TypeOf(struct{ X string }{})}},
|
||||
})
|
||||
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
if err := runSchema(f, snap, syntheticKey, true); err != nil {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
|
||||
if err := runSchema(f, syntheticKey, true); err != nil {
|
||||
t.Fatalf("runSchema json: %v", err)
|
||||
}
|
||||
|
||||
@@ -359,13 +349,12 @@ func TestSchema_JSON_IncludesSubscriptionKey(t *testing.T) {
|
||||
|
||||
func TestResolveSchemaJSON_CustomWithOverlay(t *testing.T) {
|
||||
const syntheticKey = "t.custom.overlay"
|
||||
t.Cleanup(func() { eventlib.UnregisterKeyForTest(syntheticKey) })
|
||||
|
||||
type out struct {
|
||||
SenderID string `json:"sender_id"`
|
||||
}
|
||||
// A compile that succeeds proves the overlay left no orphan pointers; the
|
||||
// entry's output contract carries the resolved schema.
|
||||
snap := compileTestSnapshot(t, eventlib.KeyDefinition{
|
||||
eventlib.RegisterKey(eventlib.KeyDefinition{
|
||||
Key: syntheticKey,
|
||||
EventType: syntheticKey,
|
||||
Schema: eventlib.SchemaDef{
|
||||
@@ -378,12 +367,13 @@ func TestResolveSchemaJSON_CustomWithOverlay(t *testing.T) {
|
||||
return nil, nil
|
||||
},
|
||||
})
|
||||
entry, ok := snap.Resolve(syntheticKey)
|
||||
if !ok {
|
||||
t.Fatalf("snap.Resolve(%q) should succeed", syntheticKey)
|
||||
def, _ := eventlib.Lookup(syntheticKey)
|
||||
resolved, orphans, err := resolveSchemaJSON(def)
|
||||
if err != nil || len(orphans) != 0 {
|
||||
t.Fatalf("resolve: err=%v orphans=%v", err, orphans)
|
||||
}
|
||||
var parsed map[string]interface{}
|
||||
if err := json.Unmarshal(entry.Output().SchemaJSON, &parsed); err != nil {
|
||||
if err := json.Unmarshal(resolved, &parsed); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := parsed["properties"].(map[string]interface{})["sender_id"].(map[string]interface{})["format"]
|
||||
@@ -392,35 +382,37 @@ func TestResolveSchemaJSON_CustomWithOverlay(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompile_EmptySpecIsRejected(t *testing.T) {
|
||||
_, err := catalog.Compile([]eventlib.KeyDefinition{{
|
||||
Key: "synthetic.empty.spec",
|
||||
EventType: "synthetic.empty.spec",
|
||||
Schema: eventlib.SchemaDef{Native: &eventlib.SchemaSpec{}},
|
||||
}}, catalog.StrategyRefs{catalog.StrategyNone})
|
||||
func TestRenderSpec_EmptySpecIsTypedInternalError(t *testing.T) {
|
||||
_, err := renderSpec(&eventlib.SchemaSpec{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for spec with neither Type nor Raw")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "exactly one of Type or Raw") {
|
||||
t.Errorf("error should reject the empty spec, got: %v", err)
|
||||
p, ok := errs.ProblemOf(err)
|
||||
if !ok {
|
||||
t.Fatalf("expected typed errs error, got %T: %v", err, err)
|
||||
}
|
||||
if p.Category != errs.CategoryInternal {
|
||||
t.Errorf("category = %s, want %s", p.Category, errs.CategoryInternal)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompile_InvalidBaseWithOverridesIsRejected(t *testing.T) {
|
||||
_, err := catalog.Compile([]eventlib.KeyDefinition{{
|
||||
Key: "synthetic.invalid.base",
|
||||
EventType: "synthetic.invalid.base",
|
||||
func TestResolveSchemaJSON_InvalidBaseWithOverridesIsTypedInternalError(t *testing.T) {
|
||||
def := &eventlib.KeyDefinition{
|
||||
Key: "synthetic.invalid.base",
|
||||
Schema: eventlib.SchemaDef{
|
||||
Custom: &eventlib.SchemaSpec{Raw: json.RawMessage("{not json")},
|
||||
FieldOverrides: map[string]schemas.FieldMeta{"x": {}},
|
||||
},
|
||||
}}, catalog.StrategyRefs{catalog.StrategyNone})
|
||||
}
|
||||
_, _, err := resolveSchemaJSON(def)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unparsable base schema")
|
||||
}
|
||||
// Garbage raw bytes are rejected by the spec check itself, before the
|
||||
// overlay machinery would even try to parse them.
|
||||
if !strings.Contains(err.Error(), "is not a JSON object") {
|
||||
t.Errorf("error should reject the unparsable base schema, got: %v", err)
|
||||
p, ok := errs.ProblemOf(err)
|
||||
if !ok {
|
||||
t.Fatalf("expected typed errs error, got %T: %v", err, err)
|
||||
}
|
||||
if p.Category != errs.CategoryInternal {
|
||||
t.Errorf("category = %s, want %s", p.Category, errs.CategoryInternal)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
eventlib "github.com/larksuite/cli/internal/event"
|
||||
appconsume "github.com/larksuite/cli/internal/event/application/consume"
|
||||
"github.com/larksuite/cli/internal/event/catalog"
|
||||
)
|
||||
|
||||
// consumeStrategies is the executable strategy set for this binary. The same
|
||||
// registry is handed to catalog compilation, so a reference the compiler
|
||||
// accepted is guaranteed to resolve here.
|
||||
var consumeStrategies = appconsume.DefaultRegistry()
|
||||
|
||||
type identityResolverFunc func(ctx context.Context, entry *catalog.Entry) (string, error)
|
||||
|
||||
func (f identityResolverFunc) Resolve(ctx context.Context, entry *catalog.Entry) (string, error) {
|
||||
return f(ctx, entry)
|
||||
}
|
||||
|
||||
type preflightReaderFunc func(ctx context.Context, entry *catalog.Entry, identity string) ([]appconsume.Precondition, error)
|
||||
|
||||
func (f preflightReaderFunc) Read(ctx context.Context, entry *catalog.Entry, identity string) ([]appconsume.Precondition, error) {
|
||||
return f(ctx, entry, identity)
|
||||
}
|
||||
|
||||
type streamRunnerFunc func(ctx context.Context, prepare appconsume.PrepareFunc) error
|
||||
|
||||
func (f streamRunnerFunc) Run(ctx context.Context, prepare appconsume.PrepareFunc) error {
|
||||
return f(ctx, prepare)
|
||||
}
|
||||
|
||||
// readPreconditions classifies the existing read-only preflight checks into
|
||||
// named preconditions. Weak dependencies that could not answer stay visible
|
||||
// as "unknown" instead of silently passing; a failed check carries the exact
|
||||
// error a real run returns, so refusal is identical on both paths.
|
||||
func readPreconditions(ctx context.Context, pf *preflightCtx, appVerErr, tokenErr error) []appconsume.Precondition {
|
||||
credentials := appconsume.Precondition{Name: "credentials_available", Status: appconsume.PreconditionOK}
|
||||
if tokenErr != nil {
|
||||
credentials.Status = appconsume.PreconditionBlocked
|
||||
credentials.Detail = tokenErr.Error()
|
||||
credentials.BlockErr = tokenErr
|
||||
}
|
||||
|
||||
console := appconsume.Precondition{Name: "console_event_published", Status: appconsume.PreconditionOK}
|
||||
switch {
|
||||
case len(pf.keyDef.RequiredConsoleEvents) == 0:
|
||||
// nothing to verify
|
||||
case pf.keyDef.SubscriptionType == eventlib.SubTypeCallback && pf.subscribedCallbacks == nil,
|
||||
pf.keyDef.SubscriptionType != eventlib.SubTypeCallback && pf.appVer == nil:
|
||||
console.Status = appconsume.PreconditionUnknown
|
||||
if appVerErr != nil {
|
||||
console.Detail = describeAppMetaErr(appVerErr)
|
||||
} else {
|
||||
console.Detail = "console ledger unavailable"
|
||||
}
|
||||
default:
|
||||
if err := preflightEventTypes(pf); err != nil {
|
||||
console.Status = appconsume.PreconditionBlocked
|
||||
console.Detail = err.Error()
|
||||
console.BlockErr = err
|
||||
}
|
||||
}
|
||||
|
||||
scopes := appconsume.Precondition{Name: "scopes_granted", Status: appconsume.PreconditionOK}
|
||||
checked, err := preflightScopes(ctx, pf)
|
||||
switch {
|
||||
case err != nil:
|
||||
scopes.Status = appconsume.PreconditionBlocked
|
||||
scopes.Detail = err.Error()
|
||||
scopes.BlockErr = err
|
||||
case !checked:
|
||||
// The scope ledger could not be read (no published version for bots,
|
||||
// no resolvable token for users). Saying "ok" here would dress up
|
||||
// "nobody looked" as "it was verified".
|
||||
scopes.Status = appconsume.PreconditionUnknown
|
||||
scopes.Detail = "granted scopes could not be read for this identity"
|
||||
}
|
||||
|
||||
return []appconsume.Precondition{credentials, console, scopes}
|
||||
}
|
||||
@@ -14,10 +14,10 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/event/adapter/localbus/busctl"
|
||||
"github.com/larksuite/cli/internal/event/adapter/localbus/busdiscover"
|
||||
"github.com/larksuite/cli/internal/event/adapter/localbus/protocol"
|
||||
"github.com/larksuite/cli/internal/event/adapter/localbus/transport"
|
||||
"github.com/larksuite/cli/internal/event/busctl"
|
||||
"github.com/larksuite/cli/internal/event/busdiscover"
|
||||
"github.com/larksuite/cli/internal/event/protocol"
|
||||
"github.com/larksuite/cli/internal/event/transport"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
)
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/larksuite/cli/internal/event/adapter/localbus/busdiscover"
|
||||
"github.com/larksuite/cli/internal/event/adapter/localbus/protocol"
|
||||
"github.com/larksuite/cli/internal/event/busdiscover"
|
||||
"github.com/larksuite/cli/internal/event/protocol"
|
||||
)
|
||||
|
||||
type fakeScanner struct {
|
||||
|
||||
@@ -13,9 +13,9 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/event/adapter/localbus/busctl"
|
||||
"github.com/larksuite/cli/internal/event/adapter/localbus/busdiscover"
|
||||
"github.com/larksuite/cli/internal/event/adapter/localbus/transport"
|
||||
"github.com/larksuite/cli/internal/event/busctl"
|
||||
"github.com/larksuite/cli/internal/event/busdiscover"
|
||||
"github.com/larksuite/cli/internal/event/transport"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/event/adapter/localbus/busdiscover"
|
||||
"github.com/larksuite/cli/internal/event/busdiscover"
|
||||
)
|
||||
|
||||
func TestDiscoverAppIDs_OnlyLiveLockHolders(t *testing.T) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/larksuite/cli/internal/event/adapter/localbus/protocol"
|
||||
"github.com/larksuite/cli/internal/event/protocol"
|
||||
)
|
||||
|
||||
type mockTransport struct {
|
||||
|
||||
@@ -9,14 +9,14 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/event/catalog"
|
||||
eventlib "github.com/larksuite/cli/internal/event"
|
||||
"github.com/larksuite/cli/internal/suggest"
|
||||
)
|
||||
|
||||
const maxSuggestions = 3
|
||||
|
||||
// suggestEventKeys returns up to maxSuggestions keys resembling input (substring match beats edit distance).
|
||||
func suggestEventKeys(snap *catalog.Snapshot, input string) []string {
|
||||
func suggestEventKeys(input string) []string {
|
||||
type match struct {
|
||||
key string
|
||||
dist int
|
||||
@@ -24,13 +24,13 @@ func suggestEventKeys(snap *catalog.Snapshot, input string) []string {
|
||||
var hits []match
|
||||
threshold := max(2, len(input)/5)
|
||||
|
||||
for _, key := range snap.Keys() {
|
||||
if strings.Contains(key, input) {
|
||||
hits = append(hits, match{key, 0})
|
||||
for _, def := range eventlib.ListAll() {
|
||||
if strings.Contains(def.Key, input) {
|
||||
hits = append(hits, match{def.Key, 0})
|
||||
continue
|
||||
}
|
||||
if d := suggest.Levenshtein(input, key); d <= threshold {
|
||||
hits = append(hits, match{key, d})
|
||||
if d := suggest.Levenshtein(input, def.Key); d <= threshold {
|
||||
hits = append(hits, match{def.Key, d})
|
||||
}
|
||||
}
|
||||
sort.Slice(hits, func(i, j int) bool { return hits[i].dist < hits[j].dist })
|
||||
@@ -59,9 +59,9 @@ func formatSuggestions(keys []string) string {
|
||||
}
|
||||
|
||||
// unknownEventKeyErr builds the shared "unknown EventKey" error with a suggestion tail when available.
|
||||
func unknownEventKeyErr(snap *catalog.Snapshot, key string) error {
|
||||
func unknownEventKeyErr(key string) error {
|
||||
msg := fmt.Sprintf("unknown EventKey: %s", key)
|
||||
if guesses := suggestEventKeys(snap, key); len(guesses) > 0 {
|
||||
if guesses := suggestEventKeys(key); len(guesses) > 0 {
|
||||
msg += " — did you mean " + formatSuggestions(guesses) + "?"
|
||||
}
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", msg).
|
||||
|
||||
@@ -6,10 +6,11 @@ package event
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
_ "github.com/larksuite/cli/events"
|
||||
)
|
||||
|
||||
func TestSuggestEventKeys(t *testing.T) {
|
||||
snap := compileCatalog()
|
||||
cases := []struct {
|
||||
name string
|
||||
input string
|
||||
@@ -40,7 +41,7 @@ func TestSuggestEventKeys(t *testing.T) {
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := suggestEventKeys(snap, tc.input)
|
||||
got := suggestEventKeys(tc.input)
|
||||
if tc.wantEmpty {
|
||||
if len(got) != 0 {
|
||||
t.Errorf("expected empty slice, got %v", got)
|
||||
@@ -97,7 +98,7 @@ func TestFormatSuggestions(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUnknownEventKeyErr_IncludesSuggestion(t *testing.T) {
|
||||
err := unknownEventKeyErr(compileCatalog(), "im.message.recieve_v1")
|
||||
err := unknownEventKeyErr("im.message.recieve_v1")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
@@ -114,7 +115,7 @@ func TestUnknownEventKeyErr_IncludesSuggestion(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUnknownEventKeyErr_NoSuggestion(t *testing.T) {
|
||||
err := unknownEventKeyErr(compileCatalog(), "xyzzy_no_such_event_key_at_all")
|
||||
err := unknownEventKeyErr("xyzzy_no_such_event_key_at_all")
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
|
||||
3023
cmd/event/testdata/golden/list_json.golden
vendored
3023
cmd/event/testdata/golden/list_json.golden
vendored
File diff suppressed because it is too large
Load Diff
42
cmd/event/testdata/golden/list_text.golden
vendored
42
cmd/event/testdata/golden/list_text.golden
vendored
@@ -1,42 +0,0 @@
|
||||
KEY AUTH PARAMS DESCRIPTION
|
||||
|
||||
── application ──
|
||||
application.bot.menu_v6 bot 0 Triggered when a user clicks a custom bot menu item whose action is configured as a push event.
|
||||
|
||||
── approval ──
|
||||
approval.instance.status_changed_v4 user 1 Triggered after an approval instance status becomes visible to the requester or approval participants
|
||||
approval.task.status_changed_v4 user 1 Triggered after an approval task status becomes visible to the requester or task approver
|
||||
|
||||
── board ──
|
||||
board.whiteboard.updated_v1 user|bot 1 Pushed when the whiteboard content is updated.
|
||||
|
||||
── card ──
|
||||
card.action.trigger bot 0 Triggered when a user interacts with an interactive card (button click, form submit, dropdown select, etc.). Output includes: token (valid 30 min, max 2 updates), action details (tag, value, name, form_value), and card_content (original card in userDSL text format, auto-fetched at consume time). To update the card: parse card_content to understand the current state, construct the new card JSON, then call `lark-cli api POST /open-apis/interactive/v1/card/update` with the token (see lark-im-card-action-reply.md).
|
||||
|
||||
── im ──
|
||||
im.chat.disbanded_v1 bot 0 Triggered after a chat is disbanded
|
||||
im.chat.member.bot.added_v1 bot 0 Triggered when the bot is added to a chat
|
||||
im.chat.member.bot.deleted_v1 bot 0 Triggered after the bot is removed from a chat
|
||||
im.chat.member.user.added_v1 bot 0 Triggered when a new user joins a chat (including topic chats)
|
||||
im.chat.member.user.deleted_v1 bot 0 Triggered when a user leaves or is removed from a chat
|
||||
im.chat.member.user.withdrawn_v1 bot 0 Triggered after a pending user invite is withdrawn
|
||||
im.chat.updated_v1 bot 0 Triggered after chat settings (owner, avatar, name, permissions, etc.) are updated
|
||||
im.message.message_read_v1 bot 0 Triggered after a user reads a P2P message sent by the bot
|
||||
im.message.reaction.created_v1 bot 0 Triggered when a reaction is added to a message
|
||||
im.message.reaction.deleted_v1 bot 0 Triggered when a reaction is removed from a message
|
||||
im.message.receive_v1 bot 0 Receive IM messages
|
||||
|
||||
── minutes ──
|
||||
minutes.minute.generated_v1 user 0 Triggered when a minute has been generated
|
||||
|
||||
── task ──
|
||||
task.task.update_user_access_v2 user|bot 0 Triggered when tasks visible to the current user or app are created, deleted, or updated
|
||||
|
||||
── vc ──
|
||||
vc.meeting.participant_meeting_ended_v1 user 0 Triggered when a meeting the current user participates in has ended
|
||||
vc.meeting.participant_meeting_joined_v1 user 0 Triggered when the current user joins a meeting
|
||||
vc.meeting.participant_meeting_started_v1 user 0 Triggered when a meeting the current user participates in has started
|
||||
vc.note.generated_v1 user 0 Triggered when a note has been generated
|
||||
vc.recording.recording_ended_v1 user 0 Triggered when a recording_bean recording ends and uploads successfully; only generated when connected to Feishu software.
|
||||
vc.recording.recording_started_v1 user 0 Triggered when a recording_bean recording starts; only generated when connected to Feishu software.
|
||||
vc.recording.recording_transcript_generated_v1 user 0 Triggered when recording_bean transcript items are generated; only generated when connected to Feishu software.
|
||||
@@ -1,127 +0,0 @@
|
||||
{
|
||||
"key": "board.whiteboard.updated_v1",
|
||||
"display_name": "Whiteboard updated",
|
||||
"description": "Pushed when the whiteboard content is updated.",
|
||||
"event_type": "board.whiteboard.updated_v1",
|
||||
"subscription_type": "event",
|
||||
"params": [
|
||||
{
|
||||
"name": "whiteboard_id",
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Whiteboard id to subscribe; subscription is per-whiteboard.",
|
||||
"subscription_key": true
|
||||
}
|
||||
],
|
||||
"schema": {
|
||||
"native": {},
|
||||
"field_overrides": {
|
||||
"/event/operator_ids/*/open_id": {
|
||||
"Description": "",
|
||||
"Enum": null,
|
||||
"Kind": "open_id"
|
||||
},
|
||||
"/event/operator_ids/*/union_id": {
|
||||
"Description": "",
|
||||
"Enum": null,
|
||||
"Kind": "union_id"
|
||||
},
|
||||
"/event/operator_ids/*/user_id": {
|
||||
"Description": "",
|
||||
"Enum": null,
|
||||
"Kind": "user_id"
|
||||
},
|
||||
"/event/whiteboard_id": {
|
||||
"Description": "whiteboard id to subscribe",
|
||||
"Enum": null,
|
||||
"Kind": "whiteboard_id"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scopes": [
|
||||
"board:whiteboard:node:read"
|
||||
],
|
||||
"auth_types": [
|
||||
"user",
|
||||
"bot"
|
||||
],
|
||||
"required_console_events": [
|
||||
"board.whiteboard.updated_v1"
|
||||
],
|
||||
"buffer_size": 100,
|
||||
"workers": 1,
|
||||
"resolved_output_schema": {
|
||||
"description": "飞书事件",
|
||||
"properties": {
|
||||
"event": {
|
||||
"properties": {
|
||||
"operator_ids": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"open_id": {
|
||||
"format": "open_id",
|
||||
"type": "string"
|
||||
},
|
||||
"union_id": {
|
||||
"format": "union_id",
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"format": "user_id",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"whiteboard_id": {
|
||||
"description": "whiteboard id to subscribe",
|
||||
"format": "whiteboard_id",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"header": {
|
||||
"description": "事件头,所有事件结构一致",
|
||||
"properties": {
|
||||
"app_id": {
|
||||
"description": "接收事件的应用 ID",
|
||||
"type": "string"
|
||||
},
|
||||
"create_time": {
|
||||
"description": "事件创建时间,毫秒时间戳字符串",
|
||||
"type": "string"
|
||||
},
|
||||
"event_id": {
|
||||
"description": "事件唯一 ID",
|
||||
"type": "string"
|
||||
},
|
||||
"event_type": {
|
||||
"description": "事件类型,用于路由",
|
||||
"type": "string"
|
||||
},
|
||||
"tenant_key": {
|
||||
"description": "租户唯一标识",
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"description": "回调校验 token",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"schema": {
|
||||
"description": "飞书事件协议版本",
|
||||
"enum": [
|
||||
"2.0"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"jq_root_path": ".event"
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
Key: board.whiteboard.updated_v1
|
||||
Description: Pushed when the whiteboard content is updated.
|
||||
Event: board.whiteboard.updated_v1
|
||||
Pre-consume: yes
|
||||
|
||||
Required Scopes:
|
||||
- board:whiteboard:node:read
|
||||
|
||||
Required Console Events (must be enabled in developer console):
|
||||
- board.whiteboard.updated_v1
|
||||
|
||||
Parameters:
|
||||
NAME TYPE REQUIRED SUB-KEY DEFAULT DESCRIPTION
|
||||
whiteboard_id string yes yes - Whiteboard id to subscribe; subscription is per-whiteboard.
|
||||
|
||||
Output Schema:
|
||||
{
|
||||
"description": "飞书事件",
|
||||
"properties": {
|
||||
"event": {
|
||||
"properties": {
|
||||
"operator_ids": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"open_id": {
|
||||
"format": "open_id",
|
||||
"type": "string"
|
||||
},
|
||||
"union_id": {
|
||||
"format": "union_id",
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"format": "user_id",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"whiteboard_id": {
|
||||
"description": "whiteboard id to subscribe",
|
||||
"format": "whiteboard_id",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"header": {
|
||||
"description": "事件头,所有事件结构一致",
|
||||
"properties": {
|
||||
"app_id": {
|
||||
"description": "接收事件的应用 ID",
|
||||
"type": "string"
|
||||
},
|
||||
"create_time": {
|
||||
"description": "事件创建时间,毫秒时间戳字符串",
|
||||
"type": "string"
|
||||
},
|
||||
"event_id": {
|
||||
"description": "事件唯一 ID",
|
||||
"type": "string"
|
||||
},
|
||||
"event_type": {
|
||||
"description": "事件类型,用于路由",
|
||||
"type": "string"
|
||||
},
|
||||
"tenant_key": {
|
||||
"description": "租户唯一标识",
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"description": "回调校验 token",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"schema": {
|
||||
"description": "飞书事件协议版本",
|
||||
"enum": [
|
||||
"2.0"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
{
|
||||
"key": "card.action.trigger",
|
||||
"display_name": "Card action",
|
||||
"description": "Triggered when a user interacts with an interactive card (button click, form submit, dropdown select, etc.). Output includes: token (valid 30 min, max 2 updates), action details (tag, value, name, form_value), and card_content (original card in userDSL text format, auto-fetched at consume time). To update the card: parse card_content to understand the current state, construct the new card JSON, then call `lark-cli api POST /open-apis/interactive/v1/card/update` with the token (see lark-im-card-action-reply.md).",
|
||||
"event_type": "card.action.trigger",
|
||||
"subscription_type": "callback",
|
||||
"schema": {
|
||||
"custom": {}
|
||||
},
|
||||
"scopes": [
|
||||
"im:message:readonly"
|
||||
],
|
||||
"auth_types": [
|
||||
"bot"
|
||||
],
|
||||
"required_console_events": [
|
||||
"card.action.trigger"
|
||||
],
|
||||
"buffer_size": 100,
|
||||
"workers": 1,
|
||||
"single_consumer": true,
|
||||
"resolved_output_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action_name": {
|
||||
"type": "string",
|
||||
"description": "Element name attribute"
|
||||
},
|
||||
"action_tag": {
|
||||
"type": "string",
|
||||
"description": "Triggered element type: button/select_static/input/checker/etc"
|
||||
},
|
||||
"action_value": {
|
||||
"type": "string",
|
||||
"description": "Developer-defined action value as JSON string"
|
||||
},
|
||||
"card_content": {
|
||||
"type": "string",
|
||||
"description": "Original card JSON content (body.content) auto-fetched via message get API at consume time using message_id; empty if message_id absent or fetch fails"
|
||||
},
|
||||
"chat_id": {
|
||||
"type": "string",
|
||||
"description": "Chat ID",
|
||||
"format": "chat_id"
|
||||
},
|
||||
"checked": {
|
||||
"type": "boolean",
|
||||
"description": "Checkbox state (for checkbox elements)"
|
||||
},
|
||||
"event_id": {
|
||||
"type": "string",
|
||||
"description": "Globally unique event ID"
|
||||
},
|
||||
"form_value": {
|
||||
"type": "string",
|
||||
"description": "Form submission values as JSON string (only on form submit)"
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"description": "Host type: im_message / im_top_notice"
|
||||
},
|
||||
"input_value": {
|
||||
"type": "string",
|
||||
"description": "Input field value (only for input elements)"
|
||||
},
|
||||
"message_id": {
|
||||
"type": "string",
|
||||
"description": "Message ID of the card",
|
||||
"format": "message_id"
|
||||
},
|
||||
"operator_id": {
|
||||
"type": "string",
|
||||
"description": "Operator open_id",
|
||||
"format": "open_id"
|
||||
},
|
||||
"option": {
|
||||
"type": "string",
|
||||
"description": "Selected option value (for single-select dropdown)"
|
||||
},
|
||||
"options": {
|
||||
"type": "string",
|
||||
"description": "Selected options, comma-separated (for multi-select)"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"description": "Event delivery time (ms timestamp string)",
|
||||
"format": "timestamp_ms"
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string",
|
||||
"description": "User timezone for date/time picker interactions"
|
||||
},
|
||||
"token": {
|
||||
"type": "string",
|
||||
"description": "Token for delay card update (valid 30 min, max 2 updates)"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Event type; always card.action.trigger"
|
||||
}
|
||||
}
|
||||
},
|
||||
"jq_root_path": "."
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
Key: card.action.trigger
|
||||
Description: Triggered when a user interacts with an interactive card (button click, form submit, dropdown select, etc.). Output includes: token (valid 30 min, max 2 updates), action details (tag, value, name, form_value), and card_content (original card in userDSL text format, auto-fetched at consume time). To update the card: parse card_content to understand the current state, construct the new card JSON, then call `lark-cli api POST /open-apis/interactive/v1/card/update` with the token (see lark-im-card-action-reply.md).
|
||||
Event: card.action.trigger
|
||||
|
||||
Required Scopes:
|
||||
- im:message:readonly
|
||||
|
||||
Required Console Events (must be enabled in developer console):
|
||||
- card.action.trigger
|
||||
|
||||
Output Schema:
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action_name": {
|
||||
"type": "string",
|
||||
"description": "Element name attribute"
|
||||
},
|
||||
"action_tag": {
|
||||
"type": "string",
|
||||
"description": "Triggered element type: button/select_static/input/checker/etc"
|
||||
},
|
||||
"action_value": {
|
||||
"type": "string",
|
||||
"description": "Developer-defined action value as JSON string"
|
||||
},
|
||||
"card_content": {
|
||||
"type": "string",
|
||||
"description": "Original card JSON content (body.content) auto-fetched via message get API at consume time using message_id; empty if message_id absent or fetch fails"
|
||||
},
|
||||
"chat_id": {
|
||||
"type": "string",
|
||||
"description": "Chat ID",
|
||||
"format": "chat_id"
|
||||
},
|
||||
"checked": {
|
||||
"type": "boolean",
|
||||
"description": "Checkbox state (for checkbox elements)"
|
||||
},
|
||||
"event_id": {
|
||||
"type": "string",
|
||||
"description": "Globally unique event ID"
|
||||
},
|
||||
"form_value": {
|
||||
"type": "string",
|
||||
"description": "Form submission values as JSON string (only on form submit)"
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"description": "Host type: im_message / im_top_notice"
|
||||
},
|
||||
"input_value": {
|
||||
"type": "string",
|
||||
"description": "Input field value (only for input elements)"
|
||||
},
|
||||
"message_id": {
|
||||
"type": "string",
|
||||
"description": "Message ID of the card",
|
||||
"format": "message_id"
|
||||
},
|
||||
"operator_id": {
|
||||
"type": "string",
|
||||
"description": "Operator open_id",
|
||||
"format": "open_id"
|
||||
},
|
||||
"option": {
|
||||
"type": "string",
|
||||
"description": "Selected option value (for single-select dropdown)"
|
||||
},
|
||||
"options": {
|
||||
"type": "string",
|
||||
"description": "Selected options, comma-separated (for multi-select)"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"description": "Event delivery time (ms timestamp string)",
|
||||
"format": "timestamp_ms"
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string",
|
||||
"description": "User timezone for date/time picker interactions"
|
||||
},
|
||||
"token": {
|
||||
"type": "string",
|
||||
"description": "Token for delay card update (valid 30 min, max 2 updates)"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Event type; always card.action.trigger"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,430 +0,0 @@
|
||||
{
|
||||
"key": "im.chat.updated_v1",
|
||||
"display_name": "Chat updated",
|
||||
"description": "Triggered after chat settings (owner, avatar, name, permissions, etc.) are updated",
|
||||
"event_type": "im.chat.updated_v1",
|
||||
"subscription_type": "event",
|
||||
"schema": {
|
||||
"native": {},
|
||||
"field_overrides": {
|
||||
"/event/after_change/owner_id/open_id": {
|
||||
"Description": "",
|
||||
"Enum": null,
|
||||
"Kind": "open_id"
|
||||
},
|
||||
"/event/after_change/owner_id/union_id": {
|
||||
"Description": "",
|
||||
"Enum": null,
|
||||
"Kind": "union_id"
|
||||
},
|
||||
"/event/after_change/owner_id/user_id": {
|
||||
"Description": "",
|
||||
"Enum": null,
|
||||
"Kind": "user_id"
|
||||
},
|
||||
"/event/before_change/owner_id/open_id": {
|
||||
"Description": "",
|
||||
"Enum": null,
|
||||
"Kind": "open_id"
|
||||
},
|
||||
"/event/before_change/owner_id/union_id": {
|
||||
"Description": "",
|
||||
"Enum": null,
|
||||
"Kind": "union_id"
|
||||
},
|
||||
"/event/before_change/owner_id/user_id": {
|
||||
"Description": "",
|
||||
"Enum": null,
|
||||
"Kind": "user_id"
|
||||
},
|
||||
"/event/chat_id": {
|
||||
"Description": "",
|
||||
"Enum": null,
|
||||
"Kind": "chat_id"
|
||||
},
|
||||
"/event/moderator_list/added_member_list/*/user_id/open_id": {
|
||||
"Description": "",
|
||||
"Enum": null,
|
||||
"Kind": "open_id"
|
||||
},
|
||||
"/event/moderator_list/added_member_list/*/user_id/union_id": {
|
||||
"Description": "",
|
||||
"Enum": null,
|
||||
"Kind": "union_id"
|
||||
},
|
||||
"/event/moderator_list/added_member_list/*/user_id/user_id": {
|
||||
"Description": "",
|
||||
"Enum": null,
|
||||
"Kind": "user_id"
|
||||
},
|
||||
"/event/moderator_list/removed_member_list/*/user_id/open_id": {
|
||||
"Description": "",
|
||||
"Enum": null,
|
||||
"Kind": "open_id"
|
||||
},
|
||||
"/event/moderator_list/removed_member_list/*/user_id/union_id": {
|
||||
"Description": "",
|
||||
"Enum": null,
|
||||
"Kind": "union_id"
|
||||
},
|
||||
"/event/moderator_list/removed_member_list/*/user_id/user_id": {
|
||||
"Description": "",
|
||||
"Enum": null,
|
||||
"Kind": "user_id"
|
||||
},
|
||||
"/event/operator_id/open_id": {
|
||||
"Description": "",
|
||||
"Enum": null,
|
||||
"Kind": "open_id"
|
||||
},
|
||||
"/event/operator_id/union_id": {
|
||||
"Description": "",
|
||||
"Enum": null,
|
||||
"Kind": "union_id"
|
||||
},
|
||||
"/event/operator_id/user_id": {
|
||||
"Description": "",
|
||||
"Enum": null,
|
||||
"Kind": "user_id"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scopes": [
|
||||
"im:chat:read"
|
||||
],
|
||||
"auth_types": [
|
||||
"bot"
|
||||
],
|
||||
"required_console_events": [
|
||||
"im.chat.updated_v1"
|
||||
],
|
||||
"buffer_size": 100,
|
||||
"workers": 1,
|
||||
"resolved_output_schema": {
|
||||
"description": "飞书事件",
|
||||
"properties": {
|
||||
"event": {
|
||||
"properties": {
|
||||
"after_change": {
|
||||
"properties": {
|
||||
"add_member_permission": {
|
||||
"type": "string"
|
||||
},
|
||||
"at_all_permission": {
|
||||
"type": "string"
|
||||
},
|
||||
"avatar": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"edit_permission": {
|
||||
"type": "string"
|
||||
},
|
||||
"group_message_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"i18n_names": {
|
||||
"properties": {
|
||||
"en_us": {
|
||||
"type": "string"
|
||||
},
|
||||
"ja_jp": {
|
||||
"type": "string"
|
||||
},
|
||||
"zh_cn": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"join_message_visibility": {
|
||||
"type": "string"
|
||||
},
|
||||
"labels": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"leave_message_visibility": {
|
||||
"type": "string"
|
||||
},
|
||||
"membership_approval": {
|
||||
"type": "string"
|
||||
},
|
||||
"moderation_permission": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"owner_id": {
|
||||
"properties": {
|
||||
"open_id": {
|
||||
"format": "open_id",
|
||||
"type": "string"
|
||||
},
|
||||
"union_id": {
|
||||
"format": "union_id",
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"format": "user_id",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"restricted_mode_setting": {
|
||||
"properties": {
|
||||
"download_has_permission_setting": {
|
||||
"type": "string"
|
||||
},
|
||||
"message_has_permission_setting": {
|
||||
"type": "string"
|
||||
},
|
||||
"screenshot_has_permission_setting": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"share_card_permission": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"before_change": {
|
||||
"properties": {
|
||||
"add_member_permission": {
|
||||
"type": "string"
|
||||
},
|
||||
"at_all_permission": {
|
||||
"type": "string"
|
||||
},
|
||||
"avatar": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"edit_permission": {
|
||||
"type": "string"
|
||||
},
|
||||
"group_message_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"i18n_names": {
|
||||
"properties": {
|
||||
"en_us": {
|
||||
"type": "string"
|
||||
},
|
||||
"ja_jp": {
|
||||
"type": "string"
|
||||
},
|
||||
"zh_cn": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"join_message_visibility": {
|
||||
"type": "string"
|
||||
},
|
||||
"labels": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"leave_message_visibility": {
|
||||
"type": "string"
|
||||
},
|
||||
"membership_approval": {
|
||||
"type": "string"
|
||||
},
|
||||
"moderation_permission": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"owner_id": {
|
||||
"properties": {
|
||||
"open_id": {
|
||||
"format": "open_id",
|
||||
"type": "string"
|
||||
},
|
||||
"union_id": {
|
||||
"format": "union_id",
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"format": "user_id",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"restricted_mode_setting": {
|
||||
"properties": {
|
||||
"download_has_permission_setting": {
|
||||
"type": "string"
|
||||
},
|
||||
"message_has_permission_setting": {
|
||||
"type": "string"
|
||||
},
|
||||
"screenshot_has_permission_setting": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"share_card_permission": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"chat_id": {
|
||||
"format": "chat_id",
|
||||
"type": "string"
|
||||
},
|
||||
"external": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"moderator_list": {
|
||||
"properties": {
|
||||
"added_member_list": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"tenant_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"properties": {
|
||||
"open_id": {
|
||||
"format": "open_id",
|
||||
"type": "string"
|
||||
},
|
||||
"union_id": {
|
||||
"format": "union_id",
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"format": "user_id",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"removed_member_list": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"tenant_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"properties": {
|
||||
"open_id": {
|
||||
"format": "open_id",
|
||||
"type": "string"
|
||||
},
|
||||
"union_id": {
|
||||
"format": "union_id",
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"format": "user_id",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"operator_id": {
|
||||
"properties": {
|
||||
"open_id": {
|
||||
"format": "open_id",
|
||||
"type": "string"
|
||||
},
|
||||
"union_id": {
|
||||
"format": "union_id",
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"format": "user_id",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"operator_tenant_key": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"header": {
|
||||
"description": "事件头,所有事件结构一致",
|
||||
"properties": {
|
||||
"app_id": {
|
||||
"description": "接收事件的应用 ID",
|
||||
"type": "string"
|
||||
},
|
||||
"create_time": {
|
||||
"description": "事件创建时间,毫秒时间戳字符串",
|
||||
"type": "string"
|
||||
},
|
||||
"event_id": {
|
||||
"description": "事件唯一 ID",
|
||||
"type": "string"
|
||||
},
|
||||
"event_type": {
|
||||
"description": "事件类型,用于路由",
|
||||
"type": "string"
|
||||
},
|
||||
"tenant_key": {
|
||||
"description": "租户唯一标识",
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"description": "回调校验 token",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"schema": {
|
||||
"description": "飞书事件协议版本",
|
||||
"enum": [
|
||||
"2.0"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"jq_root_path": ".event"
|
||||
}
|
||||
@@ -1,337 +0,0 @@
|
||||
Key: im.chat.updated_v1
|
||||
Description: Triggered after chat settings (owner, avatar, name, permissions, etc.) are updated
|
||||
Event: im.chat.updated_v1
|
||||
|
||||
Required Scopes:
|
||||
- im:chat:read
|
||||
|
||||
Required Console Events (must be enabled in developer console):
|
||||
- im.chat.updated_v1
|
||||
|
||||
Output Schema:
|
||||
{
|
||||
"description": "飞书事件",
|
||||
"properties": {
|
||||
"event": {
|
||||
"properties": {
|
||||
"after_change": {
|
||||
"properties": {
|
||||
"add_member_permission": {
|
||||
"type": "string"
|
||||
},
|
||||
"at_all_permission": {
|
||||
"type": "string"
|
||||
},
|
||||
"avatar": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"edit_permission": {
|
||||
"type": "string"
|
||||
},
|
||||
"group_message_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"i18n_names": {
|
||||
"properties": {
|
||||
"en_us": {
|
||||
"type": "string"
|
||||
},
|
||||
"ja_jp": {
|
||||
"type": "string"
|
||||
},
|
||||
"zh_cn": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"join_message_visibility": {
|
||||
"type": "string"
|
||||
},
|
||||
"labels": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"leave_message_visibility": {
|
||||
"type": "string"
|
||||
},
|
||||
"membership_approval": {
|
||||
"type": "string"
|
||||
},
|
||||
"moderation_permission": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"owner_id": {
|
||||
"properties": {
|
||||
"open_id": {
|
||||
"format": "open_id",
|
||||
"type": "string"
|
||||
},
|
||||
"union_id": {
|
||||
"format": "union_id",
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"format": "user_id",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"restricted_mode_setting": {
|
||||
"properties": {
|
||||
"download_has_permission_setting": {
|
||||
"type": "string"
|
||||
},
|
||||
"message_has_permission_setting": {
|
||||
"type": "string"
|
||||
},
|
||||
"screenshot_has_permission_setting": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"share_card_permission": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"before_change": {
|
||||
"properties": {
|
||||
"add_member_permission": {
|
||||
"type": "string"
|
||||
},
|
||||
"at_all_permission": {
|
||||
"type": "string"
|
||||
},
|
||||
"avatar": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"edit_permission": {
|
||||
"type": "string"
|
||||
},
|
||||
"group_message_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"i18n_names": {
|
||||
"properties": {
|
||||
"en_us": {
|
||||
"type": "string"
|
||||
},
|
||||
"ja_jp": {
|
||||
"type": "string"
|
||||
},
|
||||
"zh_cn": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"join_message_visibility": {
|
||||
"type": "string"
|
||||
},
|
||||
"labels": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"leave_message_visibility": {
|
||||
"type": "string"
|
||||
},
|
||||
"membership_approval": {
|
||||
"type": "string"
|
||||
},
|
||||
"moderation_permission": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"owner_id": {
|
||||
"properties": {
|
||||
"open_id": {
|
||||
"format": "open_id",
|
||||
"type": "string"
|
||||
},
|
||||
"union_id": {
|
||||
"format": "union_id",
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"format": "user_id",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"restricted_mode_setting": {
|
||||
"properties": {
|
||||
"download_has_permission_setting": {
|
||||
"type": "string"
|
||||
},
|
||||
"message_has_permission_setting": {
|
||||
"type": "string"
|
||||
},
|
||||
"screenshot_has_permission_setting": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"share_card_permission": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"chat_id": {
|
||||
"format": "chat_id",
|
||||
"type": "string"
|
||||
},
|
||||
"external": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"moderator_list": {
|
||||
"properties": {
|
||||
"added_member_list": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"tenant_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"properties": {
|
||||
"open_id": {
|
||||
"format": "open_id",
|
||||
"type": "string"
|
||||
},
|
||||
"union_id": {
|
||||
"format": "union_id",
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"format": "user_id",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"removed_member_list": {
|
||||
"items": {
|
||||
"properties": {
|
||||
"tenant_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"properties": {
|
||||
"open_id": {
|
||||
"format": "open_id",
|
||||
"type": "string"
|
||||
},
|
||||
"union_id": {
|
||||
"format": "union_id",
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"format": "user_id",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"operator_id": {
|
||||
"properties": {
|
||||
"open_id": {
|
||||
"format": "open_id",
|
||||
"type": "string"
|
||||
},
|
||||
"union_id": {
|
||||
"format": "union_id",
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"format": "user_id",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"operator_tenant_key": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"header": {
|
||||
"description": "事件头,所有事件结构一致",
|
||||
"properties": {
|
||||
"app_id": {
|
||||
"description": "接收事件的应用 ID",
|
||||
"type": "string"
|
||||
},
|
||||
"create_time": {
|
||||
"description": "事件创建时间,毫秒时间戳字符串",
|
||||
"type": "string"
|
||||
},
|
||||
"event_id": {
|
||||
"description": "事件唯一 ID",
|
||||
"type": "string"
|
||||
},
|
||||
"event_type": {
|
||||
"description": "事件类型,用于路由",
|
||||
"type": "string"
|
||||
},
|
||||
"tenant_key": {
|
||||
"description": "租户唯一标识",
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"description": "回调校验 token",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"schema": {
|
||||
"description": "飞书事件协议版本",
|
||||
"enum": [
|
||||
"2.0"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
{
|
||||
"key": "im.message.receive_v1",
|
||||
"display_name": "Receive message",
|
||||
"description": "Receive IM messages",
|
||||
"event_type": "im.message.receive_v1",
|
||||
"subscription_type": "event",
|
||||
"schema": {
|
||||
"custom": {}
|
||||
},
|
||||
"scopes": [
|
||||
"im:message.p2p_msg:readonly"
|
||||
],
|
||||
"auth_types": [
|
||||
"bot"
|
||||
],
|
||||
"required_console_events": [
|
||||
"im.message.receive_v1"
|
||||
],
|
||||
"buffer_size": 100,
|
||||
"workers": 1,
|
||||
"resolved_output_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"chat_id": {
|
||||
"type": "string",
|
||||
"description": "Chat/conversation ID; prefixed with oc_",
|
||||
"format": "chat_id"
|
||||
},
|
||||
"chat_type": {
|
||||
"type": "string",
|
||||
"description": "Conversation type",
|
||||
"enum": [
|
||||
"p2p",
|
||||
"group"
|
||||
]
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Message content. For most types (text/post/image/file/audio, etc.) this is pre-rendered human-readable text."
|
||||
},
|
||||
"create_time": {
|
||||
"type": "string",
|
||||
"description": "Message creation time (ms timestamp string)",
|
||||
"format": "timestamp_ms"
|
||||
},
|
||||
"event_id": {
|
||||
"type": "string",
|
||||
"description": "Event delivery ID. Do not use as the message deduplication key; use message_id instead."
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Message ID (legacy alias of message_id, kept for compatibility)",
|
||||
"format": "message_id"
|
||||
},
|
||||
"mentions": {
|
||||
"type": "array",
|
||||
"description": "Compact mentions aligned with im +messages-mget",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Mentioned user open_id; prefixed with ou_",
|
||||
"format": "open_id"
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "Mention placeholder key, for example @_user_1"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Mentioned display name"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"message_id": {
|
||||
"type": "string",
|
||||
"description": "Message ID; prefixed with om_. Recommended idempotency key for im.message.receive_v1 consumers.",
|
||||
"format": "message_id"
|
||||
},
|
||||
"message_type": {
|
||||
"type": "string",
|
||||
"description": "Message type"
|
||||
},
|
||||
"reply_to": {
|
||||
"type": "string",
|
||||
"description": "Parent message ID of the direct reply context, when present",
|
||||
"format": "message_id"
|
||||
},
|
||||
"root_id": {
|
||||
"type": "string",
|
||||
"description": "Root message ID of the reply/thread context, when present",
|
||||
"format": "message_id"
|
||||
},
|
||||
"sender_id": {
|
||||
"type": "string",
|
||||
"description": "Sender open_id; prefixed with ou_",
|
||||
"format": "open_id"
|
||||
},
|
||||
"sender_type": {
|
||||
"type": "string",
|
||||
"description": "Sender type",
|
||||
"enum": [
|
||||
"user",
|
||||
"bot"
|
||||
]
|
||||
},
|
||||
"thread_id": {
|
||||
"type": "string",
|
||||
"description": "Thread ID, when present"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"description": "Event delivery time (ms timestamp string); prefers header.create_time",
|
||||
"format": "timestamp_ms"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Event type; always im.message.receive_v1"
|
||||
},
|
||||
"update_time": {
|
||||
"type": "string",
|
||||
"description": "Message update time (ms timestamp string); emitted only when different from create_time",
|
||||
"format": "timestamp_ms"
|
||||
}
|
||||
}
|
||||
},
|
||||
"jq_root_path": "."
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
Key: im.message.receive_v1
|
||||
Description: Receive IM messages
|
||||
Event: im.message.receive_v1
|
||||
|
||||
Required Scopes:
|
||||
- im:message.p2p_msg:readonly
|
||||
|
||||
Required Console Events (must be enabled in developer console):
|
||||
- im.message.receive_v1
|
||||
|
||||
Output Schema:
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"chat_id": {
|
||||
"type": "string",
|
||||
"description": "Chat/conversation ID; prefixed with oc_",
|
||||
"format": "chat_id"
|
||||
},
|
||||
"chat_type": {
|
||||
"type": "string",
|
||||
"description": "Conversation type",
|
||||
"enum": [
|
||||
"p2p",
|
||||
"group"
|
||||
]
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Message content. For most types (text/post/image/file/audio, etc.) this is pre-rendered human-readable text."
|
||||
},
|
||||
"create_time": {
|
||||
"type": "string",
|
||||
"description": "Message creation time (ms timestamp string)",
|
||||
"format": "timestamp_ms"
|
||||
},
|
||||
"event_id": {
|
||||
"type": "string",
|
||||
"description": "Event delivery ID. Do not use as the message deduplication key; use message_id instead."
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Message ID (legacy alias of message_id, kept for compatibility)",
|
||||
"format": "message_id"
|
||||
},
|
||||
"mentions": {
|
||||
"type": "array",
|
||||
"description": "Compact mentions aligned with im +messages-mget",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Mentioned user open_id; prefixed with ou_",
|
||||
"format": "open_id"
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "Mention placeholder key, for example @_user_1"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Mentioned display name"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"message_id": {
|
||||
"type": "string",
|
||||
"description": "Message ID; prefixed with om_. Recommended idempotency key for im.message.receive_v1 consumers.",
|
||||
"format": "message_id"
|
||||
},
|
||||
"message_type": {
|
||||
"type": "string",
|
||||
"description": "Message type"
|
||||
},
|
||||
"reply_to": {
|
||||
"type": "string",
|
||||
"description": "Parent message ID of the direct reply context, when present",
|
||||
"format": "message_id"
|
||||
},
|
||||
"root_id": {
|
||||
"type": "string",
|
||||
"description": "Root message ID of the reply/thread context, when present",
|
||||
"format": "message_id"
|
||||
},
|
||||
"sender_id": {
|
||||
"type": "string",
|
||||
"description": "Sender open_id; prefixed with ou_",
|
||||
"format": "open_id"
|
||||
},
|
||||
"sender_type": {
|
||||
"type": "string",
|
||||
"description": "Sender type",
|
||||
"enum": [
|
||||
"user",
|
||||
"bot"
|
||||
]
|
||||
},
|
||||
"thread_id": {
|
||||
"type": "string",
|
||||
"description": "Thread ID, when present"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string",
|
||||
"description": "Event delivery time (ms timestamp string); prefers header.create_time",
|
||||
"format": "timestamp_ms"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Event type; always im.message.receive_v1"
|
||||
},
|
||||
"update_time": {
|
||||
"type": "string",
|
||||
"description": "Message update time (ms timestamp string); emitted only when different from create_time",
|
||||
"format": "timestamp_ms"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/larksuite/cli/events"
|
||||
"github.com/larksuite/cli/internal/event/catalog"
|
||||
)
|
||||
|
||||
// compileCatalog is the event command tree's single assembly point: it turns
|
||||
// the aggregated domain declarations into the immutable snapshot every
|
||||
// subcommand reads. A compile failure is a defect in declarations built into
|
||||
// this binary — there is nothing to recover at runtime, so it panics.
|
||||
func compileCatalog() *catalog.Snapshot {
|
||||
// The strategy registry that validates references is the same one that
|
||||
// executes them, so "compiled" implies "resolvable at run time".
|
||||
snap, err := catalog.Compile(events.All(), consumeStrategies)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("event catalog failed to compile: %v", err))
|
||||
}
|
||||
return snap
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ func RegisterGlobalFlags(fs *pflag.FlagSet, opts *GlobalOptions) {
|
||||
// until at least two profiles exist. Intended for the Execute entry point —
|
||||
// buildInternal must not call this directly to stay state-free.
|
||||
func isSingleAppMode() bool {
|
||||
raw, err := core.LoadMultiAppConfig()
|
||||
raw, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil || raw == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/secret"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
@@ -58,8 +60,8 @@ func TestIsSingleAppMode_NoConfig(t *testing.T) {
|
||||
|
||||
func TestIsSingleAppMode_SingleApp(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
saveAppsForTest(t, []core.AppConfig{
|
||||
{Name: "default", AppId: "cli_a", AppSecret: core.PlainSecret("x"), Brand: core.BrandFeishu},
|
||||
saveAppsForTest(t, []configpkg.AppConfig{
|
||||
{Name: "default", AppId: "cli_a", AppSecret: secret.PlainSecret("x"), Brand: brand.Feishu},
|
||||
})
|
||||
if !isSingleAppMode() {
|
||||
t.Fatal("isSingleAppMode() = false, want true for single-app config")
|
||||
@@ -68,9 +70,9 @@ func TestIsSingleAppMode_SingleApp(t *testing.T) {
|
||||
|
||||
func TestIsSingleAppMode_MultiApp(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
saveAppsForTest(t, []core.AppConfig{
|
||||
{Name: "a", AppId: "cli_a", AppSecret: core.PlainSecret("x"), Brand: core.BrandFeishu},
|
||||
{Name: "b", AppId: "cli_b", AppSecret: core.PlainSecret("y"), Brand: core.BrandFeishu},
|
||||
saveAppsForTest(t, []configpkg.AppConfig{
|
||||
{Name: "a", AppId: "cli_a", AppSecret: secret.PlainSecret("x"), Brand: brand.Feishu},
|
||||
{Name: "b", AppId: "cli_b", AppSecret: secret.PlainSecret("y"), Brand: brand.Feishu},
|
||||
})
|
||||
if isSingleAppMode() {
|
||||
t.Fatal("isSingleAppMode() = true, want false for multi-app config")
|
||||
@@ -101,10 +103,10 @@ func TestBuildInternal_DefaultShowsProfileFlag(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func saveAppsForTest(t *testing.T, apps []core.AppConfig) {
|
||||
func saveAppsForTest(t *testing.T, apps []configpkg.AppConfig) {
|
||||
t.Helper()
|
||||
multi := &core.MultiAppConfig{CurrentApp: apps[0].Name, Apps: apps}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
multi := &configpkg.MultiAppConfig{CurrentApp: apps[0].Name, Apps: apps}
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ import (
|
||||
|
||||
"github.com/larksuite/cli/extension/platform"
|
||||
"github.com/larksuite/cli/internal/cmdpolicy"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/hook"
|
||||
internalplatform "github.com/larksuite/cli/internal/platform"
|
||||
"github.com/larksuite/cli/internal/vfs"
|
||||
"github.com/larksuite/cli/internal/workspace"
|
||||
)
|
||||
|
||||
// userPolicyFileName is the conventional filename for the user-layer Rule.
|
||||
@@ -261,7 +261,7 @@ func splitCSV(s string) []string {
|
||||
// userPolicyPath returns the path of <baseConfigDir>/policy.yml.
|
||||
//
|
||||
// The base directory honours LARKSUITE_CLI_CONFIG_DIR (via
|
||||
// core.GetBaseConfigDir) so that test isolation, container deployments
|
||||
// workspace.GetBaseConfigDir) so that test isolation, container deployments
|
||||
// and per-Agent config overrides all see a consistent policy location.
|
||||
// Using vfs.UserHomeDir directly here would silently bypass the env
|
||||
// override and route every test through the real ~/.lark-cli.
|
||||
@@ -271,7 +271,7 @@ func splitCSV(s string) []string {
|
||||
// the home dir can't be resolved, and the resolver already treats a
|
||||
// missing file as "no policy".
|
||||
func userPolicyPath() (string, error) {
|
||||
return filepath.Join(core.GetBaseConfigDir(), userPolicyFileName), nil
|
||||
return filepath.Join(workspace.GetBaseConfigDir(), userPolicyFileName), nil
|
||||
}
|
||||
|
||||
// warnPolicyError writes a one-line stderr warning when the user policy
|
||||
|
||||
@@ -12,11 +12,13 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
brandpkg "github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/i18n"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
secretpkg "github.com/larksuite/cli/internal/secret"
|
||||
)
|
||||
|
||||
// NewCmdProfileAdd creates the profile add subcommand.
|
||||
@@ -53,7 +55,7 @@ func NewCmdProfileAdd(f *cmdutil.Factory) *cobra.Command {
|
||||
}
|
||||
|
||||
func profileAddRun(f *cmdutil.Factory, name, appID string, appSecretStdin bool, brand, lang string, useAfter bool) error {
|
||||
if err := core.ValidateProfileName(name); err != nil {
|
||||
if err := configpkg.ValidateProfileName(name); err != nil {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%v", err).
|
||||
WithCause(err).
|
||||
WithParam("--name")
|
||||
@@ -90,12 +92,12 @@ func profileAddRun(f *cmdutil.Factory, name, appID string, appSecretStdin bool,
|
||||
}
|
||||
|
||||
// Load or create config
|
||||
multi, err := core.LoadMultiAppConfig()
|
||||
multi, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return errs.NewInternalError(errs.SubtypeFileIO, "failed to load config: %v", err).WithCause(err)
|
||||
}
|
||||
multi = &core.MultiAppConfig{}
|
||||
multi = &configpkg.MultiAppConfig{}
|
||||
}
|
||||
|
||||
// Check name uniqueness
|
||||
@@ -115,12 +117,12 @@ func profileAddRun(f *cmdutil.Factory, name, appID string, appSecretStdin bool,
|
||||
}
|
||||
|
||||
// Store secret securely
|
||||
secret, err := core.ForStorage(appID, core.PlainSecret(appSecret), f.Keychain)
|
||||
secret, err := secretpkg.ForStorage(appID, secretpkg.PlainSecret(appSecret), f.Keychain)
|
||||
if err != nil {
|
||||
return errs.NewInternalError(errs.SubtypeStorage, "%v", err).WithCause(err)
|
||||
}
|
||||
|
||||
parsedBrand := core.ParseBrand(brand)
|
||||
parsedBrand := brandpkg.ParseBrand(brand)
|
||||
|
||||
// Capture current profile before appending (avoid setting PreviousApp to self)
|
||||
var previousName string
|
||||
@@ -131,13 +133,13 @@ func profileAddRun(f *cmdutil.Factory, name, appID string, appSecretStdin bool,
|
||||
}
|
||||
|
||||
// Append profile
|
||||
multi.Apps = append(multi.Apps, core.AppConfig{
|
||||
multi.Apps = append(multi.Apps, configpkg.AppConfig{
|
||||
Name: name,
|
||||
AppId: appID,
|
||||
AppSecret: secret,
|
||||
Brand: parsedBrand,
|
||||
Lang: i18n.Lang(lang),
|
||||
Users: []core.AppUser{},
|
||||
Users: []configpkg.AppUser{},
|
||||
})
|
||||
|
||||
if useAfter {
|
||||
@@ -147,7 +149,7 @@ func profileAddRun(f *cmdutil.Factory, name, appID string, appSecretStdin bool,
|
||||
multi.CurrentApp = name
|
||||
}
|
||||
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
return errs.NewInternalError(errs.SubtypeStorage, "failed to save config: %v", err).WithCause(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,21 +9,22 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
larkauth "github.com/larksuite/cli/internal/auth"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
)
|
||||
|
||||
// profileListItem is the JSON output for a single profile entry.
|
||||
type profileListItem struct {
|
||||
Name string `json:"name"`
|
||||
AppID string `json:"appId"`
|
||||
Brand core.LarkBrand `json:"brand"`
|
||||
Active bool `json:"active"`
|
||||
User string `json:"user,omitempty"`
|
||||
TokenStatus string `json:"tokenStatus,omitempty"`
|
||||
Name string `json:"name"`
|
||||
AppID string `json:"appId"`
|
||||
Brand brand.Brand `json:"brand"`
|
||||
Active bool `json:"active"`
|
||||
User string `json:"user,omitempty"`
|
||||
TokenStatus string `json:"tokenStatus,omitempty"`
|
||||
}
|
||||
|
||||
// NewCmdProfileList creates the profile list subcommand.
|
||||
@@ -40,7 +41,7 @@ func NewCmdProfileList(f *cmdutil.Factory) *cobra.Command {
|
||||
}
|
||||
|
||||
func profileListRun(f *cmdutil.Factory) error {
|
||||
multi, err := core.LoadMultiAppConfig()
|
||||
multi, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
output.PrintJson(f.IOStreams.Out, []profileListItem{})
|
||||
|
||||
@@ -11,11 +11,13 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/brand"
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/i18n"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/secret"
|
||||
"github.com/larksuite/cli/internal/vfs"
|
||||
)
|
||||
|
||||
@@ -75,7 +77,7 @@ func TestProfileAddRun_Lang(t *testing.T) {
|
||||
if err := profileAddRun(f, "p", "app-p", true, "feishu", in, false); err != nil {
|
||||
t.Fatalf("--lang %q: profileAddRun() error = %v", in, err)
|
||||
}
|
||||
saved, err := core.LoadMultiAppConfig()
|
||||
saved, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMultiAppConfig() error = %v", err)
|
||||
}
|
||||
@@ -92,7 +94,7 @@ func TestProfileAddRun_Lang(t *testing.T) {
|
||||
if err := profileAddRun(f, "p", "app-p", true, "feishu", "", false); err != nil {
|
||||
t.Fatalf("profileAddRun() error = %v", err)
|
||||
}
|
||||
saved, _ := core.LoadMultiAppConfig()
|
||||
saved, _ := configpkg.LoadMultiAppConfig()
|
||||
if app := saved.FindApp("p"); app == nil || app.Lang != "" {
|
||||
t.Errorf("stored Lang = %v, want \"\" (unset)", app)
|
||||
}
|
||||
@@ -115,13 +117,13 @@ func TestProfileAddRun_Lang(t *testing.T) {
|
||||
|
||||
func TestProfileAddRun_UseAfterUpdatesCurrentAndPrevious(t *testing.T) {
|
||||
setupProfileConfigDir(t)
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "default",
|
||||
Apps: []core.AppConfig{
|
||||
{Name: "default", AppId: "app-default", AppSecret: core.PlainSecret("secret-default"), Brand: core.BrandFeishu},
|
||||
Apps: []configpkg.AppConfig{
|
||||
{Name: "default", AppId: "app-default", AppSecret: secret.PlainSecret("secret-default"), Brand: brand.Feishu},
|
||||
},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
|
||||
@@ -132,7 +134,7 @@ func TestProfileAddRun_UseAfterUpdatesCurrentAndPrevious(t *testing.T) {
|
||||
t.Fatalf("profileAddRun() error = %v", err)
|
||||
}
|
||||
|
||||
saved, err := core.LoadMultiAppConfig()
|
||||
saved, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMultiAppConfig() error = %v", err)
|
||||
}
|
||||
@@ -149,15 +151,15 @@ func TestProfileAddRun_UseAfterUpdatesCurrentAndPrevious(t *testing.T) {
|
||||
|
||||
func TestProfileRemoveRun_RemovesCurrentProfileAndSwitchesToFirstRemaining(t *testing.T) {
|
||||
setupProfileConfigDir(t)
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "target",
|
||||
PreviousApp: "default",
|
||||
Apps: []core.AppConfig{
|
||||
{Name: "default", AppId: "app-default", AppSecret: core.PlainSecret("secret-default"), Brand: core.BrandFeishu},
|
||||
{Name: "target", AppId: "app-target", AppSecret: core.PlainSecret("secret-target"), Brand: core.BrandLark},
|
||||
Apps: []configpkg.AppConfig{
|
||||
{Name: "default", AppId: "app-default", AppSecret: secret.PlainSecret("secret-default"), Brand: brand.Feishu},
|
||||
{Name: "target", AppId: "app-target", AppSecret: secret.PlainSecret("secret-target"), Brand: brand.Lark},
|
||||
},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
|
||||
@@ -166,7 +168,7 @@ func TestProfileRemoveRun_RemovesCurrentProfileAndSwitchesToFirstRemaining(t *te
|
||||
t.Fatalf("profileRemoveRun() error = %v", err)
|
||||
}
|
||||
|
||||
saved, err := core.LoadMultiAppConfig()
|
||||
saved, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMultiAppConfig() error = %v", err)
|
||||
}
|
||||
@@ -183,17 +185,17 @@ func TestProfileRemoveRun_RemovesCurrentProfileAndSwitchesToFirstRemaining(t *te
|
||||
|
||||
func TestProfileRenameRun_UpdatesCurrentAndPreviousReferences(t *testing.T) {
|
||||
setupProfileConfigDir(t)
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "old",
|
||||
PreviousApp: "old",
|
||||
Apps: []core.AppConfig{{
|
||||
Apps: []configpkg.AppConfig{{
|
||||
Name: "old",
|
||||
AppId: "app-old",
|
||||
AppSecret: core.PlainSecret("secret-old"),
|
||||
Brand: core.BrandFeishu,
|
||||
AppSecret: secret.PlainSecret("secret-old"),
|
||||
Brand: brand.Feishu,
|
||||
}},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
|
||||
@@ -202,7 +204,7 @@ func TestProfileRenameRun_UpdatesCurrentAndPreviousReferences(t *testing.T) {
|
||||
t.Fatalf("profileRenameRun() error = %v", err)
|
||||
}
|
||||
|
||||
saved, err := core.LoadMultiAppConfig()
|
||||
saved, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMultiAppConfig() error = %v", err)
|
||||
}
|
||||
@@ -219,17 +221,17 @@ func TestProfileRenameRun_UpdatesCurrentAndPreviousReferences(t *testing.T) {
|
||||
|
||||
func TestProfileRenameRun_AllowsRenameToOwnAppID(t *testing.T) {
|
||||
setupProfileConfigDir(t)
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "old",
|
||||
PreviousApp: "old",
|
||||
Apps: []core.AppConfig{{
|
||||
Apps: []configpkg.AppConfig{{
|
||||
Name: "old",
|
||||
AppId: "app-old",
|
||||
AppSecret: core.PlainSecret("secret-old"),
|
||||
Brand: core.BrandFeishu,
|
||||
AppSecret: secret.PlainSecret("secret-old"),
|
||||
Brand: brand.Feishu,
|
||||
}},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
|
||||
@@ -238,7 +240,7 @@ func TestProfileRenameRun_AllowsRenameToOwnAppID(t *testing.T) {
|
||||
t.Fatalf("profileRenameRun() error = %v", err)
|
||||
}
|
||||
|
||||
saved, err := core.LoadMultiAppConfig()
|
||||
saved, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMultiAppConfig() error = %v", err)
|
||||
}
|
||||
@@ -255,15 +257,15 @@ func TestProfileRenameRun_AllowsRenameToOwnAppID(t *testing.T) {
|
||||
|
||||
func TestProfileUseRun_ToggleBackUsesPreviousProfile(t *testing.T) {
|
||||
setupProfileConfigDir(t)
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "default",
|
||||
PreviousApp: "target",
|
||||
Apps: []core.AppConfig{
|
||||
{Name: "default", AppId: "app-default", AppSecret: core.PlainSecret("secret-default"), Brand: core.BrandFeishu},
|
||||
{Name: "target", AppId: "app-target", AppSecret: core.PlainSecret("secret-target"), Brand: core.BrandLark},
|
||||
Apps: []configpkg.AppConfig{
|
||||
{Name: "default", AppId: "app-default", AppSecret: secret.PlainSecret("secret-default"), Brand: brand.Feishu},
|
||||
{Name: "target", AppId: "app-target", AppSecret: secret.PlainSecret("secret-target"), Brand: brand.Lark},
|
||||
},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
|
||||
@@ -272,7 +274,7 @@ func TestProfileUseRun_ToggleBackUsesPreviousProfile(t *testing.T) {
|
||||
t.Fatalf("profileUseRun() error = %v", err)
|
||||
}
|
||||
|
||||
saved, err := core.LoadMultiAppConfig()
|
||||
saved, err := configpkg.LoadMultiAppConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadMultiAppConfig() error = %v", err)
|
||||
}
|
||||
@@ -286,14 +288,14 @@ func TestProfileUseRun_ToggleBackUsesPreviousProfile(t *testing.T) {
|
||||
|
||||
func TestProfileListRun_OutputsProfiles(t *testing.T) {
|
||||
setupProfileConfigDir(t)
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "default",
|
||||
Apps: []core.AppConfig{
|
||||
{Name: "default", AppId: "app-default", AppSecret: core.PlainSecret("secret-default"), Brand: core.BrandFeishu},
|
||||
{Name: "target", AppId: "app-target", AppSecret: core.PlainSecret("secret-target"), Brand: core.BrandLark},
|
||||
Apps: []configpkg.AppConfig{
|
||||
{Name: "default", AppId: "app-default", AppSecret: secret.PlainSecret("secret-default"), Brand: brand.Feishu},
|
||||
{Name: "target", AppId: "app-target", AppSecret: secret.PlainSecret("secret-target"), Brand: brand.Lark},
|
||||
},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
|
||||
@@ -339,14 +341,14 @@ func TestProfileListRun_NotConfiguredReturnsEmptyList(t *testing.T) {
|
||||
|
||||
func TestProfileRemoveRun_SaveFailureReturnsStructuredError(t *testing.T) {
|
||||
setupProfileConfigDir(t)
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "target",
|
||||
Apps: []core.AppConfig{
|
||||
{Name: "default", AppId: "app-default", AppSecret: core.PlainSecret("secret-default"), Brand: core.BrandFeishu},
|
||||
{Name: "target", AppId: "app-target", AppSecret: core.PlainSecret("secret-target"), Brand: core.BrandLark},
|
||||
Apps: []configpkg.AppConfig{
|
||||
{Name: "default", AppId: "app-default", AppSecret: secret.PlainSecret("secret-default"), Brand: brand.Feishu},
|
||||
{Name: "target", AppId: "app-target", AppSecret: secret.PlainSecret("secret-target"), Brand: brand.Lark},
|
||||
},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
|
||||
@@ -364,16 +366,16 @@ func TestProfileRemoveRun_SaveFailureReturnsStructuredError(t *testing.T) {
|
||||
|
||||
func TestProfileRenameRun_SaveFailureReturnsStructuredError(t *testing.T) {
|
||||
setupProfileConfigDir(t)
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "old",
|
||||
Apps: []core.AppConfig{{
|
||||
Apps: []configpkg.AppConfig{{
|
||||
Name: "old",
|
||||
AppId: "app-old",
|
||||
AppSecret: core.PlainSecret("secret-old"),
|
||||
Brand: core.BrandFeishu,
|
||||
AppSecret: secret.PlainSecret("secret-old"),
|
||||
Brand: brand.Feishu,
|
||||
}},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
|
||||
@@ -391,14 +393,14 @@ func TestProfileRenameRun_SaveFailureReturnsStructuredError(t *testing.T) {
|
||||
|
||||
func TestProfileUseRun_SaveFailureReturnsStructuredError(t *testing.T) {
|
||||
setupProfileConfigDir(t)
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "default",
|
||||
Apps: []core.AppConfig{
|
||||
{Name: "default", AppId: "app-default", AppSecret: core.PlainSecret("secret-default"), Brand: core.BrandFeishu},
|
||||
{Name: "target", AppId: "app-target", AppSecret: core.PlainSecret("secret-target"), Brand: core.BrandLark},
|
||||
Apps: []configpkg.AppConfig{
|
||||
{Name: "default", AppId: "app-default", AppSecret: secret.PlainSecret("secret-default"), Brand: brand.Feishu},
|
||||
{Name: "target", AppId: "app-target", AppSecret: secret.PlainSecret("secret-target"), Brand: brand.Lark},
|
||||
},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
|
||||
@@ -461,14 +463,14 @@ func assertValidationError(t *testing.T, err error, wantSubtype errs.Subtype, wa
|
||||
|
||||
func saveTwoProfiles(t *testing.T) {
|
||||
t.Helper()
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "default",
|
||||
Apps: []core.AppConfig{
|
||||
{Name: "default", AppId: "app-default", AppSecret: core.PlainSecret("secret-default"), Brand: core.BrandFeishu},
|
||||
{Name: "target", AppId: "app-target", AppSecret: core.PlainSecret("secret-target"), Brand: core.BrandLark},
|
||||
Apps: []configpkg.AppConfig{
|
||||
{Name: "default", AppId: "app-default", AppSecret: secret.PlainSecret("secret-default"), Brand: brand.Feishu},
|
||||
{Name: "target", AppId: "app-target", AppSecret: secret.PlainSecret("secret-target"), Brand: brand.Lark},
|
||||
},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -609,13 +611,13 @@ func TestProfileRemoveRun_ValidationErrors(t *testing.T) {
|
||||
|
||||
t.Run("cannot remove the only profile", func(t *testing.T) {
|
||||
setupProfileConfigDir(t)
|
||||
multi := &core.MultiAppConfig{
|
||||
multi := &configpkg.MultiAppConfig{
|
||||
CurrentApp: "solo",
|
||||
Apps: []core.AppConfig{
|
||||
{Name: "solo", AppId: "app-solo", AppSecret: core.PlainSecret("secret-solo"), Brand: core.BrandFeishu},
|
||||
Apps: []configpkg.AppConfig{
|
||||
{Name: "solo", AppId: "app-solo", AppSecret: secret.PlainSecret("secret-solo"), Brand: brand.Feishu},
|
||||
},
|
||||
}
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
f, _, _, _ := cmdutil.TestFactory(t, nil)
|
||||
|
||||
@@ -12,8 +12,9 @@ import (
|
||||
"github.com/larksuite/cli/errs"
|
||||
larkauth "github.com/larksuite/cli/internal/auth"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/secret"
|
||||
)
|
||||
|
||||
// NewCmdProfileRemove creates the profile remove subcommand.
|
||||
@@ -34,7 +35,7 @@ func NewCmdProfileRemove(f *cmdutil.Factory) *cobra.Command {
|
||||
}
|
||||
|
||||
func profileRemoveRun(f *cmdutil.Factory, name string) error {
|
||||
multi, err := core.LoadOrNotConfigured()
|
||||
multi, err := configpkg.LoadOrNotConfigured()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -66,12 +67,12 @@ func profileRemoveRun(f *cmdutil.Factory, name string) error {
|
||||
multi.PreviousApp = ""
|
||||
}
|
||||
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
return errs.NewInternalError(errs.SubtypeStorage, "failed to save config: %v", err).WithCause(err)
|
||||
}
|
||||
|
||||
// Best-effort credential cleanup after config commit
|
||||
core.RemoveSecretStore(appSecret, f.Keychain)
|
||||
secret.RemoveSecretStore(appSecret, f.Keychain)
|
||||
for _, user := range users {
|
||||
larkauth.RemoveStoredToken(appId, user.UserOpenId)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
)
|
||||
|
||||
@@ -30,11 +30,11 @@ func NewCmdProfileRename(f *cmdutil.Factory) *cobra.Command {
|
||||
}
|
||||
|
||||
func profileRenameRun(f *cmdutil.Factory, oldName, newName string) error {
|
||||
if err := core.ValidateProfileName(newName); err != nil {
|
||||
if err := configpkg.ValidateProfileName(newName); err != nil {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%v", err).WithCause(err)
|
||||
}
|
||||
|
||||
multi, err := core.LoadOrNotConfigured()
|
||||
multi, err := configpkg.LoadOrNotConfigured()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -67,7 +67,7 @@ func profileRenameRun(f *cmdutil.Factory, oldName, newName string) error {
|
||||
multi.PreviousApp = newName
|
||||
}
|
||||
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
return errs.NewInternalError(errs.SubtypeStorage, "failed to save config: %v", err).WithCause(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
configpkg "github.com/larksuite/cli/internal/config"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
)
|
||||
|
||||
@@ -33,7 +33,7 @@ func NewCmdProfileUse(f *cmdutil.Factory) *cobra.Command {
|
||||
}
|
||||
|
||||
func profileUseRun(f *cmdutil.Factory, name string) error {
|
||||
multi, err := core.LoadOrNotConfigured()
|
||||
multi, err := configpkg.LoadOrNotConfigured()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -67,7 +67,7 @@ func profileUseRun(f *cmdutil.Factory, name string) error {
|
||||
}
|
||||
multi.CurrentApp = targetName
|
||||
|
||||
if err := core.SaveMultiAppConfig(multi); err != nil {
|
||||
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
|
||||
return errs.NewInternalError(errs.SubtypeStorage, "failed to save config: %v", err).WithCause(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdpolicy"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/identity"
|
||||
)
|
||||
|
||||
// pruneForStrictMode removes commands incompatible with the active strict mode.
|
||||
func pruneForStrictMode(root *cobra.Command, mode core.StrictMode) {
|
||||
func pruneForStrictMode(root *cobra.Command, mode identity.StrictMode) {
|
||||
pruneIncompatible(root, mode)
|
||||
pruneEmpty(root)
|
||||
}
|
||||
@@ -25,7 +25,7 @@ func pruneForStrictMode(root *cobra.Command, mode core.StrictMode) {
|
||||
// identities incompatible with the forced identity. Commands without annotation are kept.
|
||||
// Hidden stubs preserve direct execution so users get a strict-mode error instead
|
||||
// of Cobra's generic "unknown flag" fallback from the parent command.
|
||||
func pruneIncompatible(parent *cobra.Command, mode core.StrictMode) {
|
||||
func pruneIncompatible(parent *cobra.Command, mode identity.StrictMode) {
|
||||
forced := string(mode.ForcedIdentity())
|
||||
var toRemove []*cobra.Command
|
||||
var toAdd []*cobra.Command
|
||||
@@ -44,7 +44,7 @@ func pruneIncompatible(parent *cobra.Command, mode core.StrictMode) {
|
||||
}
|
||||
}
|
||||
|
||||
func strictModeStubFrom(child *cobra.Command, mode core.StrictMode) *cobra.Command {
|
||||
func strictModeStubFrom(child *cobra.Command, mode identity.StrictMode) *cobra.Command {
|
||||
// The denial annotations let the hook layer's populateInvocationDenial
|
||||
// recognise this command as denied, so the Wrap chain is physically
|
||||
// isolated (wrapRunE takes the DeniedByPolicy branch and calls the
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user