Compare commits

..

7 Commits

Author SHA1 Message Date
zhanghuanxu
44be8c986f fix(slides): close text-overlap false negatives and unify z-order checks
Fix missed overflow and occlusion cases in xml_text_overlap_lint: CJK
ambiguous-width and percent glyph width estimation, chart-vs-text
occlusion, full-canvas background-image exemption, and severity masking
in the width/height overflow dedupe. Consolidate the scattered raw
paint-order comparisons into is_drawn_behind / is_drawn_in_front_of so
stacking direction is decided in one place, with contract tests that
turn red if the fixes are reverted.
2026-07-31 18:30:39 +08:00
zhanghuanxu
a849ac3449 fix(slides): detect width-induced text wrap in xml_text_overlap_lint
The height-only check (text_may_overflow_shape) misses shapes that wrap
because their box is too narrow, not too short. Added a new width-axis
detector that:

- Flags single-line short labels/metrics whose estimated width exceeds
  the content box (0.85 risk band for latin runs, 1.18 tolerance for
  plain metrics, exact fit for pure CJK).
- Works independently of autoFit (shape-auto-fit only grows height).
- Preserves internal whitespace (e.g. "autofix      87%") so spaces
  are not collapsed away.
- Deduplicates with the height check so the same shape is not
  double-reported under the shared text_may_overflow_shape code.

The two axes share code="text_may_overflow_shape" and are distinguished
by overflow_axis="height"|"width". Regression test covers all three
real false-negative cases (bMP/bMp/bMm) plus negative controls.
2026-07-31 18:30:39 +08:00
zhanghuanxu
47a46658c8 fix(slides): remove order exemption in image-text occlusion detection
The order-based skip (`image.order <= text.order`) allowed images that
appear before text in XML to silently cover text glyphs. Remove it so
any geometric overlap is reported regardless of XML element order.

Also update the error hint to no longer suggest reordering XML as a
fix, since that no longer works.

Add a regression test verifying the new behavior.
2026-07-31 18:30:39 +08:00
zhanghuanxu
11f34b3e85 fix(slides): detect text-line overlap in xml_text_overlap_lint 2026-07-31 18:30:39 +08:00
wangweiming-01
7946e5c81d feat: support source file preview artifacts (#2085) 2026-07-31 17:52:31 +08:00
zhouyue-bytedance
5cf09ecfda docs(base): clarify form and file operation routing (#2110)
* docs(base): clarify form and file operation routing

* docs: clarify complete base role table rules

* docs: clarify base advanced permission status

* docs: clarify base form field lifecycle

* docs: guide base form question creation

* fix(base): address form dry-run review findings

* docs(base): add complete editable role example

* fix(base): validate form question create inputs
2026-07-31 15:23:03 +08:00
chenxingyang1019
41692b7041 feat(apps): add cache debug commands (+cache-get/-delete/-clear) (#1896)
* feat(apps): add cache debug commands (+cache-get/-delete/-clear)

Add three apps-domain cache debug shortcuts for inspecting/clearing an app's
runtime cache:
- +cache-get: read a business key's value + metadata (hit/miss)
- +cache-delete: delete a single key (idempotent, write)
- +cache-clear: clear all cache in an environment (high-risk-write, --yes)

value renders raw on --format json, deserialized on --format pretty;
value_size_bytes is computed CLI-side; --environment auto-selects the branch
when omitted. Includes unit tests (hit/miss/dry-run/confirmation) and the
lark-apps cache skill reference.

* fix(apps): normalize cache numeric output fields and tidy comments

Follow-up hardening for the cache debug commands (+cache-get/-delete/-clear):

- Normalize ttl_ms / deleted_key_count via a new cacheInt() helper so
  --format json emits a stable JSON number (or null) regardless of whether
  the server sends the value as a number or a string. Aligns with the
  repo convention that numeric wire fields may arrive as strings; previously
  these were passed through raw, leaving the output type at the server's mercy.
- Add unit tests locking the string-wire -> JSON number contract for both
  cache-get ttl_ms and cache-delete deleted_key_count.
- Tidy two comments: soften cacheBool's speculative "historical wire form"
  claim to a defensive-tolerance note, and drop implementation jargon from
  cache-delete's risk-level rationale.
2026-07-31 14:13:56 +08:00
434 changed files with 6664 additions and 8554 deletions

View File

@@ -62,6 +62,19 @@ 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

View File

@@ -119,8 +119,6 @@ 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)

View File

@@ -62,21 +62,10 @@ 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 |
| `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/core/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.

View File

@@ -49,7 +49,6 @@ 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

View File

@@ -13,8 +13,7 @@ import (
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/client"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/identity"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/validate"
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
@@ -34,7 +33,7 @@ type APIOptions struct {
// Flags
Params string
Data string
As identity.Identity
As core.Identity
Output string
PageAll bool
PageSize int
@@ -88,7 +87,7 @@ Examples:
opts.Path = args[1]
opts.Cmd = cmd
opts.Ctx = cmd.Context()
opts.As = identity.Identity(asStr)
opts.As = core.Identity(asStr)
if runF != nil {
return runF(opts)
}
@@ -305,7 +304,7 @@ func apiRun(opts *APIOptions) error {
return nil
}
func apiDryRun(f *cmdutil.Factory, request client.RawApiRequest, config *configpkg.CliConfig, opts *APIOptions) error {
func apiDryRun(f *cmdutil.Factory, request client.RawApiRequest, config *core.CliConfig, opts *APIOptions) error {
return cmdutil.PrintDryRun(request, config, dryRunOutputOptions(f, opts))
}

View File

@@ -13,13 +13,11 @@ 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"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/httpmock"
"github.com/larksuite/cli/internal/identity"
"github.com/larksuite/cli/internal/output"
)
@@ -46,10 +44,10 @@ func newAPIPaginateTestHarness(t *testing.T) (*client.APIClient, *bytes.Buffer,
output.PendingNotice = nil
t.Cleanup(func() { output.PendingNotice = previousNotice })
config := &configpkg.CliConfig{
config := &core.CliConfig{
AppID: "test-app",
AppSecret: "test-secret",
Brand: brand.Feishu,
Brand: core.BrandFeishu,
}
f, out, errOut, reg := cmdutil.TestFactory(t, config)
ac, err := f.NewAPIClientWithConfig(config)
@@ -64,7 +62,7 @@ func apiPaginateRequest() client.RawApiRequest {
return client.RawApiRequest{
Method: "GET",
URL: "/open-apis/test/v1/items",
As: identity.AsBot,
As: core.AsBot,
}
}

View File

@@ -16,13 +16,11 @@ 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"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/httpmock"
"github.com/larksuite/cli/internal/identity"
"github.com/spf13/cobra"
)
@@ -42,8 +40,8 @@ func newTestRootCmd() *cobra.Command {
}
func TestApiCmd_FlagParsing(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
var gotOpts *APIOptions
@@ -62,7 +60,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 != identity.AsBot {
if gotOpts.As != core.AsBot {
t.Errorf("expected as=bot, got %s", gotOpts.As)
}
if !gotOpts.DryRun {
@@ -71,8 +69,8 @@ func TestApiCmd_FlagParsing(t *testing.T) {
}
func TestApiCmd_DryRun(t *testing.T) {
f, stdout, stderr, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, stdout, stderr, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := newTestApiCmd(f, nil)
@@ -106,8 +104,8 @@ func TestApiCmd_DryRun(t *testing.T) {
}
func TestApiCmd_DryRunWithJq(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := newTestApiCmd(f, nil)
@@ -124,8 +122,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, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := newTestApiCmd(f, nil)
@@ -139,8 +137,8 @@ func TestApiCmd_NullParamsWithPageSize(t *testing.T) {
}
func TestApiCmd_BotMode(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
// Register API endpoint stub
@@ -172,8 +170,8 @@ func TestApiCmd_BotMode(t *testing.T) {
}
func TestApiCmd_MissingArgs(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := newTestApiCmd(f, nil)
@@ -185,8 +183,8 @@ func TestApiCmd_MissingArgs(t *testing.T) {
}
func TestApiCmd_EmptyMethodRejected(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := newTestApiCmd(f, nil)
@@ -201,8 +199,8 @@ func TestApiCmd_EmptyMethodRejected(t *testing.T) {
}
func TestApiCmd_InvalidParamsJSON(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := newTestApiCmd(f, nil)
@@ -214,8 +212,8 @@ func TestApiCmd_InvalidParamsJSON(t *testing.T) {
}
func TestApiValidArgsFunction(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := newTestApiCmd(f, nil)
@@ -280,8 +278,8 @@ func TestApiValidArgsFunction(t *testing.T) {
}
func TestNewCmdApi_StrictModeHidesAsFlag(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu, SupportedIdentities: 2,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu, SupportedIdentities: 2,
})
cmd := newTestApiCmd(f, nil)
@@ -298,8 +296,8 @@ func TestNewCmdApi_StrictModeHidesAsFlag(t *testing.T) {
}
func TestApiCmd_PageLimitDefault(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
var gotOpts *APIOptions
@@ -318,8 +316,8 @@ func TestApiCmd_PageLimitDefault(t *testing.T) {
}
func TestApiCmd_ParamsAndDataBothStdinConflict(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := newTestApiCmd(f, nil)
@@ -334,8 +332,8 @@ func TestApiCmd_ParamsAndDataBothStdinConflict(t *testing.T) {
}
func TestApiCmd_OutputAndPageAllConflict(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
var gotOpts *APIOptions
@@ -357,8 +355,8 @@ func TestApiCmd_BinaryResponse_AutoSave(t *testing.T) {
dir := t.TempDir()
cmdutil.TestChdir(t, dir)
f, stdout, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-bin", AppSecret: "test-secret-bin", Brand: brand.Feishu,
f, stdout, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-bin", AppSecret: "test-secret-bin", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -407,8 +405,8 @@ func TestApiCmd_BinaryResponse_AutoSave(t *testing.T) {
}
func TestApiCmd_PageAll_NonBatchAPI_FallbackToJSON(t *testing.T) {
f, stdout, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-pageall1", AppSecret: "test-secret-pageall1", Brand: brand.Feishu,
f, stdout, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-pageall1", AppSecret: "test-secret-pageall1", Brand: core.BrandFeishu,
})
// Register a non-batch API that returns scalar data (no array field)
@@ -451,8 +449,8 @@ func TestApiCmd_PageAll_NonBatchAPI_FallbackToJSON(t *testing.T) {
}
func TestApiCmd_PageAll_NonBatchAPI_ErrorStillOutputsJSON(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-pageall-err", AppSecret: "test-secret-pageall-err", Brand: brand.Feishu,
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-pageall-err", AppSecret: "test-secret-pageall-err", Brand: core.BrandFeishu,
})
// Non-batch API that returns a business error (code != 0)
@@ -488,8 +486,8 @@ func TestApiCmd_PageAll_NonBatchAPI_ErrorStillOutputsJSON(t *testing.T) {
}
func TestApiCmd_PageAll_BatchAPI_StreamsItems(t *testing.T) {
f, stdout, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-pageall2", AppSecret: "test-secret-pageall2", Brand: brand.Feishu,
f, stdout, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-pageall2", AppSecret: "test-secret-pageall2", Brand: core.BrandFeishu,
})
// Register a batch API that returns an array field
@@ -521,8 +519,8 @@ func TestApiCmd_PageAll_BatchAPI_StreamsItems(t *testing.T) {
}
func TestApiCmd_PageAll_StreamBusinessErrorDoesNotDumpJSON(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-pageall-stream-err", AppSecret: "test-secret-pageall-stream-err", Brand: brand.Feishu,
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-pageall-stream-err", AppSecret: "test-secret-pageall-stream-err", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -563,8 +561,8 @@ func TestApiCmd_PageAll_StreamBusinessErrorDoesNotDumpJSON(t *testing.T) {
}
func TestApiCmd_PageAll_BatchAPI_DefaultJSONEnvelope(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-pageall-json", AppSecret: "test-secret-pageall-json", Brand: brand.Feishu,
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-pageall-json", AppSecret: "test-secret-pageall-json", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -629,8 +627,8 @@ func TestApiCmd_PageAll_DefaultJSONRunsContentSafety(t *testing.T) {
extcs.Register(provider)
t.Cleanup(func() { extcs.Register(nil) })
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-pageall-safety", AppSecret: "test-secret-pageall-safety", Brand: brand.Feishu,
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-pageall-safety", AppSecret: "test-secret-pageall-safety", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -680,8 +678,8 @@ func TestApiCmd_PageAll_StreamFormatRunsContentSafety(t *testing.T) {
extcs.Register(provider)
t.Cleanup(func() { extcs.Register(nil) })
f, stdout, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-pageall-stream-safety", AppSecret: "test-secret-pageall-stream-safety", Brand: brand.Feishu,
f, stdout, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-pageall-stream-safety", AppSecret: "test-secret-pageall-stream-safety", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -725,8 +723,8 @@ func TestApiCmd_PageAll_StreamFormatBlockSkipsBlockedPage(t *testing.T) {
extcs.Register(provider)
t.Cleanup(func() { extcs.Register(nil) })
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-pageall-stream-block", AppSecret: "test-secret-pageall-stream-block", Brand: brand.Feishu,
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-pageall-stream-block", AppSecret: "test-secret-pageall-stream-block", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -811,8 +809,8 @@ func TestNormalisePath_StripsQueryAndFragment(t *testing.T) {
}
func TestApiCmd_JqFlag_Parsing(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
var gotOpts *APIOptions
@@ -831,8 +829,8 @@ func TestApiCmd_JqFlag_Parsing(t *testing.T) {
}
func TestApiCmd_JqFlag_ShortForm(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
var gotOpts *APIOptions
@@ -851,8 +849,8 @@ func TestApiCmd_JqFlag_ShortForm(t *testing.T) {
}
func TestApiCmd_JqAndOutputConflict(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
@@ -869,8 +867,8 @@ func TestApiCmd_JqAndOutputConflict(t *testing.T) {
}
func TestApiCmd_JqFilter_AppliesExpression(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-jq", AppSecret: "test-secret-jq", Brand: brand.Feishu,
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-jq", AppSecret: "test-secret-jq", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -903,8 +901,8 @@ func TestApiCmd_JqFilter_AppliesExpression(t *testing.T) {
}
func TestApiCmd_JqAndFormatConflict(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
@@ -921,8 +919,8 @@ func TestApiCmd_JqAndFormatConflict(t *testing.T) {
}
func TestApiCmd_JqInvalidExpression(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
@@ -939,8 +937,8 @@ func TestApiCmd_JqInvalidExpression(t *testing.T) {
}
func TestApiCmd_PageAll_WithJq(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-pjq", AppSecret: "test-secret-pjq", Brand: brand.Feishu,
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-pjq", AppSecret: "test-secret-pjq", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -970,8 +968,8 @@ func TestApiCmd_PageAll_WithJq(t *testing.T) {
}
func TestApiCmd_MethodUppercase(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
var gotOpts *APIOptions
@@ -990,8 +988,8 @@ func TestApiCmd_MethodUppercase(t *testing.T) {
}
func TestApiCmd_FileFlagParsing(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
var gotOpts *APIOptions
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
@@ -1009,8 +1007,8 @@ func TestApiCmd_FileFlagParsing(t *testing.T) {
}
func TestApiCmd_FileAndOutputConflict(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
return apiRun(opts)
@@ -1026,8 +1024,8 @@ func TestApiCmd_FileAndOutputConflict(t *testing.T) {
}
func TestApiCmd_FileWithGET(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
return apiRun(opts)
@@ -1043,8 +1041,8 @@ func TestApiCmd_FileWithGET(t *testing.T) {
}
func TestApiCmd_FileStdinConflictWithData(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := newTestApiCmd(f, func(opts *APIOptions) error {
return apiRun(opts)
@@ -1066,8 +1064,8 @@ func TestApiCmd_DryRunWithFile(t *testing.T) {
t.Fatal(err)
}
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := newTestApiCmd(f, nil)
cmd.SetArgs([]string{"POST", "/open-apis/im/v1/images", "--file", "image=" + tmpFile, "--data", `{"image_type":"message"}`, "--dry-run", "--as", "bot"})
@@ -1104,8 +1102,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, &configpkg.CliConfig{
AppID: "cli_test_perm", AppSecret: "secret", Brand: brand.Feishu,
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "cli_test_perm", AppSecret: "secret", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -1143,8 +1141,8 @@ func TestApiCmd_PermissionError_DerivesFirstClassFields(t *testing.T) {
}
func TestApiCmd_JsonFlag_Accepted(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
var gotOpts *APIOptions
@@ -1196,8 +1194,8 @@ func parseMultipartFilenames(t *testing.T, stub *httpmock.Stub) (map[string]stri
}
func TestApiCmd_FileUpload_PreservesFilename(t *testing.T) {
f, _, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
dir := t.TempDir()
@@ -1225,8 +1223,8 @@ func TestApiCmd_FileUpload_PreservesFilename(t *testing.T) {
}
func TestApiCmd_FileUpload_FieldPrefixKeepsBasename(t *testing.T) {
f, _, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
dir := t.TempDir()
@@ -1260,8 +1258,8 @@ func TestApiCmd_FileUpload_FieldPrefixKeepsBasename(t *testing.T) {
}
func TestApiCmd_FileUpload_WithDataFields(t *testing.T) {
f, _, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
dir := t.TempDir()
@@ -1293,8 +1291,8 @@ func TestApiCmd_FileUpload_WithDataFields(t *testing.T) {
}
func TestApiCmd_FileUpload_StdinFallsBackToUnknown(t *testing.T) {
f, _, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
f.IOStreams.In = bytes.NewReader([]byte("stdin-bytes"))

View File

@@ -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,
}, identity.AsBot)
}, core.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(identity.AsBot)}
cc := errclass.ClassifyContext{Identity: string(core.AsBot)}
if cfg, _ := f.Config(); cfg != nil {
cc.Brand = string(cfg.Brand)
cc.AppID = appId

View File

@@ -12,11 +12,10 @@ 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"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/credential"
"github.com/larksuite/cli/internal/httpmock"
"github.com/larksuite/cli/internal/output"
@@ -24,8 +23,8 @@ import (
)
func TestAuthLoginCmd_FlagParsing(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
var gotOpts *LoginOptions
@@ -47,8 +46,8 @@ func TestAuthLoginCmd_FlagParsing(t *testing.T) {
}
func TestAuthLoginCmd_HelpGuidesNonStreamingAgentsToSplitFlow(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := NewCmdAuthLogin(f, func(opts *LoginOptions) error { return nil })
@@ -73,8 +72,8 @@ func TestAuthLoginCmd_HelpGuidesNonStreamingAgentsToSplitFlow(t *testing.T) {
}
func TestAuthCheckCmd_FlagParsing(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
var gotOpts *CheckOptions
@@ -93,8 +92,8 @@ func TestAuthCheckCmd_FlagParsing(t *testing.T) {
}
func TestAuthCheckCmd_AcceptsJSONFlag(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
var gotOpts *CheckOptions
@@ -193,8 +192,8 @@ func TestAuthListCmd_AcceptsJSONFlag(t *testing.T) {
}
func TestAuthStatusCmd_FlagParsing(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
var gotOpts *StatusOptions
@@ -212,8 +211,8 @@ func TestAuthStatusCmd_FlagParsing(t *testing.T) {
}
func TestAuthStatusCmd_AcceptsJSONFlag(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
var gotOpts *StatusOptions
@@ -235,8 +234,8 @@ func TestAuthStatusCmd_AcceptsJSONFlag(t *testing.T) {
}
func TestAuthStatusCmd_VerifyFlag(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
var gotOpts *StatusOptions
@@ -337,8 +336,8 @@ func TestDomainFlagCompletion(t *testing.T) {
}
func TestAuthScopesCmd_FlagParsing(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
var gotOpts *ScopesOptions
@@ -357,8 +356,8 @@ func TestAuthScopesCmd_FlagParsing(t *testing.T) {
}
func TestAuthScopesCmd_JSONFlagForcesJSONFormat(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
var gotOpts *ScopesOptions
@@ -383,8 +382,8 @@ func TestAuthScopesCmd_JSONFlagForcesJSONFormat(t *testing.T) {
}
func TestAuthScopesRun_UsesTenantAccessTokenFromCredentialProvider(t *testing.T) {
f, _, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "", Brand: brand.Feishu,
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "", Brand: core.BrandFeishu,
})
tokenResolver := &authScopesTokenResolver{}
f.Credential = credential.NewCredentialProvider(nil, nil, tokenResolver, nil)
@@ -439,8 +438,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, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
tokenResolver := &authScopesTokenResolver{}
f.Credential = credential.NewCredentialProvider(nil, nil, tokenResolver, nil)

View File

@@ -9,10 +9,9 @@ import (
"testing"
"time"
"github.com/larksuite/cli/brand"
larkauth "github.com/larksuite/cli/internal/auth"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/output"
"github.com/zalando/go-keyring"
)
@@ -24,8 +23,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, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, stdout, stderr, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
// UserOpenId left empty: triggers the not_logged_in branch.
})
@@ -56,8 +55,8 @@ func TestAuthCheckRun_NotLoggedIn_ExitOneWithStdoutOnly(t *testing.T) {
}
func TestAuthCheckRun_NoStoredToken_ExitOneWithStdoutOnly(t *testing.T) {
f, stdout, stderr, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, stdout, stderr, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
UserOpenId: "ou_user", UserName: "tester",
})
@@ -93,10 +92,10 @@ func TestAuthCheckRun_ScopedTokenPresent_ExitZero(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("LARKSUITE_CLI_DATA_DIR", t.TempDir())
cfg := &configpkg.CliConfig{
cfg := &core.CliConfig{
AppID: "test-app",
AppSecret: "test-secret",
Brand: brand.Feishu,
Brand: core.BrandFeishu,
UserOpenId: "ou_user",
UserName: "tester",
}
@@ -151,8 +150,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, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
err := authCheckRun(&CheckOptions{Factory: f, Scope: " "})

View File

@@ -12,7 +12,7 @@ import (
"github.com/larksuite/cli/errs"
larkauth "github.com/larksuite/cli/internal/auth"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"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, _ := configpkg.LoadMultiAppConfig()
multi, _ := core.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(configpkg.NotConfiguredError(), &cfgErr) {
if errors.As(core.NotConfiguredError(), &cfgErr) {
fmt.Fprintln(f.IOStreams.ErrOut, cfgErr.Message)
if cfgErr.Hint != "" {
fmt.Fprintln(f.IOStreams.ErrOut, " hint: "+cfgErr.Hint)

View File

@@ -9,7 +9,7 @@ import (
"testing"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/workspace"
"github.com/larksuite/cli/internal/core"
)
// 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 := workspace.CurrentWorkspace()
t.Cleanup(func() { workspace.SetCurrentWorkspace(prev) })
workspace.SetCurrentWorkspace(workspace.WorkspaceOpenClaw)
prev := core.CurrentWorkspace()
t.Cleanup(func() { core.SetCurrentWorkspace(prev) })
core.SetCurrentWorkspace(core.WorkspaceOpenClaw)
f, _, stderr, _ := cmdutil.TestFactory(t, nil)
if err := authListRun(&ListOptions{Factory: f}); err != nil {

View File

@@ -13,14 +13,12 @@ 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"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"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"
@@ -57,7 +55,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 == identity.StrictModeBot {
if mode := f.ResolveStrictMode(cmd.Context()); mode == core.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)")
@@ -74,7 +72,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 brandpkg.Brand
var helpBrand core.LarkBrand
if f != nil && f.Config != nil {
if cfg, err := f.Config(); err == nil && cfg != nil {
helpBrand = cfg.Brand
@@ -127,7 +125,7 @@ func authLoginRun(opts *LoginOptions) error {
// Determine UI language from saved config
var lang i18n.Lang
if multi, _ := configpkg.LoadMultiAppConfig(); multi != nil {
if multi, _ := core.LoadMultiAppConfig(); multi != nil {
if app := multi.FindApp(config.ProfileName); app != nil {
lang = app.Lang
}
@@ -393,7 +391,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 *configpkg.CliConfig, msg *loginMsg, log func(string, ...interface{})) error {
func authLoginPollDeviceCode(opts *LoginOptions, config *core.CliConfig, msg *loginMsg, log func(string, ...interface{})) error {
f := opts.Factory
httpClient, err := f.HttpClient()
@@ -476,7 +474,7 @@ func authLoginPollDeviceCode(opts *LoginOptions, config *configpkg.CliConfig, ms
// syncLoginUserToProfile persists the logged-in user info into the named profile.
func syncLoginUserToProfile(profileName, appID, openID, userName string) error {
multi, err := configpkg.LoadMultiAppConfig()
multi, err := core.LoadMultiAppConfig()
if err != nil {
return errs.NewInternalError(errs.SubtypeStorage, "load config: %v", err).WithCause(err)
}
@@ -486,9 +484,9 @@ func syncLoginUserToProfile(profileName, appID, openID, userName string) error {
return errs.NewConfigError(errs.SubtypeNotConfigured, "profile %q not found in config", profileName)
}
oldUsers := append([]configpkg.AppUser(nil), app.Users...)
app.Users = []configpkg.AppUser{{UserOpenId: openID, UserName: userName}}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
oldUsers := append([]core.AppUser(nil), app.Users...)
app.Users = []core.AppUser{{UserOpenId: openID, UserName: userName}}
if err := core.SaveMultiAppConfig(multi); err != nil {
return errs.NewInternalError(errs.SubtypeStorage, "save config: %v", err).WithCause(err)
}
@@ -501,7 +499,7 @@ func syncLoginUserToProfile(profileName, appID, openID, userName string) error {
}
// findProfileByName returns the AppConfig matching profileName, or nil.
func findProfileByName(multi *configpkg.MultiAppConfig, profileName string) *configpkg.AppConfig {
func findProfileByName(multi *core.MultiAppConfig, profileName string) *core.AppConfig {
for i := range multi.Apps {
if multi.Apps[i].ProfileName() == profileName {
return &multi.Apps[i]
@@ -514,7 +512,7 @@ func findProfileByName(multi *configpkg.MultiAppConfig, profileName string) *con
// 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 brandpkg.Brand) []string {
func collectScopesForDomains(domains []string, identity string, brand core.LarkBrand) []string {
scopeSet := make(map[string]bool)
// 1. API scopes from from_meta projects
@@ -555,7 +553,7 @@ func collectScopesForDomains(domains []string, identity string, brand brandpkg.B
// 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 brandpkg.Brand) map[string]bool {
func allKnownDomains(brand core.LarkBrand) map[string]bool {
domains := make(map[string]bool)
for _, p := range registry.ListFromMetaProjects() {
if !registry.HasAuthDomain(p) {
@@ -574,7 +572,7 @@ func allKnownDomains(brand brandpkg.Brand) map[string]bool {
}
// sortedKnownDomains returns all valid domain names sorted alphabetically.
func sortedKnownDomains(brand brandpkg.Brand) []string {
func sortedKnownDomains(brand core.LarkBrand) []string {
m := allKnownDomains(brand)
domains := make([]string, 0, len(m))
for d := range m {

View File

@@ -6,26 +6,26 @@ package auth
import (
"testing"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/internal/core"
)
func TestBrandFilter_AppsExcludedOnLark(t *testing.T) {
feishuDomains := allKnownDomains(brand.Feishu)
feishuDomains := allKnownDomains(core.BrandFeishu)
if !feishuDomains["apps"] {
t.Errorf("expected apps domain to be known on Feishu brand")
}
larkDomains := allKnownDomains(brand.Lark)
larkDomains := allKnownDomains(core.BrandLark)
if larkDomains["apps"] {
t.Errorf("expected apps domain to be EXCLUDED on Lark brand")
}
feishuScopes := collectScopesForDomains([]string{"apps"}, "user", brand.Feishu)
feishuScopes := collectScopesForDomains([]string{"apps"}, "user", core.BrandFeishu)
if len(feishuScopes) == 0 {
t.Errorf("expected non-empty scopes for apps on Feishu brand, got %d", len(feishuScopes))
}
larkScopes := collectScopesForDomains([]string{"apps"}, "user", brand.Lark)
larkScopes := collectScopesForDomains([]string{"apps"}, "user", core.BrandLark)
if len(larkScopes) != 0 {
t.Errorf("expected empty scopes for apps on Lark brand, got %d: %v", len(larkScopes), larkScopes)
}

View File

@@ -7,7 +7,7 @@ import (
"strings"
"testing"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
)
func setupLoginConfigDir(t *testing.T) {
@@ -17,22 +17,22 @@ func setupLoginConfigDir(t *testing.T) {
func TestSyncLoginUserToProfile_UpdatesOnlyTargetProfile(t *testing.T) {
setupLoginConfigDir(t)
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "target",
Apps: []configpkg.AppConfig{
Apps: []core.AppConfig{
{
Name: "target",
AppId: "app-target",
Users: []configpkg.AppUser{{UserOpenId: "ou_old", UserName: "old"}},
Users: []core.AppUser{{UserOpenId: "ou_old", UserName: "old"}},
},
{
Name: "other",
AppId: "app-other",
Users: []configpkg.AppUser{{UserOpenId: "ou_other", UserName: "other"}},
Users: []core.AppUser{{UserOpenId: "ou_other", UserName: "other"}},
},
},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.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 := configpkg.LoadMultiAppConfig()
saved, err := core.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 := &configpkg.MultiAppConfig{
Apps: []configpkg.AppConfig{{
multi := &core.MultiAppConfig{
Apps: []core.AppConfig{{
Name: "default",
AppId: "app-default",
}},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}

View File

@@ -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 brandpkg.Brand) (*interactiveResult, error) {
func runInteractiveLogin(ios *cmdutil.IOStreams, lang string, msg *loginMsg, brand core.LarkBrand) (*interactiveResult, error) {
allDomains := getDomainMetadata(lang)
// Build multi-select options

View File

@@ -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(workspace.GetConfigDir(), "cache", "auth_login_scopes")
return filepath.Join(core.GetConfigDir(), "cache", "auth_login_scopes")
}
// loginScopeCachePath returns the cache file path for a given device_code.

View File

@@ -9,11 +9,11 @@ import (
extcred "github.com/larksuite/cli/extension/credential"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
)
func TestAuthLogin_StrictModeBot_Blocked(t *testing.T) {
cfg := &configpkg.CliConfig{
cfg := &core.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 := &configpkg.CliConfig{
cfg := &core.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, &configpkg.CliConfig{AppID: "a", AppSecret: "s"})
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "a", AppSecret: "s"})
var called bool
cmd := NewCmdAuthLogin(f, func(opts *LoginOptions) error {

View File

@@ -14,10 +14,9 @@ import (
"strings"
"testing"
brandpkg "github.com/larksuite/cli/brand"
larkauth "github.com/larksuite/cli/internal/auth"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/httpmock"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/registry"
@@ -309,8 +308,8 @@ func TestGetDomainMetadata_HasTitleAndDescription(t *testing.T) {
}
func TestAuthLoginRun_NonTerminal_NoFlags_RejectsWithHint(t *testing.T) {
f, _, stderr, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "cli_test", AppSecret: "secret", Brand: brandpkg.Feishu,
f, _, stderr, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "cli_test", AppSecret: "secret", Brand: core.BrandFeishu,
})
// TestFactory has IsTerminal=false by default
opts := &LoginOptions{Factory: f, Ctx: context.Background()}
@@ -601,21 +600,21 @@ func TestAuthLoginRun_MissingRequestedScopeAlignsWithLoginSuccess(t *testing.T)
setupLoginConfigDir(t)
t.Setenv("HOME", t.TempDir())
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "default",
Apps: []configpkg.AppConfig{
Apps: []core.AppConfig{
{Name: "default", AppId: "cli_test"},
},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
f, _, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
f, _, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
ProfileName: "default",
AppID: "cli_test",
AppSecret: "secret",
Brand: brandpkg.Feishu,
Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -697,7 +696,7 @@ func TestAuthLoginRun_MissingRequestedScopeAlignsWithLoginSuccess(t *testing.T)
if stored.Scope != "offline_access" {
t.Fatalf("stored scope = %q", stored.Scope)
}
cfg, err := configpkg.LoadMultiAppConfig()
cfg, err := core.LoadMultiAppConfig()
if err != nil {
t.Fatalf("LoadMultiAppConfig() error = %v", err)
}
@@ -717,21 +716,21 @@ func TestAuthLoginRun_DeviceCodeUsesCachedRequestedScopes(t *testing.T) {
setupLoginConfigDir(t)
t.Setenv("HOME", t.TempDir())
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "default",
Apps: []configpkg.AppConfig{
Apps: []core.AppConfig{
{Name: "default", AppId: "cli_test"},
},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
f, stdout, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
f, stdout, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
ProfileName: "default",
AppID: "cli_test",
AppSecret: "secret",
Brand: brandpkg.Feishu,
Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -848,15 +847,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 brandpkg.Brand, deviceCode string, interval, expiresIn int, errOut io.Writer) *larkauth.DeviceFlowResult {
pollDeviceToken = func(ctx context.Context, httpClient *http.Client, appId, appSecret string, brand core.LarkBrand, deviceCode string, interval, expiresIn int, errOut io.Writer) *larkauth.DeviceFlowResult {
return &larkauth.DeviceFlowResult{OK: true, Token: nil}
}
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
ProfileName: "default",
AppID: "cli_test",
AppSecret: "secret",
Brand: brandpkg.Feishu,
Brand: core.BrandFeishu,
})
err := authLoginRun(&LoginOptions{
@@ -887,15 +886,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 brandpkg.Brand, deviceCode string, interval, expiresIn int, errOut io.Writer) *larkauth.DeviceFlowResult {
pollDeviceToken = func(ctx context.Context, httpClient *http.Client, appId, appSecret string, brand core.LarkBrand, 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, &configpkg.CliConfig{
f, stdout, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
ProfileName: "default",
AppID: "cli_test",
AppSecret: "secret",
Brand: brandpkg.Feishu,
Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -957,11 +956,11 @@ func TestAuthLoginRun_JSONAbort_StdoutEventOnly_StderrEmpty(t *testing.T) {
}
func TestAuthLoginRun_JSONWriteFailure_NoWaitReturnsWriterError(t *testing.T) {
f, _, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
ProfileName: "default",
AppID: "cli_test",
AppSecret: "secret",
Brand: brandpkg.Feishu,
Brand: core.BrandFeishu,
})
f.IOStreams.Out = failWriter{}
@@ -994,11 +993,11 @@ func TestAuthLoginRun_JSONWriteFailure_NoWaitReturnsWriterError(t *testing.T) {
}
func TestAuthLoginRun_NoWaitJSONHintIncludesRawURLGuidance(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
ProfileName: "default",
AppID: "cli_test",
AppSecret: "secret",
Brand: brandpkg.Feishu,
Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -1068,11 +1067,11 @@ func TestAuthLoginRun_NoWaitJSONHintIncludesRawURLGuidance(t *testing.T) {
}
func TestAuthLoginRun_JSONWriteFailure_DeviceAuthorizationReturnsWriterError(t *testing.T) {
f, _, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
ProfileName: "default",
AppID: "cli_test",
AppSecret: "secret",
Brand: brandpkg.Feishu,
Brand: core.BrandFeishu,
})
f.IOStreams.Out = failWriter{}
@@ -1106,11 +1105,11 @@ func TestAuthLoginRun_JSONWriteFailure_DeviceAuthorizationReturnsWriterError(t *
}
func TestAuthLoginRun_JSONDeviceAuthorizationAgentHintIncludesRawURLGuidance(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
ProfileName: "default",
AppID: "cli_test",
AppSecret: "secret",
Brand: brandpkg.Feishu,
Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{

View File

@@ -11,9 +11,8 @@ import (
"github.com/larksuite/cli/errs"
larkauth "github.com/larksuite/cli/internal/auth"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/secret"
)
// LogoutOptions holds all inputs for auth logout.
@@ -45,7 +44,7 @@ func NewCmdAuthLogout(f *cmdutil.Factory, runF func(*LogoutOptions) error) *cobr
func authLogoutRun(opts *LogoutOptions) error {
f := opts.Factory
multi, _ := configpkg.LoadMultiAppConfig()
multi, _ := core.LoadMultiAppConfig()
if multi == nil || len(multi.Apps) == 0 {
if opts.JSON {
output.PrintJson(f.IOStreams.Out, map[string]interface{}{
@@ -74,7 +73,7 @@ func authLogoutRun(opts *LogoutOptions) error {
}
httpClient, httpErr := f.HttpClient()
appSecret, secretErr := secret.ResolveSecretInput(app.AppSecret, f.Keychain)
appSecret, secretErr := core.ResolveSecretInput(app.AppSecret, f.Keychain)
for _, user := range app.Users {
if httpErr == nil && secretErr == nil {
@@ -95,8 +94,8 @@ func authLogoutRun(opts *LogoutOptions) error {
}
}
app.Users = []configpkg.AppUser{}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
app.Users = []core.AppUser{}
if err := core.SaveMultiAppConfig(multi); err != nil {
return errs.NewInternalError(errs.SubtypeStorage, "failed to save config: %v", err).WithCause(err)
}
if opts.JSON {

View File

@@ -9,24 +9,22 @@ import (
"strings"
"testing"
"github.com/larksuite/cli/brand"
larkauth "github.com/larksuite/cli/internal/auth"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/httpmock"
"github.com/larksuite/cli/internal/secret"
"github.com/zalando/go-keyring"
)
func writeLogoutConfig(t *testing.T, users []configpkg.AppUser) {
func writeLogoutConfig(t *testing.T, users []core.AppUser) {
t.Helper()
if err := configpkg.SaveMultiAppConfig(&configpkg.MultiAppConfig{
if err := core.SaveMultiAppConfig(&core.MultiAppConfig{
CurrentApp: "test-app",
Apps: []configpkg.AppConfig{
Apps: []core.AppConfig{
{
AppId: "test-app",
AppSecret: secret.PlainSecret("test-secret"),
Brand: brand.Feishu,
AppSecret: core.PlainSecret("test-secret"),
Brand: core.BrandFeishu,
Users: users,
},
},
@@ -93,7 +91,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, []configpkg.AppUser{{UserOpenId: "ou_user", UserName: "tester"}})
writeLogoutConfig(t, []core.AppUser{{UserOpenId: "ou_user", UserName: "tester"}})
if err := larkauth.SetStoredToken(&larkauth.StoredUAToken{
AppId: "test-app",
UserOpenId: "ou_user",
@@ -129,7 +127,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, []configpkg.AppUser{{UserOpenId: "ou_user", UserName: "tester"}})
writeLogoutConfig(t, []core.AppUser{{UserOpenId: "ou_user", UserName: "tester"}})
if err := larkauth.SetStoredToken(&larkauth.StoredUAToken{
AppId: "test-app",
UserOpenId: "ou_user",
@@ -155,19 +153,19 @@ func TestAuthLogoutRun_RevokesTokenAndClearsLocalState(t *testing.T) {
setupLoginConfigDir(t)
t.Setenv("HOME", t.TempDir())
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "default",
Apps: []configpkg.AppConfig{
Apps: []core.AppConfig{
{
Name: "default",
AppId: "cli_test",
AppSecret: secret.PlainSecret("secret"),
Brand: brand.Feishu,
Users: []configpkg.AppUser{{UserOpenId: "ou_user", UserName: "tester"}},
AppSecret: core.PlainSecret("secret"),
Brand: core.BrandFeishu,
Users: []core.AppUser{{UserOpenId: "ou_user", UserName: "tester"}},
},
},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
if err := larkauth.SetStoredToken(&larkauth.StoredUAToken{
@@ -179,11 +177,11 @@ func TestAuthLogoutRun_RevokesTokenAndClearsLocalState(t *testing.T) {
t.Fatalf("SetStoredToken() error = %v", err)
}
f, _, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
f, _, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
ProfileName: "default",
AppID: "cli_test",
AppSecret: "secret",
Brand: brand.Feishu,
Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -212,7 +210,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 := configpkg.LoadMultiAppConfig()
saved, err := core.LoadMultiAppConfig()
if err != nil {
t.Fatalf("LoadMultiAppConfig() error = %v", err)
}
@@ -226,19 +224,19 @@ func TestAuthLogoutRun_FallsBackToAccessTokenWhenRefreshTokenMissing(t *testing.
setupLoginConfigDir(t)
t.Setenv("HOME", t.TempDir())
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "default",
Apps: []configpkg.AppConfig{
Apps: []core.AppConfig{
{
Name: "default",
AppId: "cli_test",
AppSecret: secret.PlainSecret("secret"),
Brand: brand.Feishu,
Users: []configpkg.AppUser{{UserOpenId: "ou_user", UserName: "tester"}},
AppSecret: core.PlainSecret("secret"),
Brand: core.BrandFeishu,
Users: []core.AppUser{{UserOpenId: "ou_user", UserName: "tester"}},
},
},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
if err := larkauth.SetStoredToken(&larkauth.StoredUAToken{
@@ -249,11 +247,11 @@ func TestAuthLogoutRun_FallsBackToAccessTokenWhenRefreshTokenMissing(t *testing.
t.Fatalf("SetStoredToken() error = %v", err)
}
f, _, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
f, _, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
ProfileName: "default",
AppID: "cli_test",
AppSecret: "secret",
Brand: brand.Feishu,
Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -282,7 +280,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 := configpkg.LoadMultiAppConfig()
saved, err := core.LoadMultiAppConfig()
if err != nil {
t.Fatalf("LoadMultiAppConfig() error = %v", err)
}
@@ -296,19 +294,19 @@ func TestAuthLogoutRun_RevokeFailureStillClearsLocalState(t *testing.T) {
setupLoginConfigDir(t)
t.Setenv("HOME", t.TempDir())
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "default",
Apps: []configpkg.AppConfig{
Apps: []core.AppConfig{
{
Name: "default",
AppId: "cli_test",
AppSecret: secret.PlainSecret("secret"),
Brand: brand.Feishu,
Users: []configpkg.AppUser{{UserOpenId: "ou_user", UserName: "tester"}},
AppSecret: core.PlainSecret("secret"),
Brand: core.BrandFeishu,
Users: []core.AppUser{{UserOpenId: "ou_user", UserName: "tester"}},
},
},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
if err := larkauth.SetStoredToken(&larkauth.StoredUAToken{
@@ -320,11 +318,11 @@ func TestAuthLogoutRun_RevokeFailureStillClearsLocalState(t *testing.T) {
t.Fatalf("SetStoredToken() error = %v", err)
}
f, _, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
f, _, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
ProfileName: "default",
AppID: "cli_test",
AppSecret: "secret",
Brand: brand.Feishu,
Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -348,7 +346,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 := configpkg.LoadMultiAppConfig()
saved, err := core.LoadMultiAppConfig()
if err != nil {
t.Fatalf("LoadMultiAppConfig() error = %v", err)
}

View File

@@ -11,15 +11,14 @@ import (
"strings"
"testing"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/output"
)
func TestNewCmdAuthQRCode_FlagParsing(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
var gotOpts *QRCodeOptions
@@ -46,8 +45,8 @@ func TestNewCmdAuthQRCode_FlagParsing(t *testing.T) {
}
func TestNewCmdAuthQRCode_ASCIIFlag(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
var gotOpts *QRCodeOptions

View File

@@ -9,10 +9,9 @@ import (
"fmt"
"testing"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
)
// stubGetAppInfoErr swaps getAppInfoFn for the duration of t so authScopesRun
@@ -32,10 +31,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, &configpkg.CliConfig{
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app",
AppSecret: "test-secret",
Brand: brand.Feishu,
Brand: core.BrandFeishu,
})
return &ScopesOptions{
Factory: f,

View File

@@ -8,15 +8,14 @@ import (
"net/http"
"testing"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/httpmock"
)
func TestAuthStatusRun_SplitsBotAndUserIdentity(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "secret", Brand: brand.Feishu,
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "secret", Brand: core.BrandFeishu,
})
if err := authStatusRun(&StatusOptions{Factory: f}); err != nil {
@@ -39,8 +38,8 @@ func TestAuthStatusRun_SplitsBotAndUserIdentity(t *testing.T) {
}
func TestAuthStatusRun_VerifyReportsBotIdentity(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "secret", Brand: brand.Feishu,
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "secret", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
Method: http.MethodGet,

View File

@@ -8,7 +8,6 @@ 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"
@@ -26,6 +25,7 @@ import (
"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"
@@ -44,7 +44,7 @@ type buildConfig struct {
skipStrictMode bool
skipService bool
serviceCatalog *apicatalog.Catalog
startupBrand brandpkg.Brand
startupBrand core.LarkBrand
}
// WithStartupBrand initializes the API registry with the given brand before
@@ -52,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 brandpkg.Brand) BuildOption {
func WithStartupBrand(brand core.LarkBrand) BuildOption {
return func(c *buildConfig) {
c.startupBrand = brand
}

View File

@@ -14,15 +14,12 @@ import (
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"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.
@@ -131,8 +128,8 @@ func configBindRun(opts *BindOptions) error {
if err != nil {
return err
}
workspace.SetCurrentWorkspace(workspace.Workspace(source))
targetConfigPath := workspace.GetConfigPath()
core.SetCurrentWorkspace(core.Workspace(source))
targetConfigPath := core.GetConfigPath()
existing, err := reconcileExistingBinding(opts, source, targetConfigPath)
if err != nil {
@@ -189,12 +186,12 @@ func finalizeSource(opts *BindOptions) (string, error) {
}
var detected string
switch workspace.DetectWorkspaceFromEnv(os.Getenv) {
case workspace.WorkspaceOpenClaw:
switch core.DetectWorkspaceFromEnv(os.Getenv) {
case core.WorkspaceOpenClaw:
detected = "openclaw"
case workspace.WorkspaceHermes:
case core.WorkspaceHermes:
detected = "hermes"
case workspace.WorkspaceLarkChannel:
case core.WorkspaceLarkChannel:
detected = "lark-channel"
}
@@ -267,7 +264,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) (*configpkg.AppConfig, error) {
func resolveAccount(opts *BindOptions, source string) (*core.AppConfig, error) {
binder, err := newBinder(source, opts)
if err != nil {
return nil, err
@@ -310,12 +307,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 configpkg.MultiAppConfig
var multi core.MultiAppConfig
if err := json.Unmarshal(data, &multi); err != nil {
return false
}
for _, app := range multi.Apps {
if app.StrictMode != nil && *app.StrictMode == identity.StrictModeBot {
if app.StrictMode != nil && *app.StrictMode == core.StrictModeBot {
return true
}
}
@@ -372,16 +369,16 @@ func preferredLang(requested, prior i18n.Lang) i18n.Lang {
return prior
}
func applyPreferences(appConfig *configpkg.AppConfig, opts *BindOptions, prior i18n.Lang) {
func applyPreferences(appConfig *core.AppConfig, opts *BindOptions, prior i18n.Lang) {
switch opts.Identity {
case "bot-only":
sm := identity.StrictModeBot
sm := core.StrictModeBot
appConfig.StrictMode = &sm
appConfig.DefaultAs = identity.AsBot
appConfig.DefaultAs = core.AsBot
case "user-default":
sm := identity.StrictModeOff
sm := core.StrictModeOff
appConfig.StrictMode = &sm
appConfig.DefaultAs = identity.AsUser
appConfig.DefaultAs = core.AsUser
}
appConfig.Lang = preferredLang(i18n.Lang(opts.Lang), prior)
}
@@ -392,7 +389,7 @@ func applyPreferences(appConfig *configpkg.AppConfig, opts *BindOptions, prior i
// 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 configpkg.MultiAppConfig
var multi core.MultiAppConfig
if json.Unmarshal(previousConfigBytes, &multi) != nil {
return ""
}
@@ -407,10 +404,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 *configpkg.AppConfig, previousConfigBytes []byte, source, configPath string) error {
multi := &configpkg.MultiAppConfig{Apps: []configpkg.AppConfig{*appConfig}}
func commitBinding(opts *BindOptions, appConfig *core.AppConfig, previousConfigBytes []byte, source, configPath string) error {
multi := &core.MultiAppConfig{Apps: []core.AppConfig{*appConfig}}
if err := vfs.MkdirAll(workspace.GetConfigDir(), 0700); err != nil {
if err := vfs.MkdirAll(core.GetConfigDir(), 0700); err != nil {
return errs.NewInternalError(errs.SubtypeFileIO, "failed to create workspace directory: %v", err).WithCause(err)
}
data, err := json.MarshalIndent(multi, "", " ")
@@ -479,8 +476,8 @@ func commitBinding(opts *BindOptions, appConfig *configpkg.AppConfig, previousCo
// 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 *configpkg.AppConfig) {
var multi configpkg.MultiAppConfig
func cleanupKeychainFromData(kc keychain.KeychainAccess, data []byte, keep *core.AppConfig) {
var multi core.MultiAppConfig
if err := json.Unmarshal(data, &multi); err != nil {
return
}
@@ -492,7 +489,7 @@ func cleanupKeychainFromData(kc keychain.KeychainAccess, data []byte, keep *conf
if keepID != "" && app.AppSecret.Ref != nil && app.AppSecret.Ref.Source == "keychain" && app.AppSecret.Ref.ID == keepID {
continue
}
secret.RemoveSecretStore(app.AppSecret, kc)
core.RemoveSecretStore(app.AppSecret, kc)
}
}
@@ -506,13 +503,13 @@ func tuiSelectSource(opts *BindOptions) (string, error) {
var source string
// Pre-select based on detected env signals
detected := workspace.DetectWorkspaceFromEnv(os.Getenv)
detected := core.DetectWorkspaceFromEnv(os.Getenv)
switch detected {
case workspace.WorkspaceOpenClaw:
case core.WorkspaceOpenClaw:
source = "openclaw"
case workspace.WorkspaceHermes:
case core.WorkspaceHermes:
source = "hermes"
case workspace.WorkspaceLarkChannel:
case core.WorkspaceLarkChannel:
source = "lark-channel"
default:
source = "openclaw" // default first option
@@ -585,7 +582,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 configpkg.MultiAppConfig
var multi core.MultiAppConfig
if json.Unmarshal(data, &multi) == nil && len(multi.Apps) > 0 {
app := multi.Apps[0]
existingSummary = fmt.Sprintf(msg.ConflictDesc,

View File

@@ -13,15 +13,11 @@ import (
"strings"
"testing"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"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
@@ -84,8 +80,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 := workspace.CurrentWorkspace()
t.Cleanup(func() { workspace.SetCurrentWorkspace(orig) })
orig := core.CurrentWorkspace()
t.Cleanup(func() { core.SetCurrentWorkspace(orig) })
}
// ── Command flag parsing tests (aligned with config_test.go pattern) ──
@@ -233,7 +229,7 @@ func TestConfigBindRun_EmptyLangIsNoOp(t *testing.T) {
t.Fatalf("configBindRun(--lang %q) = %v, want nil", tc.lang, err)
}
multi, err := configpkg.LoadMultiAppConfig()
multi, err := core.LoadMultiAppConfig()
if err != nil {
t.Fatalf("LoadMultiAppConfig: %v", err)
}
@@ -269,7 +265,7 @@ func TestConfigBindRun_OmitLangPreservesPrior(t *testing.T) {
t.Fatalf("re-bind (no --lang): %v", err)
}
multi, err := configpkg.LoadMultiAppConfig()
multi, err := core.LoadMultiAppConfig()
if err != nil {
t.Fatalf("LoadMultiAppConfig: %v", err)
}
@@ -283,9 +279,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 := configpkg.MultiAppConfig{
multi := core.MultiAppConfig{
CurrentApp: "active",
Apps: []configpkg.AppConfig{
Apps: []core.AppConfig{
{Name: "stale", AppId: "cli_stale", Lang: i18n.LangJaJP},
{Name: "active", AppId: "cli_active", Lang: i18n.LangEnUS},
},
@@ -304,8 +300,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 := configpkg.MultiAppConfig{
Apps: []configpkg.AppConfig{
multi := core.MultiAppConfig{
Apps: []core.AppConfig{
{AppId: "cli_only", Lang: i18n.LangJaJP},
},
}
@@ -643,8 +639,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.
workspace.SetCurrentWorkspace(workspace.WorkspaceLarkChannel)
multi, err := configpkg.LoadMultiAppConfig()
core.SetCurrentWorkspace(core.WorkspaceLarkChannel)
multi, err := core.LoadMultiAppConfig()
if err != nil {
t.Fatalf("load workspace config: %v", err)
}
@@ -690,8 +686,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)
}
workspace.SetCurrentWorkspace(workspace.WorkspaceLarkChannel)
multi, err := configpkg.LoadMultiAppConfig()
core.SetCurrentWorkspace(core.WorkspaceLarkChannel)
multi, err := core.LoadMultiAppConfig()
if err != nil {
t.Fatalf("load workspace config: %v", err)
}
@@ -805,16 +801,16 @@ func TestConfigShowRun_WorkspaceField(t *testing.T) {
configDir := t.TempDir()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", configDir)
workspace.SetCurrentWorkspace(workspace.WorkspaceLocal)
core.SetCurrentWorkspace(core.WorkspaceLocal)
multi := &configpkg.MultiAppConfig{
Apps: []configpkg.AppConfig{{
multi := &core.MultiAppConfig{
Apps: []core.AppConfig{{
AppId: "cli_local_test",
AppSecret: secret.PlainSecret("secret"),
Brand: brand.Feishu,
AppSecret: core.PlainSecret("secret"),
Brand: core.BrandFeishu,
}},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("save: %v", err)
}
@@ -831,7 +827,7 @@ func TestConfigShowRun_AgentWorkspaceNotBound(t *testing.T) {
saveWorkspace(t)
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
workspace.SetCurrentWorkspace(workspace.WorkspaceOpenClaw)
core.SetCurrentWorkspace(core.WorkspaceOpenClaw)
f, _, _, _ := cmdutil.TestFactory(t, nil)
err := configShowRun(&ConfigShowOptions{Factory: f})
@@ -1002,7 +998,7 @@ func TestConfigBindRun_HermesSuccess(t *testing.T) {
if err != nil {
t.Fatalf("read config.json: %v", err)
}
var multi configpkg.MultiAppConfig
var multi core.MultiAppConfig
if err := json.Unmarshal(data, &multi); err != nil {
t.Fatalf("unmarshal config.json: %v", err)
}
@@ -1012,8 +1008,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 != brand.Lark {
t.Errorf("brand = %q, want %q", multi.Apps[0].Brand, brand.Lark)
if multi.Apps[0].Brand != core.BrandLark {
t.Errorf("brand = %q, want %q", multi.Apps[0].Brand, core.BrandLark)
}
}
@@ -1279,7 +1275,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"),
identity.StrictModeBot, identity.AsBot)
core.StrictModeBot, core.AsBot)
}
// TestConfigBindRun_FlagModeDefaultsToBotOnly verifies the flag-mode default
@@ -1314,7 +1310,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"),
identity.StrictModeBot, identity.AsBot)
core.StrictModeBot, core.AsBot)
}
// TestConfigBindRun_WarnsOnIdentityEscalationWithoutForce verifies the
@@ -1410,7 +1406,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"),
identity.StrictModeOff, identity.AsUser)
core.StrictModeOff, core.AsUser)
}
// TestConfigBindRun_AllowsRebindSameBotOnly verifies re-binding the same
@@ -1446,7 +1442,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"),
identity.StrictModeBot, identity.AsBot)
core.StrictModeBot, core.AsBot)
}
// TestConfigBindRun_AllowsUserDefaultOnUserDefaultConfig verifies that if the
@@ -1483,18 +1479,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"),
identity.StrictModeOff, identity.AsUser)
core.StrictModeOff, core.AsUser)
}
// assertPresetApplied verifies the on-disk config.json applied the identity
// preset's StrictMode + DefaultAs expansion.
func assertPresetApplied(t *testing.T, configPath string, wantStrict identity.StrictMode, wantDefault identity.Identity) {
func assertPresetApplied(t *testing.T, configPath string, wantStrict core.StrictMode, wantDefault core.Identity) {
t.Helper()
data, err := os.ReadFile(configPath)
if err != nil {
t.Fatalf("read %s: %v", configPath, err)
}
var multi configpkg.MultiAppConfig
var multi core.MultiAppConfig
if err := json.Unmarshal(data, &multi); err != nil {
t.Fatalf("unmarshal %s: %v", configPath, err)
}
@@ -1791,10 +1787,10 @@ func TestCleanupKeychainFromData_KeepsSecretSharedWithNewApp(t *testing.T) {
}
oldConfig := []byte(`{"apps":[{"appId":"cli_shared","appSecret":{"source":"keychain","id":"` + sharedID + `"}}]}`)
newApp := &configpkg.AppConfig{
newApp := &core.AppConfig{
AppId: "cli_shared",
AppSecret: secret.SecretInput{
Ref: &secret.SecretRef{Source: "keychain", ID: sharedID},
AppSecret: core.SecretInput{
Ref: &core.SecretRef{Source: "keychain", ID: sharedID},
},
}
@@ -1821,10 +1817,10 @@ func TestCleanupKeychainFromData_RemovesStaleSecretWhenAppIDChanges(t *testing.T
}
oldConfig := []byte(`{"apps":[{"appId":"cli_old","appSecret":{"source":"keychain","id":"` + oldID + `"}}]}`)
newApp := &configpkg.AppConfig{
newApp := &core.AppConfig{
AppId: "cli_new",
AppSecret: secret.SecretInput{
Ref: &secret.SecretRef{Source: "keychain", ID: newID},
AppSecret: core.SecretInput{
Ref: &core.SecretRef{Source: "keychain", ID: newID},
},
}

View File

@@ -9,11 +9,9 @@ import (
"path/filepath"
"strings"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/errs"
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/binding"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/vfs"
)
@@ -38,7 +36,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) (*configpkg.AppConfig, error)
Build(appID string) (*core.AppConfig, error)
}
// newBinder constructs the SourceBinder for the given source name.
@@ -140,15 +138,15 @@ type openclawBinder struct {
path string
// Cached between ListCandidates and Build so we don't re-read / re-parse.
cfg *openclawbind.OpenClawRoot
rawApps []openclawbind.CandidateApp
cfg *binding.OpenClawRoot
rawApps []binding.CandidateApp
}
func (b *openclawBinder) Name() string { return "openclaw" }
func (b *openclawBinder) ConfigPath() string { return b.path }
func (b *openclawBinder) ListCandidates() ([]Candidate, error) {
cfg, err := openclawbind.ReadOpenClawConfig(b.path)
cfg, err := binding.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").
@@ -159,7 +157,7 @@ func (b *openclawBinder) ListCandidates() ([]Candidate, error) {
WithHint("configure Feishu in OpenClaw first")
}
raw := openclawbind.ListCandidateApps(cfg.Channels.Feishu)
raw := binding.ListCandidateApps(cfg.Channels.Feishu)
b.cfg = cfg
b.rawApps = raw
@@ -170,12 +168,12 @@ func (b *openclawBinder) ListCandidates() ([]Candidate, error) {
return result, nil
}
func (b *openclawBinder) Build(appID string) (*configpkg.AppConfig, error) {
func (b *openclawBinder) Build(appID string) (*core.AppConfig, error) {
if b.cfg == nil {
return nil, errs.NewInternalError(errs.SubtypeSDKError, "internal: Build called before ListCandidates")
}
var selected *openclawbind.CandidateApp
var selected *binding.CandidateApp
for i := range b.rawApps {
if b.rawApps[i].AppID == appID {
selected = &b.rawApps[i]
@@ -190,24 +188,24 @@ func (b *openclawBinder) Build(appID string) (*configpkg.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 := openclawbind.ResolveSecretInput(selected.AppSecret, b.cfg.Secrets, os.Getenv)
secret, err := binding.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 := secretpkg.ForStorage(selected.AppID, secretpkg.PlainSecret(secret), b.opts.Factory.Keychain)
stored, err := core.ForStorage(selected.AppID, core.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 &configpkg.AppConfig{
return &core.AppConfig{
AppId: selected.AppID,
AppSecret: stored,
Brand: brand.ParseBrand(selected.Brand),
Brand: core.ParseBrand(selected.Brand),
}, nil
}
@@ -240,7 +238,7 @@ func (b *hermesBinder) ListCandidates() ([]Candidate, error) {
return []Candidate{{AppID: appID, Label: "default"}}, nil
}
func (b *hermesBinder) Build(appID string) (*configpkg.AppConfig, error) {
func (b *hermesBinder) Build(appID string) (*core.AppConfig, error) {
if b.envMap == nil {
return nil, errs.NewInternalError(errs.SubtypeSDKError, "internal: Build called before ListCandidates")
}
@@ -253,17 +251,17 @@ func (b *hermesBinder) Build(appID string) (*configpkg.AppConfig, error) {
WithHint("run 'hermes setup' to configure Feishu credentials")
}
stored, err := secretpkg.ForStorage(appID, secretpkg.PlainSecret(appSecret), b.opts.Factory.Keychain)
stored, err := core.ForStorage(appID, core.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 &configpkg.AppConfig{
return &core.AppConfig{
AppId: appID,
AppSecret: stored,
Brand: brand.ParseBrand(b.envMap["FEISHU_DOMAIN"]),
Brand: core.ParseBrand(b.envMap["FEISHU_DOMAIN"]),
}, nil
}
@@ -276,14 +274,14 @@ type larkChannelBinder struct {
path string
// Cached between ListCandidates and Build so we don't re-read the file.
cfg *openclawbind.LarkChannelRoot
cfg *binding.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 := openclawbind.ReadLarkChannelConfig(b.path)
cfg, err := binding.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").
@@ -297,7 +295,7 @@ func (b *larkChannelBinder) ListCandidates() ([]Candidate, error) {
return []Candidate{{AppID: cfg.Accounts.App.ID, Label: "default"}}, nil
}
func (b *larkChannelBinder) Build(appID string) (*configpkg.AppConfig, error) {
func (b *larkChannelBinder) Build(appID string) (*core.AppConfig, error) {
if b.cfg == nil {
return nil, errs.NewInternalError(errs.SubtypeSDKError, "internal: Build called before ListCandidates")
}
@@ -311,24 +309,24 @@ func (b *larkChannelBinder) Build(appID string) (*configpkg.AppConfig, error) {
// Resolve through the same SecretInput pipeline openclaw uses, so
// bridge configs can use ${VAR} / env / file / exec just like openclaw.
secret, err := openclawbind.ResolveSecretInput(b.cfg.Accounts.App.Secret, b.cfg.Secrets, os.Getenv)
secret, err := binding.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 := secretpkg.ForStorage(appID, secretpkg.PlainSecret(secret), b.opts.Factory.Keychain)
stored, err := core.ForStorage(appID, core.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 &configpkg.AppConfig{
return &core.AppConfig{
AppId: appID,
AppSecret: stored,
Brand: brand.ParseBrand(b.cfg.Accounts.App.Tenant),
Brand: core.ParseBrand(b.cfg.Accounts.App.Tenant),
}, nil
}

View File

@@ -8,7 +8,7 @@ import (
"reflect"
"testing"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"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) (*configpkg.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) (*core.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

View File

@@ -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) brand.Brand {
return brand.ParseBrand(value)
func parseBrand(value string) core.LarkBrand {
return core.ParseBrand(value)
}

View File

@@ -12,16 +12,14 @@ 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"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"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{}
@@ -68,8 +66,8 @@ func TestConfigInitCmd_FlagParsing(t *testing.T) {
}
func TestConfigShowCmd_FlagParsing(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
var gotOpts *ConfigShowOptions
@@ -110,16 +108,16 @@ func TestConfigShowRun_NotConfiguredReturnsStructuredError(t *testing.T) {
func TestConfigShowRun_NoActiveProfileReturnsStructuredError(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "missing",
Apps: []configpkg.AppConfig{{
Apps: []core.AppConfig{{
Name: "default",
AppId: "app-default",
AppSecret: secret.PlainSecret("secret-default"),
Brand: brand.Feishu,
AppSecret: core.PlainSecret("secret-default"),
Brand: core.BrandFeishu,
}},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
@@ -188,18 +186,18 @@ func TestSaveInitConfig_OmitLangPreservesPrior(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
f, _, _, _ := cmdutil.TestFactory(t, nil)
existing := &configpkg.MultiAppConfig{Apps: []configpkg.AppConfig{
{AppId: "cli_x", AppSecret: secret.PlainSecret("s"), Brand: brand.Feishu, Lang: i18n.LangJaJP},
existing := &core.MultiAppConfig{Apps: []core.AppConfig{
{AppId: "cli_x", AppSecret: core.PlainSecret("s"), Brand: core.BrandFeishu, Lang: i18n.LangJaJP},
}}
if err := configpkg.SaveMultiAppConfig(existing); err != nil {
if err := core.SaveMultiAppConfig(existing); err != nil {
t.Fatalf("seed config: %v", err)
}
if err := saveInitConfig("", existing, f, "cli_x", secret.PlainSecret("s2"), brand.Feishu, ""); err != nil {
if err := saveInitConfig("", existing, f, "cli_x", core.PlainSecret("s2"), core.BrandFeishu, ""); err != nil {
t.Fatalf("saveInitConfig (no --lang): %v", err)
}
got, err := configpkg.LoadMultiAppConfig()
got, err := core.LoadMultiAppConfig()
if err != nil {
t.Fatalf("LoadMultiAppConfig: %v", err)
}
@@ -320,17 +318,17 @@ func TestConfigRemoveRun_SaveFailurePreservesExistingConfigAndSecrets(t *testing
configDir := t.TempDir()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", configDir)
multi := &configpkg.MultiAppConfig{
Apps: []configpkg.AppConfig{{
multi := &core.MultiAppConfig{
Apps: []core.AppConfig{{
AppId: "app-test",
AppSecret: secret.SecretInput{
Ref: &secret.SecretRef{Source: "keychain", ID: "appsecret:app-test"},
AppSecret: core.SecretInput{
Ref: &core.SecretRef{Source: "keychain", ID: "appsecret:app-test"},
},
Brand: brand.Feishu,
Users: []configpkg.AppUser{{UserOpenId: "ou_1", UserName: "Tester"}},
Brand: core.BrandFeishu,
Users: []core.AppUser{{UserOpenId: "ou_1", UserName: "Tester"}},
}},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
@@ -359,7 +357,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 := configpkg.LoadMultiAppConfig()
saved, err := core.LoadMultiAppConfig()
if err != nil {
t.Fatalf("LoadMultiAppConfig() error = %v", err)
}
@@ -379,18 +377,18 @@ func TestConfigRemoveRun_SaveFailurePreservesExistingConfigAndSecrets(t *testing
func TestSaveAsProfile_RejectsProfileNameCollisionWithExistingAppID(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
existing := &configpkg.MultiAppConfig{
Apps: []configpkg.AppConfig{
existing := &core.MultiAppConfig{
Apps: []core.AppConfig{
{
Name: "prod",
AppId: "cli_prod",
AppSecret: secret.PlainSecret("secret"),
Brand: brand.Feishu,
AppSecret: core.PlainSecret("secret"),
Brand: core.BrandFeishu,
},
},
}
err := saveAsProfile(existing, keychain.KeychainAccess(&noopConfigKeychain{}), "cli_prod", "app-new", secret.PlainSecret("new-secret"), brand.Lark, "en")
err := saveAsProfile(existing, keychain.KeychainAccess(&noopConfigKeychain{}), "cli_prod", "app-new", core.PlainSecret("new-secret"), core.BrandLark, "en")
if err == nil {
t.Fatal("expected conflict error")
}
@@ -430,21 +428,21 @@ func TestWrapSaveConfigError_PassesTypedValidationThrough(t *testing.T) {
}
func TestUpdateExistingProfileWithoutSecret_RejectsAppIDChange(t *testing.T) {
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "prod",
Apps: []configpkg.AppConfig{
Apps: []core.AppConfig{
{
Name: "prod",
AppId: "app-old",
AppSecret: secret.SecretInput{Ref: &secret.SecretRef{Source: "keychain", ID: "appsecret:app-old"}},
Brand: brand.Feishu,
AppSecret: core.SecretInput{Ref: &core.SecretRef{Source: "keychain", ID: "appsecret:app-old"}},
Brand: core.BrandFeishu,
Lang: "zh",
Users: []configpkg.AppUser{{UserOpenId: "ou_1", UserName: "User"}},
Users: []core.AppUser{{UserOpenId: "ou_1", UserName: "User"}},
},
},
}
err := updateExistingProfileWithoutSecret(multi, "", "app-new", brand.Lark, "en")
err := updateExistingProfileWithoutSecret(multi, "", "app-new", core.BrandLark, "en")
if err == nil {
t.Fatal("expected error when changing app ID without a new secret")
}

View File

@@ -8,8 +8,7 @@ import (
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/identity"
"github.com/larksuite/cli/internal/core"
"github.com/spf13/cobra"
)
@@ -21,14 +20,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 := configpkg.LoadOrNotConfigured()
multi, err := core.LoadOrNotConfigured()
if err != nil {
return err
}
app := multi.CurrentAppConfig(f.Invocation.Profile)
if app == nil {
return configpkg.NoActiveProfileError()
return core.NoActiveProfileError()
}
if len(args) == 0 {
@@ -45,8 +44,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 = identity.Identity(value)
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
app.DefaultAs = core.Identity(value)
if err := core.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)

View File

@@ -13,16 +13,13 @@ 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"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"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.
@@ -124,7 +121,7 @@ func guardAgentWorkspace(opts *ConfigInitOptions) error {
if opts.ForceInit {
return nil
}
ws := workspace.DetectWorkspaceFromEnv(os.Getenv)
ws := core.DetectWorkspaceFromEnv(os.Getenv)
if ws.IsLocal() {
return nil
}
@@ -139,7 +136,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 *configpkg.MultiAppConfig, f *cmdutil.Factory, skipAppID string) {
func cleanupOldConfig(existing *core.MultiAppConfig, f *cmdutil.Factory, skipAppID string) {
if existing == nil {
return
}
@@ -147,7 +144,7 @@ func cleanupOldConfig(existing *configpkg.MultiAppConfig, f *cmdutil.Factory, sk
if app.AppId == skipAppID {
continue
}
secretpkg.RemoveSecretStore(app.AppSecret, f.Keychain)
core.RemoveSecretStore(app.AppSecret, f.Keychain)
for _, user := range app.Users {
auth.RemoveStoredToken(app.AppId, user.UserOpenId)
}
@@ -155,19 +152,19 @@ func cleanupOldConfig(existing *configpkg.MultiAppConfig, f *cmdutil.Factory, sk
}
// saveAsOnlyApp overwrites config.json with a single-app config.
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{},
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{},
}},
}
return configpkg.SaveMultiAppConfig(config)
return core.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 *configpkg.MultiAppConfig, f *cmdutil.Factory, appId string, secret secretpkg.SecretInput, brand brandpkg.Brand, lang string) error {
func saveInitConfig(profileName string, existing *core.MultiAppConfig, f *cmdutil.Factory, appId string, secret core.SecretInput, brand core.LarkBrand, lang string) error {
if profileName != "" {
return saveAsProfile(existing, f.Keychain, profileName, appId, secret, brand, lang)
}
@@ -198,20 +195,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 *configpkg.MultiAppConfig, kc keychain.KeychainAccess, profileName, appId string, secret secretpkg.SecretInput, brand brandpkg.Brand, lang string) error {
func saveAsProfile(existing *core.MultiAppConfig, kc keychain.KeychainAccess, profileName, appId string, secret core.SecretInput, brand core.LarkBrand, lang string) error {
multi := existing
if multi == nil {
multi = &configpkg.MultiAppConfig{}
multi = &core.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 {
secretpkg.RemoveSecretStore(multi.Apps[idx].AppSecret, kc)
core.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 = []configpkg.AppUser{}
multi.Apps[idx].Users = []core.AppUser{}
}
multi.Apps[idx].AppId = appId
multi.Apps[idx].AppSecret = secret
@@ -224,19 +221,19 @@ func saveAsProfile(existing *configpkg.MultiAppConfig, kc keychain.KeychainAcces
WithParam("--name")
}
// Append new profile
multi.Apps = append(multi.Apps, configpkg.AppConfig{
multi.Apps = append(multi.Apps, core.AppConfig{
Name: profileName,
AppId: appId,
AppSecret: secret,
Brand: brand,
Lang: i18n.Lang(lang),
Users: []configpkg.AppUser{},
Users: []core.AppUser{},
})
}
return configpkg.SaveMultiAppConfig(multi)
return core.SaveMultiAppConfig(multi)
}
func findProfileIndexByName(multi *configpkg.MultiAppConfig, profileName string) int {
func findProfileIndexByName(multi *core.MultiAppConfig, profileName string) int {
if multi == nil {
return -1
}
@@ -248,7 +245,7 @@ func findProfileIndexByName(multi *configpkg.MultiAppConfig, profileName string)
return -1
}
func findAppIndexByAppID(multi *configpkg.MultiAppConfig, appID string) int {
func findAppIndexByAppID(multi *core.MultiAppConfig, appID string) int {
if multi == nil {
return -1
}
@@ -275,13 +272,13 @@ func wrapUpdateExistingProfileErr(err error) error {
return errs.NewInternalError(errs.SubtypeSDKError, "failed to save config: %v", err).WithCause(err)
}
func updateExistingProfileWithoutSecret(existing *configpkg.MultiAppConfig, profileName, appID string, brand brandpkg.Brand, lang string) error {
func updateExistingProfileWithoutSecret(existing *core.MultiAppConfig, profileName, appID string, brand core.LarkBrand, lang string) error {
if existing == nil {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "App Secret cannot be empty for new configuration").
WithParam("--app-secret")
}
var app *configpkg.AppConfig
var app *core.AppConfig
if profileName != "" {
if idx := findProfileIndexByName(existing, profileName); idx >= 0 {
app = &existing.Apps[idx]
@@ -305,7 +302,7 @@ func updateExistingProfileWithoutSecret(existing *configpkg.MultiAppConfig, prof
app.AppId = appID
app.Brand = brand
app.Lang = preferredLang(i18n.Lang(lang), app.Lang)
return configpkg.SaveMultiAppConfig(existing)
return core.SaveMultiAppConfig(existing)
}
func configInitRun(opts *ConfigInitOptions) error {
@@ -326,14 +323,14 @@ func configInitRun(opts *ConfigInitOptions) error {
}
}
existing, err := configpkg.LoadMultiAppConfig()
existing, err := core.LoadMultiAppConfig()
if err != nil {
existing = nil // treat as empty
}
// Validate --profile name if set
if opts.ProfileName != "" {
if err := configpkg.ValidateProfileName(opts.ProfileName); err != nil {
if err := core.ValidateProfileName(opts.ProfileName); err != nil {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%v", err).WithCause(err)
}
}
@@ -341,14 +338,14 @@ func configInitRun(opts *ConfigInitOptions) error {
// Mode 1: Non-interactive
if opts.AppID != "" && opts.appSecret != "" {
brand := parseBrand(opts.Brand)
secret, err := secretpkg.ForStorage(opts.AppID, secretpkg.PlainSecret(opts.appSecret), f.Keychain)
secret, err := core.ForStorage(opts.AppID, core.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", workspace.GetConfigPath()))
output.PrintSuccess(f.IOStreams.ErrOut, fmt.Sprintf("Configuration saved to %s", core.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 {
@@ -380,8 +377,8 @@ func configInitRun(opts *ConfigInitOptions) error {
if result == nil {
return errs.NewInternalError(errs.SubtypeSDKError, "app creation returned no result")
}
existing, _ := configpkg.LoadMultiAppConfig()
secret, err := secretpkg.ForStorage(result.AppID, secretpkg.PlainSecret(result.AppSecret), f.Keychain)
existing, _ := core.LoadMultiAppConfig()
secret, err := core.ForStorage(result.AppID, core.PlainSecret(result.AppSecret), f.Keychain)
if err != nil {
return errs.NewInternalError(errs.SubtypeSDKError, "%v", err).WithCause(err)
}
@@ -407,11 +404,11 @@ func configInitRun(opts *ConfigInitOptions) error {
WithParam("--app-id")
}
existing, _ := configpkg.LoadMultiAppConfig()
existing, _ := core.LoadMultiAppConfig()
if result.AppSecret != "" {
// New secret provided (either from "create" or "existing" with input)
secret, err := secretpkg.ForStorage(result.AppID, secretpkg.PlainSecret(result.AppSecret), f.Keychain)
secret, err := core.ForStorage(result.AppID, core.PlainSecret(result.AppSecret), f.Keychain)
if err != nil {
return errs.NewInternalError(errs.SubtypeSDKError, "%v", err).WithCause(err)
}
@@ -446,7 +443,7 @@ func configInitRun(opts *ConfigInitOptions) error {
}
// Mode 5: Legacy interactive (readline fallback)
firstApp := (*configpkg.AppConfig)(nil)
firstApp := (*core.AppConfig)(nil)
if existing != nil {
firstApp = existing.CurrentAppConfig("")
}
@@ -497,9 +494,9 @@ func configInitRun(opts *ConfigInitOptions) error {
if resolvedAppId == "" && firstApp != nil {
resolvedAppId = firstApp.AppId
}
var resolvedSecret secretpkg.SecretInput
var resolvedSecret core.SecretInput
if appSecretInput != "" {
resolvedSecret = secretpkg.PlainSecret(appSecretInput)
resolvedSecret = core.PlainSecret(appSecretInput)
} else if firstApp != nil {
resolvedSecret = firstApp.AppSecret
}
@@ -516,14 +513,14 @@ func configInitRun(opts *ConfigInitOptions) error {
WithParam("--app-id")
}
storedSecret, err := secretpkg.ForStorage(resolvedAppId, resolvedSecret, f.Keychain)
storedSecret, err := core.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", workspace.GetConfigPath()))
output.PrintSuccess(f.IOStreams.ErrOut, fmt.Sprintf("Configuration saved to %s", core.GetConfigPath()))
printLangPreferenceConfirmation(opts)
if appSecretInput != "" {
if err := runProbe(opts.Ctx, f, resolvedAppId, appSecretInput, parseBrand(resolvedBrand)); err != nil {

View File

@@ -10,14 +10,13 @@ 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"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/transport"
)
@@ -25,7 +24,7 @@ import (
// configInitResult holds the result of the interactive config init flow.
type configInitResult struct {
Mode string // "create" or "existing"
Brand brand.Brand
Brand core.LarkBrand
AppID string
AppSecret string
}
@@ -63,8 +62,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, _ := configpkg.LoadMultiAppConfig()
var firstApp *configpkg.AppConfig
existing, _ := core.LoadMultiAppConfig()
var firstApp *core.AppConfig
if existing != nil {
firstApp = existing.CurrentAppConfig("")
}
@@ -151,8 +150,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 brand.Brand, msg *initMsg) (*configInitResult, error) {
var larkBrand brand.Brand
func runCreateAppFlow(ctx context.Context, f *cmdutil.Factory, brandOverride core.LarkBrand, msg *initMsg) (*configInitResult, error) {
var larkBrand core.LarkBrand
if brandOverride != "" {
larkBrand = brandOverride
} else {

View File

@@ -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 brandpkg.Brand) error {
func runProbe(parent context.Context, factory *cmdutil.Factory, appID, appSecret string, brand core.LarkBrand) 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 := brandpkg.ResolveEndpoints(brand).Open + "/open-apis/application/v6/larksuite_cli_app/probe"
url := core.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 {

View File

@@ -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", brand.Feishu)
err := runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu)
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", brand.Feishu), errBuf)
assertConfigRejection(t, runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu), 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", brand.Feishu)
err := runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu)
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", brand.Feishu), errBuf)
assertSilent(t, runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu), 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", brand.Feishu), errBuf)
assertSilent(t, runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu), 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", brand.Feishu)
err := runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu)
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", brand.Feishu)
err := runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu)
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", brand.Feishu); err != nil {
if err := runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu); 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", brand.Lark); err != nil {
if err := runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandLark); 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", brand.Feishu), errBuf)
assertSilent(t, runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu), 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", brand.Feishu)
err := runProbe(context.Background(), f, "cli_x", "secret_y", core.BrandFeishu)
elapsed := time.Since(start)
if elapsed > 4*time.Second {

View File

@@ -8,11 +8,9 @@ import (
"fmt"
"testing"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/errs"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/secret"
)
// updateExistingProfileWithoutSecret guards four blank-input scenarios. Each
@@ -21,47 +19,47 @@ import (
// not for missing user input.
func TestUpdateExistingProfileWithoutSecret_NilConfig_EmitsValidationError(t *testing.T) {
err := updateExistingProfileWithoutSecret(nil, "", "cli_test", brand.Feishu, "en")
err := updateExistingProfileWithoutSecret(nil, "", "cli_test", core.BrandFeishu, "en")
assertValidationParam(t, err, "--app-secret")
}
func TestUpdateExistingProfileWithoutSecret_UnknownProfile_EmitsValidationError(t *testing.T) {
existing := &configpkg.MultiAppConfig{
Apps: []configpkg.AppConfig{{
existing := &core.MultiAppConfig{
Apps: []core.AppConfig{{
Name: "default",
AppId: "app-default",
AppSecret: secret.PlainSecret("secret-default"),
Brand: brand.Feishu,
AppSecret: core.PlainSecret("secret-default"),
Brand: core.BrandFeishu,
}},
}
err := updateExistingProfileWithoutSecret(existing, "missing-profile", "cli_test", brand.Feishu, "en")
err := updateExistingProfileWithoutSecret(existing, "missing-profile", "cli_test", core.BrandFeishu, "en")
assertValidationParam(t, err, "--app-secret")
}
func TestUpdateExistingProfileWithoutSecret_NoCurrentApp_EmitsValidationError(t *testing.T) {
existing := &configpkg.MultiAppConfig{
existing := &core.MultiAppConfig{
CurrentApp: "missing",
Apps: []configpkg.AppConfig{{
Apps: []core.AppConfig{{
Name: "default",
AppId: "app-default",
AppSecret: secret.PlainSecret("secret-default"),
Brand: brand.Feishu,
AppSecret: core.PlainSecret("secret-default"),
Brand: core.BrandFeishu,
}},
}
err := updateExistingProfileWithoutSecret(existing, "", "cli_test", brand.Feishu, "en")
err := updateExistingProfileWithoutSecret(existing, "", "cli_test", core.BrandFeishu, "en")
assertValidationParam(t, err, "--app-secret")
}
func TestUpdateExistingProfileWithoutSecret_AppIdMismatch_EmitsValidationError(t *testing.T) {
existing := &configpkg.MultiAppConfig{
Apps: []configpkg.AppConfig{{
existing := &core.MultiAppConfig{
Apps: []core.AppConfig{{
Name: "default",
AppId: "app-default",
AppSecret: secret.PlainSecret("secret-default"),
Brand: brand.Feishu,
AppSecret: core.PlainSecret("secret-default"),
Brand: core.BrandFeishu,
}},
}
err := updateExistingProfileWithoutSecret(existing, "", "cli_different", brand.Feishu, "en")
err := updateExistingProfileWithoutSecret(existing, "", "cli_different", core.BrandFeishu, "en")
assertValidationParam(t, err, "--app-secret")
}

View File

@@ -9,9 +9,8 @@ import (
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/auth"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/secret"
"github.com/spf13/cobra"
)
@@ -42,21 +41,21 @@ func NewCmdConfigRemove(f *cmdutil.Factory, runF func(*ConfigRemoveOptions) erro
func configRemoveRun(opts *ConfigRemoveOptions) error {
f := opts.Factory
config, err := configpkg.LoadMultiAppConfig()
config, err := core.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 := &configpkg.MultiAppConfig{Apps: []configpkg.AppConfig{}}
if err := configpkg.SaveMultiAppConfig(empty); err != nil {
empty := &core.MultiAppConfig{Apps: []core.AppConfig{}}
if err := core.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 {
secret.RemoveSecretStore(app.AppSecret, f.Keychain)
core.RemoveSecretStore(app.AppSecret, f.Keychain)
for _, user := range app.Users {
_ = auth.RemoveStoredToken(app.AppId, user.UserOpenId)
}

View File

@@ -10,7 +10,7 @@ import (
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
)
// 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 := configpkg.LoadOrNotConfigured()
config, err := core.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 := configpkg.SaveMultiAppConfig(config); err != nil {
if err := core.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 *configpkg.MultiAppConfig) {
func printRiskControl(f *cmdutil.Factory, config *core.MultiAppConfig) {
source := "default"
if config.RiskControl != nil {
source = "workspace"

View File

@@ -8,19 +8,17 @@ import (
"strings"
"testing"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/secret"
"github.com/larksuite/cli/internal/core"
)
func TestRiskControlWorkspacePolicy(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
config := &configpkg.MultiAppConfig{Apps: []configpkg.AppConfig{{
AppId: "cli_test", AppSecret: secret.PlainSecret("secret"), Brand: brand.Feishu,
config := &core.MultiAppConfig{Apps: []core.AppConfig{{
AppId: "cli_test", AppSecret: core.PlainSecret("secret"), Brand: core.BrandFeishu,
}}}
if err := configpkg.SaveMultiAppConfig(config); err != nil {
if err := core.SaveMultiAppConfig(config); err != nil {
t.Fatal(err)
}
@@ -30,7 +28,7 @@ func TestRiskControlWorkspacePolicy(t *testing.T) {
if err := cmd.Execute(); err != nil {
t.Fatalf("set off: %v", err)
}
loaded, err := configpkg.LoadMultiAppConfig()
loaded, err := core.LoadMultiAppConfig()
if err != nil {
t.Fatal(err)
}
@@ -55,7 +53,7 @@ func TestRiskControlWorkspacePolicy(t *testing.T) {
if err := cmd.Execute(); err != nil {
t.Fatalf("set on: %v", err)
}
loaded, err = configpkg.LoadMultiAppConfig()
loaded, err = core.LoadMultiAppConfig()
if err != nil {
t.Fatal(err)
}
@@ -68,7 +66,7 @@ func TestRiskControlWorkspacePolicy(t *testing.T) {
if err := cmd.Execute(); err != nil {
t.Fatalf("reset default: %v", err)
}
loaded, err = configpkg.LoadMultiAppConfig()
loaded, err = core.LoadMultiAppConfig()
if err != nil {
t.Fatal(err)
}
@@ -88,8 +86,8 @@ func TestRiskControlWorkspacePolicy(t *testing.T) {
func TestRiskControlWorkspacePolicyRejectsInvalidValue(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
if err := configpkg.SaveMultiAppConfig(&configpkg.MultiAppConfig{Apps: []configpkg.AppConfig{{
AppId: "cli_test", AppSecret: secret.PlainSecret("secret"), Brand: brand.Feishu,
if err := core.SaveMultiAppConfig(&core.MultiAppConfig{Apps: []core.AppConfig{{
AppId: "cli_test", AppSecret: core.PlainSecret("secret"), Brand: core.BrandFeishu,
}}}); err != nil {
t.Fatal(err)
}
@@ -109,10 +107,10 @@ func TestRiskControlWorkspacePolicyRejectsInvalidValue(t *testing.T) {
func TestRiskControlWorkspacePolicyAllowedWithExternalCredentials(t *testing.T) {
f := newConfigFactoryWithExternalProvider(t)
config := &configpkg.MultiAppConfig{Apps: []configpkg.AppConfig{{
AppId: "cli_test", AppSecret: secret.PlainSecret("secret"), Brand: brand.Feishu,
config := &core.MultiAppConfig{Apps: []core.AppConfig{{
AppId: "cli_test", AppSecret: core.PlainSecret("secret"), Brand: core.BrandFeishu,
}}}
if err := configpkg.SaveMultiAppConfig(config); err != nil {
if err := core.SaveMultiAppConfig(config); err != nil {
t.Fatal(err)
}
@@ -122,7 +120,7 @@ func TestRiskControlWorkspacePolicyAllowedWithExternalCredentials(t *testing.T)
t.Fatalf("set off with external credentials: %v", err)
}
loaded, err := configpkg.LoadMultiAppConfig()
loaded, err := core.LoadMultiAppConfig()
if err != nil {
t.Fatal(err)
}

View File

@@ -11,9 +11,8 @@ import (
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/workspace"
"github.com/spf13/cobra"
)
@@ -44,15 +43,15 @@ func NewCmdConfigShow(f *cmdutil.Factory, runF func(*ConfigShowOptions) error) *
func configShowRun(opts *ConfigShowOptions) error {
f := opts.Factory
config, err := configpkg.LoadMultiAppConfig()
config, err := core.LoadMultiAppConfig()
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return configpkg.NotConfiguredError()
return core.NotConfiguredError()
}
return errs.NewConfigError(errs.SubtypeInvalidConfig, "failed to load config: %v", err).WithCause(err)
}
if config == nil || len(config.Apps) == 0 {
return configpkg.NotConfiguredError()
return core.NotConfiguredError()
}
app := config.CurrentAppConfig(f.Invocation.Profile)
if app == nil {
@@ -67,7 +66,7 @@ func configShowRun(opts *ConfigShowOptions) error {
users = strings.Join(userStrs, ", ")
}
output.PrintJson(f.IOStreams.Out, map[string]interface{}{
"workspace": workspace.CurrentWorkspace().Display(),
"workspace": core.CurrentWorkspace().Display(),
"profile": app.ProfileName(),
"appId": app.AppId,
"appSecret": "****",
@@ -75,6 +74,6 @@ func configShowRun(opts *ConfigShowOptions) error {
"lang": app.Lang,
"users": users,
})
fmt.Fprintf(f.IOStreams.ErrOut, "\nConfig file path: %s\n", workspace.GetConfigPath())
fmt.Fprintf(f.IOStreams.ErrOut, "\nConfig file path: %s\n", core.GetConfigPath())
return nil
}

View File

@@ -9,8 +9,7 @@ import (
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/identity"
"github.com/larksuite/cli/internal/core"
"github.com/spf13/cobra"
)
@@ -38,7 +37,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 := configpkg.LoadOrNotConfigured()
multi, err := core.LoadOrNotConfigured()
if err != nil {
return err
}
@@ -46,20 +45,20 @@ explicit user confirmation — never run on your own initiative.`,
if reset {
app := multi.CurrentAppConfig(f.Invocation.Profile)
if app == nil {
return configpkg.NoActiveProfileError()
return core.NoActiveProfileError()
}
return resetStrictMode(f, multi, app, global, args)
}
if len(args) == 0 {
app := multi.CurrentAppConfig(f.Invocation.Profile)
if app == nil {
return configpkg.NoActiveProfileError()
return core.NoActiveProfileError()
}
return showStrictMode(cmd.Context(), f, multi, app)
}
app := multi.CurrentAppConfig(f.Invocation.Profile)
if !global && app == nil {
return configpkg.NoActiveProfileError()
return core.NoActiveProfileError()
}
return setStrictMode(f, multi, app, args[0], global)
},
@@ -72,7 +71,7 @@ explicit user confirmation — never run on your own initiative.`,
return cmd
}
func resetStrictMode(f *cmdutil.Factory, multi *configpkg.MultiAppConfig, app *configpkg.AppConfig, global bool, args []string) error {
func resetStrictMode(f *cmdutil.Factory, multi *core.MultiAppConfig, app *core.AppConfig, global bool, args []string) error {
if global {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--reset cannot be used with --global").WithParam("--reset")
}
@@ -80,14 +79,14 @@ func resetStrictMode(f *cmdutil.Factory, multi *configpkg.MultiAppConfig, app *c
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--reset cannot be used with a value argument").WithParam("--reset")
}
app.StrictMode = nil
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.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 *configpkg.MultiAppConfig, app *configpkg.AppConfig) error {
func showStrictMode(ctx context.Context, f *cmdutil.Factory, multi *core.MultiAppConfig, app *core.AppConfig) error {
// Runtime effective mode from credential provider chain is the source of truth.
runtime := f.ResolveStrictMode(ctx)
configMode, configSource := resolveStrictModeStatus(multi, app)
@@ -100,10 +99,10 @@ func showStrictMode(ctx context.Context, f *cmdutil.Factory, multi *configpkg.Mu
return nil
}
func setStrictMode(f *cmdutil.Factory, multi *configpkg.MultiAppConfig, app *configpkg.AppConfig, value string, global bool) error {
mode := identity.StrictMode(value)
func setStrictMode(f *cmdutil.Factory, multi *core.MultiAppConfig, app *core.AppConfig, value string, global bool) error {
mode := core.StrictMode(value)
switch mode {
case identity.StrictModeBot, identity.StrictModeUser, identity.StrictModeOff:
case core.StrictModeBot, core.StrictModeUser, core.StrictModeOff:
default:
return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid value %q, valid values: bot | user | off", value)
}
@@ -119,7 +118,7 @@ func setStrictMode(f *cmdutil.Factory, multi *configpkg.MultiAppConfig, app *con
// 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 identity.StrictMode
var oldMode core.StrictMode
if global {
oldMode = multi.StrictMode
} else {
@@ -139,16 +138,16 @@ func setStrictMode(f *cmdutil.Factory, multi *configpkg.MultiAppConfig, app *con
}
} else {
if app == nil {
return configpkg.NoActiveProfileError()
return core.NoActiveProfileError()
}
app.StrictMode = &mode
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
return errs.NewInternalError(errs.SubtypeStorage, "failed to save config: %v", err).WithCause(err)
}
if oldMode == identity.StrictModeBot && (mode == identity.StrictModeUser || mode == identity.StrictModeOff) {
if oldMode == core.StrictModeBot && (mode == core.StrictModeUser || mode == core.StrictModeOff) {
fmt.Fprintln(f.IOStreams.ErrOut, "⚠️ "+strictModeRelaxLang(app).IdentityEscalationMessage)
}
@@ -163,19 +162,19 @@ func setStrictMode(f *cmdutil.Factory, multi *configpkg.MultiAppConfig, app *con
// 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 *configpkg.AppConfig) *bindMsg {
func strictModeRelaxLang(app *core.AppConfig) *bindMsg {
if app != nil {
return getBindMsg(app.Lang)
}
return getBindMsg("")
}
func resolveStrictModeStatus(multi *configpkg.MultiAppConfig, app *configpkg.AppConfig) (identity.StrictMode, string) {
func resolveStrictModeStatus(multi *core.MultiAppConfig, app *core.AppConfig) (core.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 identity.StrictModeOff, "global (default)"
return core.StrictModeOff, "global (default)"
}

View File

@@ -7,32 +7,29 @@ import (
"strings"
"testing"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/identity"
"github.com/larksuite/cli/internal/secret"
"github.com/larksuite/cli/internal/core"
)
func setupStrictModeTestConfig(t *testing.T) {
t.Helper()
dir := t.TempDir()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
multi := &configpkg.MultiAppConfig{
Apps: []configpkg.AppConfig{{
multi := &core.MultiAppConfig{
Apps: []core.AppConfig{{
AppId: "test-app",
AppSecret: secret.PlainSecret("secret"),
Brand: brand.Feishu,
AppSecret: core.PlainSecret("secret"),
Brand: core.BrandFeishu,
}},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatal(err)
}
}
func TestStrictMode_Show_Default(t *testing.T) {
setupStrictModeTestConfig(t)
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test-app", AppSecret: "secret"})
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test-app", AppSecret: "secret"})
cmd := NewCmdConfigStrictMode(f)
cmd.SetArgs([]string{})
if err := cmd.Execute(); err != nil {
@@ -45,37 +42,37 @@ func TestStrictMode_Show_Default(t *testing.T) {
func TestStrictMode_SetBot_Profile(t *testing.T) {
setupStrictModeTestConfig(t)
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test-app", AppSecret: "secret"})
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test-app", AppSecret: "secret"})
cmd := NewCmdConfigStrictMode(f)
cmd.SetArgs([]string{"bot"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
multi, _ := configpkg.LoadMultiAppConfig()
multi, _ := core.LoadMultiAppConfig()
app := multi.CurrentAppConfig("")
if app.StrictMode == nil || *app.StrictMode != identity.StrictModeBot {
if app.StrictMode == nil || *app.StrictMode != core.StrictModeBot {
t.Error("expected StrictMode=bot on profile")
}
}
func TestStrictMode_SetUser_Profile(t *testing.T) {
setupStrictModeTestConfig(t)
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test-app", AppSecret: "secret"})
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test-app", AppSecret: "secret"})
cmd := NewCmdConfigStrictMode(f)
cmd.SetArgs([]string{"user"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
multi, _ := configpkg.LoadMultiAppConfig()
multi, _ := core.LoadMultiAppConfig()
app := multi.CurrentAppConfig("")
if app.StrictMode == nil || *app.StrictMode != identity.StrictModeUser {
if app.StrictMode == nil || *app.StrictMode != core.StrictModeUser {
t.Error("expected StrictMode=user on profile")
}
}
func TestStrictMode_SetOff_Profile(t *testing.T) {
setupStrictModeTestConfig(t)
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test-app", AppSecret: "secret"})
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test-app", AppSecret: "secret"})
cmd := NewCmdConfigStrictMode(f)
cmd.SetArgs([]string{"bot"})
cmd.Execute()
@@ -84,23 +81,23 @@ func TestStrictMode_SetOff_Profile(t *testing.T) {
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
multi, _ := configpkg.LoadMultiAppConfig()
multi, _ := core.LoadMultiAppConfig()
app := multi.CurrentAppConfig("")
if app.StrictMode == nil || *app.StrictMode != identity.StrictModeOff {
if app.StrictMode == nil || *app.StrictMode != core.StrictModeOff {
t.Error("expected StrictMode=off on profile")
}
}
func TestStrictMode_SetBot_Global(t *testing.T) {
setupStrictModeTestConfig(t)
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test-app", AppSecret: "secret"})
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test-app", AppSecret: "secret"})
cmd := NewCmdConfigStrictMode(f)
cmd.SetArgs([]string{"bot", "--global"})
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
multi, _ := configpkg.LoadMultiAppConfig()
if multi.StrictMode != identity.StrictModeBot {
multi, _ := core.LoadMultiAppConfig()
if multi.StrictMode != core.StrictModeBot {
t.Error("expected global StrictMode=bot")
}
}
@@ -108,38 +105,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 := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "missing-profile",
Apps: []configpkg.AppConfig{{
Apps: []core.AppConfig{{
Name: "default",
AppId: "test-app",
AppSecret: secret.PlainSecret("secret"),
Brand: brand.Feishu,
AppSecret: core.PlainSecret("secret"),
Brand: core.BrandFeishu,
}},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatal(err)
}
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test-app", AppSecret: "secret"})
f, _, _, _ := cmdutil.TestFactory(t, &core.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 := configpkg.LoadMultiAppConfig()
saved, err := core.LoadMultiAppConfig()
if err != nil {
t.Fatalf("LoadMultiAppConfig() error = %v", err)
}
if saved.StrictMode != identity.StrictModeBot {
t.Fatalf("StrictMode = %q, want %q", saved.StrictMode, identity.StrictModeBot)
if saved.StrictMode != core.StrictModeBot {
t.Fatalf("StrictMode = %q, want %q", saved.StrictMode, core.StrictModeBot)
}
}
func TestStrictMode_Reset(t *testing.T) {
setupStrictModeTestConfig(t)
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test-app", AppSecret: "secret"})
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test-app", AppSecret: "secret"})
cmd := NewCmdConfigStrictMode(f)
cmd.SetArgs([]string{"bot"})
cmd.Execute()
@@ -148,7 +145,7 @@ func TestStrictMode_Reset(t *testing.T) {
if err := cmd.Execute(); err != nil {
t.Fatal(err)
}
multi, _ := configpkg.LoadMultiAppConfig()
multi, _ := core.LoadMultiAppConfig()
app := multi.CurrentAppConfig("")
if app.StrictMode != nil {
t.Errorf("expected nil StrictMode after reset, got %v", *app.StrictMode)
@@ -157,7 +154,7 @@ func TestStrictMode_Reset(t *testing.T) {
func TestStrictMode_InvalidValue(t *testing.T) {
setupStrictModeTestConfig(t)
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test-app", AppSecret: "secret"})
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test-app", AppSecret: "secret"})
cmd := NewCmdConfigStrictMode(f)
cmd.SetArgs([]string{"on"})
err := cmd.Execute()

View File

@@ -8,7 +8,7 @@ import (
"testing"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
)
// 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, &configpkg.CliConfig{AppID: "test-app", AppSecret: "secret"})
f, _, stderr, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test-app", AppSecret: "secret"})
cmd := NewCmdConfigStrictMode(f)
cmd.SetArgs(args)
if err := cmd.Execute(); err != nil {

View File

@@ -14,16 +14,14 @@ 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"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"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.
@@ -87,7 +85,7 @@ func doctorRun(opts *DoctorOptions) error {
}
// ── 1. Config file ──
_, err := configpkg.LoadMultiAppConfig()
_, err := core.LoadMultiAppConfig()
if err != nil {
// For "config not present" cases, prefer the workspace-aware
// NotConfiguredError message + hint (e.g. "openclaw context
@@ -98,7 +96,7 @@ func doctorRun(opts *DoctorOptions) error {
msg, hint := err.Error(), ""
if errors.Is(err, os.ErrNotExist) {
var cfgErr *errs.ConfigError
if errors.As(configpkg.NotConfiguredError(), &cfgErr) {
if errors.As(core.NotConfiguredError(), &cfgErr) {
msg, hint = cfgErr.Message, cfgErr.Hint
}
}
@@ -120,7 +118,7 @@ func doctorRun(opts *DoctorOptions) error {
}
checks = append(checks, pass("app_resolved", fmt.Sprintf("app: %s (%s)", cfg.AppID, cfg.Brand)))
ep := brand.ResolveEndpoints(cfg.Brand)
ep := core.ResolveEndpoints(cfg.Brand)
// ── 3. Identity readiness ──
diagnostics := identitydiag.Diagnose(opts.Ctx, f, cfg, !opts.Offline)
@@ -151,7 +149,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 brand.Endpoints) []checkResult {
func networkChecks(ctx context.Context, opts *DoctorOptions, ep core.Endpoints) []checkResult {
if opts.Offline {
return []checkResult{
skip("endpoint_open", "skipped (--offline)"),
@@ -241,7 +239,7 @@ func finishDoctor(f *cmdutil.Factory, checks []checkResult) error {
result := map[string]interface{}{
"ok": allOK,
"workspace": workspace.CurrentWorkspace().Display(),
"workspace": core.CurrentWorkspace().Display(),
"checks": checks,
}
output.PrintJson(f.IOStreams.Out, result)

View File

@@ -13,17 +13,15 @@ import (
"github.com/spf13/cobra"
"github.com/larksuite/cli/brand"
extcred "github.com/larksuite/cli/extension/credential"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/credential"
"github.com/larksuite/cli/internal/secret"
)
func TestNewCmdDoctor_FlagParsing(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := NewCmdDoctor(f)
@@ -90,7 +88,7 @@ func TestFinishDoctor(t *testing.T) {
}
func TestNetworkChecks_Offline(t *testing.T) {
ep := brand.Endpoints{Open: "https://open.feishu.cn", MCP: "https://mcp.feishu.cn"}
ep := core.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 {
@@ -105,22 +103,22 @@ func TestNetworkChecks_Offline(t *testing.T) {
func TestDoctorRun_SplitsBotAndMissingUserIdentity(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
if err := configpkg.SaveMultiAppConfig(&configpkg.MultiAppConfig{
if err := core.SaveMultiAppConfig(&core.MultiAppConfig{
CurrentApp: "default",
Apps: []configpkg.AppConfig{
Apps: []core.AppConfig{
{
Name: "default",
AppId: "test-app",
AppSecret: secret.PlainSecret("secret"),
Brand: brand.Feishu,
AppSecret: core.PlainSecret("secret"),
Brand: core.BrandFeishu,
},
},
}); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "secret", Brand: brand.Feishu,
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "secret", Brand: core.BrandFeishu,
})
err := doctorRun(&DoctorOptions{
Factory: f,
@@ -182,16 +180,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 := configpkg.SaveMultiAppConfig(&configpkg.MultiAppConfig{
if err := core.SaveMultiAppConfig(&core.MultiAppConfig{
CurrentApp: "default",
Apps: []configpkg.AppConfig{{Name: "default", AppId: "cli_x", AppSecret: secret.PlainSecret("secret"), Brand: brand.Feishu}},
Apps: []core.AppConfig{{Name: "default", AppId: "cli_x", AppSecret: core.PlainSecret("secret"), Brand: core.BrandFeishu}},
}); 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 := &configpkg.CliConfig{AppID: "cli_x", Brand: brand.Feishu, SupportedIdentities: uint8(extcred.SupportsUser)}
cfg := &core.CliConfig{AppID: "cli_x", Brand: core.BrandFeishu, SupportedIdentities: uint8(extcred.SupportsUser)}
cred := credential.NewCredentialProvider(
[]extcred.Provider{&fakeExtProvider{name: "corp-sso", account: &extcred.Account{AppID: "cli_x"}}},
nil, nil,
@@ -199,7 +197,7 @@ func TestDoctor_ExternalProvider_IdentityReadyHintNotBlockedCommand(t *testing.T
)
out := &bytes.Buffer{}
f := &cmdutil.Factory{
Config: func() (*configpkg.CliConfig, error) { return cfg, nil },
Config: func() (*core.CliConfig, error) { return cfg, nil },
Credential: cred,
IOStreams: &cmdutil.IOStreams{Out: out, ErrOut: &bytes.Buffer{}},
}

View File

@@ -14,7 +14,7 @@ import (
"github.com/larksuite/cli/internal/apicatalog"
internalauth "github.com/larksuite/cli/internal/auth"
"github.com/larksuite/cli/internal/cmdutil"
identitypkg "github.com/larksuite/cli/internal/identity"
"github.com/larksuite/cli/internal/core"
"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(identitypkg.AsUser)
identity = string(core.AsUser)
}
if identity != string(identitypkg.AsUser) && identity != string(identitypkg.AsBot) {
if identity != string(core.AsUser) && identity != string(core.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(identitypkg.AsUser)}
authTypes = []string{string(core.AsUser)}
}
for _, authType := range authTypes {
if authType == identity {

View File

@@ -14,10 +14,10 @@ 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/bus"
"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.
@@ -35,7 +35,7 @@ func NewCmdBus(f *cmdutil.Factory) *cobra.Command {
}
// Sanitize AppID: an unsanitized value could escape events/ via ".." or separators.
eventsDir := filepath.Join(workspace.GetConfigDir(), "events", event.SanitizeAppID(cfg.AppID))
eventsDir := filepath.Join(core.GetConfigDir(), "events", event.SanitizeAppID(cfg.AppID))
logger, err := bus.SetupBusLogger(eventsDir)
if err != nil {

View File

@@ -8,10 +8,9 @@ import (
"path/filepath"
"testing"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
)
// The hidden `event _bus` daemon command must exit with a typed file_io error
@@ -25,8 +24,8 @@ func TestBusCommandLoggerSetupFailureIsTypedFileIO(t *testing.T) {
t.Fatal(err)
}
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "cli_bus_test", AppSecret: "secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "cli_bus_test", AppSecret: "secret", Brand: core.BrandFeishu,
})
cmd := NewCmdBus(f)
cmd.SetArgs([]string{})

View File

@@ -10,9 +10,8 @@ import (
"encoding/json"
"fmt"
brandpkg "github.com/larksuite/cli/brand"
"github.com/larksuite/cli/internal/core"
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
@@ -68,23 +67,23 @@ func encodeAddons(a ManifestAddons) (string, error) {
}
// consoleAddonsURL builds the scan-to-enable deep link carrying incremental scopes/events/callbacks.
func consoleAddonsURL(brand brandpkg.Brand, appID string, a ManifestAddons) (string, error) {
func consoleAddonsURL(brand core.LarkBrand, appID string, a ManifestAddons) (string, error) {
encoded, err := encodeAddons(a)
if err != nil {
return "", err
}
host := brandpkg.ResolveEndpoints(brand).Open
host := core.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 brandpkg.Brand, appID string) string {
host := brandpkg.ResolveEndpoints(brand).Open
func consoleLandingURL(brand core.LarkBrand, appID string) string {
host := core.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 brandpkg.Brand, appID string, a ManifestAddons) string {
func addonsHintURL(brand core.LarkBrand, appID string, a ManifestAddons) string {
url, err := consoleAddonsURL(brand, appID, a)
if err != nil {
return consoleLandingURL(brand, appID)
@@ -95,7 +94,7 @@ func addonsHintURL(brand brandpkg.Brand, 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 identitypkg.Identity, missing []string) ManifestAddons {
func missingScopeAddons(identity core.Identity, missing []string) ManifestAddons {
s := &AddonsScopes{Tenant: []string{}, User: []string{}}
if identity.IsBot() {
s.Tenant = missing
@@ -107,7 +106,7 @@ func missingScopeAddons(identity identitypkg.Identity, missing []string) Manifes
// 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 identitypkg.Identity, missing []string) ManifestAddons {
func missingSubscriptionAddons(subType eventlib.SubscriptionType, identity core.Identity, missing []string) ManifestAddons {
if subType == eventlib.SubTypeCallback {
return ManifestAddons{Callbacks: &AddonsCallbacks{Items: missing}}
}

View File

@@ -12,9 +12,8 @@ import (
"strings"
"testing"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/internal/core"
eventlib "github.com/larksuite/cli/internal/event"
"github.com/larksuite/cli/internal/identity"
)
func decodeAddons(t *testing.T, encoded string) ManifestAddons {
@@ -56,11 +55,11 @@ func TestEncodeAddons_RoundTrip(t *testing.T) {
}
func TestConsoleAddonsURL_FormatAndBrandHost(t *testing.T) {
url, err := consoleAddonsURL(brand.Feishu, "cli_x", ManifestAddons{Callbacks: &AddonsCallbacks{Items: []string{"card.action.trigger"}}})
url, err := consoleAddonsURL(core.BrandFeishu, "cli_x", ManifestAddons{Callbacks: &AddonsCallbacks{Items: []string{"card.action.trigger"}}})
if err != nil {
t.Fatalf("url: %v", err)
}
host := brand.ResolveEndpoints(brand.Feishu).Open
host := core.ResolveEndpoints(core.BrandFeishu).Open
prefix := host + "/page/launcher?clientID=cli_x&addons="
if !strings.HasPrefix(url, prefix) {
t.Errorf("url = %q, want prefix %q", url, prefix)
@@ -72,22 +71,22 @@ func TestConsoleAddonsURL_FormatAndBrandHost(t *testing.T) {
}
func TestMissingScopeAddons_ByIdentity(t *testing.T) {
bot := missingScopeAddons(identity.AsBot, []string{"im:message"})
bot := missingScopeAddons(core.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(identity.AsUser, []string{"im:message"})
user := missingScopeAddons(core.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, identity.AsBot, []string{"im.message.receive_v1"})
ev := missingSubscriptionAddons(eventlib.SubTypeEvent, core.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, identity.AsBot, []string{"card.action.trigger"})
cb := missingSubscriptionAddons(eventlib.SubTypeCallback, core.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)
}
@@ -97,9 +96,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(identity.AsBot, []string{"im:message"}),
missingScopeAddons(identity.AsUser, []string{"im:message"}),
missingSubscriptionAddons(eventlib.SubTypeEvent, identity.AsBot, []string{"im.message.receive_v1"}),
missingScopeAddons(core.AsBot, []string{"im:message"}),
missingScopeAddons(core.AsUser, []string{"im:message"}),
missingSubscriptionAddons(eventlib.SubTypeEvent, core.AsBot, []string{"im.message.receive_v1"}),
}
for i, a := range cases {
raw, err := json.Marshal(a)

View File

@@ -16,16 +16,15 @@ import (
"github.com/spf13/cobra"
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/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"
)
@@ -119,7 +118,7 @@ func runConsume(cmd *cobra.Command, f *cmdutil.Factory, eventKey string, o consu
outputDir = safePath
}
domain := brandpkg.ResolveEndpoints(cfg.Brand).Open
domain := core.ResolveEndpoints(cfg.Brand).Open
// Surface auth errors before forking the bus daemon.
if _, err := resolveTenantToken(cmd.Context(), f, cfg.AppID); err != nil {
@@ -132,7 +131,7 @@ func runConsume(cmd *cobra.Command, f *cmdutil.Factory, eventKey string, o consu
}
runtime := &consumeRuntime{client: apiClient, accessIdentity: identity}
// botRuntime pins AsBot: /app_versions rejects UAT (99991668) and /connection is app-level.
botRuntime := &consumeRuntime{client: apiClient, accessIdentity: identitypkg.AsBot}
botRuntime := &consumeRuntime{client: apiClient, accessIdentity: core.AsBot}
// Weak-dependency fetch: failures leave appVer==nil and downgrade preflight to a no-op.
preflightErrOut := f.IOStreams.ErrOut
@@ -225,8 +224,8 @@ func runConsume(cmd *cobra.Command, f *cmdutil.Factory, eventKey string, o consu
}
// resolveIdentity resolves the session identity and enforces keyDef.AuthTypes as a whitelist.
func resolveIdentity(cmd *cobra.Command, f *cmdutil.Factory, keyDef *eventlib.KeyDefinition) (identitypkg.Identity, error) {
flagAs := identitypkg.Identity(cmd.Flag("as").Value.String())
func resolveIdentity(cmd *cobra.Command, f *cmdutil.Factory, keyDef *eventlib.KeyDefinition) (core.Identity, error) {
flagAs := core.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 {
@@ -239,9 +238,9 @@ func resolveIdentity(cmd *cobra.Command, f *cmdutil.Factory, keyDef *eventlib.Ke
type preflightCtx struct {
factory *cmdutil.Factory
appID string
brand brandpkg.Brand
brand core.LarkBrand
eventKey string
identity identitypkg.Identity
identity core.Identity
keyDef *eventlib.KeyDefinition
appVer *appmeta.AppVersion
// subscribedCallbacks is the application/get 底账 for callback-type EventKeys;
@@ -265,7 +264,7 @@ func preflightScopes(ctx context.Context, pf *preflightCtx) error {
return nil
}
storedScopes = strings.Join(pf.appVer.TenantScopes, " ")
case pf.identity == identitypkg.AsUser:
case pf.identity == core.AsUser:
result, err := pf.factory.Credential.ResolveToken(ctx, credential.NewTokenSpec(pf.identity, pf.appID))
if err != nil || result == nil || result.Scopes == "" {
return nil //nolint:nilerr // best-effort: bus handshake will surface real auth error
@@ -292,7 +291,7 @@ func preflightScopes(ctx context.Context, pf *preflightCtx) error {
// 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 brandpkg.Brand, appID string, identity identitypkg.Identity, missing []string) string {
func scopeRemediationHint(brand core.LarkBrand, appID string, identity core.Identity, missing []string) string {
if identity.IsBot() {
return fmt.Sprintf("grant these scopes by scanning: %s",
addonsHintURL(brand, appID, missingScopeAddons(identity, missing)))
@@ -369,7 +368,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(identitypkg.AsBot, appID))
result, err := f.Credential.ResolveToken(ctx, credential.NewTokenSpec(core.AsBot, appID))
if err != nil {
if _, ok := errs.ProblemOf(err); ok {
return "", err

View File

@@ -11,7 +11,7 @@ import (
"time"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/event/protocol"
"github.com/larksuite/cli/internal/output"
)
@@ -287,7 +287,7 @@ func errorAs(err error, target interface{}) bool {
}
func TestNewCmdFactories_WireFlags(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "cli_XXXXXXXXXXXXXXXX"})
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "cli_XXXXXXXXXXXXXXXX"})
t.Run("consume", func(t *testing.T) {
cmd := NewCmdConsume(f)

View File

@@ -9,7 +9,7 @@ import (
"testing"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
eventlib "github.com/larksuite/cli/internal/event"
_ "github.com/larksuite/cli/events"
@@ -29,7 +29,7 @@ func TestEventLookup_VCMeetingLifecycleKeys(t *testing.T) {
}
func TestRunList_TextOutput(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
if err := runList(f, false); err != nil {
t.Fatalf("runList: %v", err)
@@ -53,7 +53,7 @@ func TestRunList_TextOutput(t *testing.T) {
}
func TestRunList_JSONOutput(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
if err := runList(f, true); err != nil {
t.Fatalf("runList json: %v", err)

View File

@@ -8,14 +8,13 @@ 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 brandpkg.Brand, identity identitypkg.Identity, keyDef *eventlib.KeyDefinition, appVer *appmeta.AppVersion) *preflightCtx {
func newPreflightCtx(appID string, brand core.LarkBrand, identity core.Identity, keyDef *eventlib.KeyDefinition, appVer *appmeta.AppVersion) *preflightCtx {
key := ""
if keyDef != nil {
key = keyDef.Key
@@ -109,7 +108,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", identitypkg.AsBot, def, nil))
err := preflightScopes(nil, newPreflightCtx("cli_x", "feishu", core.AsBot, def, nil))
if err != nil {
t.Fatalf("bot + nil appVer should skip, got: %v", err)
}
@@ -125,7 +124,7 @@ func TestPreflightScopes_Bot_AllGranted_Passes(t *testing.T) {
"im:message.group_at_msg",
"contact:user:readonly",
}}
err := preflightScopes(nil, newPreflightCtx("cli_x", "feishu", identitypkg.AsBot, def, appVer))
err := preflightScopes(nil, newPreflightCtx("cli_x", "feishu", core.AsBot, def, appVer))
if err != nil {
t.Fatalf("all scopes granted, unexpected error: %v", err)
}
@@ -137,7 +136,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", identitypkg.AsBot, def, appVer))
err := preflightScopes(nil, newPreflightCtx("cli_x", "feishu", core.AsBot, def, appVer))
if err == nil {
t.Fatal("expected error for missing scope")
}
@@ -170,7 +169,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", identitypkg.AsBot, def, nil)); err != nil {
if err := preflightScopes(nil, newPreflightCtx("cli_x", "feishu", core.AsBot, def, nil)); err != nil {
t.Fatalf("no required scopes means nothing to verify, got: %v", err)
}
}
@@ -178,9 +177,9 @@ func TestPreflightScopes_NoRequiredScopes_SkipsCheck(t *testing.T) {
func TestPreflightEventTypes_CallbackMissing(t *testing.T) {
pf := &preflightCtx{
appID: "cli_x",
brand: brandpkg.Feishu,
brand: core.BrandFeishu,
eventKey: "test.cb",
identity: identitypkg.AsBot,
identity: core.AsBot,
subscribedCallbacks: []string{"profile.view.get"},
keyDef: &eventlib.KeyDefinition{
Key: "test.cb",
@@ -207,9 +206,9 @@ func TestPreflightEventTypes_CallbackMissing(t *testing.T) {
func TestPreflightEventTypes_CallbackSkippedWhenNil(t *testing.T) {
pf := &preflightCtx{
appID: "cli_x",
brand: brandpkg.Feishu,
brand: core.BrandFeishu,
eventKey: "test.cb",
identity: identitypkg.AsBot,
identity: core.AsBot,
subscribedCallbacks: nil, // fetch 失败/拿不到 -> 弱依赖跳过
keyDef: &eventlib.KeyDefinition{
Key: "test.cb",
@@ -228,9 +227,9 @@ func TestPreflightEventTypes_CallbackEmptyReportsMissing(t *testing.T) {
// not skipped as a weak dependency.
pf := &preflightCtx{
appID: "cli_x",
brand: brandpkg.Feishu,
brand: core.BrandFeishu,
eventKey: "test.cb",
identity: identitypkg.AsBot,
identity: core.AsBot,
subscribedCallbacks: []string{}, // fetched, none subscribed
keyDef: &eventlib.KeyDefinition{
Key: "test.cb",
@@ -250,9 +249,9 @@ func TestPreflightEventTypes_CallbackEmptyReportsMissing(t *testing.T) {
func TestPreflightEventTypes_CallbackAllSubscribed_Passes(t *testing.T) {
pf := &preflightCtx{
appID: "cli_x",
brand: brandpkg.Feishu,
brand: core.BrandFeishu,
eventKey: "test.cb",
identity: identitypkg.AsBot,
identity: core.AsBot,
subscribedCallbacks: []string{"card.action.trigger", "profile.view.get"},
keyDef: &eventlib.KeyDefinition{
Key: "test.cb",
@@ -267,12 +266,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(brandpkg.Feishu, "cli_x", identitypkg.AsBot, []string{"im:message"})
bot := scopeRemediationHint(core.BrandFeishu, "cli_x", core.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(brandpkg.Feishu, "cli_x", identitypkg.AsUser, []string{"im:message"})
user := scopeRemediationHint(core.BrandFeishu, "cli_x", core.AsUser, []string{"im:message"})
if !strings.Contains(user, "auth login --scope") {
t.Errorf("user hint should direct to auth login, got: %s", user)
}

View File

@@ -9,13 +9,13 @@ import (
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/client"
"github.com/larksuite/cli/internal/identity"
"github.com/larksuite/cli/internal/core"
)
// consumeRuntime routes event.APIClient calls through the shared client.APIClient with a pinned identity.
type consumeRuntime struct {
client *client.APIClient
accessIdentity identity.Identity
accessIdentity core.Identity
}
func (r *consumeRuntime) CallAPI(ctx context.Context, method, path string, body interface{}) (json.RawMessage, error) {

View File

@@ -14,12 +14,10 @@ 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"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/credential"
"github.com/larksuite/cli/internal/identity"
)
// staticTokenResolver always returns a fixed token without any HTTP calls.
@@ -47,9 +45,9 @@ func newTestConsumeRuntime(rt http.RoundTripper) *consumeRuntime {
SDK: sdk,
ErrOut: io.Discard,
Credential: credential.NewCredentialProvider(nil, nil, &staticTokenResolver{}, nil),
Config: &configpkg.CliConfig{AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu},
Config: &core.CliConfig{AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu},
},
accessIdentity: identity.AsBot,
accessIdentity: core.AsBot,
}
}

View File

@@ -12,7 +12,7 @@ import (
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
eventlib "github.com/larksuite/cli/internal/event"
"github.com/larksuite/cli/internal/event/schemas"
@@ -43,7 +43,7 @@ type approvalSchemaJSONProperty struct {
}
func TestRunSchema_ProcessedKey_Text(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
if err := runSchema(f, "im.message.receive_v1", false); err != nil {
t.Fatalf("runSchema: %v", err)
@@ -63,7 +63,7 @@ func TestRunSchema_ProcessedKey_Text(t *testing.T) {
}
func TestRunSchema_NativeKey_WrapsEnvelope(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
if err := runSchema(f, "im.message.message_read_v1", false); err != nil {
t.Fatalf("runSchema: %v", err)
@@ -83,7 +83,7 @@ func TestRunSchema_NativeKey_WrapsEnvelope(t *testing.T) {
}
func TestRunSchema_UnknownKey_SuggestsAlternatives(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
err := runSchema(f, "im.message.recieve_v1", false)
if err == nil {
@@ -99,7 +99,7 @@ func TestRunSchema_UnknownKey_SuggestsAlternatives(t *testing.T) {
}
func TestRunSchema_JSONOutput(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
if err := runSchema(f, "im.message.receive_v1", true); err != nil {
t.Fatalf("runSchema json: %v", err)
@@ -120,7 +120,7 @@ func TestRunSchema_JSONOutput(t *testing.T) {
}
func TestRunSchema_ReceiveMessageAgentFieldsJSON(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
if err := runSchema(f, "im.message.receive_v1", true); err != nil {
t.Fatalf("runSchema json: %v", err)
@@ -154,7 +154,7 @@ func TestRunSchema_ReceiveMessageAgentFieldsJSON(t *testing.T) {
}
func TestRunSchema_TaskUpdateUserAccessJSON(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
if err := runSchema(f, "task.task.update_user_access_v2", true); err != nil {
t.Fatalf("runSchema json: %v", err)
@@ -193,7 +193,7 @@ 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, &configpkg.CliConfig{AppID: "test"})
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
if err := runSchema(f, tc.key, true); err != nil {
t.Fatalf("runSchema json: %v", err)
@@ -241,7 +241,7 @@ 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, &configpkg.CliConfig{AppID: "test"})
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
if err := runSchema(f, key, true); err != nil {
t.Fatalf("runSchema json: %v", err)
@@ -288,7 +288,7 @@ func TestSchema_RendersSubscriptionKeyMarker(t *testing.T) {
Schema: eventlib.SchemaDef{Native: &eventlib.SchemaSpec{Type: reflect.TypeOf(struct{ X string }{})}},
})
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
if err := runSchema(f, syntheticKey, false); err != nil {
t.Fatalf("runSchema: %v", err)
}
@@ -334,7 +334,7 @@ func TestSchema_JSON_IncludesSubscriptionKey(t *testing.T) {
Schema: eventlib.SchemaDef{Native: &eventlib.SchemaSpec{Type: reflect.TypeOf(struct{ X string }{})}},
})
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{AppID: "test"})
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
if err := runSchema(f, syntheticKey, true); err != nil {
t.Fatalf("runSchema json: %v", err)
}

View File

@@ -4,7 +4,7 @@
package cmd
import (
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"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 := configpkg.LoadMultiAppConfig()
raw, err := core.LoadMultiAppConfig()
if err != nil || raw == nil {
return true
}

View File

@@ -8,10 +8,8 @@ import (
"os"
"testing"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/secret"
"github.com/larksuite/cli/internal/core"
"github.com/spf13/pflag"
)
@@ -60,8 +58,8 @@ func TestIsSingleAppMode_NoConfig(t *testing.T) {
func TestIsSingleAppMode_SingleApp(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
saveAppsForTest(t, []configpkg.AppConfig{
{Name: "default", AppId: "cli_a", AppSecret: secret.PlainSecret("x"), Brand: brand.Feishu},
saveAppsForTest(t, []core.AppConfig{
{Name: "default", AppId: "cli_a", AppSecret: core.PlainSecret("x"), Brand: core.BrandFeishu},
})
if !isSingleAppMode() {
t.Fatal("isSingleAppMode() = false, want true for single-app config")
@@ -70,9 +68,9 @@ func TestIsSingleAppMode_SingleApp(t *testing.T) {
func TestIsSingleAppMode_MultiApp(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
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},
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},
})
if isSingleAppMode() {
t.Fatal("isSingleAppMode() = true, want false for multi-app config")
@@ -103,10 +101,10 @@ func TestBuildInternal_DefaultShowsProfileFlag(t *testing.T) {
}
}
func saveAppsForTest(t *testing.T, apps []configpkg.AppConfig) {
func saveAppsForTest(t *testing.T, apps []core.AppConfig) {
t.Helper()
multi := &configpkg.MultiAppConfig{CurrentApp: apps[0].Name, Apps: apps}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
multi := &core.MultiAppConfig{CurrentApp: apps[0].Name, Apps: apps}
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
}

View File

@@ -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
// workspace.GetBaseConfigDir) so that test isolation, container deployments
// core.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(workspace.GetBaseConfigDir(), userPolicyFileName), nil
return filepath.Join(core.GetBaseConfigDir(), userPolicyFileName), nil
}
// warnPolicyError writes a one-line stderr warning when the user policy

View File

@@ -12,13 +12,11 @@ import (
"github.com/spf13/cobra"
brandpkg "github.com/larksuite/cli/brand"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"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.
@@ -55,7 +53,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 := configpkg.ValidateProfileName(name); err != nil {
if err := core.ValidateProfileName(name); err != nil {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%v", err).
WithCause(err).
WithParam("--name")
@@ -92,12 +90,12 @@ func profileAddRun(f *cmdutil.Factory, name, appID string, appSecretStdin bool,
}
// Load or create config
multi, err := configpkg.LoadMultiAppConfig()
multi, err := core.LoadMultiAppConfig()
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return errs.NewInternalError(errs.SubtypeFileIO, "failed to load config: %v", err).WithCause(err)
}
multi = &configpkg.MultiAppConfig{}
multi = &core.MultiAppConfig{}
}
// Check name uniqueness
@@ -117,12 +115,12 @@ func profileAddRun(f *cmdutil.Factory, name, appID string, appSecretStdin bool,
}
// Store secret securely
secret, err := secretpkg.ForStorage(appID, secretpkg.PlainSecret(appSecret), f.Keychain)
secret, err := core.ForStorage(appID, core.PlainSecret(appSecret), f.Keychain)
if err != nil {
return errs.NewInternalError(errs.SubtypeStorage, "%v", err).WithCause(err)
}
parsedBrand := brandpkg.ParseBrand(brand)
parsedBrand := core.ParseBrand(brand)
// Capture current profile before appending (avoid setting PreviousApp to self)
var previousName string
@@ -133,13 +131,13 @@ func profileAddRun(f *cmdutil.Factory, name, appID string, appSecretStdin bool,
}
// Append profile
multi.Apps = append(multi.Apps, configpkg.AppConfig{
multi.Apps = append(multi.Apps, core.AppConfig{
Name: name,
AppId: appID,
AppSecret: secret,
Brand: parsedBrand,
Lang: i18n.Lang(lang),
Users: []configpkg.AppUser{},
Users: []core.AppUser{},
})
if useAfter {
@@ -149,7 +147,7 @@ func profileAddRun(f *cmdutil.Factory, name, appID string, appSecretStdin bool,
multi.CurrentApp = name
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
return errs.NewInternalError(errs.SubtypeStorage, "failed to save config: %v", err).WithCause(err)
}

View File

@@ -9,22 +9,21 @@ 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"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"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 brand.Brand `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 core.LarkBrand `json:"brand"`
Active bool `json:"active"`
User string `json:"user,omitempty"`
TokenStatus string `json:"tokenStatus,omitempty"`
}
// NewCmdProfileList creates the profile list subcommand.
@@ -41,7 +40,7 @@ func NewCmdProfileList(f *cmdutil.Factory) *cobra.Command {
}
func profileListRun(f *cmdutil.Factory) error {
multi, err := configpkg.LoadMultiAppConfig()
multi, err := core.LoadMultiAppConfig()
if err != nil {
if errors.Is(err, os.ErrNotExist) {
output.PrintJson(f.IOStreams.Out, []profileListItem{})

View File

@@ -11,13 +11,11 @@ import (
"strings"
"testing"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/i18n"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/secret"
"github.com/larksuite/cli/internal/vfs"
)
@@ -77,7 +75,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 := configpkg.LoadMultiAppConfig()
saved, err := core.LoadMultiAppConfig()
if err != nil {
t.Fatalf("LoadMultiAppConfig() error = %v", err)
}
@@ -94,7 +92,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, _ := configpkg.LoadMultiAppConfig()
saved, _ := core.LoadMultiAppConfig()
if app := saved.FindApp("p"); app == nil || app.Lang != "" {
t.Errorf("stored Lang = %v, want \"\" (unset)", app)
}
@@ -117,13 +115,13 @@ func TestProfileAddRun_Lang(t *testing.T) {
func TestProfileAddRun_UseAfterUpdatesCurrentAndPrevious(t *testing.T) {
setupProfileConfigDir(t)
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "default",
Apps: []configpkg.AppConfig{
{Name: "default", AppId: "app-default", AppSecret: secret.PlainSecret("secret-default"), Brand: brand.Feishu},
Apps: []core.AppConfig{
{Name: "default", AppId: "app-default", AppSecret: core.PlainSecret("secret-default"), Brand: core.BrandFeishu},
},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
@@ -134,7 +132,7 @@ func TestProfileAddRun_UseAfterUpdatesCurrentAndPrevious(t *testing.T) {
t.Fatalf("profileAddRun() error = %v", err)
}
saved, err := configpkg.LoadMultiAppConfig()
saved, err := core.LoadMultiAppConfig()
if err != nil {
t.Fatalf("LoadMultiAppConfig() error = %v", err)
}
@@ -151,15 +149,15 @@ func TestProfileAddRun_UseAfterUpdatesCurrentAndPrevious(t *testing.T) {
func TestProfileRemoveRun_RemovesCurrentProfileAndSwitchesToFirstRemaining(t *testing.T) {
setupProfileConfigDir(t)
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "target",
PreviousApp: "default",
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},
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},
},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
@@ -168,7 +166,7 @@ func TestProfileRemoveRun_RemovesCurrentProfileAndSwitchesToFirstRemaining(t *te
t.Fatalf("profileRemoveRun() error = %v", err)
}
saved, err := configpkg.LoadMultiAppConfig()
saved, err := core.LoadMultiAppConfig()
if err != nil {
t.Fatalf("LoadMultiAppConfig() error = %v", err)
}
@@ -185,17 +183,17 @@ func TestProfileRemoveRun_RemovesCurrentProfileAndSwitchesToFirstRemaining(t *te
func TestProfileRenameRun_UpdatesCurrentAndPreviousReferences(t *testing.T) {
setupProfileConfigDir(t)
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "old",
PreviousApp: "old",
Apps: []configpkg.AppConfig{{
Apps: []core.AppConfig{{
Name: "old",
AppId: "app-old",
AppSecret: secret.PlainSecret("secret-old"),
Brand: brand.Feishu,
AppSecret: core.PlainSecret("secret-old"),
Brand: core.BrandFeishu,
}},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
@@ -204,7 +202,7 @@ func TestProfileRenameRun_UpdatesCurrentAndPreviousReferences(t *testing.T) {
t.Fatalf("profileRenameRun() error = %v", err)
}
saved, err := configpkg.LoadMultiAppConfig()
saved, err := core.LoadMultiAppConfig()
if err != nil {
t.Fatalf("LoadMultiAppConfig() error = %v", err)
}
@@ -221,17 +219,17 @@ func TestProfileRenameRun_UpdatesCurrentAndPreviousReferences(t *testing.T) {
func TestProfileRenameRun_AllowsRenameToOwnAppID(t *testing.T) {
setupProfileConfigDir(t)
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "old",
PreviousApp: "old",
Apps: []configpkg.AppConfig{{
Apps: []core.AppConfig{{
Name: "old",
AppId: "app-old",
AppSecret: secret.PlainSecret("secret-old"),
Brand: brand.Feishu,
AppSecret: core.PlainSecret("secret-old"),
Brand: core.BrandFeishu,
}},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
@@ -240,7 +238,7 @@ func TestProfileRenameRun_AllowsRenameToOwnAppID(t *testing.T) {
t.Fatalf("profileRenameRun() error = %v", err)
}
saved, err := configpkg.LoadMultiAppConfig()
saved, err := core.LoadMultiAppConfig()
if err != nil {
t.Fatalf("LoadMultiAppConfig() error = %v", err)
}
@@ -257,15 +255,15 @@ func TestProfileRenameRun_AllowsRenameToOwnAppID(t *testing.T) {
func TestProfileUseRun_ToggleBackUsesPreviousProfile(t *testing.T) {
setupProfileConfigDir(t)
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "default",
PreviousApp: "target",
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},
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},
},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
@@ -274,7 +272,7 @@ func TestProfileUseRun_ToggleBackUsesPreviousProfile(t *testing.T) {
t.Fatalf("profileUseRun() error = %v", err)
}
saved, err := configpkg.LoadMultiAppConfig()
saved, err := core.LoadMultiAppConfig()
if err != nil {
t.Fatalf("LoadMultiAppConfig() error = %v", err)
}
@@ -288,14 +286,14 @@ func TestProfileUseRun_ToggleBackUsesPreviousProfile(t *testing.T) {
func TestProfileListRun_OutputsProfiles(t *testing.T) {
setupProfileConfigDir(t)
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "default",
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},
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},
},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
@@ -341,14 +339,14 @@ func TestProfileListRun_NotConfiguredReturnsEmptyList(t *testing.T) {
func TestProfileRemoveRun_SaveFailureReturnsStructuredError(t *testing.T) {
setupProfileConfigDir(t)
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "target",
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},
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},
},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
@@ -366,16 +364,16 @@ func TestProfileRemoveRun_SaveFailureReturnsStructuredError(t *testing.T) {
func TestProfileRenameRun_SaveFailureReturnsStructuredError(t *testing.T) {
setupProfileConfigDir(t)
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "old",
Apps: []configpkg.AppConfig{{
Apps: []core.AppConfig{{
Name: "old",
AppId: "app-old",
AppSecret: secret.PlainSecret("secret-old"),
Brand: brand.Feishu,
AppSecret: core.PlainSecret("secret-old"),
Brand: core.BrandFeishu,
}},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
@@ -393,14 +391,14 @@ func TestProfileRenameRun_SaveFailureReturnsStructuredError(t *testing.T) {
func TestProfileUseRun_SaveFailureReturnsStructuredError(t *testing.T) {
setupProfileConfigDir(t)
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "default",
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},
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},
},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
@@ -463,14 +461,14 @@ func assertValidationError(t *testing.T, err error, wantSubtype errs.Subtype, wa
func saveTwoProfiles(t *testing.T) {
t.Helper()
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "default",
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},
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},
},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
}
@@ -611,13 +609,13 @@ func TestProfileRemoveRun_ValidationErrors(t *testing.T) {
t.Run("cannot remove the only profile", func(t *testing.T) {
setupProfileConfigDir(t)
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "solo",
Apps: []configpkg.AppConfig{
{Name: "solo", AppId: "app-solo", AppSecret: secret.PlainSecret("secret-solo"), Brand: brand.Feishu},
Apps: []core.AppConfig{
{Name: "solo", AppId: "app-solo", AppSecret: core.PlainSecret("secret-solo"), Brand: core.BrandFeishu},
},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
f, _, _, _ := cmdutil.TestFactory(t, nil)

View File

@@ -12,9 +12,8 @@ import (
"github.com/larksuite/cli/errs"
larkauth "github.com/larksuite/cli/internal/auth"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/secret"
)
// NewCmdProfileRemove creates the profile remove subcommand.
@@ -35,7 +34,7 @@ func NewCmdProfileRemove(f *cmdutil.Factory) *cobra.Command {
}
func profileRemoveRun(f *cmdutil.Factory, name string) error {
multi, err := configpkg.LoadOrNotConfigured()
multi, err := core.LoadOrNotConfigured()
if err != nil {
return err
}
@@ -67,12 +66,12 @@ func profileRemoveRun(f *cmdutil.Factory, name string) error {
multi.PreviousApp = ""
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
return errs.NewInternalError(errs.SubtypeStorage, "failed to save config: %v", err).WithCause(err)
}
// Best-effort credential cleanup after config commit
secret.RemoveSecretStore(appSecret, f.Keychain)
core.RemoveSecretStore(appSecret, f.Keychain)
for _, user := range users {
larkauth.RemoveStoredToken(appId, user.UserOpenId)
}

View File

@@ -11,7 +11,7 @@ import (
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"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 := configpkg.ValidateProfileName(newName); err != nil {
if err := core.ValidateProfileName(newName); err != nil {
return errs.NewValidationError(errs.SubtypeInvalidArgument, "%v", err).WithCause(err)
}
multi, err := configpkg.LoadOrNotConfigured()
multi, err := core.LoadOrNotConfigured()
if err != nil {
return err
}
@@ -67,7 +67,7 @@ func profileRenameRun(f *cmdutil.Factory, oldName, newName string) error {
multi.PreviousApp = newName
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
return errs.NewInternalError(errs.SubtypeStorage, "failed to save config: %v", err).WithCause(err)
}

View File

@@ -11,7 +11,7 @@ import (
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"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 := configpkg.LoadOrNotConfigured()
multi, err := core.LoadOrNotConfigured()
if err != nil {
return err
}
@@ -67,7 +67,7 @@ func profileUseRun(f *cmdutil.Factory, name string) error {
}
multi.CurrentApp = targetName
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
return errs.NewInternalError(errs.SubtypeStorage, "failed to save config: %v", err).WithCause(err)
}

View File

@@ -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/identity"
"github.com/larksuite/cli/internal/core"
)
// pruneForStrictMode removes commands incompatible with the active strict mode.
func pruneForStrictMode(root *cobra.Command, mode identity.StrictMode) {
func pruneForStrictMode(root *cobra.Command, mode core.StrictMode) {
pruneIncompatible(root, mode)
pruneEmpty(root)
}
@@ -25,7 +25,7 @@ func pruneForStrictMode(root *cobra.Command, mode identity.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 identity.StrictMode) {
func pruneIncompatible(parent *cobra.Command, mode core.StrictMode) {
forced := string(mode.ForcedIdentity())
var toRemove []*cobra.Command
var toAdd []*cobra.Command
@@ -44,7 +44,7 @@ func pruneIncompatible(parent *cobra.Command, mode identity.StrictMode) {
}
}
func strictModeStubFrom(child *cobra.Command, mode identity.StrictMode) *cobra.Command {
func strictModeStubFrom(child *cobra.Command, mode core.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

View File

@@ -12,7 +12,7 @@ import (
"github.com/larksuite/cli/extension/platform"
"github.com/larksuite/cli/internal/cmdpolicy"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/identity"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/output"
"github.com/spf13/cobra"
)
@@ -75,7 +75,7 @@ func findCmd(root *cobra.Command, names ...string) *cobra.Command {
func TestPruneForStrictMode_Bot(t *testing.T) {
root := newTestTree()
pruneForStrictMode(root, identity.StrictModeBot)
pruneForStrictMode(root, core.StrictModeBot)
if cmd := findCmd(root, "im", "+search"); cmd == nil || !cmd.Hidden {
t.Error("+search (user-only) should be replaced by a hidden stub in bot mode")
@@ -99,7 +99,7 @@ func TestPruneForStrictMode_Bot(t *testing.T) {
func TestPruneForStrictMode_User(t *testing.T) {
root := newTestTree()
pruneForStrictMode(root, identity.StrictModeUser)
pruneForStrictMode(root, core.StrictModeUser)
if findCmd(root, "im", "+search") == nil {
t.Error("+search (user-only) should be kept in user mode")
@@ -117,7 +117,7 @@ func TestPruneForStrictMode_User(t *testing.T) {
func TestPruneEmpty(t *testing.T) {
root := newTestTree()
pruneForStrictMode(root, identity.StrictModeBot)
pruneForStrictMode(root, core.StrictModeBot)
if cmd := findCmd(root, "im", "messages"); cmd == nil || !cmd.Hidden {
t.Error("resource 'messages' should be kept hidden when only hidden stubs remain")
@@ -144,7 +144,7 @@ func TestPruneForStrictMode_Bot_DirectUserShortcutReturnsStrictMode(t *testing.T
root := newTestTree()
root.SilenceErrors = true
root.SilenceUsage = true
pruneForStrictMode(root, identity.StrictModeBot)
pruneForStrictMode(root, core.StrictModeBot)
root.SetArgs([]string{"im", "+search", "--query", "hello"})
err := root.Execute()
@@ -160,7 +160,7 @@ func TestPruneForStrictMode_Bot_DirectNestedUserMethodReturnsStrictMode(t *testi
root := newTestTree()
root.SilenceErrors = true
root.SilenceUsage = true
pruneForStrictMode(root, identity.StrictModeBot)
pruneForStrictMode(root, core.StrictModeBot)
root.SetArgs([]string{"im", "messages", "search", "--query", "hello"})
err := root.Execute()
@@ -176,7 +176,7 @@ func TestPruneForStrictMode_Bot_DirectAuthLoginReturnsStrictMode(t *testing.T) {
root := newTestTree()
root.SilenceErrors = true
root.SilenceUsage = true
pruneForStrictMode(root, identity.StrictModeBot)
pruneForStrictMode(root, core.StrictModeBot)
root.SetArgs([]string{"auth", "login", "--json", "--scope", "im:message.send_as_user"})
err := root.Execute()
@@ -192,7 +192,7 @@ func TestPruneForStrictMode_User_DirectBotShortcutReturnsStrictMode(t *testing.T
root := newTestTree()
root.SilenceErrors = true
root.SilenceUsage = true
pruneForStrictMode(root, identity.StrictModeUser)
pruneForStrictMode(root, core.StrictModeUser)
root.SetArgs([]string{"im", "+subscribe", "--topic", "x"})
err := root.Execute()
@@ -215,7 +215,7 @@ func TestPruneForStrictMode_User_DirectBotShortcutReturnsStrictMode(t *testing.T
// stops at the stub and proceeds to its RunE.
func TestStrictModeStub_BypassesParentPersistentPreRunE(t *testing.T) {
root := newTestTree()
pruneForStrictMode(root, identity.StrictModeBot)
pruneForStrictMode(root, core.StrictModeBot)
stub := findCmd(root, "auth", "login")
if stub == nil {
t.Fatal("auth/login stub should exist after StrictModeBot")
@@ -235,7 +235,7 @@ func TestStrictModeStub_BypassesParentPersistentPreRunE(t *testing.T) {
// stub's RunE.
func TestStrictModeStub_BypassesArgsValidator(t *testing.T) {
root := newTestTree()
pruneForStrictMode(root, identity.StrictModeBot)
pruneForStrictMode(root, core.StrictModeBot)
stub := findCmd(root, "auth", "login")
if stub == nil {
t.Fatal("auth/login stub should exist after StrictModeBot")
@@ -256,7 +256,7 @@ func TestStrictModeStub_BypassesArgsValidator(t *testing.T) {
// still inspect the structured denial taxonomy via errors.As.
func TestStrictModeStub_StructuredEnvelope(t *testing.T) {
root := newTestTree()
pruneForStrictMode(root, identity.StrictModeBot)
pruneForStrictMode(root, core.StrictModeBot)
stub := findCmd(root, "im", "+search")
if stub == nil {
t.Fatalf("expected im/+search stub")
@@ -318,7 +318,7 @@ func TestStrictModeStub_StructuredEnvelope(t *testing.T) {
// and silently return nil, swallowing the strict-mode error.
func TestStrictModeStub_HasDenialAnnotation(t *testing.T) {
root := newTestTree()
pruneForStrictMode(root, identity.StrictModeBot)
pruneForStrictMode(root, core.StrictModeBot)
// im/+search is user-only -> replaced by a stub in StrictModeBot.
stub := findCmd(root, "im", "+search")
@@ -356,7 +356,7 @@ func TestStrictModeStub_PreservesOriginalMetadata(t *testing.T) {
cmdutil.SetRisk(userOnly, "read")
svc.AddCommand(userOnly)
pruneForStrictMode(root, identity.StrictModeBot)
pruneForStrictMode(root, core.StrictModeBot)
stub := findCmd(root, "im", "+search")
if stub == nil {

View File

@@ -237,7 +237,7 @@ func configureFlagCompletions(args []string) {
// render via the typed envelope writer, which lifts extension fields
// (missing_scopes, console_url, challenge_url, ...) to the top level.
// Routed by errs.CategoryOf via ExitCodeOf. Auth and config errors are
// constructed typed at their origin (internal/auth, internal/config), so the
// constructed typed at their origin (internal/auth, internal/core), so the
// dispatcher no longer promotes any legacy shape here.
// 2. PartialFailure / BareError signals: the result envelope is already on
// stdout; honor the exit code and write nothing to stderr.

View File

@@ -11,20 +11,17 @@ import (
"strings"
"testing"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/cmd/api"
"github.com/larksuite/cli/cmd/auth"
"github.com/larksuite/cli/cmd/service"
"github.com/larksuite/cli/envnames"
"github.com/larksuite/cli/internal/apicatalog"
"github.com/larksuite/cli/internal/build"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/envvars"
"github.com/larksuite/cli/internal/httpmock"
"github.com/larksuite/cli/internal/identity"
"github.com/larksuite/cli/internal/meta"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/secret"
"github.com/larksuite/cli/internal/skillscheck"
"github.com/larksuite/cli/internal/update"
"github.com/larksuite/cli/shortcuts"
@@ -158,37 +155,37 @@ func strictModeFixtureCatalog() apicatalog.Catalog {
})
}
func newStrictModeDefaultFactory(t *testing.T, profile string, mode identity.StrictMode) (*cmdutil.Factory, *bytes.Buffer, *bytes.Buffer) {
func newStrictModeDefaultFactory(t *testing.T, profile string, mode core.StrictMode) (*cmdutil.Factory, *bytes.Buffer, *bytes.Buffer) {
t.Helper()
t.Setenv(envnames.CliAppID, "")
t.Setenv(envnames.CliAppSecret, "")
t.Setenv(envnames.CliUserAccessToken, "")
t.Setenv(envnames.CliTenantAccessToken, "")
t.Setenv(envnames.CliDefaultAs, "")
t.Setenv(envvars.CliAppID, "")
t.Setenv(envvars.CliAppSecret, "")
t.Setenv(envvars.CliUserAccessToken, "")
t.Setenv(envvars.CliTenantAccessToken, "")
t.Setenv(envvars.CliDefaultAs, "")
dir := t.TempDir()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
targetMode := mode
multi := &configpkg.MultiAppConfig{
multi := &core.MultiAppConfig{
CurrentApp: "default",
Apps: []configpkg.AppConfig{
Apps: []core.AppConfig{
{
Name: "default",
AppId: "app-default",
AppSecret: secret.PlainSecret("secret-default"),
Brand: brand.Feishu,
AppSecret: core.PlainSecret("secret-default"),
Brand: core.BrandFeishu,
},
{
Name: "target",
AppId: "app-target",
AppSecret: secret.PlainSecret("secret-target"),
Brand: brand.Feishu,
AppSecret: core.PlainSecret("secret-target"),
Brand: core.BrandFeishu,
StrictMode: &targetMode,
},
},
}
if err := configpkg.SaveMultiAppConfig(multi); err != nil {
if err := core.SaveMultiAppConfig(multi); err != nil {
t.Fatalf("SaveMultiAppConfig() error = %v", err)
}
@@ -209,7 +206,7 @@ func resetBuffers(stdout *bytes.Buffer, stderr *bytes.Buffer) {
// --- service command ---
func TestIntegration_StrictModeBot_ProfileOverride_HidesCommandsInHelp(t *testing.T) {
f, stdout, stderr := newStrictModeDefaultFactory(t, "target", identity.StrictModeBot)
f, stdout, stderr := newStrictModeDefaultFactory(t, "target", core.StrictModeBot)
rootCmd := buildStrictModeIntegrationRootCmd(t, f)
code := executeRootIntegration(t, f, rootCmd, []string{"auth", "--help"})
@@ -241,7 +238,7 @@ func TestIntegration_StrictModeBot_ProfileOverride_HidesCommandsInHelp(t *testin
}
func TestIntegration_StrictModeBot_ProfileOverride_DirectAuthLoginReturnsEnvelope(t *testing.T) {
f, stdout, stderr := newStrictModeDefaultFactory(t, "target", identity.StrictModeBot)
f, stdout, stderr := newStrictModeDefaultFactory(t, "target", core.StrictModeBot)
rootCmd := buildStrictModeIntegrationRootCmd(t, f)
code := executeRootIntegration(t, f, rootCmd, []string{
@@ -318,7 +315,7 @@ func assertCheckStrictModeEnvelope(t *testing.T, env typedErrorEnvelope, wantMes
}
func TestIntegration_StrictModeBot_ProfileOverride_DirectUserShortcutReturnsEnvelope(t *testing.T) {
f, stdout, stderr := newStrictModeDefaultFactory(t, "target", identity.StrictModeBot)
f, stdout, stderr := newStrictModeDefaultFactory(t, "target", core.StrictModeBot)
rootCmd := buildStrictModeIntegrationRootCmd(t, f)
code := executeRootIntegration(t, f, rootCmd, []string{
@@ -338,7 +335,7 @@ func TestIntegration_StrictModeBot_ProfileOverride_DirectUserShortcutReturnsEnve
func TestIntegration_StrictModeUser_ProfileOverride_ChatCreateDryRunSucceeds(t *testing.T) {
// +chat-create supports both user and bot identities, so strict mode user
// should allow it and force user identity.
f, stdout, stderr := newStrictModeDefaultFactory(t, "target", identity.StrictModeUser)
f, stdout, stderr := newStrictModeDefaultFactory(t, "target", core.StrictModeUser)
rootCmd := buildStrictModeIntegrationRootCmd(t, f)
code := executeRootIntegration(t, f, rootCmd, []string{
@@ -355,7 +352,7 @@ func TestIntegration_StrictModeUser_ProfileOverride_ChatCreateDryRunSucceeds(t *
}
func TestIntegration_StrictModeUser_ProfileOverride_ShortcutExplicitBotReturnsEnvelope(t *testing.T) {
f, stdout, stderr := newStrictModeDefaultFactory(t, "target", identity.StrictModeUser)
f, stdout, stderr := newStrictModeDefaultFactory(t, "target", core.StrictModeUser)
rootCmd := buildStrictModeIntegrationRootCmd(t, f)
code := executeRootIntegration(t, f, rootCmd, []string{
@@ -373,7 +370,7 @@ func TestIntegration_StrictModeUser_ProfileOverride_ShortcutExplicitBotReturnsEn
}
func TestIntegration_StrictModeBot_ProfileOverride_ServiceExplicitUserReturnsEnvelope(t *testing.T) {
f, stdout, stderr := newStrictModeDefaultFactory(t, "target", identity.StrictModeBot)
f, stdout, stderr := newStrictModeDefaultFactory(t, "target", core.StrictModeBot)
catalog := strictModeFixtureCatalog()
rootCmd := buildStrictModeIntegrationRootCmdWithCatalog(t, f, &catalog)
@@ -392,7 +389,7 @@ func TestIntegration_StrictModeBot_ProfileOverride_ServiceExplicitUserReturnsEnv
}
func TestIntegration_StrictModeUser_ProfileOverride_ServiceBotOnlyMethodReturnsEnvelope(t *testing.T) {
f, stdout, stderr := newStrictModeDefaultFactory(t, "target", identity.StrictModeUser)
f, stdout, stderr := newStrictModeDefaultFactory(t, "target", core.StrictModeUser)
catalog := strictModeFixtureCatalog()
rootCmd := buildStrictModeIntegrationRootCmdWithCatalog(t, f, &catalog)
@@ -411,7 +408,7 @@ func TestIntegration_StrictModeUser_ProfileOverride_ServiceBotOnlyMethodReturnsE
}
func TestIntegration_StrictModeBot_ProfileOverride_APIExplicitUserReturnsEnvelope(t *testing.T) {
f, stdout, stderr := newStrictModeDefaultFactory(t, "target", identity.StrictModeBot)
f, stdout, stderr := newStrictModeDefaultFactory(t, "target", core.StrictModeBot)
rootCmd := buildStrictModeIntegrationRootCmd(t, f)
code := executeRootIntegration(t, f, rootCmd, []string{
@@ -431,8 +428,8 @@ func TestIntegration_StrictModeBot_ProfileOverride_APIExplicitUserReturnsEnvelop
// --- shortcut command ---
func TestIntegration_Shortcut_BusinessError_OutputsEnvelope(t *testing.T) {
f, stdout, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "e2e-sc-err", AppSecret: "secret", Brand: brand.Feishu,
f, stdout, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "e2e-sc-err", AppSecret: "secret", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
URL: "/open-apis/im/v1/messages",

View File

@@ -13,7 +13,6 @@ import (
"github.com/spf13/cobra"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/cmd/api"
"github.com/larksuite/cli/cmd/auth"
cmdconfig "github.com/larksuite/cli/cmd/config"
@@ -21,9 +20,8 @@ import (
"github.com/larksuite/cli/errs"
internalauth "github.com/larksuite/cli/internal/auth"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/deprecation"
"github.com/larksuite/cli/internal/identity"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/registry"
)
@@ -307,7 +305,7 @@ func TestHandleRootError_DeprecatedAliasMissingFlagStructured(t *testing.T) {
// TestHandleRootError_AuthConfigWireGolden is the wire-consistency regression
// baseline for auth/config errors: it pins the typed envelope and exit code the
// dispatcher produces for the two source-of-truth shapes, which are constructed
// typed at their origin in internal/auth and internal/configpkg.
// typed at their origin in internal/auth and internal/core.
func TestHandleRootError_AuthConfigWireGolden(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
@@ -347,7 +345,7 @@ func TestHandleRootError_AuthConfigWireGolden(t *testing.T) {
errOut := &bytes.Buffer{}
f.IOStreams.ErrOut = errOut
exit := handleRootError(f, configpkg.NotConfiguredError())
exit := handleRootError(f, core.NotConfiguredError())
if exit != int(output.ExitAuth) {
t.Errorf("exit = %d, want %d (config shares ExitAuth)", exit, int(output.ExitAuth))
}
@@ -514,10 +512,10 @@ func TestHandleRootError_TypedAuthErrorWithLegacyCausePreserved(t *testing.T) {
func TestApplyNeedAuthorizationHint_ServiceMethodUsesLocalScopesWhenNoUAT(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
f.ResolvedIdentity = identity.AsUser
f.ResolvedIdentity = core.AsUser
var target registry.CommandEntry
for _, entry := range registry.CollectCommandScopes([]string{"calendar"}, "user") {
@@ -562,10 +560,10 @@ func TestApplyNeedAuthorizationHint_ServiceMethodUsesLocalScopesWhenNoUAT(t *tes
func TestApplyNeedAuthorizationHint_ShortcutUsesDeclaredScopesWhenNoUAT(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
f.ResolvedIdentity = identity.AsUser
f.ResolvedIdentity = core.AsUser
root := &cobra.Command{Use: "lark-cli"}
serviceCmd := &cobra.Command{Use: "docs"}
@@ -587,10 +585,10 @@ func TestApplyNeedAuthorizationHint_ShortcutUsesDeclaredScopesWhenNoUAT(t *testi
func TestApplyNeedAuthorizationHint_ShortcutIncludesConditionalScopes(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
f.ResolvedIdentity = identity.AsUser
f.ResolvedIdentity = core.AsUser
root := &cobra.Command{Use: "lark-cli"}
serviceCmd := &cobra.Command{Use: "drive"}
@@ -613,10 +611,10 @@ func TestApplyNeedAuthorizationHint_ShortcutIncludesConditionalScopes(t *testing
func TestApplyNeedAuthorizationHint_AppendsExistingHint(t *testing.T) {
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
f.ResolvedIdentity = identity.AsUser
f.ResolvedIdentity = core.AsUser
root := &cobra.Command{Use: "lark-cli"}
serviceCmd := &cobra.Command{Use: "docs"}

View File

@@ -14,7 +14,7 @@ import (
"github.com/larksuite/cli/internal/build"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/workspace"
"github.com/larksuite/cli/internal/core"
"github.com/spf13/cobra"
)
@@ -68,9 +68,9 @@ func TestOfferRootUpgrade(t *testing.T) {
// workspace detection; pin the process-global workspace to Local so
// statePath() resolves under LARKSUITE_CLI_CONFIG_DIR rather than a stale
// subdir inherited from a prior test in the package.
origWS := workspace.CurrentWorkspace()
t.Cleanup(func() { workspace.SetCurrentWorkspace(origWS) })
workspace.SetCurrentWorkspace(workspace.WorkspaceLocal)
origWS := core.CurrentWorkspace()
t.Cleanup(func() { core.SetCurrentWorkspace(origWS) })
core.SetCurrentWorkspace(core.WorkspaceLocal)
cases := []struct {
name string

View File

@@ -12,7 +12,7 @@ import (
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/apicatalog"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/identity"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/registry"
"github.com/larksuite/cli/internal/schema"
@@ -91,7 +91,7 @@ func schemaRun(opts *SchemaOptions) error {
// schema owns rendering (Envelope/Envelopes); this adapter only chooses the
// output shape — a single resolved method renders as one envelope object,
// anything broader as an array — and maps resolve failures to hints.
func runSchema(out io.Writer, parts []string, mode identity.StrictMode) error {
func runSchema(out io.Writer, parts []string, mode core.StrictMode) error {
catalog := registry.SchemaCatalog()
if len(catalog.Services()) == 0 {
// No embedded metadata and the runtime fallback is empty too: offline

View File

@@ -9,10 +9,9 @@ import (
"strings"
"testing"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
)
func TestSchemaCmd_FlagParsing(t *testing.T) {
@@ -199,8 +198,8 @@ func TestSchemaCmd_NoYesForReadRisk(t *testing.T) {
}
func TestSchemaCmd_UnknownService(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := NewCmdSchema(f, nil)
@@ -228,8 +227,8 @@ func TestSchemaCmd_UnknownService(t *testing.T) {
// JSON-mode unknown-method path: *errs.ValidationError with
// subtype invalid_argument and a hint listing the available methods.
func TestSchemaCmd_UnknownMethod_TypedValidation(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := NewCmdSchema(f, nil)

View File

@@ -16,10 +16,9 @@ import (
"github.com/larksuite/cli/internal/client"
"github.com/larksuite/cli/internal/cmdmeta"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/credential"
"github.com/larksuite/cli/internal/errclass"
identitypkg "github.com/larksuite/cli/internal/identity"
"github.com/larksuite/cli/internal/meta"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/registry"
@@ -135,7 +134,7 @@ type ServiceMethodOptions struct {
// Flags
Params string
Data string
As identitypkg.Identity
As core.Identity
Output string
PageAll bool
PageLimit int
@@ -268,7 +267,7 @@ func buildMethodCommand(ctx context.Context, f *cmdutil.Factory, spec methodComm
RunE: func(cmd *cobra.Command, args []string) error {
opts.Cmd = cmd
opts.Ctx = cmd.Context()
opts.As = identitypkg.Identity(asStr)
opts.As = core.Identity(asStr)
if runF != nil {
return runF(opts)
}
@@ -371,7 +370,7 @@ func serviceMethodRun(opts *ServiceMethodOptions) error {
return err
}
// Check if this API method supports the resolved identitypkg.
// Check if this API method supports the resolved identity.
if opts.Method.RestrictsIdentity() {
if err := f.CheckIdentity(opts.As, opts.Method.Identities()); err != nil {
return err
@@ -454,7 +453,7 @@ func serviceMethodRun(opts *ServiceMethodOptions) error {
}
// checkServiceScopes pre-checks user scopes before making the API call.
func checkServiceScopes(ctx context.Context, cred *credential.CredentialProvider, identity identitypkg.Identity, config *configpkg.CliConfig, method meta.Method) error {
func checkServiceScopes(ctx context.Context, cred *credential.CredentialProvider, identity core.Identity, config *core.CliConfig, method meta.Method) error {
if ctx.Err() != nil {
return ctx.Err()
}
@@ -668,7 +667,7 @@ func buildServiceRequest(opts *ServiceMethodOptions) (client.RawApiRequest, *cmd
return request, nil, nil
}
func serviceDryRun(f *cmdutil.Factory, request client.RawApiRequest, config *configpkg.CliConfig, opts *ServiceMethodOptions) error {
func serviceDryRun(f *cmdutil.Factory, request client.RawApiRequest, config *core.CliConfig, opts *ServiceMethodOptions) error {
return cmdutil.PrintDryRun(request, config, serviceDryRunOutputOptions(f, opts))
}
@@ -683,7 +682,7 @@ func serviceDryRunOutputOptions(f *cmdutil.Factory, opts *ServiceMethodOptions)
}
}
func servicePaginate(ctx context.Context, ac *client.APIClient, request client.RawApiRequest, format output.Format, jqExpr string, out, errOut io.Writer, commandPath string, pagOpts client.PaginationOptions, checkErr func(interface{}, identitypkg.Identity) error) error {
func servicePaginate(ctx context.Context, ac *client.APIClient, request client.RawApiRequest, format output.Format, jqExpr string, out, errOut io.Writer, commandPath string, pagOpts client.PaginationOptions, checkErr func(interface{}, core.Identity) error) error {
if pagOpts.Identity == "" {
pagOpts.Identity = request.As
}

View File

@@ -13,13 +13,11 @@ 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"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/httpmock"
"github.com/larksuite/cli/internal/identity"
"github.com/larksuite/cli/internal/output"
)
@@ -46,10 +44,10 @@ func newServicePaginateTestHarness(t *testing.T) (*client.APIClient, *bytes.Buff
output.PendingNotice = nil
t.Cleanup(func() { output.PendingNotice = previousNotice })
config := &configpkg.CliConfig{
config := &core.CliConfig{
AppID: "test-app",
AppSecret: "test-secret",
Brand: brand.Feishu,
Brand: core.BrandFeishu,
}
f, out, errOut, reg := cmdutil.TestFactory(t, config)
ac, err := f.NewAPIClientWithConfig(config)
@@ -64,7 +62,7 @@ func servicePaginateRequest() client.RawApiRequest {
return client.RawApiRequest{
Method: "GET",
URL: "/open-apis/test/v1/items",
As: identity.AsBot,
As: core.AsBot,
}
}

View File

@@ -15,21 +15,19 @@ 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"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/httpmock"
"github.com/larksuite/cli/internal/identity"
"github.com/larksuite/cli/internal/meta"
"github.com/spf13/cobra"
)
// ── helpers ──
var testConfig = &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
var testConfig = &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
}
func driveSpec() meta.Service {
@@ -133,8 +131,8 @@ func TestRegisterService_MergesExistingCommand(t *testing.T) {
}
func TestNewCmdServiceMethod_StrictModeHidesAsFlag(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu, SupportedIdentities: 2,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu, SupportedIdentities: 2,
})
cmd := NewCmdServiceMethod(f, driveSpec(), driveMethod("GET", nil), "copy", "files", nil)
@@ -195,7 +193,7 @@ func TestNewCmdServiceMethod_RunFCallback(t *testing.T) {
if captured == nil {
t.Fatal("runF was not called")
}
if captured.As != identity.AsBot {
if captured.As != core.AsBot {
t.Errorf("expected As=bot, got %s", captured.As)
}
if captured.SchemaPath != "drive.files.list" {
@@ -465,8 +463,8 @@ func TestServiceMethod_BotMode_Success(t *testing.T) {
}
func TestServiceMethod_BotMode_PageAll_JSON(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-page", AppSecret: "test-secret-page", Brand: brand.Feishu,
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-page", AppSecret: "test-secret-page", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -533,8 +531,8 @@ func TestServiceMethod_PageAll_DefaultJSONRunsContentSafety(t *testing.T) {
extcs.Register(provider)
t.Cleanup(func() { extcs.Register(nil) })
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-service-safety", AppSecret: "test-secret-service-safety", Brand: brand.Feishu,
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-service-safety", AppSecret: "test-secret-service-safety", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -587,8 +585,8 @@ func TestServiceMethod_PageAll_StreamFormatRunsContentSafety(t *testing.T) {
extcs.Register(provider)
t.Cleanup(func() { extcs.Register(nil) })
f, stdout, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-service-stream-safety", AppSecret: "test-secret-service-stream-safety", Brand: brand.Feishu,
f, stdout, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-service-stream-safety", AppSecret: "test-secret-service-stream-safety", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -635,8 +633,8 @@ func TestServiceMethod_PageAll_StreamFormatBlockSkipsBlockedPage(t *testing.T) {
extcs.Register(provider)
t.Cleanup(func() { extcs.Register(nil) })
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-service-stream-block", AppSecret: "test-secret-service-stream-block", Brand: brand.Feishu,
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-service-stream-block", AppSecret: "test-secret-service-stream-block", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -691,8 +689,8 @@ func TestServiceMethod_PageAll_StreamFormatBlockSkipsBlockedPage(t *testing.T) {
}
func TestServiceMethod_BusinessErrorReturnsTypedErrorWithoutSuccessEnvelope(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-service-err", AppSecret: "test-secret-service-err", Brand: brand.Feishu,
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-service-err", AppSecret: "test-secret-service-err", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -722,8 +720,8 @@ func TestServiceMethod_BusinessErrorReturnsTypedErrorWithoutSuccessEnvelope(t *t
}
func TestServiceMethod_PageAll_DefaultBusinessErrorOutputsRawResponse(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-service-pageall-err", AppSecret: "test-secret-service-pageall-err", Brand: brand.Feishu,
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-service-pageall-err", AppSecret: "test-secret-service-pageall-err", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -752,8 +750,8 @@ func TestServiceMethod_PageAll_DefaultBusinessErrorOutputsRawResponse(t *testing
}
func TestServiceMethod_PageAll_StreamBusinessErrorDoesNotDumpJSON(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-service-pageall-stream-err", AppSecret: "test-secret-service-pageall-stream-err", Brand: brand.Feishu,
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-service-pageall-stream-err", AppSecret: "test-secret-service-pageall-stream-err", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -798,8 +796,8 @@ func TestServiceMethod_PageAll_StreamBusinessErrorDoesNotDumpJSON(t *testing.T)
}
func TestServiceMethod_UnknownFormat_Warning(t *testing.T) {
f, _, stderr, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-fmt", AppSecret: "test-secret-fmt", Brand: brand.Feishu,
f, _, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-fmt", AppSecret: "test-secret-fmt", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -882,8 +880,8 @@ func TestServiceMethod_JqAndOutputConflict(t *testing.T) {
}
func TestServiceMethod_JqFilter_AppliesExpression(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-jq", AppSecret: "test-secret-jq", Brand: brand.Feishu,
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-jq", AppSecret: "test-secret-jq", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -953,8 +951,8 @@ func TestServiceMethod_JqInvalidExpression(t *testing.T) {
}
func TestServiceMethod_PageAll_WithJq(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-spjq", AppSecret: "test-secret-spjq", Brand: brand.Feishu,
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-spjq", AppSecret: "test-secret-spjq", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{
@@ -986,8 +984,8 @@ func TestServiceMethod_PageAll_WithJq(t *testing.T) {
}
func TestServiceMethod_PageAll_WithJqBusinessErrorOutputsRawResponse(t *testing.T) {
f, stdout, _, reg := cmdutil.TestFactory(t, &configpkg.CliConfig{
AppID: "test-app-spjq-err", AppSecret: "test-secret-spjq-err", Brand: brand.Feishu,
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
AppID: "test-app-spjq-err", AppSecret: "test-secret-spjq-err", Brand: core.BrandFeishu,
})
reg.Register(&httpmock.Stub{

View File

@@ -6,9 +6,8 @@ package cmd
import (
"os"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/envnames"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/envvars"
)
// ResolveStartupBrand resolves the brand before the command tree is built, so
@@ -16,14 +15,14 @@ import (
// first catalog access. It mirrors the credential chain's brand precedence —
// environment, then the active profile's raw config entry — without touching
// the keychain (no secrets are needed to know the brand).
func ResolveStartupBrand(profile string) brand.Brand {
if raw := os.Getenv(envnames.CliBrand); raw != "" {
return brand.ParseBrand(raw)
func ResolveStartupBrand(profile string) core.LarkBrand {
if raw := os.Getenv(envvars.CliBrand); raw != "" {
return core.ParseBrand(raw)
}
if cfg, err := configpkg.LoadMultiAppConfig(); err == nil {
if cfg, err := core.LoadMultiAppConfig(); err == nil {
if app := cfg.CurrentAppConfig(profile); app != nil {
return brand.ParseBrand(string(app.Brand))
return core.ParseBrand(string(app.Brand))
}
}
return brand.Feishu
return core.BrandFeishu
}

View File

@@ -14,8 +14,8 @@ import (
"testing"
"github.com/google/uuid"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/registry"
)
@@ -48,7 +48,7 @@ func TestResolveStartupBrand_Precedence(t *testing.T) {
os.Unsetenv("LARKSUITE_CLI_BRAND")
// No config at all → default brand.
if got := ResolveStartupBrand(""); got != brand.Feishu {
if got := ResolveStartupBrand(""); got != core.BrandFeishu {
t.Errorf("empty state brand = %q, want feishu", got)
}
@@ -59,16 +59,16 @@ func TestResolveStartupBrand_Precedence(t *testing.T) {
if err := os.WriteFile(filepath.Join(tmp, "config.json"), []byte(raw), 0600); err != nil {
t.Fatal(err)
}
if got := ResolveStartupBrand(""); got != brand.Feishu {
if got := ResolveStartupBrand(""); got != core.BrandFeishu {
t.Errorf("default profile brand = %q, want feishu", got)
}
if got := ResolveStartupBrand("lark-prof"); got != brand.Lark {
if got := ResolveStartupBrand("lark-prof"); got != core.BrandLark {
t.Errorf("lark profile brand = %q, want lark (normalized)", got)
}
// Environment wins over the config file.
t.Setenv("LARKSUITE_CLI_BRAND", "lark")
if got := ResolveStartupBrand(""); got != brand.Lark {
if got := ResolveStartupBrand(""); got != core.BrandLark {
t.Errorf("env brand = %q, want lark", got)
}
}

View File

@@ -11,11 +11,10 @@ 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"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/selfupdate"
"github.com/larksuite/cli/internal/skillscheck"
@@ -176,17 +175,17 @@ func updateRun(opts *UpdateOptions) error {
// resolveSkillsBrand returns the skills-source brand: resolved config first,
// then the active profile's raw config entry (the brand is not a secret; a
// locked keychain must not flip the source), then the default with a notice.
func resolveSkillsBrand(f *cmdutil.Factory, errOut stdio.Writer) brand.Brand {
func resolveSkillsBrand(f *cmdutil.Factory, errOut stdio.Writer) core.LarkBrand {
if cfg, err := f.Config(); err == nil && cfg != nil {
return brand.ParseBrand(string(cfg.Brand))
return core.ParseBrand(string(cfg.Brand))
}
if raw, err := configpkg.LoadMultiAppConfig(); err == nil {
if raw, err := core.LoadMultiAppConfig(); err == nil {
if app := raw.CurrentAppConfig(f.Invocation.Profile); app != nil {
return brand.ParseBrand(string(app.Brand))
return core.ParseBrand(string(app.Brand))
}
}
fmt.Fprintf(errOut, "note: could not resolve the configured brand; syncing skills from the default source\n")
return brand.Feishu
return core.BrandFeishu
}
// --- Output helpers ---

View File

@@ -16,10 +16,9 @@ import (
"testing"
"time"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/internal/selfupdate"
"github.com/larksuite/cli/internal/skillscheck"
@@ -30,7 +29,7 @@ const runLiveSkillsTestsEnv = "LARKSUITE_CLI_RUN_LIVE_SKILLS_TESTS"
// newTestFactory creates a test factory with minimal config.
func newTestFactory(t *testing.T) (*cmdutil.Factory, *bytes.Buffer, *bytes.Buffer) {
t.Helper()
f, stdout, stderr, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{})
f, stdout, stderr, _ := cmdutil.TestFactory(t, &core.CliConfig{})
return f, stdout, stderr
}
@@ -1865,10 +1864,10 @@ func containsString(values []string, target string) bool {
func TestResolveSkillsBrand_LayeredFallback(t *testing.T) {
// Layer 1: resolved config wins.
var errBuf bytes.Buffer
f := &cmdutil.Factory{Config: func() (*configpkg.CliConfig, error) {
return &configpkg.CliConfig{Brand: brand.Brand(" LARK ")}, nil
f := &cmdutil.Factory{Config: func() (*core.CliConfig, error) {
return &core.CliConfig{Brand: core.LarkBrand(" LARK ")}, nil
}}
if got := resolveSkillsBrand(f, &errBuf); got != brand.Lark {
if got := resolveSkillsBrand(f, &errBuf); got != core.BrandLark {
t.Errorf("resolved-config brand = %q, want lark", got)
}
@@ -1880,9 +1879,9 @@ func TestResolveSkillsBrand_LayeredFallback(t *testing.T) {
if err := os.WriteFile(filepath.Join(tmp, "config.json"), []byte(raw), 0600); err != nil {
t.Fatal(err)
}
f = &cmdutil.Factory{Config: func() (*configpkg.CliConfig, error) { return nil, errors.New("keychain locked") }}
f = &cmdutil.Factory{Config: func() (*core.CliConfig, error) { return nil, errors.New("keychain locked") }}
errBuf.Reset()
if got := resolveSkillsBrand(f, &errBuf); got != brand.Lark {
if got := resolveSkillsBrand(f, &errBuf); got != core.BrandLark {
t.Errorf("raw-config brand = %q, want lark", got)
}
if errBuf.Len() != 0 {
@@ -1892,7 +1891,7 @@ func TestResolveSkillsBrand_LayeredFallback(t *testing.T) {
// Layer 3: nothing readable → default brand with a notice.
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
errBuf.Reset()
if got := resolveSkillsBrand(f, &errBuf); got != brand.Feishu {
if got := resolveSkillsBrand(f, &errBuf); got != core.BrandFeishu {
t.Errorf("fallback brand = %q, want feishu", got)
}
if !strings.Contains(errBuf.String(), "could not resolve the configured brand") {
@@ -1912,10 +1911,10 @@ func TestResolveSkillsBrand_RespectsActiveProfile(t *testing.T) {
}
f := &cmdutil.Factory{
Invocation: cmdutil.InvocationContext{Profile: "lark-prof"},
Config: func() (*configpkg.CliConfig, error) { return nil, errors.New("keychain locked") },
Config: func() (*core.CliConfig, error) { return nil, errors.New("keychain locked") },
}
var errBuf bytes.Buffer
if got := resolveSkillsBrand(f, &errBuf); got != brand.Lark {
if got := resolveSkillsBrand(f, &errBuf); got != core.BrandLark {
t.Errorf("brand = %q, want lark (the active profile's brand)", got)
}
if errBuf.Len() != 0 {

View File

@@ -8,10 +8,8 @@ import (
"github.com/spf13/cobra"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/internal/cmdutil"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/identity"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/identitydiag"
"github.com/larksuite/cli/internal/output"
)
@@ -27,7 +25,7 @@ import (
type whoamiResult struct {
Profile string `json:"profile"`
AppID string `json:"appId"`
Brand brand.Brand `json:"brand"`
Brand core.LarkBrand `json:"brand"`
DefaultAs string `json:"defaultAs"`
Identity string `json:"identity"`
IdentitySource string `json:"identitySource"`
@@ -82,7 +80,7 @@ func whoamiRun(cmd *cobra.Command, opts *Options) error {
return err
}
ctx := cmd.Context()
flagAs := identity.Identity(opts.As)
flagAs := core.Identity(opts.As)
as := f.ResolveAs(ctx, cmd, flagAs)
// Validate as a real API call does (strict mode, then identity) so whoami
// can't preview an identity the next call would refuse.
@@ -109,8 +107,8 @@ func whoamiRun(cmd *cobra.Command, opts *Options) error {
// auto-detected result means auto-detect; otherwise a strict-mode forced
// identity means strict-mode; otherwise it came from configured default-as.
// Values are snake_case to match the other enum fields (e.g. tokenStatus).
func resolveSource(changedAs bool, flagAs identity.Identity, autoDetected bool, strictForced identity.Identity) string {
if changedAs && (flagAs == identity.AsUser || flagAs == identity.AsBot) {
func resolveSource(changedAs bool, flagAs core.Identity, autoDetected bool, strictForced core.Identity) string {
if changedAs && (flagAs == core.AsUser || flagAs == core.AsBot) {
return "flag"
}
if autoDetected {
@@ -124,10 +122,10 @@ func resolveSource(changedAs bool, flagAs identity.Identity, autoDetected bool,
// buildResult maps the resolved identity and local diagnostics into the output.
// ResolveAs only ever returns user or bot, so the default branch handles user.
func buildResult(cfg *configpkg.CliConfig, as identity.Identity, source string, diag identitydiag.Result) *whoamiResult {
func buildResult(cfg *core.CliConfig, as core.Identity, source string, diag identitydiag.Result) *whoamiResult {
defaultAs := cfg.DefaultAs
if defaultAs == "" {
defaultAs = identity.AsAuto
defaultAs = core.AsAuto
}
res := &whoamiResult{
Profile: cfg.ProfileName,
@@ -140,7 +138,7 @@ func buildResult(cfg *configpkg.CliConfig, as identity.Identity, source string,
// Use the diagnosed hint as-is: it is tailored to the credential source, so
// it never says "auth login" when that is blocked under an external provider.
switch as {
case identity.AsBot:
case core.AsBot:
res.Available = diag.Bot.Available
res.TokenStatus = diag.Bot.Status
if !diag.Bot.Available {

View File

@@ -13,13 +13,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"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/credential"
"github.com/larksuite/cli/internal/identity"
"github.com/larksuite/cli/internal/identitydiag"
)
@@ -27,16 +25,16 @@ func TestResolveSource(t *testing.T) {
tests := []struct {
name string
changedAs bool
flagAs identity.Identity
flagAs core.Identity
autoDetected bool
strictForced identity.Identity
strictForced core.Identity
want string
}{
{"explicit flag user", true, identity.AsUser, false, "", "flag"},
{"explicit flag bot", true, identity.AsBot, false, "", "flag"},
{"flag auto falls through to auto-detect", true, identity.AsAuto, true, "", "auto_detect"},
{"explicit flag user", true, core.AsUser, false, "", "flag"},
{"explicit flag bot", true, core.AsBot, false, "", "flag"},
{"flag auto falls through to auto-detect", true, core.AsAuto, true, "", "auto_detect"},
{"auto detected", false, "", true, "", "auto_detect"},
{"strict mode", false, "", false, identity.AsBot, "strict_mode"},
{"strict mode", false, "", false, core.AsBot, "strict_mode"},
{"default_as", false, "", false, "", "default_as"},
}
for _, tt := range tests {
@@ -50,11 +48,11 @@ func TestResolveSource(t *testing.T) {
}
func TestBuildResult_UserValid(t *testing.T) {
cfg := &configpkg.CliConfig{ProfileName: "my-app", AppID: "cli_x", Brand: brand.Lark, DefaultAs: identity.AsAuto}
cfg := &core.CliConfig{ProfileName: "my-app", AppID: "cli_x", Brand: core.BrandLark, DefaultAs: core.AsAuto}
diag := identitydiag.Result{
User: identitydiag.Identity{Available: true, Status: "ready", TokenStatus: "valid", OpenID: "ou_x", UserName: "Alice"},
}
r := buildResult(cfg, identity.AsUser, "auto_detect", diag)
r := buildResult(cfg, core.AsUser, "auto_detect", diag)
if r.Identity != "user" || r.IdentitySource != "auto_detect" {
t.Fatalf("identity/source = %q/%q", r.Identity, r.IdentitySource)
@@ -69,17 +67,17 @@ func TestBuildResult_UserValid(t *testing.T) {
if r.Hint != "" {
t.Fatalf("hint = %q, want empty", r.Hint)
}
if r.Profile != "my-app" || r.AppID != "cli_x" || r.Brand != brand.Lark {
if r.Profile != "my-app" || r.AppID != "cli_x" || r.Brand != core.BrandLark {
t.Fatalf("app context = %#v", r)
}
}
func TestBuildResult_UserMissingToken(t *testing.T) {
cfg := &configpkg.CliConfig{ProfileName: "p", AppID: "cli_x", Brand: brand.Lark}
cfg := &core.CliConfig{ProfileName: "p", AppID: "cli_x", Brand: core.BrandLark}
diag := identitydiag.Result{
User: identitydiag.Identity{Available: false, Status: "missing", Hint: "run: lark-cli auth login --help"}, // never logged in
}
r := buildResult(cfg, identity.AsUser, "auto_detect", diag)
r := buildResult(cfg, core.AsUser, "auto_detect", diag)
if r.Available {
t.Fatalf("available = true, want false")
@@ -98,11 +96,11 @@ func TestBuildResult_UserMissingToken(t *testing.T) {
}
func TestBuildResult_BotReady(t *testing.T) {
cfg := &configpkg.CliConfig{ProfileName: "p", AppID: "cli_x", Brand: brand.Feishu, DefaultAs: identity.AsBot}
cfg := &core.CliConfig{ProfileName: "p", AppID: "cli_x", Brand: core.BrandFeishu, DefaultAs: core.AsBot}
diag := identitydiag.Result{
Bot: identitydiag.Identity{Available: true, Status: "ready"},
}
r := buildResult(cfg, identity.AsBot, "default_as", diag)
r := buildResult(cfg, core.AsBot, "default_as", diag)
if r.Identity != "bot" || r.IdentitySource != "default_as" {
t.Fatalf("identity/source = %q/%q", r.Identity, r.IdentitySource)
@@ -119,11 +117,11 @@ func TestBuildResult_BotReady(t *testing.T) {
}
func TestBuildResult_BotNotConfigured(t *testing.T) {
cfg := &configpkg.CliConfig{ProfileName: "p", AppID: "cli_x", Brand: brand.Feishu}
cfg := &core.CliConfig{ProfileName: "p", AppID: "cli_x", Brand: core.BrandFeishu}
diag := identitydiag.Result{
Bot: identitydiag.Identity{Available: false, Status: "not_configured", Hint: "run: lark-cli config --help"},
}
r := buildResult(cfg, identity.AsBot, "auto_detect", diag)
r := buildResult(cfg, core.AsBot, "auto_detect", diag)
if r.Available {
t.Fatalf("available = true, want false")
@@ -137,8 +135,8 @@ func TestBuildResult_BotNotConfigured(t *testing.T) {
}
func TestWhoami_BotJSON(t *testing.T) {
f, stdout, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
ProfileName: "test-profile", AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
ProfileName: "test-profile", AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := NewCmdWhoami(f)
@@ -171,8 +169,8 @@ func TestWhoami_BotJSON(t *testing.T) {
func TestWhoami_RejectsInvalidAs(t *testing.T) {
for _, bad := range []string{"admin", "USER", "bogus123", ""} {
t.Run("as="+bad, func(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
ProfileName: "p", AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
ProfileName: "p", AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
cmd := NewCmdWhoami(f)
cmd.SetArgs([]string{"--as", bad})
@@ -197,11 +195,11 @@ func TestWhoami_RejectsInvalidAs(t *testing.T) {
}
func TestWhoami_ConfigErrorPropagates(t *testing.T) {
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
ProfileName: "p", AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
ProfileName: "p", AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})
wantErr := fmt.Errorf("boom")
f.Config = func() (*configpkg.CliConfig, error) { return nil, wantErr }
f.Config = func() (*core.CliConfig, error) { return nil, wantErr }
cmd := NewCmdWhoami(f)
cmd.SetArgs([]string{"--json"})
@@ -220,8 +218,8 @@ func TestWhoami_StrictModeRejectsCrossIdentity(t *testing.T) {
// Bot-only account → strict mode bot. A real `--as user` call would be
// rejected by CheckStrictMode; whoami must reject it identically rather than
// previewing a user identity the next call would refuse.
f, _, _, _ := cmdutil.TestFactory(t, &configpkg.CliConfig{
ProfileName: "p", AppID: "test-app", AppSecret: "test-secret", Brand: brand.Feishu,
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
ProfileName: "p", AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
SupportedIdentities: 2, // bot only
})
cmd := NewCmdWhoami(f)
@@ -249,7 +247,7 @@ func (p *fakeExtProvider) ResolveToken(context.Context, extcred.TokenSpec) (*ext
return nil, nil // no UAT served locally; whoami runs with verify=false
}
func externalWhoamiFactory(cfg *configpkg.CliConfig) (*cmdutil.Factory, *bytes.Buffer) {
func externalWhoamiFactory(cfg *core.CliConfig) (*cmdutil.Factory, *bytes.Buffer) {
cred := credential.NewCredentialProvider(
[]extcred.Provider{&fakeExtProvider{name: "corp-sso", account: &extcred.Account{AppID: cfg.AppID}}},
nil, nil,
@@ -257,7 +255,7 @@ func externalWhoamiFactory(cfg *configpkg.CliConfig) (*cmdutil.Factory, *bytes.B
)
out := &bytes.Buffer{}
f := &cmdutil.Factory{
Config: func() (*configpkg.CliConfig, error) { return cfg, nil },
Config: func() (*core.CliConfig, error) { return cfg, nil },
Credential: cred,
IOStreams: &cmdutil.IOStreams{Out: out, ErrOut: &bytes.Buffer{}},
}
@@ -268,8 +266,8 @@ func externalWhoamiFactory(cfg *configpkg.CliConfig) (*cmdutil.Factory, *bytes.B
// an extension provider, a signed-in user must read as available, and an
// unavailable identity must not be told to "auth login" (which is blocked).
func TestWhoami_ExternalProvider_UserReady(t *testing.T) {
cfg := &configpkg.CliConfig{
ProfileName: "p", AppID: "cli_x", Brand: brand.Feishu,
cfg := &core.CliConfig{
ProfileName: "p", AppID: "cli_x", Brand: core.BrandFeishu,
SupportedIdentities: uint8(extcred.SupportsAll), UserOpenId: "ou_x", UserName: "Alice",
}
f, out := externalWhoamiFactory(cfg)
@@ -295,8 +293,8 @@ func TestWhoami_ExternalProvider_UserReady(t *testing.T) {
}
func TestWhoami_ExternalProvider_UserHintNotKeychain(t *testing.T) {
cfg := &configpkg.CliConfig{
ProfileName: "p", AppID: "cli_x", Brand: brand.Feishu,
cfg := &core.CliConfig{
ProfileName: "p", AppID: "cli_x", Brand: core.BrandFeishu,
SupportedIdentities: uint8(extcred.SupportsUser), // user supported but not signed in
}
f, out := externalWhoamiFactory(cfg)

View File

@@ -1,18 +0,0 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
// Package envnames defines environment variable names shared by the CLI and
// its public extension packages.
package envnames
const (
CliAppID = "LARKSUITE_CLI_APP_ID"
CliAppSecret = "LARKSUITE_CLI_APP_SECRET"
CliBrand = "LARKSUITE_CLI_BRAND"
CliUserAccessToken = "LARKSUITE_CLI_USER_ACCESS_TOKEN"
CliTenantAccessToken = "LARKSUITE_CLI_TENANT_ACCESS_TOKEN"
CliDefaultAs = "LARKSUITE_CLI_DEFAULT_AS"
CliStrictMode = "LARKSUITE_CLI_STRICT_MODE"
CliAuthProxy = "LARKSUITE_CLI_AUTH_PROXY"
CliProxyKey = "LARKSUITE_CLI_PROXY_KEY"
)

View File

@@ -8,7 +8,7 @@ import (
"encoding/json"
"github.com/larksuite/cli/internal/event"
"github.com/larksuite/cli/internal/imcontent"
convertlib "github.com/larksuite/cli/shortcuts/im/convert_lib"
)
// ImMessageReceiveOutput is the flattened shape for im.message.receive_v1; `desc` tags drive the reflected schema.
@@ -74,11 +74,11 @@ func processImMessageReceive(_ context.Context, _ event.APIClient, raw *event.Ra
msg := envelope.Event.Message
var content string
if msg.MessageType == "interactive" {
content = imcontent.ConvertInteractiveEventContent(msg.Content, msg.Mentions)
content = convertlib.ConvertInteractiveEventContent(msg.Content, msg.Mentions)
} else {
content = imcontent.ConvertBodyContent(msg.MessageType, &imcontent.ConvertContext{
content = convertlib.ConvertBodyContent(msg.MessageType, &convertlib.ConvertContext{
RawContent: msg.Content,
MentionMap: imcontent.BuildMentionKeyMap(msg.Mentions),
MentionMap: convertlib.BuildMentionKeyMap(msg.Mentions),
})
}

View File

@@ -8,8 +8,9 @@ import (
"fmt"
"os"
"github.com/larksuite/cli/envnames"
"github.com/larksuite/cli/extension/credential"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/envvars"
)
// Provider resolves credentials from environment variables.
@@ -18,33 +19,33 @@ type Provider struct{}
func (p *Provider) Name() string { return "env" }
func (p *Provider) ResolveAccount(ctx context.Context) (*credential.Account, error) {
appID := os.Getenv(envnames.CliAppID)
appSecret := os.Getenv(envnames.CliAppSecret)
hasUAT := os.Getenv(envnames.CliUserAccessToken) != ""
hasTAT := os.Getenv(envnames.CliTenantAccessToken) != ""
appID := os.Getenv(envvars.CliAppID)
appSecret := os.Getenv(envvars.CliAppSecret)
hasUAT := os.Getenv(envvars.CliUserAccessToken) != ""
hasTAT := os.Getenv(envvars.CliTenantAccessToken) != ""
if appID == "" && appSecret == "" {
switch {
case hasUAT:
return nil, &credential.BlockError{Provider: "env", Reason: envnames.CliUserAccessToken + " is set but " + envnames.CliAppID + " is missing"}
return nil, &credential.BlockError{Provider: "env", Reason: envvars.CliUserAccessToken + " is set but " + envvars.CliAppID + " is missing"}
case hasTAT:
return nil, &credential.BlockError{Provider: "env", Reason: envnames.CliTenantAccessToken + " is set but " + envnames.CliAppID + " is missing"}
return nil, &credential.BlockError{Provider: "env", Reason: envvars.CliTenantAccessToken + " is set but " + envvars.CliAppID + " is missing"}
default:
return nil, nil
}
}
if appID == "" {
return nil, &credential.BlockError{Provider: "env", Reason: envnames.CliAppSecret + " is set but " + envnames.CliAppID + " is missing"}
return nil, &credential.BlockError{Provider: "env", Reason: envvars.CliAppSecret + " is set but " + envvars.CliAppID + " is missing"}
}
if appSecret == "" && !hasUAT && !hasTAT {
return nil, &credential.BlockError{
Provider: "env",
Reason: envnames.CliAppID + " is set but no app secret or access token is available",
Reason: envvars.CliAppID + " is set but no app secret or access token is available",
}
}
brand := credential.ParseBrand(os.Getenv(envnames.CliBrand))
brand := credential.Brand(core.ParseBrand(os.Getenv(envvars.CliBrand)))
acct := &credential.Account{AppID: appID, AppSecret: appSecret, Brand: brand}
switch id := credential.Identity(os.Getenv(envnames.CliDefaultAs)); id {
switch id := credential.Identity(os.Getenv(envvars.CliDefaultAs)); id {
case "", credential.IdentityAuto:
acct.DefaultAs = id
case credential.IdentityUser, credential.IdentityBot:
@@ -52,12 +53,12 @@ func (p *Provider) ResolveAccount(ctx context.Context) (*credential.Account, err
default:
return nil, &credential.BlockError{
Provider: "env",
Reason: fmt.Sprintf("invalid %s %q (want user, bot, or auto)", envnames.CliDefaultAs, id),
Reason: fmt.Sprintf("invalid %s %q (want user, bot, or auto)", envvars.CliDefaultAs, id),
}
}
// Explicit strict mode policy takes priority
switch strictMode := os.Getenv(envnames.CliStrictMode); strictMode {
switch strictMode := os.Getenv(envvars.CliStrictMode); strictMode {
case "bot":
acct.SupportedIdentities = credential.SupportsBot
case "user":
@@ -75,7 +76,7 @@ func (p *Provider) ResolveAccount(ctx context.Context) (*credential.Account, err
default:
return nil, &credential.BlockError{
Provider: "env",
Reason: fmt.Sprintf("invalid %s %q (want bot, user, or off)", envnames.CliStrictMode, strictMode),
Reason: fmt.Sprintf("invalid %s %q (want bot, user, or off)", envvars.CliStrictMode, strictMode),
}
}
@@ -95,9 +96,9 @@ func (p *Provider) ResolveToken(ctx context.Context, req credential.TokenSpec) (
var envKey string
switch req.Type {
case credential.TokenTypeUAT:
envKey = envnames.CliUserAccessToken
envKey = envvars.CliUserAccessToken
case credential.TokenTypeTAT:
envKey = envnames.CliTenantAccessToken
envKey = envvars.CliTenantAccessToken
default:
return nil, nil
}

View File

@@ -9,8 +9,8 @@ import (
"strings"
"testing"
"github.com/larksuite/cli/envnames"
"github.com/larksuite/cli/extension/credential"
"github.com/larksuite/cli/internal/envvars"
)
func TestProvider_Name(t *testing.T) {
@@ -20,9 +20,9 @@ func TestProvider_Name(t *testing.T) {
}
func TestResolveAccount_BothSet(t *testing.T) {
t.Setenv(envnames.CliAppID, "cli_test")
t.Setenv(envnames.CliAppSecret, "secret_test")
t.Setenv(envnames.CliBrand, " LARK ")
t.Setenv(envvars.CliAppID, "cli_test")
t.Setenv(envvars.CliAppSecret, "secret_test")
t.Setenv(envvars.CliBrand, " LARK ")
acct, err := (&Provider{}).ResolveAccount(context.Background())
if err != nil {
@@ -41,7 +41,7 @@ func TestResolveAccount_NeitherSet(t *testing.T) {
}
func TestResolveAccount_OnlyIDSet(t *testing.T) {
t.Setenv(envnames.CliAppID, "cli_test")
t.Setenv(envvars.CliAppID, "cli_test")
_, err := (&Provider{}).ResolveAccount(context.Background())
var blockErr *credential.BlockError
if !errors.As(err, &blockErr) {
@@ -50,8 +50,8 @@ func TestResolveAccount_OnlyIDSet(t *testing.T) {
}
func TestResolveAccount_AppIDAndUserTokenWithoutSecret(t *testing.T) {
t.Setenv(envnames.CliAppID, "cli_test")
t.Setenv(envnames.CliUserAccessToken, "uat_test")
t.Setenv(envvars.CliAppID, "cli_test")
t.Setenv(envvars.CliUserAccessToken, "uat_test")
acct, err := (&Provider{}).ResolveAccount(context.Background())
if err != nil {
@@ -69,7 +69,7 @@ func TestResolveAccount_AppIDAndUserTokenWithoutSecret(t *testing.T) {
}
func TestResolveAccount_OnlySecretSet(t *testing.T) {
t.Setenv(envnames.CliAppSecret, "secret_test")
t.Setenv(envvars.CliAppSecret, "secret_test")
_, err := (&Provider{}).ResolveAccount(context.Background())
var blockErr *credential.BlockError
if !errors.As(err, &blockErr) {
@@ -78,21 +78,21 @@ func TestResolveAccount_OnlySecretSet(t *testing.T) {
}
func TestResolveAccount_OnlyTokenSetWithoutAppID(t *testing.T) {
t.Setenv(envnames.CliUserAccessToken, "uat_test")
t.Setenv(envvars.CliUserAccessToken, "uat_test")
_, err := (&Provider{}).ResolveAccount(context.Background())
var blockErr *credential.BlockError
if !errors.As(err, &blockErr) {
t.Fatalf("expected BlockError, got %v", err)
}
if !strings.Contains(err.Error(), envnames.CliAppID) {
t.Fatalf("error = %v, want mention of %s", err, envnames.CliAppID)
if !strings.Contains(err.Error(), envvars.CliAppID) {
t.Fatalf("error = %v, want mention of %s", err, envvars.CliAppID)
}
}
func TestResolveAccount_DefaultBrand(t *testing.T) {
t.Setenv(envnames.CliAppID, "cli_test")
t.Setenv(envnames.CliAppSecret, "secret_test")
t.Setenv(envvars.CliAppID, "cli_test")
t.Setenv(envvars.CliAppSecret, "secret_test")
acct, _ := (&Provider{}).ResolveAccount(context.Background())
if acct.Brand != "feishu" {
t.Errorf("expected 'feishu', got %q", acct.Brand)
@@ -100,9 +100,9 @@ func TestResolveAccount_DefaultBrand(t *testing.T) {
}
func TestResolveAccount_DefaultAsFromEnv(t *testing.T) {
t.Setenv(envnames.CliAppID, "cli_test")
t.Setenv(envnames.CliAppSecret, "secret_test")
t.Setenv(envnames.CliDefaultAs, "user")
t.Setenv(envvars.CliAppID, "cli_test")
t.Setenv(envvars.CliAppSecret, "secret_test")
t.Setenv(envvars.CliDefaultAs, "user")
acct, err := (&Provider{}).ResolveAccount(context.Background())
if err != nil {
@@ -114,23 +114,23 @@ func TestResolveAccount_DefaultAsFromEnv(t *testing.T) {
}
func TestResolveToken_UATSet(t *testing.T) {
t.Setenv(envnames.CliUserAccessToken, "u-env")
t.Setenv(envvars.CliUserAccessToken, "u-env")
tok, err := (&Provider{}).ResolveToken(context.Background(), credential.TokenSpec{Type: credential.TokenTypeUAT})
if err != nil {
t.Fatal(err)
}
if tok.Value != "u-env" || tok.Source != "env:"+envnames.CliUserAccessToken {
if tok.Value != "u-env" || tok.Source != "env:"+envvars.CliUserAccessToken {
t.Errorf("unexpected: %+v", tok)
}
}
func TestResolveToken_TATSet(t *testing.T) {
t.Setenv(envnames.CliTenantAccessToken, "t-env")
t.Setenv(envvars.CliTenantAccessToken, "t-env")
tok, err := (&Provider{}).ResolveToken(context.Background(), credential.TokenSpec{Type: credential.TokenTypeTAT})
if err != nil {
t.Fatal(err)
}
if tok.Value != "t-env" || tok.Source != "env:"+envnames.CliTenantAccessToken {
if tok.Value != "t-env" || tok.Source != "env:"+envvars.CliTenantAccessToken {
t.Errorf("unexpected: %+v", tok)
}
}
@@ -143,9 +143,9 @@ func TestResolveToken_NotSet(t *testing.T) {
}
func TestResolveAccount_StrictModeBot(t *testing.T) {
t.Setenv(envnames.CliAppID, "app")
t.Setenv(envnames.CliAppSecret, "secret")
t.Setenv(envnames.CliStrictMode, "bot")
t.Setenv(envvars.CliAppID, "app")
t.Setenv(envvars.CliAppSecret, "secret")
t.Setenv(envvars.CliStrictMode, "bot")
acct, err := (&Provider{}).ResolveAccount(context.Background())
if err != nil {
t.Fatal(err)
@@ -156,9 +156,9 @@ func TestResolveAccount_StrictModeBot(t *testing.T) {
}
func TestResolveAccount_StrictModeUser(t *testing.T) {
t.Setenv(envnames.CliAppID, "app")
t.Setenv(envnames.CliAppSecret, "secret")
t.Setenv(envnames.CliStrictMode, "user")
t.Setenv(envvars.CliAppID, "app")
t.Setenv(envvars.CliAppSecret, "secret")
t.Setenv(envvars.CliStrictMode, "user")
acct, err := (&Provider{}).ResolveAccount(context.Background())
if err != nil {
t.Fatal(err)
@@ -169,9 +169,9 @@ func TestResolveAccount_StrictModeUser(t *testing.T) {
}
func TestResolveAccount_StrictModeOff(t *testing.T) {
t.Setenv(envnames.CliAppID, "app")
t.Setenv(envnames.CliAppSecret, "secret")
t.Setenv(envnames.CliStrictMode, "off")
t.Setenv(envvars.CliAppID, "app")
t.Setenv(envvars.CliAppSecret, "secret")
t.Setenv(envvars.CliStrictMode, "off")
acct, err := (&Provider{}).ResolveAccount(context.Background())
if err != nil {
t.Fatal(err)
@@ -182,9 +182,9 @@ func TestResolveAccount_StrictModeOff(t *testing.T) {
}
func TestResolveAccount_InferFromUATOnly(t *testing.T) {
t.Setenv(envnames.CliAppID, "app")
t.Setenv(envnames.CliAppSecret, "secret")
t.Setenv(envnames.CliUserAccessToken, "u-tok")
t.Setenv(envvars.CliAppID, "app")
t.Setenv(envvars.CliAppSecret, "secret")
t.Setenv(envvars.CliUserAccessToken, "u-tok")
acct, err := (&Provider{}).ResolveAccount(context.Background())
if err != nil {
t.Fatal(err)
@@ -198,9 +198,9 @@ func TestResolveAccount_InferFromUATOnly(t *testing.T) {
}
func TestResolveAccount_InferFromTATOnly(t *testing.T) {
t.Setenv(envnames.CliAppID, "app")
t.Setenv(envnames.CliAppSecret, "secret")
t.Setenv(envnames.CliTenantAccessToken, "t-tok")
t.Setenv(envvars.CliAppID, "app")
t.Setenv(envvars.CliAppSecret, "secret")
t.Setenv(envvars.CliTenantAccessToken, "t-tok")
acct, err := (&Provider{}).ResolveAccount(context.Background())
if err != nil {
t.Fatal(err)
@@ -214,10 +214,10 @@ func TestResolveAccount_InferFromTATOnly(t *testing.T) {
}
func TestResolveAccount_InferBothTokens(t *testing.T) {
t.Setenv(envnames.CliAppID, "app")
t.Setenv(envnames.CliAppSecret, "secret")
t.Setenv(envnames.CliUserAccessToken, "u-tok")
t.Setenv(envnames.CliTenantAccessToken, "t-tok")
t.Setenv(envvars.CliAppID, "app")
t.Setenv(envvars.CliAppSecret, "secret")
t.Setenv(envvars.CliUserAccessToken, "u-tok")
t.Setenv(envvars.CliTenantAccessToken, "t-tok")
acct, err := (&Provider{}).ResolveAccount(context.Background())
if err != nil {
t.Fatal(err)
@@ -231,11 +231,11 @@ func TestResolveAccount_InferBothTokens(t *testing.T) {
}
func TestResolveAccount_StrictModeOverridesTokenInference(t *testing.T) {
t.Setenv(envnames.CliAppID, "app")
t.Setenv(envnames.CliAppSecret, "secret")
t.Setenv(envnames.CliUserAccessToken, "u-tok")
t.Setenv(envnames.CliTenantAccessToken, "t-tok")
t.Setenv(envnames.CliStrictMode, "bot")
t.Setenv(envvars.CliAppID, "app")
t.Setenv(envvars.CliAppSecret, "secret")
t.Setenv(envvars.CliUserAccessToken, "u-tok")
t.Setenv(envvars.CliTenantAccessToken, "t-tok")
t.Setenv(envvars.CliStrictMode, "bot")
acct, err := (&Provider{}).ResolveAccount(context.Background())
if err != nil {
t.Fatal(err)
@@ -246,9 +246,9 @@ func TestResolveAccount_StrictModeOverridesTokenInference(t *testing.T) {
}
func TestResolveAccount_InvalidStrictModeRejected(t *testing.T) {
t.Setenv(envnames.CliAppID, "app")
t.Setenv(envnames.CliAppSecret, "secret")
t.Setenv(envnames.CliStrictMode, "invalid")
t.Setenv(envvars.CliAppID, "app")
t.Setenv(envvars.CliAppSecret, "secret")
t.Setenv(envvars.CliStrictMode, "invalid")
_, err := (&Provider{}).ResolveAccount(context.Background())
if err == nil {
@@ -258,15 +258,15 @@ func TestResolveAccount_InvalidStrictModeRejected(t *testing.T) {
if !errors.As(err, &blockErr) {
t.Fatalf("expected BlockError, got %T", err)
}
if !strings.Contains(err.Error(), envnames.CliStrictMode) {
t.Fatalf("error = %v, want mention of %s", err, envnames.CliStrictMode)
if !strings.Contains(err.Error(), envvars.CliStrictMode) {
t.Fatalf("error = %v, want mention of %s", err, envvars.CliStrictMode)
}
}
func TestResolveAccount_InvalidDefaultAsRejected(t *testing.T) {
t.Setenv(envnames.CliAppID, "app")
t.Setenv(envnames.CliAppSecret, "secret")
t.Setenv(envnames.CliDefaultAs, "invalid")
t.Setenv(envvars.CliAppID, "app")
t.Setenv(envvars.CliAppSecret, "secret")
t.Setenv(envvars.CliDefaultAs, "invalid")
_, err := (&Provider{}).ResolveAccount(context.Background())
if err == nil {
@@ -276,7 +276,7 @@ func TestResolveAccount_InvalidDefaultAsRejected(t *testing.T) {
if !errors.As(err, &blockErr) {
t.Fatalf("expected BlockError, got %T", err)
}
if !strings.Contains(err.Error(), envnames.CliDefaultAs) {
t.Fatalf("error = %v, want mention of %s", err, envnames.CliDefaultAs)
if !strings.Contains(err.Error(), envvars.CliDefaultAs) {
t.Fatalf("error = %v, want mention of %s", err, envvars.CliDefaultAs)
}
}

View File

@@ -15,8 +15,9 @@ import (
"fmt"
"os"
"github.com/larksuite/cli/envnames"
"github.com/larksuite/cli/extension/credential"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/envvars"
"github.com/larksuite/cli/sidecar"
)
@@ -31,7 +32,7 @@ func (p *Provider) Priority() int { return 0 }
// placeholder secret, and SupportedIdentities derived from STRICT_MODE.
// Returns nil, nil when sidecar mode is not active (AUTH_PROXY not set).
func (p *Provider) ResolveAccount(ctx context.Context) (*credential.Account, error) {
proxyAddr := os.Getenv(envnames.CliAuthProxy)
proxyAddr := os.Getenv(envvars.CliAuthProxy)
if proxyAddr == "" {
return nil, nil // not in sidecar mode, skip
}
@@ -39,26 +40,26 @@ func (p *Provider) ResolveAccount(ctx context.Context) (*credential.Account, err
if err := sidecar.ValidateProxyAddr(proxyAddr); err != nil {
return nil, &credential.BlockError{
Provider: "sidecar",
Reason: fmt.Sprintf("invalid %s %q: %v", envnames.CliAuthProxy, proxyAddr, err),
Reason: fmt.Sprintf("invalid %s %q: %v", envvars.CliAuthProxy, proxyAddr, err),
}
}
appID := os.Getenv(envnames.CliAppID)
appID := os.Getenv(envvars.CliAppID)
if appID == "" {
return nil, &credential.BlockError{
Provider: "sidecar",
Reason: envnames.CliAuthProxy + " is set but " + envnames.CliAppID + " is missing",
Reason: envvars.CliAuthProxy + " is set but " + envvars.CliAppID + " is missing",
}
}
if os.Getenv(envnames.CliProxyKey) == "" {
if os.Getenv(envvars.CliProxyKey) == "" {
return nil, &credential.BlockError{
Provider: "sidecar",
Reason: envnames.CliAuthProxy + " is set but " + envnames.CliProxyKey + " is missing",
Reason: envvars.CliAuthProxy + " is set but " + envvars.CliProxyKey + " is missing",
}
}
brand := credential.ParseBrand(os.Getenv(envnames.CliBrand))
brand := credential.Brand(core.ParseBrand(os.Getenv(envvars.CliBrand)))
acct := &credential.Account{
AppID: appID,
@@ -67,7 +68,7 @@ func (p *Provider) ResolveAccount(ctx context.Context) (*credential.Account, err
}
// Parse DefaultAs
switch id := credential.Identity(os.Getenv(envnames.CliDefaultAs)); id {
switch id := credential.Identity(os.Getenv(envvars.CliDefaultAs)); id {
case "", credential.IdentityAuto:
acct.DefaultAs = id
case credential.IdentityUser, credential.IdentityBot:
@@ -75,12 +76,12 @@ func (p *Provider) ResolveAccount(ctx context.Context) (*credential.Account, err
default:
return nil, &credential.BlockError{
Provider: "sidecar",
Reason: fmt.Sprintf("invalid %s %q (want user, bot, or auto)", envnames.CliDefaultAs, id),
Reason: fmt.Sprintf("invalid %s %q (want user, bot, or auto)", envvars.CliDefaultAs, id),
}
}
// Parse SupportedIdentities from STRICT_MODE, default to SupportsAll.
switch strictMode := os.Getenv(envnames.CliStrictMode); strictMode {
switch strictMode := os.Getenv(envvars.CliStrictMode); strictMode {
case "bot":
acct.SupportedIdentities = credential.SupportsBot
case "user":
@@ -90,7 +91,7 @@ func (p *Provider) ResolveAccount(ctx context.Context) (*credential.Account, err
default:
return nil, &credential.BlockError{
Provider: "sidecar",
Reason: fmt.Sprintf("invalid %s %q (want bot, user, or off)", envnames.CliStrictMode, strictMode),
Reason: fmt.Sprintf("invalid %s %q (want bot, user, or off)", envvars.CliStrictMode, strictMode),
}
}
@@ -102,7 +103,7 @@ func (p *Provider) ResolveAccount(ctx context.Context) (*credential.Account, err
// (user vs bot), strips it, and the sidecar injects the real token.
// Returns nil, nil when sidecar mode is not active.
func (p *Provider) ResolveToken(ctx context.Context, req credential.TokenSpec) (*credential.Token, error) {
if os.Getenv(envnames.CliAuthProxy) == "" {
if os.Getenv(envvars.CliAuthProxy) == "" {
return nil, nil
}

View File

@@ -10,8 +10,8 @@ import (
"os"
"testing"
"github.com/larksuite/cli/envnames"
"github.com/larksuite/cli/extension/credential"
"github.com/larksuite/cli/internal/envvars"
"github.com/larksuite/cli/sidecar"
)
@@ -40,7 +40,7 @@ func unsetEnv(t *testing.T, key string) {
}
func TestResolveAccount_NotActive(t *testing.T) {
unsetEnv(t, envnames.CliAuthProxy)
unsetEnv(t, envvars.CliAuthProxy)
p := &Provider{}
acct, err := p.ResolveAccount(context.Background())
@@ -53,12 +53,12 @@ func TestResolveAccount_NotActive(t *testing.T) {
}
func TestResolveAccount_Active(t *testing.T) {
setEnv(t, envnames.CliAuthProxy, "http://127.0.0.1:16384")
setEnv(t, envnames.CliProxyKey, "test-key")
setEnv(t, envnames.CliAppID, "cli_test123")
setEnv(t, envnames.CliBrand, " LARK ")
unsetEnv(t, envnames.CliDefaultAs)
unsetEnv(t, envnames.CliStrictMode)
setEnv(t, envvars.CliAuthProxy, "http://127.0.0.1:16384")
setEnv(t, envvars.CliProxyKey, "test-key")
setEnv(t, envvars.CliAppID, "cli_test123")
setEnv(t, envvars.CliBrand, " LARK ")
unsetEnv(t, envvars.CliDefaultAs)
unsetEnv(t, envvars.CliStrictMode)
p := &Provider{}
acct, err := p.ResolveAccount(context.Background())
@@ -83,9 +83,9 @@ func TestResolveAccount_Active(t *testing.T) {
}
func TestResolveAccount_MissingProxyKey(t *testing.T) {
setEnv(t, envnames.CliAuthProxy, "http://127.0.0.1:16384")
unsetEnv(t, envnames.CliProxyKey)
setEnv(t, envnames.CliAppID, "cli_test")
setEnv(t, envvars.CliAuthProxy, "http://127.0.0.1:16384")
unsetEnv(t, envvars.CliProxyKey)
setEnv(t, envvars.CliAppID, "cli_test")
p := &Provider{}
_, err := p.ResolveAccount(context.Background())
@@ -98,9 +98,9 @@ func TestResolveAccount_MissingProxyKey(t *testing.T) {
}
func TestResolveAccount_MissingAppID(t *testing.T) {
setEnv(t, envnames.CliAuthProxy, "http://127.0.0.1:16384")
setEnv(t, envnames.CliProxyKey, "test-key")
unsetEnv(t, envnames.CliAppID)
setEnv(t, envvars.CliAuthProxy, "http://127.0.0.1:16384")
setEnv(t, envvars.CliProxyKey, "test-key")
unsetEnv(t, envvars.CliAppID)
p := &Provider{}
_, err := p.ResolveAccount(context.Background())
@@ -113,9 +113,9 @@ func TestResolveAccount_MissingAppID(t *testing.T) {
}
func TestResolveAccount_StrictMode(t *testing.T) {
setEnv(t, envnames.CliAuthProxy, "http://127.0.0.1:16384")
setEnv(t, envnames.CliProxyKey, "test-key")
setEnv(t, envnames.CliAppID, "cli_test")
setEnv(t, envvars.CliAuthProxy, "http://127.0.0.1:16384")
setEnv(t, envvars.CliProxyKey, "test-key")
setEnv(t, envvars.CliAppID, "cli_test")
tests := []struct {
mode string
@@ -131,9 +131,9 @@ func TestResolveAccount_StrictMode(t *testing.T) {
for _, tt := range tests {
t.Run("strict_"+tt.mode, func(t *testing.T) {
if tt.mode == "" {
unsetEnv(t, envnames.CliStrictMode)
unsetEnv(t, envvars.CliStrictMode)
} else {
setEnv(t, envnames.CliStrictMode, tt.mode)
setEnv(t, envvars.CliStrictMode, tt.mode)
}
acct, err := p.ResolveAccount(context.Background())
if err != nil {
@@ -147,7 +147,7 @@ func TestResolveAccount_StrictMode(t *testing.T) {
}
func TestResolveToken_NotActive(t *testing.T) {
unsetEnv(t, envnames.CliAuthProxy)
unsetEnv(t, envvars.CliAuthProxy)
p := &Provider{}
tok, err := p.ResolveToken(context.Background(), credential.TokenSpec{Type: credential.TokenTypeUAT})
@@ -160,8 +160,8 @@ func TestResolveToken_NotActive(t *testing.T) {
}
func TestResolveToken_Sentinels(t *testing.T) {
setEnv(t, envnames.CliAuthProxy, "http://127.0.0.1:16384")
setEnv(t, envnames.CliProxyKey, "test-key")
setEnv(t, envvars.CliAuthProxy, "http://127.0.0.1:16384")
setEnv(t, envvars.CliProxyKey, "test-key")
p := &Provider{}

View File

@@ -3,29 +3,16 @@
package credential
import (
"context"
"github.com/larksuite/cli/brand"
)
import "context"
// Brand represents the Lark platform brand.
type Brand = brand.Brand
type Brand string
const (
BrandLark = brand.Lark
BrandFeishu = brand.Feishu
BrandLark Brand = "lark"
BrandFeishu Brand = "feishu"
)
// ParseBrand maps a brand string to a Brand, defaulting to BrandFeishu.
// It forwards to the repository-root brand package so every credential source
// and the rest of the CLI resolve the brand through one implementation;
// re-spelling the rule here would let the two copies drift when the brand set
// changes. It stays exported so SDK callers need not import brand directly.
func ParseBrand(value string) Brand {
return brand.ParseBrand(value)
}
// NoAppSecret marks that a credential source does not provide a real app secret.
// Token-only sources should return this value instead of inventing placeholder text.
const NoAppSecret = ""

View File

@@ -20,8 +20,8 @@ import (
"os"
"strings"
"github.com/larksuite/cli/envnames"
"github.com/larksuite/cli/extension/transport"
"github.com/larksuite/cli/internal/envvars"
"github.com/larksuite/cli/sidecar"
)
@@ -36,15 +36,15 @@ func (p *Provider) Name() string { return "sidecar" }
// the non-sidecar transport path (where the credential layer will typically
// block them for lack of a valid account).
func (p *Provider) ResolveInterceptor(ctx context.Context) transport.Interceptor {
proxyAddr := os.Getenv(envnames.CliAuthProxy)
proxyAddr := os.Getenv(envvars.CliAuthProxy)
if proxyAddr == "" {
return nil
}
if err := sidecar.ValidateProxyAddr(proxyAddr); err != nil {
fmt.Fprintf(os.Stderr, "WARNING: invalid %s, sidecar interceptor disabled: %v\n", envnames.CliAuthProxy, err)
fmt.Fprintf(os.Stderr, "WARNING: invalid %s, sidecar interceptor disabled: %v\n", envvars.CliAuthProxy, err)
return nil
}
key := os.Getenv(envnames.CliProxyKey)
key := os.Getenv(envvars.CliProxyKey)
return &Interceptor{
key: []byte(key),
sidecarHost: sidecar.ProxyHost(proxyAddr),
@@ -166,12 +166,12 @@ func detectSentinel(req *http.Request) (identity, authHeader string) {
}
func init() {
proxyAddr := os.Getenv(envnames.CliAuthProxy)
proxyAddr := os.Getenv(envvars.CliAuthProxy)
if proxyAddr == "" {
return
}
if err := sidecar.ValidateProxyAddr(proxyAddr); err != nil {
fmt.Fprintf(os.Stderr, "WARNING: ignoring invalid %s: %v\n", envnames.CliAuthProxy, err)
fmt.Fprintf(os.Stderr, "WARNING: ignoring invalid %s: %v\n", envvars.CliAuthProxy, err)
return
}
transport.Register(&Provider{})

View File

@@ -14,8 +14,7 @@ import (
"strings"
"time"
brandpkg "github.com/larksuite/cli/brand"
"github.com/larksuite/cli/internal/authlog"
"github.com/larksuite/cli/internal/core"
)
// Terminal registration outcomes, exposed for typed classification by callers.
@@ -27,7 +26,7 @@ var (
// Protocol defaults, mirroring the official SDK registration flow.
const (
registrationBootstrapBrand = brandpkg.Feishu
registrationBootstrapBrand = core.BrandFeishu
defaultPollIntervalSeconds = 5
defaultExpireInSeconds = 600
beginRequestTimeout = 30 * time.Second
@@ -82,14 +81,14 @@ type AppRegUserInfo struct {
}
// appRegistrationEndpoint returns the brand's accounts registration endpoint.
func appRegistrationEndpoint(brand brandpkg.Brand) string {
return brandpkg.ResolveEndpoints(brand).Accounts + PathAppRegistration
func appRegistrationEndpoint(brand core.LarkBrand) string {
return core.ResolveEndpoints(brand).Accounts + PathAppRegistration
}
// RequestAppRegistration initiates the device flow. The registration protocol
// always bootstraps on Feishu; brand selects the user-facing verification host.
// The request is bounded by ctx and a begin timeout.
func RequestAppRegistration(ctx context.Context, httpClient *http.Client, brand brandpkg.Brand, errOut io.Writer) (*AppRegistrationResponse, error) {
func RequestAppRegistration(ctx context.Context, httpClient *http.Client, brand core.LarkBrand, errOut io.Writer) (*AppRegistrationResponse, error) {
if errOut == nil {
errOut = io.Discard
}
@@ -97,7 +96,7 @@ func RequestAppRegistration(ctx context.Context, httpClient *http.Client, brand
ctx, cancel := context.WithTimeout(ctx, beginRequestTimeout)
defer cancel()
ep := brandpkg.ResolveEndpoints(brand)
ep := core.ResolveEndpoints(brand)
endpoint := appRegistrationEndpoint(registrationBootstrapBrand)
form := url.Values{}
@@ -117,7 +116,7 @@ func RequestAppRegistration(ctx context.Context, httpClient *http.Client, brand
return nil, err
}
defer resp.Body.Close()
logHTTPResponse(newAuthLogger(), resp)
logHTTPResponse(resp)
body, err := io.ReadAll(resp.Body)
if err != nil {
@@ -181,7 +180,7 @@ func BuildVerificationURL(baseURL, cliVersion string) string {
}
// pollOnce performs one ctx-bound poll request and decodes the payload.
func pollOnce(ctx context.Context, httpClient *http.Client, brand brandpkg.Brand, deviceCode string, logger *authlog.Logger) (map[string]interface{}, error) {
func pollOnce(ctx context.Context, httpClient *http.Client, brand core.LarkBrand, deviceCode string) (map[string]interface{}, error) {
form := url.Values{}
form.Set("action", "poll")
form.Set("device_code", deviceCode)
@@ -197,7 +196,7 @@ func pollOnce(ctx context.Context, httpClient *http.Client, brand brandpkg.Brand
return nil, fmt.Errorf("poll network error: %w", err)
}
defer resp.Body.Close()
logHTTPResponse(logger, resp)
logHTTPResponse(resp)
body, err := io.ReadAll(resp.Body)
if err != nil {
@@ -215,7 +214,7 @@ func pollOnce(ctx context.Context, httpClient *http.Client, brand brandpkg.Brand
// non-error responses without complete credentials keep polling, and one
// deadline from the begin expiry bounds all waits and in-flight requests.
// The returned brand is the one the credentials were issued on.
func RegisterAppWithDiscovery(ctx context.Context, httpClient *http.Client, resp *AppRegistrationResponse, errOut io.Writer) (*AppRegistrationResult, brandpkg.Brand, error) {
func RegisterAppWithDiscovery(ctx context.Context, httpClient *http.Client, resp *AppRegistrationResponse, errOut io.Writer) (*AppRegistrationResult, core.LarkBrand, error) {
if errOut == nil {
errOut = io.Discard
}
@@ -231,7 +230,6 @@ func RegisterAppWithDiscovery(ctx context.Context, httpClient *http.Client, resp
effectiveBrand := currentBrand
switched := false
waitBeforePoll := false
authLogger := newAuthLogger()
for {
if waitBeforePoll {
@@ -246,7 +244,7 @@ func RegisterAppWithDiscovery(ctx context.Context, httpClient *http.Client, resp
return nil, effectiveBrand, registrationContextError(ctx)
}
data, err := pollOnce(ctx, httpClient, currentBrand, resp.DeviceCode, authLogger)
data, err := pollOnce(ctx, httpClient, currentBrand, resp.DeviceCode)
if err != nil {
fmt.Fprintf(errOut, "[lark-cli] [WARN] app-registration: %v\n", err)
interval = minInt(interval+1, maxPollIntervalSeconds)
@@ -259,7 +257,7 @@ func RegisterAppWithDiscovery(ctx context.Context, httpClient *http.Client, resp
if !switched {
if userInfoRaw, ok := data["user_info"].(map[string]interface{}); ok {
if tb := getStr(userInfoRaw, "tenant_brand"); tb != "" {
if actual := brandpkg.ParseBrand(tb); actual != currentBrand {
if actual := core.ParseBrand(tb); actual != currentBrand {
currentBrand = actual
effectiveBrand = actual
switched = true
@@ -287,7 +285,7 @@ func RegisterAppWithDiscovery(ctx context.Context, httpClient *http.Client, resp
// The issuing domain is authoritative; a contradictory final
// tenant report is a protocol violation, not a brand override.
if result.UserInfo != nil && result.UserInfo.TenantBrand != "" &&
brandpkg.ParseBrand(result.UserInfo.TenantBrand) != effectiveBrand {
core.ParseBrand(result.UserInfo.TenantBrand) != effectiveBrand {
return nil, effectiveBrand, fmt.Errorf("app registration returned credentials with a contradictory tenant brand %q", result.UserInfo.TenantBrand)
}
return result, effectiveBrand, nil

View File

@@ -12,7 +12,7 @@ import (
"testing"
"time"
brandpkg "github.com/larksuite/cli/brand"
"github.com/larksuite/cli/internal/core"
"github.com/smartystreets/goconvey/convey"
)
@@ -51,11 +51,11 @@ func Test_BuildVerificationURL(t *testing.T) {
func TestAppRegistrationEndpoint(t *testing.T) {
cases := []struct {
brand brandpkg.Brand
brand core.LarkBrand
want string
}{
{brandpkg.Feishu, "https://accounts.feishu.cn" + PathAppRegistration},
{brandpkg.Lark, "https://accounts.larksuite.com" + PathAppRegistration},
{core.BrandFeishu, "https://accounts.feishu.cn" + PathAppRegistration},
{core.BrandLark, "https://accounts.larksuite.com" + PathAppRegistration},
}
for _, c := range cases {
if got := appRegistrationEndpoint(c.brand); got != c.want {
@@ -66,11 +66,11 @@ func TestAppRegistrationEndpoint(t *testing.T) {
func TestRequestAppRegistration_UsesFeishuBootstrapAndConfiguredVerificationBrand(t *testing.T) {
cases := []struct {
brand brandpkg.Brand
brand core.LarkBrand
verificationHost string
}{
{brandpkg.Feishu, "open.feishu.cn"},
{brandpkg.Lark, "open.larksuite.com"},
{core.BrandFeishu, "open.feishu.cn"},
{core.BrandLark, "open.larksuite.com"},
}
for _, c := range cases {
t.Run(string(c.brand), func(t *testing.T) {
@@ -115,7 +115,7 @@ func TestRegisterAppWithDiscovery_LarkFlowUsesProtocolBootstrap(t *testing.T) {
t.Errorf("unexpected host polled: %s", r.URL.Host)
return jsonResponse(`{}`), nil
})}
resp, err := RequestAppRegistration(context.Background(), client, brandpkg.Lark, io.Discard)
resp, err := RequestAppRegistration(context.Background(), client, core.BrandLark, io.Discard)
if err != nil {
t.Fatalf("RequestAppRegistration error = %v", err)
}
@@ -127,8 +127,8 @@ func TestRegisterAppWithDiscovery_LarkFlowUsesProtocolBootstrap(t *testing.T) {
if err != nil {
t.Fatalf("RegisterAppWithDiscovery error = %v, want nil", err)
}
if finalBrand != brandpkg.Lark {
t.Errorf("finalBrand = %q, want %q (credentials were issued on the lark domain)", finalBrand, brandpkg.Lark)
if finalBrand != core.BrandLark {
t.Errorf("finalBrand = %q, want %q (credentials were issued on the lark domain)", finalBrand, core.BrandLark)
}
if result.ClientID != "cli_x" || result.ClientSecret != "test-secret" {
t.Errorf("credentials = (%q, %q), want (cli_x, test-secret)", result.ClientID, result.ClientSecret)
@@ -162,8 +162,8 @@ func TestRegisterAppWithDiscovery_BootstrapBrandSinglePoll(t *testing.T) {
if err != nil {
t.Fatalf("RegisterAppWithDiscovery error = %v, want nil", err)
}
if finalBrand != brandpkg.Feishu {
t.Errorf("finalBrand = %q, want %q", finalBrand, brandpkg.Feishu)
if finalBrand != core.BrandFeishu {
t.Errorf("finalBrand = %q, want %q", finalBrand, core.BrandFeishu)
}
if polls != 1 {
t.Errorf("polls = %d, want 1", polls)
@@ -214,7 +214,7 @@ func TestRegisterAppWithDiscovery_PollsUntilCredentials(t *testing.T) {
if polls != 3 {
t.Errorf("polls = %d, want 3", polls)
}
if result.ClientSecret != "test-secret" || finalBrand != brandpkg.Feishu {
if result.ClientSecret != "test-secret" || finalBrand != core.BrandFeishu {
t.Errorf("result = (%q, %q), want (test-secret, feishu)", result.ClientSecret, finalBrand)
}
}
@@ -240,7 +240,7 @@ func TestRegisterAppWithDiscovery_ImmediateFirstPollAndSwitch(t *testing.T) {
if elapsed := time.Since(start); elapsed > 2*time.Second {
t.Errorf("discovery waited an interval somewhere: took %v", elapsed)
}
if finalBrand != brandpkg.Lark || result.ClientSecret != "test-secret" {
if finalBrand != core.BrandLark || result.ClientSecret != "test-secret" {
t.Errorf("result = (%q, %q), want (test-secret, lark)", result.ClientSecret, finalBrand)
}
want := []string{"accounts.feishu.cn", "accounts.larksuite.com"}
@@ -286,7 +286,7 @@ func TestRequestAppRegistration_ProtocolFields(t *testing.T) {
}
resp, err := RequestAppRegistration(context.Background(),
serve(`{"device_code":"d","expire_in":60,"interval":3}`), brandpkg.Feishu, io.Discard)
serve(`{"device_code":"d","expire_in":60,"interval":3}`), core.BrandFeishu, io.Discard)
if err != nil {
t.Fatalf("begin error = %v", err)
}
@@ -295,7 +295,7 @@ func TestRequestAppRegistration_ProtocolFields(t *testing.T) {
}
resp, err = RequestAppRegistration(context.Background(),
serve(`{"device_code":"d","expires_in":45}`), brandpkg.Feishu, io.Discard)
serve(`{"device_code":"d","expires_in":45}`), core.BrandFeishu, io.Discard)
if err != nil {
t.Fatalf("legacy begin error = %v", err)
}
@@ -304,7 +304,7 @@ func TestRequestAppRegistration_ProtocolFields(t *testing.T) {
}
resp, err = RequestAppRegistration(context.Background(),
serve(`{"device_code":"d","interval":0}`), brandpkg.Feishu, io.Discard)
serve(`{"device_code":"d","interval":0}`), core.BrandFeishu, io.Discard)
if err != nil {
t.Fatalf("defaults begin error = %v", err)
}
@@ -313,7 +313,7 @@ func TestRequestAppRegistration_ProtocolFields(t *testing.T) {
}
if _, err := RequestAppRegistration(context.Background(),
serve(`{"interval":5}`), brandpkg.Feishu, io.Discard); err == nil {
serve(`{"interval":5}`), core.BrandFeishu, io.Discard); err == nil {
t.Error("missing device_code: expected error, got nil")
}
}
@@ -335,7 +335,7 @@ func TestRegisterAppWithDiscovery_PendingWithTenantSignalSwitches(t *testing.T)
if err != nil {
t.Fatalf("RegisterAppWithDiscovery error = %v, want nil", err)
}
if finalBrand != brandpkg.Lark || result.ClientSecret != "test-secret" {
if finalBrand != core.BrandLark || result.ClientSecret != "test-secret" {
t.Errorf("result = (%q, %q), want (test-secret, lark)", result.ClientSecret, finalBrand)
}
want := []string{"accounts.feishu.cn", "accounts.larksuite.com"}
@@ -394,7 +394,7 @@ func TestRequestAppRegistration_BodyReadCancelKeepsCause(t *testing.T) {
Header: make(http.Header),
}, nil
})}
_, err := RequestAppRegistration(context.Background(), client, brandpkg.Feishu, io.Discard)
_, err := RequestAppRegistration(context.Background(), client, core.BrandFeishu, io.Discard)
if !errors.Is(err, context.Canceled) {
t.Errorf("err = %v, want a context.Canceled cause", err)
}

View File

@@ -6,25 +6,14 @@ package auth
import (
"net/http"
"github.com/larksuite/cli/internal/authlog"
"github.com/larksuite/cli/internal/keychain"
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
)
type authLogger interface {
LogResponse(path string, status int, logID string)
}
// newAuthLogger returns the process-wide authentication logger. It is shared on
// purpose: one file handle and one log-prune per process, and keychain errors
// land in the same file as the responses they explain.
func newAuthLogger() *authlog.Logger {
return authlog.Shared()
}
// logHTTPResponse logs the HTTP response details for an authentication request.
// It extracts the request path, status code, and x-tt-logid from the given HTTP response.
func logHTTPResponse(logger authLogger, resp *http.Response) {
if logger == nil || resp == nil {
func logHTTPResponse(resp *http.Response) {
if resp == nil {
return
}
@@ -33,23 +22,20 @@ func logHTTPResponse(logger authLogger, resp *http.Response) {
path = resp.Request.URL.Path
}
logger.LogResponse(path, resp.StatusCode, resp.Header.Get("x-tt-logid"))
keychain.LogAuthResponse(path, resp.StatusCode, resp.Header.Get("x-tt-logid"))
}
// logSDKResponse logs the SDK response details for an authentication request.
// It extracts the status code and x-tt-logid from the given API response object.
func logSDKResponse(logger authLogger, path string, apiResp *larkcore.ApiResp) {
if logger == nil {
return
}
func logSDKResponse(path string, apiResp *larkcore.ApiResp) {
if path == "" {
path = "missing"
}
if apiResp == nil {
logger.LogResponse(path, 0, "")
keychain.LogAuthResponse(path, 0, "")
return
}
logger.LogResponse(path, apiResp.StatusCode, apiResp.Header.Get("x-tt-logid"))
keychain.LogAuthResponse(path, apiResp.StatusCode, apiResp.Header.Get("x-tt-logid"))
}

View File

@@ -14,7 +14,7 @@ import (
"strings"
"time"
brandpkg "github.com/larksuite/cli/brand"
"github.com/larksuite/cli/internal/core"
)
// DeviceAuthResponse is the response from the device authorization endpoint.
@@ -52,8 +52,8 @@ type OAuthEndpoints struct {
}
// ResolveOAuthEndpoints resolves OAuth endpoint URLs based on brand.
func ResolveOAuthEndpoints(brand brandpkg.Brand) OAuthEndpoints {
ep := brandpkg.ResolveEndpoints(brand)
func ResolveOAuthEndpoints(brand core.LarkBrand) OAuthEndpoints {
ep := core.ResolveEndpoints(brand)
return OAuthEndpoints{
DeviceAuthorization: ep.Accounts + PathDeviceAuthorization,
Revoke: ep.Accounts + PathOAuthRevoke,
@@ -62,11 +62,7 @@ func ResolveOAuthEndpoints(brand brandpkg.Brand) OAuthEndpoints {
}
// RequestDeviceAuthorization requests a device authorization code.
func RequestDeviceAuthorization(httpClient *http.Client, appId, appSecret string, brand brandpkg.Brand, scope string, errOut io.Writer) (*DeviceAuthResponse, error) {
return requestDeviceAuthorization(httpClient, appId, appSecret, brand, scope, errOut, newAuthLogger())
}
func requestDeviceAuthorization(httpClient *http.Client, appId, appSecret string, brand brandpkg.Brand, scope string, errOut io.Writer, logger authLogger) (*DeviceAuthResponse, error) {
func RequestDeviceAuthorization(httpClient *http.Client, appId, appSecret string, brand core.LarkBrand, scope string, errOut io.Writer) (*DeviceAuthResponse, error) {
if errOut == nil {
errOut = io.Discard
}
@@ -99,7 +95,7 @@ func requestDeviceAuthorization(httpClient *http.Client, appId, appSecret string
return nil, err
}
defer resp.Body.Close()
logHTTPResponse(logger, resp)
logHTTPResponse(resp)
body, err := io.ReadAll(resp.Body)
if err != nil {
@@ -143,7 +139,7 @@ func requestDeviceAuthorization(httpClient *http.Client, appId, appSecret string
}
// PollDeviceToken polls the token endpoint until authorization completes or times out.
func PollDeviceToken(ctx context.Context, httpClient *http.Client, appId, appSecret string, brand brandpkg.Brand, deviceCode string, interval, expiresIn int, errOut io.Writer) *DeviceFlowResult {
func PollDeviceToken(ctx context.Context, httpClient *http.Client, appId, appSecret string, brand core.LarkBrand, deviceCode string, interval, expiresIn int, errOut io.Writer) *DeviceFlowResult {
if errOut == nil {
errOut = io.Discard
}
@@ -159,7 +155,6 @@ func PollDeviceToken(ctx context.Context, httpClient *http.Client, appId, appSec
deadline := time.Now().Add(time.Duration(expiresIn) * time.Second)
currentInterval := interval
attempts := 0
authLogger := newAuthLogger()
for time.Now().Before(deadline) && attempts < maxPollAttempts {
attempts++
@@ -191,7 +186,7 @@ func PollDeviceToken(ctx context.Context, httpClient *http.Client, appId, appSec
currentInterval = minInt(currentInterval+1, maxPollInterval)
continue
}
logHTTPResponse(authLogger, resp)
logHTTPResponse(resp)
body, err := io.ReadAll(resp.Body)
resp.Body.Close()

View File

@@ -14,9 +14,9 @@ import (
"testing"
"time"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/internal/authlog"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/httpmock"
"github.com/larksuite/cli/internal/keychain"
)
type roundTripFunc func(*http.Request) (*http.Response, error)
@@ -25,17 +25,9 @@ func (fn roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return fn(req)
}
func testAuthLogger(buf *bytes.Buffer, now func() time.Time, args func() []string) *authlog.Logger {
return authlog.New(authlog.Options{
Logger: log.New(buf, "", 0),
Now: now,
Args: args,
})
}
// TestResolveOAuthEndpoints_Feishu validates endpoints for the Feishu brand.
func TestResolveOAuthEndpoints_Feishu(t *testing.T) {
ep := ResolveOAuthEndpoints(brand.Feishu)
ep := ResolveOAuthEndpoints(core.BrandFeishu)
if ep.DeviceAuthorization != "https://accounts.feishu.cn/oauth/v1/device_authorization" {
t.Errorf("DeviceAuthorization = %q", ep.DeviceAuthorization)
}
@@ -49,7 +41,7 @@ func TestResolveOAuthEndpoints_Feishu(t *testing.T) {
// TestResolveOAuthEndpoints_Lark validates endpoints for the Lark brand.
func TestResolveOAuthEndpoints_Lark(t *testing.T) {
ep := ResolveOAuthEndpoints(brand.Lark)
ep := ResolveOAuthEndpoints(core.BrandLark)
if ep.DeviceAuthorization != "https://accounts.larksuite.com/oauth/v1/device_authorization" {
t.Errorf("DeviceAuthorization = %q", ep.DeviceAuthorization)
}
@@ -84,13 +76,14 @@ func TestRequestDeviceAuthorization_LogsResponse(t *testing.T) {
})
var buf bytes.Buffer
authLogger := testAuthLogger(&buf, func() time.Time {
restore := keychain.SetAuthLogHooksForTest(log.New(&buf, "", 0), func() time.Time {
return time.Date(2026, 4, 2, 3, 4, 5, 0, time.UTC)
}, func() []string {
return []string{"lark-cli", "auth", "login", "--device-code", "device-code-secret", "--app-secret=top-secret"}
})
t.Cleanup(restore)
_, err := requestDeviceAuthorization(httpmock.NewClient(reg), "cli_a", "secret_b", brand.Feishu, "", nil, authLogger)
_, err := RequestDeviceAuthorization(httpmock.NewClient(reg), "cli_a", "secret_b", core.BrandFeishu, "", nil)
if err != nil {
t.Fatalf("RequestDeviceAuthorization() error: %v", err)
}
@@ -115,7 +108,7 @@ func TestRequestDeviceAuthorization_LogsResponse(t *testing.T) {
// TestFormatAuthCmdline_TruncatesExtraArgs verifies that long command lines are truncated.
func TestFormatAuthCmdline_TruncatesExtraArgs(t *testing.T) {
got := authlog.FormatAuthCmdline([]string{
got := keychain.FormatAuthCmdline([]string{
"lark-cli",
"auth",
"login",
@@ -133,10 +126,11 @@ func TestFormatAuthCmdline_TruncatesExtraArgs(t *testing.T) {
// TestLogAuthResponse_IgnoresTypedNilHTTPResponse tests that a typed nil HTTP response is ignored gracefully.
func TestLogAuthResponse_IgnoresTypedNilHTTPResponse(t *testing.T) {
var buf bytes.Buffer
authLogger := testAuthLogger(&buf, nil, nil)
restore := keychain.SetAuthLogHooksForTest(log.New(&buf, "", 0), nil, nil)
t.Cleanup(restore)
var resp *http.Response
logHTTPResponse(authLogger, resp)
logHTTPResponse(resp)
if got := buf.String(); got != "" {
t.Fatalf("expected no log output, got %q", got)
@@ -146,13 +140,14 @@ func TestLogAuthResponse_IgnoresTypedNilHTTPResponse(t *testing.T) {
// TestLogAuthResponse_HandlesNilSDKResponse verifies that a nil SDK response is handled without panicking.
func TestLogAuthResponse_HandlesNilSDKResponse(t *testing.T) {
var buf bytes.Buffer
authLogger := testAuthLogger(&buf, func() time.Time {
restore := keychain.SetAuthLogHooksForTest(log.New(&buf, "", 0), func() time.Time {
return time.Date(2026, 4, 2, 3, 4, 5, 0, time.UTC)
}, func() []string {
return []string{"lark-cli", "auth", "status", "--verify"}
})
t.Cleanup(restore)
logSDKResponse(authLogger, PathUserInfoV1, nil)
logSDKResponse(PathUserInfoV1, nil)
got := buf.String()
if !strings.Contains(got, "path="+PathUserInfoV1) {
@@ -165,13 +160,14 @@ func TestLogAuthResponse_HandlesNilSDKResponse(t *testing.T) {
func TestLogAuthError_RecordsStructuredEntry(t *testing.T) {
var buf bytes.Buffer
authLogger := testAuthLogger(&buf, func() time.Time {
restore := keychain.SetAuthLogHooksForTest(log.New(&buf, "", 0), func() time.Time {
return time.Date(2026, 4, 2, 3, 4, 5, 0, time.UTC)
}, func() []string {
return []string{"lark-cli", "auth", "login", "--device-code", "secret"}
})
t.Cleanup(restore)
authLogger.LogError("keychain", "Set", fmt.Errorf("keychain Set error: %w", http.ErrUseLastResponse))
keychain.LogAuthError("keychain", "Set", fmt.Errorf("keychain Set error: %w", http.ErrUseLastResponse))
got := buf.String()
if !strings.Contains(got, "auth-error") {
@@ -209,7 +205,7 @@ func TestPollDeviceToken_DefaultsZeroIntervalToFiveSeconds(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
t.Cleanup(cancel)
result := PollDeviceToken(ctx, client, "cli_a", "secret_b", brand.Feishu, "device-code", 0, 10, nil)
result := PollDeviceToken(ctx, client, "cli_a", "secret_b", core.BrandFeishu, "device-code", 0, 10, nil)
if result == nil {
t.Fatal("PollDeviceToken() returned nil result")
}

View File

@@ -11,12 +11,12 @@ import (
"net/url"
"strings"
brandpkg "github.com/larksuite/cli/brand"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/core"
)
// RevokeToken revokes a previously issued OAuth token.
func RevokeToken(httpClient *http.Client, appId, appSecret string, brand brandpkg.Brand, token, tokenTypeHint string) error {
func RevokeToken(httpClient *http.Client, appId, appSecret string, brand core.LarkBrand, token, tokenTypeHint string) error {
endpoints := ResolveOAuthEndpoints(brand)
form := url.Values{}
@@ -38,7 +38,7 @@ func RevokeToken(httpClient *http.Client, appId, appSecret string, brand brandpk
return errs.NewNetworkError(errs.SubtypeNetworkTransport, "token revoke transport error: %v", err).WithCause(err)
}
defer resp.Body.Close()
logHTTPResponse(newAuthLogger(), resp)
logHTTPResponse(resp)
body, err := io.ReadAll(resp.Body)
if err != nil {

View File

@@ -10,8 +10,8 @@ import (
"strings"
"testing"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/httpmock"
)
@@ -54,7 +54,7 @@ func TestRevokeToken_PostsExpectedForm(t *testing.T) {
}
reg.Register(stub)
err := RevokeToken(httpmock.NewClient(reg), "cli_a", "secret_b", brand.Feishu, "user-access-token", "access_token")
err := RevokeToken(httpmock.NewClient(reg), "cli_a", "secret_b", core.BrandFeishu, "user-access-token", "access_token")
if err != nil {
t.Fatalf("RevokeToken() error = %v", err)
}
@@ -71,7 +71,7 @@ func TestRevokeToken_DoFailureReturnsTypedNetworkError(t *testing.T) {
}),
}
err := RevokeToken(httpClient, "cli_a", "secret_b", brand.Feishu, "user-access-token", "access_token")
err := RevokeToken(httpClient, "cli_a", "secret_b", core.BrandFeishu, "user-access-token", "access_token")
if err == nil {
t.Fatal("expected error")
}
@@ -98,7 +98,7 @@ func TestRevokeToken_ReportsHTTPError(t *testing.T) {
Body: map[string]interface{}{"error": "invalid_token"},
})
err := RevokeToken(httpmock.NewClient(reg), "cli_a", "secret_b", brand.Feishu, "user-access-token", "access_token")
err := RevokeToken(httpmock.NewClient(reg), "cli_a", "secret_b", core.BrandFeishu, "user-access-token", "access_token")
if err == nil {
t.Fatal("expected error")
}
@@ -127,7 +127,7 @@ func TestRevokeToken_ReportsOAuthCodeErrorAsTypedAPIError(t *testing.T) {
},
})
err := RevokeToken(httpmock.NewClient(reg), "cli_a", "secret_b", brand.Feishu, "user-access-token", "access_token")
err := RevokeToken(httpmock.NewClient(reg), "cli_a", "secret_b", core.BrandFeishu, "user-access-token", "access_token")
if err == nil {
t.Fatal("expected error")
}
@@ -156,7 +156,7 @@ func TestRevokeToken_ReportsOAuthErrorFieldAsTypedAPIError(t *testing.T) {
},
})
err := RevokeToken(httpmock.NewClient(reg), "cli_a", "secret_b", brand.Feishu, "user-access-token", "access_token")
err := RevokeToken(httpmock.NewClient(reg), "cli_a", "secret_b", core.BrandFeishu, "user-access-token", "access_token")
if err == nil {
t.Fatal("expected error")
}
@@ -184,7 +184,7 @@ func TestRevokeToken_ReadFailureReturnsTypedInternalError(t *testing.T) {
}),
}
err := RevokeToken(httpClient, "cli_a", "secret_b", brand.Feishu, "user-access-token", "access_token")
err := RevokeToken(httpClient, "cli_a", "secret_b", core.BrandFeishu, "user-access-token", "access_token")
if err == nil {
t.Fatal("expected error")
}

View File

@@ -18,12 +18,10 @@ import (
"time"
"github.com/gofrs/flock"
"github.com/larksuite/cli/brand"
"github.com/larksuite/cli/errs"
configpkg "github.com/larksuite/cli/internal/config"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/errclass"
"github.com/larksuite/cli/internal/vfs"
"github.com/larksuite/cli/internal/workspace"
)
var safeIDChars = regexp.MustCompile(`[^a-zA-Z0-9._-]`)
@@ -38,7 +36,7 @@ type UATCallOptions struct {
UserOpenId string
AppId string
AppSecret string
Domain brand.Brand
Domain core.LarkBrand
ErrOut io.Writer // diagnostic/status output (caller injects f.IOStreams.ErrOut)
}
@@ -54,7 +52,7 @@ type UATStatus struct {
}
// NewUATCallOptions creates UATCallOptions from a CLI config.
func NewUATCallOptions(cfg *configpkg.CliConfig, errOut io.Writer) UATCallOptions {
func NewUATCallOptions(cfg *core.CliConfig, errOut io.Writer) UATCallOptions {
if errOut == nil {
errOut = os.Stderr
}
@@ -130,7 +128,7 @@ func refreshWithLock(httpClient *http.Client, opts UATCallOptions, stored *Store
// 2. Cross-process lock using flock
// We use the same underlying storage directory resolution as keychain_other.go
// to ensure locks are isolated properly alongside other sensitive data.
configDir := workspace.GetConfigDir()
configDir := core.GetConfigDir()
lockDir := filepath.Join(configDir, "locks")
if err := vfs.MkdirAll(lockDir, 0700); err != nil {
@@ -189,7 +187,6 @@ func doRefreshToken(httpClient *http.Client, opts UATCallOptions, stored *Stored
}
endpoints := ResolveOAuthEndpoints(opts.Domain)
authLogger := newAuthLogger()
callEndpoint := func() (map[string]interface{}, error) {
form := url.Values{}
@@ -209,7 +206,7 @@ func doRefreshToken(httpClient *http.Client, opts UATCallOptions, stored *Stored
return nil, err
}
defer resp.Body.Close()
logHTTPResponse(authLogger, resp)
logHTTPResponse(resp)
body, err := io.ReadAll(resp.Body)
if err != nil {

Some files were not shown because too many files have changed in this diff Show More