Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
511e534
feat(event): add event subscription & consume system with orphan bus …
liuxinyanglxy Apr 24, 2026
429fe78
fix(event): address CodeRabbit findings + lift patch coverage above 60%
liuxinyanglxy Apr 24, 2026
85b95a3
test(event): cover format helpers + cobra factories
liuxinyanglxy Apr 24, 2026
85f19d4
fix(event): address lint + deadcode CI failures
liuxinyanglxy Apr 24, 2026
972a0ab
fix(event): drop stale enum + simplify protocol test type helper
liuxinyanglxy Apr 24, 2026
dab5db2
docs(event): polish skill docs + rename root_path_hint to jq_root_path
liuxinyanglxy Apr 24, 2026
9ee5e5f
chore(event): strip excessive comments
liuxinyanglxy Apr 25, 2026
e99bd1b
fix(event): dedup self-eviction race + protocol oversized-frame test
liuxinyanglxy Apr 25, 2026
7e864d2
docs(event): clarify .content shape per message_type + add sender fil…
liuxinyanglxy Apr 25, 2026
1c28946
fix(event): replace cmdline-regex bus discovery with PID file + close…
liuxinyanglxy Apr 25, 2026
8180fd9
style(event): gofmt bus.go
liuxinyanglxy Apr 25, 2026
52c5804
fix(event): swap os.Remove/Rename for vfs.* and silence forbidigo on …
liuxinyanglxy Apr 25, 2026
ad84fee
fix(event): cross-platform --all + clean SIGPIPE shutdown for consume
liuxinyanglxy Apr 27, 2026
6836805
docs(event): translate IM EventKey descriptions and field tags to Eng…
liuxinyanglxy Apr 27, 2026
85daedb
docs(event): drop misleading AuthTypes[0] auto-default claim
liuxinyanglxy Apr 27, 2026
32665b8
revert(comments): restore original comments on 3rd-party files
liuxinyanglxy Apr 27, 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
3 changes: 3 additions & 0 deletions cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import (
"github.com/larksuite/cli/cmd/completion"
cmdconfig "github.com/larksuite/cli/cmd/config"
"github.com/larksuite/cli/cmd/doctor"
cmdevent "github.com/larksuite/cli/cmd/event"
"github.com/larksuite/cli/cmd/profile"
"github.com/larksuite/cli/cmd/schema"
"github.com/larksuite/cli/cmd/service"
cmdupdate "github.com/larksuite/cli/cmd/update"
_ "github.com/larksuite/cli/events"
"github.com/larksuite/cli/internal/build"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/keychain"
Expand Down Expand Up @@ -117,6 +119,7 @@ func buildInternal(ctx context.Context, inv cmdutil.InvocationContext, opts ...B
rootCmd.AddCommand(schema.NewCmdSchema(f, nil))
rootCmd.AddCommand(completion.NewCmdCompletion(f))
rootCmd.AddCommand(cmdupdate.NewCmdUpdate(f))
rootCmd.AddCommand(cmdevent.NewCmdEvents(f))
service.RegisterServiceCommandsWithContext(ctx, rootCmd, f)
shortcuts.RegisterShortcutsWithContext(ctx, rootCmd, f)

Expand Down
25 changes: 25 additions & 0 deletions cmd/event/appmeta_err.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package event

import (
"fmt"
"regexp"
)

// authURLPattern matches the grant-scope URL embedded in 99991672 errors; widen when adding brands in consoleScopeGrantURL.
var authURLPattern = regexp.MustCompile(`https?://open\.(?:feishu\.cn|larksuite\.com)/app/[^/\s"']+/auth\?q=[^\s"'<>]+`)

// describeAppMetaErr reduces a FetchCurrentPublished error to a one-line stderr summary.
func describeAppMetaErr(err error) string {
msg := err.Error()
if url := authURLPattern.FindString(msg); url != "" {
return fmt.Sprintf("bot is missing scopes needed for app-version metadata; grant at: %s", url)
}
const maxErrLen = 200
if len(msg) > maxErrLen {
return msg[:maxErrLen] + "…"
}
return msg
}
54 changes: 54 additions & 0 deletions cmd/event/appmeta_err_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package event

import (
"errors"
"strings"
"testing"
)

const realisticPermError = `API GET /open-apis/application/v6/applications/cli_XXXXXXXXXXXXXXXX/app_versions?lang=zh_cn&page_size=2 returned 400: {"code":99991672,"msg":"Access denied. One of the following scopes is required: [application:application:self_manage, application:application.app_version:readonly].应用尚未开通所需的应用身份权限:[application:application:self_manage, application:application.app_version:readonly],点击链接申请并开通任一权限即可:https://open.feishu.cn/app/cli_XXXXXXXXXXXXXXXX/auth?q=application:application:self_manage,application:application.app_version:readonly&op_from=openapi&token_type=tenant","error":{"message":"Refer to the documentation...","log_id":"20260421101203E2A5F141245B6F43B3A6"}}`

func TestDescribeAppMetaErr_PermissionDeniedShort(t *testing.T) {
got := describeAppMetaErr(errors.New(realisticPermError))
if len(got) > 400 {
t.Errorf("summary too long (%d chars): %q", len(got), got)
}
if !strings.Contains(got, "scope") {
t.Errorf("summary should mention scope requirement, got: %q", got)
}
wantURL := "https://open.feishu.cn/app/cli_XXXXXXXXXXXXXXXX/auth?q=application:application:self_manage,application:application.app_version:readonly&op_from=openapi&token_type=tenant"
if !strings.Contains(got, wantURL) {
t.Errorf("summary missing grant URL\ngot: %q\nwant: %q", got, wantURL)
}
for _, noise := range []string{"log_id", `"error":`, "Refer to the documentation"} {
if strings.Contains(got, noise) {
t.Errorf("summary leaked noise %q: %q", noise, got)
}
}
}

func TestDescribeAppMetaErr_UnknownErrorTruncated(t *testing.T) {
long := strings.Repeat("x", 500)
got := describeAppMetaErr(errors.New(long))
if len(got) > 220 {
t.Errorf("unknown error not truncated, len=%d", len(got))
}
}

func TestDescribeAppMetaErr_ShortErrorPassesThrough(t *testing.T) {
got := describeAppMetaErr(errors.New("network unreachable"))
if got != "network unreachable" {
t.Errorf("short err should pass through unchanged, got: %q", got)
}
}

func TestDescribeAppMetaErr_LarkOfficeDomain(t *testing.T) {
msg := `... grant link: https://open.larksuite.com/app/cli_xyz/auth?q=application:application:self_manage&op_from=openapi&token_type=tenant ...`
got := describeAppMetaErr(errors.New(msg))
if !strings.Contains(got, "open.larksuite.com") {
t.Errorf("want larksuite URL extracted, got: %q", got)
}
}
69 changes: 69 additions & 0 deletions cmd/event/bus.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package event

import (
"context"
"os"
"os/signal"
"path/filepath"
"syscall"

"github.com/spf13/cobra"

"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"
)

// NewCmdBus creates the hidden `event _bus` daemon subcommand, forked by the consume client; fork argv lives in consume/startup.go.
func NewCmdBus(f *cmdutil.Factory) *cobra.Command {
var domain string

cmd := &cobra.Command{
Use: "_bus",
Short: "Internal event bus daemon (do not call directly)",
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := f.Config()
if err != nil {
return err

Check warning on line 33 in cmd/event/bus.go

View check run for this annotation

Codecov / codecov/patch

cmd/event/bus.go#L31-L33

Added lines #L31 - L33 were not covered by tests
}

// Sanitize AppID: an unsanitized value could escape events/ via ".." or separators.
eventsDir := filepath.Join(core.GetConfigDir(), "events", event.SanitizeAppID(cfg.AppID))

Check warning on line 37 in cmd/event/bus.go

View check run for this annotation

Codecov / codecov/patch

cmd/event/bus.go#L37

Added line #L37 was not covered by tests

logger, err := bus.SetupBusLogger(eventsDir)
if err != nil {
return err

Check warning on line 41 in cmd/event/bus.go

View check run for this annotation

Codecov / codecov/patch

cmd/event/bus.go#L39-L41

Added lines #L39 - L41 were not covered by tests
}

tr := transport.New()
b := bus.NewBus(cfg.AppID, cfg.AppSecret, domain, tr, logger)

Check warning on line 45 in cmd/event/bus.go

View check run for this annotation

Codecov / codecov/patch

cmd/event/bus.go#L44-L45

Added lines #L44 - L45 were not covered by tests

ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()

Check warning on line 48 in cmd/event/bus.go

View check run for this annotation

Codecov / codecov/patch

cmd/event/bus.go#L47-L48

Added lines #L47 - L48 were not covered by tests

sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
defer signal.Stop(sigCh)
go func() {
select {
case <-sigCh:
cancel()
case <-ctx.Done():

Check warning on line 57 in cmd/event/bus.go

View check run for this annotation

Codecov / codecov/patch

cmd/event/bus.go#L50-L57

Added lines #L50 - L57 were not covered by tests
}
}()

return b.Run(ctx)

Check warning on line 61 in cmd/event/bus.go

View check run for this annotation

Codecov / codecov/patch

cmd/event/bus.go#L61

Added line #L61 was not covered by tests
},
}

cmd.Flags().StringVar(&domain, "domain", "", "API domain")
_ = cmd.Flags().MarkHidden("domain")

return cmd
}
24 changes: 24 additions & 0 deletions cmd/event/console_url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package event

import (
"fmt"
"strings"

"github.com/larksuite/cli/internal/core"
)

// consoleScopeGrantURL builds the developer-console "apply & grant scopes" deep link; scopes are comma-joined without URL encoding.
func consoleScopeGrantURL(brand core.LarkBrand, appID string, scopes []string) string {
host := core.ResolveEndpoints(brand).Open
return fmt.Sprintf("%s/app/%s/auth?q=%s&op_from=openapi&token_type=tenant",
host, appID, strings.Join(scopes, ","))
}

// consoleEventSubscriptionURL points at the app's event subscription console page.
func consoleEventSubscriptionURL(brand core.LarkBrand, appID string) string {
host := core.ResolveEndpoints(brand).Open
return fmt.Sprintf("%s/app/%s/event", host, appID)
}
36 changes: 36 additions & 0 deletions cmd/event/console_url_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package event

import (
"testing"

"github.com/larksuite/cli/internal/core"
)

func TestConsoleScopeGrantURL_Feishu(t *testing.T) {
got := consoleScopeGrantURL(core.BrandFeishu, "cli_XXXXXXXXXXXXXXXX", []string{
"im:message:readonly",
"im:message.group_at_msg",
})
want := "https://open.feishu.cn/app/cli_XXXXXXXXXXXXXXXX/auth?q=im:message:readonly,im:message.group_at_msg&op_from=openapi&token_type=tenant"
if got != want {
t.Errorf("url\n got: %s\nwant: %s", got, want)
}
}

func TestConsoleScopeGrantURL_LarkBrand(t *testing.T) {
got := consoleScopeGrantURL(core.BrandLark, "cli_x", []string{"im:message"})
want := "https://open.larksuite.com/app/cli_x/auth?q=im:message&op_from=openapi&token_type=tenant"
if got != want {
t.Errorf("url\n got: %s\nwant: %s", got, want)
}
}

func TestConsoleScopeGrantURL_EmptyBrandDefaultsFeishu(t *testing.T) {
got := consoleScopeGrantURL("", "cli_x", []string{"im:message"})
if got != "https://open.feishu.cn/app/cli_x/auth?q=im:message&op_from=openapi&token_type=tenant" {
t.Errorf("unexpected url: %s", got)
}
}
Loading
Loading