-
Notifications
You must be signed in to change notification settings - Fork 581
refactor: split Execute into Build + Execute with explicit IO and keychain injection #371
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
fe461d9
refactor(cmd): split Execute into Build with IO/Keychain injection
tuxedomm 719bc63
feat(schema): filter methods by strict mode in schema output
tuxedomm 2a1602c
refactor: centralize strict-mode as flag registration
tuxedomm bdd5b55
fix(cmd): align strict-mode completion and build context; drop dead r…
tuxedomm a8455db
refactor(cmd): hide --profile in single-app mode via build option
tuxedomm fb1bc4d
feat(transport): extension abort hook and shared base transport
tuxedomm 06f552a
fix: unblock Build() zero-opts path and sidecar demo build
tuxedomm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| // Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package cmd | ||
|
|
||
| import ( | ||
| "context" | ||
| "io" | ||
|
|
||
| "github.com/larksuite/cli/cmd/api" | ||
| "github.com/larksuite/cli/cmd/auth" | ||
| "github.com/larksuite/cli/cmd/completion" | ||
| cmdconfig "github.com/larksuite/cli/cmd/config" | ||
| "github.com/larksuite/cli/cmd/doctor" | ||
| "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/internal/build" | ||
| "github.com/larksuite/cli/internal/cmdutil" | ||
| "github.com/larksuite/cli/internal/keychain" | ||
| "github.com/larksuite/cli/shortcuts" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| // BuildOption configures optional aspects of the command tree construction. | ||
| type BuildOption func(*buildConfig) | ||
|
|
||
| type buildConfig struct { | ||
| streams *cmdutil.IOStreams | ||
| keychain keychain.KeychainAccess | ||
| globals GlobalOptions | ||
| } | ||
|
|
||
| // WithIO sets the IO streams for the CLI by wrapping raw reader/writers. | ||
| // Terminal detection is delegated to cmdutil.NewIOStreams. | ||
| func WithIO(in io.Reader, out, errOut io.Writer) BuildOption { | ||
| return func(c *buildConfig) { | ||
| c.streams = cmdutil.NewIOStreams(in, out, errOut) | ||
| } | ||
| } | ||
|
|
||
| // WithKeychain sets the secret storage backend. If not provided, the platform keychain is used. | ||
| func WithKeychain(kc keychain.KeychainAccess) BuildOption { | ||
| return func(c *buildConfig) { | ||
| c.keychain = kc | ||
| } | ||
| } | ||
|
|
||
| // HideProfile sets the visibility policy for the root-level --profile flag. | ||
| // When hide is true the flag stays registered (so existing invocations still | ||
| // parse) but is omitted from help and shell completion. Typically called as | ||
| // HideProfile(isSingleAppMode()). | ||
| func HideProfile(hide bool) BuildOption { | ||
| return func(c *buildConfig) { | ||
| c.globals.HideProfile = hide | ||
| } | ||
| } | ||
|
|
||
| // Build constructs the full command tree without executing. | ||
| // Returns only the cobra.Command; Factory is internal. | ||
| // Use Execute for the standard production entry point. | ||
| func Build(ctx context.Context, inv cmdutil.InvocationContext, opts ...BuildOption) *cobra.Command { | ||
| _, rootCmd := buildInternal(ctx, inv, opts...) | ||
| return rootCmd | ||
| } | ||
|
|
||
| // buildInternal is a pure assembly function: it wires the command tree from | ||
| // inv and BuildOptions alone. Any state-dependent decision (disk, network, | ||
| // env) belongs in the caller and must be threaded in via BuildOption. | ||
| func buildInternal(ctx context.Context, inv cmdutil.InvocationContext, opts ...BuildOption) (*cmdutil.Factory, *cobra.Command) { | ||
| // cfg.globals.Profile is left zero here; it's bound to the --profile | ||
| // flag in RegisterGlobalFlags and filled by cobra's parse step. | ||
| cfg := &buildConfig{} | ||
| for _, o := range opts { | ||
| if o != nil { | ||
| o(cfg) | ||
| } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| // Default streams when WithIO is not supplied so the root command's | ||
| // SetIn/Out/Err calls below don't deref nil. NewDefault also normalizes | ||
| // partial streams internally; keep both in sync so cfg.streams reflects | ||
| // the same values the Factory ends up using. | ||
| if cfg.streams == nil { | ||
| cfg.streams = cmdutil.SystemIO() | ||
| } | ||
|
|
||
| f := cmdutil.NewDefault(cfg.streams, inv) | ||
| if cfg.keychain != nil { | ||
| f.Keychain = cfg.keychain | ||
| } | ||
| rootCmd := &cobra.Command{ | ||
| Use: "lark-cli", | ||
| Short: "Lark/Feishu CLI — OAuth authorization, UAT management, API calls", | ||
| Long: rootLong, | ||
| Version: build.Version, | ||
| } | ||
|
|
||
| rootCmd.SetContext(ctx) | ||
| rootCmd.SetIn(cfg.streams.In) | ||
| rootCmd.SetOut(cfg.streams.Out) | ||
| rootCmd.SetErr(cfg.streams.ErrOut) | ||
|
|
||
| installTipsHelpFunc(rootCmd) | ||
| rootCmd.SilenceErrors = true | ||
|
|
||
| RegisterGlobalFlags(rootCmd.PersistentFlags(), &cfg.globals) | ||
| rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { | ||
| cmd.SilenceUsage = true | ||
| } | ||
|
|
||
| rootCmd.AddCommand(cmdconfig.NewCmdConfig(f)) | ||
| rootCmd.AddCommand(auth.NewCmdAuth(f)) | ||
| rootCmd.AddCommand(profile.NewCmdProfile(f)) | ||
| rootCmd.AddCommand(doctor.NewCmdDoctor(f)) | ||
| rootCmd.AddCommand(api.NewCmdApiWithContext(ctx, f, nil)) | ||
| rootCmd.AddCommand(schema.NewCmdSchema(f, nil)) | ||
| rootCmd.AddCommand(completion.NewCmdCompletion(f)) | ||
| rootCmd.AddCommand(cmdupdate.NewCmdUpdate(f)) | ||
| service.RegisterServiceCommandsWithContext(ctx, rootCmd, f) | ||
| shortcuts.RegisterShortcutsWithContext(ctx, rootCmd, f) | ||
|
|
||
| // Prune commands incompatible with strict mode. | ||
| if mode := f.ResolveStrictMode(ctx); mode.IsActive() { | ||
| pruneForStrictMode(rootCmd, mode) | ||
| } | ||
|
|
||
| return f, rootCmd | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| // Copyright (c) 2026 Lark Technologies Pte. Ltd. | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| package cmd | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "testing" | ||
|
|
||
| "github.com/larksuite/cli/internal/cmdutil" | ||
| "github.com/larksuite/cli/internal/vfs" | ||
| ) | ||
|
|
||
| // noopKeychain is a zero-side-effect KeychainAccess for exercising | ||
| // WithKeychain without touching the platform keychain. | ||
| type noopKeychain struct{} | ||
|
|
||
| func (noopKeychain) Get(service, account string) (string, error) { return "", nil } | ||
| func (noopKeychain) Set(service, account, value string) error { return nil } | ||
| func (noopKeychain) Remove(service, account string) error { return nil } | ||
|
|
||
| // TestBuild_ExternalAPI asserts the library surface that external consumers | ||
| // (e.g. cli-server) depend on: Build composes a root command from an | ||
| // InvocationContext plus BuildOptions (WithIO, WithKeychain, HideProfile), | ||
| // and SetDefaultFS swaps the global VFS. This test is the contract guard. | ||
| func TestBuild_ExternalAPI(t *testing.T) { | ||
| // Exercise SetDefaultFS both directions. Passing nil restores the OS FS. | ||
| SetDefaultFS(vfs.OsFs{}) | ||
| SetDefaultFS(nil) | ||
|
|
||
| var in, out, errOut bytes.Buffer | ||
| rootCmd := Build( | ||
| context.Background(), | ||
| cmdutil.InvocationContext{}, | ||
| WithIO(&in, &out, &errOut), | ||
| WithKeychain(noopKeychain{}), | ||
| HideProfile(true), | ||
| ) | ||
|
|
||
| if rootCmd == nil { | ||
| t.Fatal("Build returned nil root command") | ||
| } | ||
| if rootCmd.Use != "lark-cli" { | ||
| t.Errorf("rootCmd.Use = %q, want %q", rootCmd.Use, "lark-cli") | ||
| } | ||
| if len(rootCmd.Commands()) == 0 { | ||
| t.Error("Build produced a root command with no subcommands") | ||
| } | ||
| } | ||
|
|
||
| // TestBuild_NoOptions guards against regression of the nil-streams panic: | ||
| // calling Build without WithIO must fall back to SystemIO rather than | ||
| // deref nil at rootCmd.SetIn/Out/Err. | ||
| func TestBuild_NoOptions(t *testing.T) { | ||
| rootCmd := Build(context.Background(), cmdutil.InvocationContext{}) | ||
| if rootCmd == nil { | ||
| t.Fatal("Build returned nil root command") | ||
| } | ||
| if rootCmd.Use != "lark-cli" { | ||
| t.Errorf("rootCmd.Use = %q, want %q", rootCmd.Use, "lark-cli") | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.