Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c0fef4b
feat: add strict mode identity filter, profile management and credent…
liangshuo-1 Apr 3, 2026
9bb60b1
chore: go mod tidy
liangshuo-1 Apr 3, 2026
d625180
fix: fix test failures from credential provider migration
liangshuo-1 Apr 3, 2026
2dec7e2
refactor: migrate remaining os calls to internal/vfs
tuxedomm Apr 3, 2026
cb3fe91
fix: resolve gofmt and goimports formatting issues
tuxedomm Apr 3, 2026
7914f70
feat: add Flag.Input support for @file and stdin input sources
liangshuo-1 Apr 4, 2026
0b26e33
fix: fix pre-existing test failures in task, minutes, and registry
liangshuo-1 Apr 4, 2026
107b8d1
fix: suppress nilerr lint for intentional nil returns
liangshuo-1 Apr 4, 2026
77fa6e6
fix: address CodeRabbit review feedback
liangshuo-1 Apr 4, 2026
fbc10e7
fix: tighten credential resolution and profile flows
liangshuo-1 Apr 5, 2026
7f7e64b
refactor: centralize identity hint resolution
liangshuo-1 Apr 5, 2026
faea09d
fix: surface unverified extension identities
liangshuo-1 Apr 5, 2026
b3bfd52
fix: honor runtime credential sources in config views
liangshuo-1 Apr 5, 2026
4f9db3a
fix: prefer runtime values in config show commands
liangshuo-1 Apr 5, 2026
7c4436f
Revert "fix: prefer runtime values in config show commands"
liangshuo-1 Apr 5, 2026
899e3d4
Revert "fix: honor runtime credential sources in config views"
liangshuo-1 Apr 5, 2026
aee70bc
fix: harden profile flows and credential boundaries
liangshuo-1 Apr 5, 2026
290be66
fix: optimize profile and config inspection for agents
liangshuo-1 Apr 5, 2026
7f8b203
refactor: unify credential env contracts
liangshuo-1 Apr 5, 2026
dcdd8fe
docs: expand AGENTS guidance
liangshuo-1 Apr 5, 2026
e981d19
fix: resolve regression bugs found during PR #252 review
liangshuo-1 Apr 6, 2026
e13d2a6
fix: remove flaky TestColdStart_UsesEmbedded (race in registry)
liangshuo-1 Apr 6, 2026
b202b18
merge: resolve conflict with origin/main for drive multipart upload
tuxedomm Apr 7, 2026
0866c94
refactor: type-strengthen Brand and DefaultAs across credential chain
liangshuo-1 Apr 7, 2026
5a09cbd
style: fix gofmt alignment in extension/credential/types.go
liangshuo-1 Apr 7, 2026
a8644a8
fix: remove file/stdin input support from task comment content flag
liangshuo-1 Apr 7, 2026
f9e3148
refactor(cmdutil): remove dead code autoDetectIdentity
tuxedomm Apr 7, 2026
dd1dcdc
refactor(cmdutil): add ctx parameter to resolveIdentityHint
tuxedomm Apr 7, 2026
db0d4f1
refactor(cmdutil): add ctx parameter to ResolveStrictMode
tuxedomm Apr 7, 2026
04ef0d1
refactor(cmdutil): add ctx parameter to CheckStrictMode
tuxedomm Apr 7, 2026
184bd03
refactor(cmdutil): add ctx parameter to ResolveAs
tuxedomm Apr 7, 2026
8f67f23
test: fix gofmt in cmdutil factory tests
tuxedomm Apr 7, 2026
5583909
fix: remove file/stdin input support from im send/reply and drive com…
liangshuo-1 Apr 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ linters:
- reassign # checks that package variables are not reassigned
- unconvert # removes unnecessary type conversions
- unused # checks for unused constants, variables, functions and types
- forbidigo # forbids specific function calls

# To enable later after fixing existing issues:
# - errcheck # checks for unchecked errors
Expand All @@ -44,8 +45,89 @@ linters:
linters:
- bodyclose
- gocritic
- forbidigo
- path-except: (shortcuts/|internal/)
linters:
- forbidigo
- path: internal/vfs/
linters:
- forbidigo

settings:
forbidigo:
forbid:
# ── Filesystem operations: use internal/vfs instead ──
- pattern: os\.Stat\b
msg: "use vfs.Stat() from internal/vfs"
- pattern: os\.Lstat\b
msg: "use vfs.Lstat() from internal/vfs"
- pattern: os\.Open\b
msg: "use vfs.Open() from internal/vfs"
- pattern: os\.OpenFile\b
msg: "use vfs.OpenFile() from internal/vfs"
- pattern: os\.Create\b
msg: "use vfs.OpenFile() from internal/vfs"
- pattern: os\.CreateTemp\b
msg: >-
internal/: use vfs.CreateTemp() from internal/vfs.
shortcuts/: avoid temp files entirely — use io.Reader streaming or in-memory buffers instead.
- pattern: os\.Mkdir\b
msg: "use vfs.MkdirAll() from internal/vfs"
- pattern: os\.MkdirAll\b
msg: "use vfs.MkdirAll() from internal/vfs"
- pattern: os\.Remove\b
msg: >-
internal/: use vfs.Remove() from internal/vfs.
shortcuts/: avoid temp files entirely — use io.Reader streaming or in-memory buffers instead.
- pattern: os\.RemoveAll\b
msg: >-
internal/: add RemoveAll to internal/vfs/fs.go first, then use vfs.RemoveAll().
shortcuts/: avoid temp files entirely — use io.Reader streaming or in-memory buffers instead.
- pattern: os\.Rename\b
msg: "use vfs.Rename() from internal/vfs"
- pattern: os\.ReadFile\b
msg: "use vfs.ReadFile() from internal/vfs"
- pattern: os\.WriteFile\b
msg: "use vfs.WriteFile() from internal/vfs"
- pattern: os\.ReadDir\b
msg: "add ReadDir to internal/vfs/fs.go first, then use vfs.ReadDir()"
- pattern: os\.Getwd\b
msg: "use vfs.Getwd() from internal/vfs"
- pattern: os\.Chdir\b
msg: "add Chdir to internal/vfs/fs.go first, then use vfs.Chdir()"
- pattern: os\.UserHomeDir\b
msg: "use vfs.UserHomeDir() from internal/vfs"
- pattern: os\.Chmod\b
msg: "add Chmod to internal/vfs/fs.go first, then use vfs.Chmod()"
- pattern: os\.Chown\b
msg: "add Chown to internal/vfs/fs.go first, then use vfs.Chown()"
- pattern: os\.Lchown\b
msg: "add Lchown to internal/vfs/fs.go first, then use vfs.Lchown()"
- pattern: os\.Link\b
msg: "add Link to internal/vfs/fs.go first, then use vfs.Link()"
- pattern: os\.Symlink\b
msg: "add Symlink to internal/vfs/fs.go first, then use vfs.Symlink()"
- pattern: os\.Readlink\b
msg: "add Readlink to internal/vfs/fs.go first, then use vfs.Readlink()"
- pattern: os\.Truncate\b
msg: "add Truncate to internal/vfs/fs.go first, then use vfs.Truncate()"
- pattern: os\.DirFS\b
msg: "add DirFS to internal/vfs/fs.go first, then use vfs.DirFS()"
- pattern: os\.SameFile\b
msg: "add SameFile to internal/vfs/fs.go first, then use vfs.SameFile()"
# ── IO streams: use IOStreams from cmdutil instead ──
- pattern: os\.Stdin\b
msg: "use IOStreams.In instead of os.Stdin"
- pattern: os\.Stdout\b
msg: "use IOStreams.Out instead of os.Stdout"
- pattern: os\.Stderr\b
msg: "use IOStreams.ErrOut instead of os.Stderr"
# ── Process-level rules ──
- pattern: os\.Exit\b
msg: >-
Do not use os.Exit in shortcuts/. Return an error instead and let
the caller (cmd layer) decide how to terminate.
analyze-types: true
gocritic:
disabled-checks:
- appendAssign
Expand Down
85 changes: 65 additions & 20 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,78 @@
# AGENTS.md
Concise maintainer/developer guide for building, testing, and opening high-quality PRs in this repo.

## Goal (pick one per PR)

- Make CLI better: improve UX, error messages, help text, flags, and output clarity.
- Improve reliability: fix bugs, edge cases, and regressions with tests.
- Improve developer velocity: simplify code paths, reduce complexity, keep behavior explicit.
- Improve quality gates: strengthen tests/lint/checks without adding heavy process.

## Fast Dev Loop
1. `make build` (runs `python3 scripts/fetch_meta.py` first)
2. `make unit-test` (required before PR)
3. Run changed command(s) manually via `./lark-cli ...`
## Build & Test

```bash
make build # Build (runs fetch_meta first)
make unit-test # Required before PR (runs with -race)
make test # Full: vet + unit + integration
```

## Pre-PR Checks (match CI gates)

1. `make unit-test`
2. `go mod tidy` (must not change `go.mod`/`go.sum`)
2. `go mod tidy` must not change `go.mod`/`go.sum`
3. `go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 run --new-from-rev=origin/main`
4. If dependencies changed: `go run github.com/google/go-licenses/v2@v2.0.1 check ./... --disallowed_types=forbidden,restricted,reciprocal,unknown`
5. Optional full local suite: `make test` (vet + unit + integration)

## Test/Check Commands
- Unit: `make unit-test`
- Integration: `make integration-test`
- Full: `make test`
- Vet only: `make vet`
- Coverage (local): `go test -race -coverprofile=coverage.txt -covermode=atomic ./...`

## Commit/PR Rules
- Use Conventional Commits in English: `feat: ...`, `fix: ...`, `docs: ...`, `ci: ...`, `test: ...`, `chore: ...`, `refactor: ...`
- Keep PR title in the same Conventional Commit format (squash merge keeps it).
- Before opening a real PR, draft/fill description from `.github/pull_request_template.md` and ensure Summary/Changes/Test Plan are complete.
- Never commit secrets/tokens/internal sensitive data.

## Commit & PR

- Conventional Commits in English: `feat:`, `fix:`, `docs:`, `test:`, `refactor:`, `chore:`, `ci:`
- PR title in the same format. Fill `.github/pull_request_template.md` completely.
- Never commit secrets, tokens, or internal sensitive data.

## Source Layout

| Path | What it does |
|------|-------------|
| `cmd/root.go` | Entry point, command registration, strict mode pruning |
| `cmd/profile/` | Multi-profile management (add/list/use/rename/remove) |
| `cmd/config/` | Config init, show, strict-mode |
| `cmd/service/` | Auto-registered API commands from embedded metadata |
| `shortcuts/common/runner.go` | Shortcut execution pipeline, Flag.Input (@file/stdin) resolution |
| `shortcuts/` | Domain-specific shortcut implementations |
| `internal/cmdutil/factory.go` | Factory pattern — identity resolution, credential, config |
| `internal/cmdutil/factory_default.go` | Production factory wiring |
| `internal/credential/` | Credential provider chain (extension → default) |
| `extension/credential/` | Plugin-facing credential interfaces and env provider |
| `internal/client/client.go` | APIClient: DoSDKRequest, DoStream |
| `internal/core/config.go` | Multi-profile config loading/saving |
| `internal/vfs/` | Filesystem abstraction (use `vfs.*` instead of `os.*`) |
| `internal/validate/path.go` | Path safety validation |

## 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.

The one rule to internalize: **every error message you write will be parsed by an AI to decide its next action.** Make errors structured, actionable, and specific.

## Code Conventions

### Structured errors in commands

`RunE` functions must return `output.Errorf` / `output.ErrWithHint` — never bare `fmt.Errorf`. AI agents parse stderr as JSON; bare errors break this contract.

### stdout is data, stderr is everything else

Program output (JSON envelopes) goes to stdout. Progress, warnings, hints go to stderr. Mixing them corrupts pipe chains.

### Use `vfs.*` instead of `os.*`

All filesystem access goes through `internal/vfs`. This enables test mocking.

### Validate paths before reading

CLI arguments are untrusted (they come from AI agents). Call `validate.SafeInputPath` before any file I/O.

### Tests

- Every behavior change needs a test alongside the change.
- `cmdutil.TestFactory(t, config)` for test factories.
- `t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())` to isolate config state.
8 changes: 6 additions & 2 deletions cmd/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,11 @@ func buildAPIRequest(opts *APIOptions) (client.RawApiRequest, error) {

func apiRun(opts *APIOptions) error {
f := opts.Factory
opts.As = f.ResolveAs(opts.Cmd, opts.As)
opts.As = f.ResolveAs(opts.Ctx, opts.Cmd, opts.As)

if err := f.CheckStrictMode(opts.Ctx, opts.As); err != nil {
return err
}

if opts.PageAll && opts.Output != "" {
return output.ErrValidation("--output and --page-all are mutually exclusive")
Expand All @@ -166,7 +170,7 @@ func apiRun(opts *APIOptions) error {
return err
}

config, err := f.ResolveConfig(opts.As)
config, err := f.Config()
if err != nil {
return err
}
Expand Down
75 changes: 0 additions & 75 deletions cmd/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,6 @@ func TestApiCmd_BotMode(t *testing.T) {
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
})

// Register tenant_access_token stub
reg.Register(&httpmock.Stub{
URL: "/open-apis/auth/v3/tenant_access_token/internal",
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
"tenant_access_token": "t-test-token",
"expire": 7200,
},
})
// Register API endpoint stub
reg.Register(&httpmock.Stub{
URL: "/open-apis/test",
Expand Down Expand Up @@ -234,13 +224,6 @@ func TestApiCmd_BinaryResponse_AutoSave(t *testing.T) {
AppID: "test-app-bin", AppSecret: "test-secret-bin", Brand: core.BrandFeishu,
})

reg.Register(&httpmock.Stub{
URL: "/open-apis/auth/v3/tenant_access_token/internal",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"tenant_access_token": "t-test-token-bin", "expire": 7200,
},
})
reg.Register(&httpmock.Stub{
URL: "/open-apis/drive/v1/files/xxx/download",
RawBody: []byte("fake-binary-content"),
Expand All @@ -266,14 +249,6 @@ func TestApiCmd_PageAll_NonBatchAPI_FallbackToJSON(t *testing.T) {
AppID: "test-app-pageall1", AppSecret: "test-secret-pageall1", Brand: core.BrandFeishu,
})

// Register tenant_access_token stub
reg.Register(&httpmock.Stub{
URL: "/open-apis/auth/v3/tenant_access_token/internal",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"tenant_access_token": "t-test-token-pa1", "expire": 7200,
},
})
// Register a non-batch API that returns scalar data (no array field)
reg.Register(&httpmock.Stub{
URL: "/open-apis/contact/v3/users/u123",
Expand Down Expand Up @@ -310,13 +285,6 @@ func TestApiCmd_PageAll_NonBatchAPI_ErrorStillOutputsJSON(t *testing.T) {
AppID: "test-app-pageall-err", AppSecret: "test-secret-pageall-err", Brand: core.BrandFeishu,
})

reg.Register(&httpmock.Stub{
URL: "/open-apis/auth/v3/tenant_access_token/internal",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"tenant_access_token": "t-test-token-err", "expire": 7200,
},
})
// Non-batch API that returns a business error (code != 0)
reg.Register(&httpmock.Stub{
URL: "/open-apis/im/v1/chats/oc_xxx/announcement",
Expand Down Expand Up @@ -346,14 +314,6 @@ func TestApiCmd_PageAll_BatchAPI_StreamsItems(t *testing.T) {
AppID: "test-app-pageall2", AppSecret: "test-secret-pageall2", Brand: core.BrandFeishu,
})

// Register tenant_access_token stub (unique app credentials => new token request)
reg.Register(&httpmock.Stub{
URL: "/open-apis/auth/v3/tenant_access_token/internal",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"tenant_access_token": "t-test-token-pa2", "expire": 7200,
},
})
// Register a batch API that returns an array field
reg.Register(&httpmock.Stub{
URL: "/open-apis/contact/v3/users",
Expand Down Expand Up @@ -409,13 +369,6 @@ func TestApiCmd_APIError_IsRaw(t *testing.T) {
AppID: "test-app-raw", AppSecret: "test-secret-raw", Brand: core.BrandFeishu,
})

reg.Register(&httpmock.Stub{
URL: "/open-apis/auth/v3/tenant_access_token/internal",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"tenant_access_token": "t-test-token-raw", "expire": 7200,
},
})
// Return a permission error from the API
reg.Register(&httpmock.Stub{
URL: "/open-apis/test/perm",
Expand Down Expand Up @@ -456,13 +409,6 @@ func TestApiCmd_APIError_PreservesOriginalMessage(t *testing.T) {
AppID: "test-app-origmsg", AppSecret: "test-secret-origmsg", Brand: core.BrandFeishu,
})

reg.Register(&httpmock.Stub{
URL: "/open-apis/auth/v3/tenant_access_token/internal",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"tenant_access_token": "t-test-token-origmsg", "expire": 7200,
},
})
reg.Register(&httpmock.Stub{
URL: "/open-apis/test/origmsg",
Body: map[string]interface{}{
Expand Down Expand Up @@ -505,13 +451,6 @@ func TestApiCmd_PageAll_APIError_IsRaw(t *testing.T) {
AppID: "test-app-rawpage", AppSecret: "test-secret-rawpage", Brand: core.BrandFeishu,
})

reg.Register(&httpmock.Stub{
URL: "/open-apis/auth/v3/tenant_access_token/internal",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"tenant_access_token": "t-test-token-rawpage", "expire": 7200,
},
})
reg.Register(&httpmock.Stub{
URL: "/open-apis/test/rawpage",
Body: map[string]interface{}{
Expand Down Expand Up @@ -599,13 +538,6 @@ func TestApiCmd_JqFilter_AppliesExpression(t *testing.T) {
AppID: "test-app-jq", AppSecret: "test-secret-jq", Brand: core.BrandFeishu,
})

reg.Register(&httpmock.Stub{
URL: "/open-apis/auth/v3/tenant_access_token/internal",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"tenant_access_token": "t-test-token-jq", "expire": 7200,
},
})
reg.Register(&httpmock.Stub{
URL: "/open-apis/test/jq",
Body: map[string]interface{}{
Expand Down Expand Up @@ -676,13 +608,6 @@ func TestApiCmd_PageAll_WithJq(t *testing.T) {
AppID: "test-app-pjq", AppSecret: "test-secret-pjq", Brand: core.BrandFeishu,
})

reg.Register(&httpmock.Stub{
URL: "/open-apis/auth/v3/tenant_access_token/internal",
Body: map[string]interface{}{
"code": 0, "msg": "ok",
"tenant_access_token": "t-test-token-pjq", "expire": 7200,
},
})
reg.Register(&httpmock.Stub{
URL: "/open-apis/contact/v3/users",
Body: map[string]interface{}{
Expand Down
4 changes: 2 additions & 2 deletions cmd/auth/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ func authListRun(opts *ListOptions) error {
return nil
}

app := multi.Apps[0]
if len(app.Users) == 0 {
app := multi.CurrentAppConfig(f.Invocation.Profile)
if app == nil || len(app.Users) == 0 {
fmt.Fprintln(f.IOStreams.ErrOut, "No logged-in users. Run `lark-cli auth login` to log in.")
return nil
}
Expand Down
Loading
Loading