Skip to content

refactor: split Execute into Build + Execute with explicit IO and keychain injection#371

Merged
liangshuo-1 merged 7 commits intomainfrom
refactor/build-execute-split
Apr 21, 2026
Merged

refactor: split Execute into Build + Execute with explicit IO and keychain injection#371
liangshuo-1 merged 7 commits intomainfrom
refactor/build-execute-split

Conversation

@tuxedomm
Copy link
Copy Markdown
Collaborator

@tuxedomm tuxedomm commented Apr 9, 2026

Summary

This PR bundles three threads that together prepare lark-cli's public API for embedded/server-mode consumers.

1. Build/Execute split (fe461d9)

  • Split Execute() into Build() (public, returns *cobra.Command) + buildInternal() (private, also returns *Factory)
  • Add functional options: WithIO, WithKeychain, and a build option to hide --profile in single-app mode (a8455db)
  • Make IO explicit via SystemIO() instead of hardcoded os.Stdin/Stdout/Stderr
  • Late-bind keychain via func() closure in credential provider
  • Converge WarnIfProxied on f.IOStreams.ErrOut

2. Strict-mode plumbing (719bc63, 2a1602c, bdd5b55)

  • Filter methods by strict mode in schema output
  • Centralize strict-mode as a flag registration rather than ad-hoc checks
  • Align strict-mode completion with the build context; drop dead register shims

3. Transport unification (fb1bc4d)

  • Introduce a shared base transport and an extension abort hook (extension/transport/{errors,types}.go)
  • Route internal/cmdutil/transport.go, internal/util/proxy.go, internal/update/update.go through the shared base; fix a response-body leak along the way

Test plan

  • go build ./...
  • go test -race ./internal/... ./cmd/... ./extension/...
  • golangci-lint run — 0 issues on changed packages
  • No new os.Stdin/Stdout/Stderr leaks

Summary by CodeRabbit

  • New Features

    • New programmatic CLI constructor for embedding and external wiring.
    • Ability to replace the process-wide default filesystem used by CLI operations.
    • Schema output and method listings now respect strict-mode filtering.
  • Refactor

    • Centralized IO and terminal detection for consistent CLI behavior.
    • Keychain/credential resolution made lazy and replaceable at runtime.
  • Tests

    • Adjusted tests to align with the new constructor and IO/credential behavior.

@github-actions github-actions Bot added the size/L Large or sensitive change across domains or core paths label Apr 9, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Extracts CLI construction into a reusable Build(ctx, inv, opts...) entry point; refactors Factory to accept injected IOStreams and lazy keychain factory; adds SystemIO() and SetDefaultFS(fs); updates callers/tests and wires strict-mode filtering into schema command behavior.

Changes

Cohort / File(s) Summary
Command tree & Build entry
cmd/build.go, cmd/root.go, cmd/root_integration_test.go
Added Build/buildInternal to construct and return the Cobra root command (wiring context, IO, flags, subcommands); Execute() now delegates to buildInternal; tests updated to new Factory/IO signature.
Factory & IOStreams
internal/cmdutil/factory_default.go, internal/cmdutil/factory_default_test.go, internal/cmdutil/iostreams.go
NewDefault now accepts *IOStreams (falls back to SystemIO()); added SystemIO() with terminal detection; cached HTTP/Lark clients route proxy warnings to f.IOStreams.ErrOut.
Keychain lazy factory
internal/credential/default_provider.go, internal/credential/integration_test.go
Keychain stored as func() keychain.KeychainAccess (lazy factory) and constructors updated to accept a factory function; tests adapted to pass closures.
HTTP client tests
internal/cmdutil/factory_http_test.go
cachedHttpClientFunc signature updated to accept *Factory; tests create Factory with IOStreams.ErrOut = io.Discard.
Filesystem setter
cmd/init.go
Added SetDefaultFS(fs vfs.FS) to replace or reset the process-wide vfs.DefaultFS.
Schema strict-mode filtering
cmd/schema/schema.go
Added Ctx context.Context to SchemaOptions and filterMethodsByStrictMode(...) to filter resource methods when strict mode is active; applied in listing, JSON output, and method lookup.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Caller
    participant Build as Build (cmd/build.go)
    participant SystemIO as SystemIO()
    participant NewDefault as NewDefault(streams, inv)
    participant Factory as Factory
    participant Keychain as KeychainFactory
    participant Cobra as Cobra Root Cmd

    Caller->>Build: Build(ctx, inv, opts...)
    alt WithIO provided
        Build->>Build: apply WithIO streams
    else
        Build->>SystemIO: SystemIO()
        SystemIO-->>Build: *IOStreams
    end
    Build->>NewDefault: NewDefault(streams, inv)
    NewDefault-->>Factory: returns *Factory (with IOStreams)
    Build->>Factory: apply WithKeychain? (replace kc factory)
    Factory->>Keychain: keychain() (lazy resolve when needed)
    Build->>Cobra: construct root cmd, wire ctx & IO, register subcommands
    Build-->>Caller: return *cobra.Command
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

size/L, domain/ccm

Suggested reviewers

  • liangshuo-1
  • kongenpei

Poem

🐰 I hopped through streams and keys anew,

I nudged the Factory, let lazy kc brew.
IO set, root assembled with care,
Methods pruned for strictness over there.
A little rabbit cheers the CLI repair.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.93% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main refactoring: splitting Execute into Build and Execute with explicit IO and keychain injection mechanisms.
Description check ✅ Passed The PR description provides a comprehensive summary with clear motivation, organized changes (Build/Execute split, Strict-mode plumbing, Transport unification), and a detailed test plan covering build, test, linting, and regression checks.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/build-execute-split

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 9, 2026

Greptile Summary

This PR splits Execute() into a public Build() function (returns *cobra.Command) and private buildInternal(), and adds WithIO/WithKeychain functional options to support external consumers needing custom IO and credential backends. It also centralizes --as flag registration into addIdentityFlag and converges WarnIfProxied to use f.IOStreams.ErrOut.

The refactor is architecturally sound. The one noteworthy concern is that the centralized addIdentityFlag now calls f.ResolveStrictMode(ctx) at registration time for every service method and shortcut command, driving O(N) LoadMultiAppConfig() disk reads per invocation where a single read would suffice.

Confidence Score: 5/5

Safe to merge; no correctness or security issues introduced — all findings are P2 style/performance concerns.

The refactor correctly migrates IO injection and keychain late-binding. The keychain closure pattern is applied consistently through the credential chain. The only new issues are O(N) config reads during command registration (performance, not correctness) and the non-thread-safe global FS setter. All remaining findings are P2.

internal/cmdutil/identity_flag.go — ResolveStrictMode called once per command registration, causing repeated disk reads.

Important Files Changed

Filename Overview
cmd/build.go New file: public Build() + buildInternal() split, WithIO/WithKeychain options. Terminal detection in WithIO only checks in, consistent with SystemIO(). Confident overall; see comment about N-fold config reads triggered through AddAPIIdentityFlag.
internal/cmdutil/identity_flag.go New file centralizing --as flag registration. Calls ResolveStrictMode at registration time for each command, causing repeated LoadMultiAppConfig() reads. Flag hiding and default-locking in strict mode is correct; execution-time enforcement is elsewhere.
internal/cmdutil/factory_default.go Keychain now injected via closure (func() KeychainAccess) enabling late binding after construction; IOStreams explicitly passed in; WarnIfProxied converged to f.IOStreams.ErrOut. Clean refactor.
internal/credential/default_provider.go DefaultAccountProvider.keychain field changed to func() KeychainAccess; NewDefaultAccountProvider updated accordingly. Lazy resolution is correct and consistent with the factory-level closure.
internal/cmdutil/iostreams.go SystemIO() helper extracted from NewDefault; centralises os.Stdin/Stdout/Stderr wiring and terminal detection in one place.
cmd/init.go New file: SetDefaultFS() wraps a global vfs.DefaultFS replacement with nil-guard. Intended as a pre-boot call (documented), not concurrency-safe.
cmd/root_integration_test.go newStrictModeDefaultFactory replaces f.IOStreams post-construction, so credential provider's errOut still targets os.Stderr (pre-existing pattern, noted in previous thread). Tests otherwise well-structured.
cmd/service/service.go NewCmdServiceMethodWithContext now calls AddAPIIdentityFlag(ctx, ...) per method, triggering ResolveStrictMode (and a config disk read) for every method command registered.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant build.go
    participant cmdutil
    participant credential
    participant fs as core/vfs

    Caller->>build.go: Build(ctx, inv, WithIO(...), WithKeychain(kc))
    build.go->>build.go: apply BuildOptions → buildConfig
    build.go->>cmdutil: NewDefault(streams, inv)
    cmdutil->>cmdutil: f.Keychain = keychain.Default()
    cmdutil->>cmdutil: buildCredentialProvider(Keychain closure)
    build.go->>build.go: f.Keychain = cfg.keychain (late bind)
    loop For each service method & shortcut
        build.go->>cmdutil: AddAPIIdentityFlag(ctx, cmd, f, ...)
        cmdutil->>credential: f.ResolveStrictMode(ctx) → ResolveAccount
        credential->>fs: LoadMultiAppConfig() [disk read]
        fs-->>credential: MultiAppConfig
        credential-->>cmdutil: StrictMode
        cmdutil->>cmdutil: register --as flag (hidden or visible)
    end
    build.go->>credential: f.ResolveStrictMode(ctx) [disk read again]
    build.go->>build.go: pruneForStrictMode(rootCmd, mode)
    build.go-->>Caller: *cobra.Command

    Caller->>build.go: rootCmd.Execute()
    build.go->>credential: ResolveAccount (first real credential use)
    credential->>fs: LoadMultiAppConfig() [disk read again]
Loading

Reviews (5): Last reviewed commit: "fix: align strict-mode completion and bu..." | Re-trigger Greptile

Comment thread cmd/root_integration_test.go Outdated
Comment thread cmd/build.go
Comment thread internal/cmdutil/factory_default.go
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 9, 2026

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@06f552af2c3a39cabb6738f1a6fc7cb227623c78

🧩 Skill update

npx skills add larksuite/cli#refactor/build-execute-split -y -g

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
internal/cmdutil/factory_http_test.go (1)

12-40: Use TestFactory instead of hand-rolled Factory values in these tests.

These three tests now build minimal Factory literals inline. That works today, but it bypasses the repo’s standard test wiring and makes the tests easier to drift as Factory defaults evolve. As per coding guidelines **/*_test.go: Use cmdutil.TestFactory(t, config) for creating test factories in Go tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cmdutil/factory_http_test.go` around lines 12 - 40, Replace the
inline Factory literals in the tests that call cachedHttpClientFunc (used in
TestCachedHttpClientFunc, TestCachedHttpClientFunc_HasTimeout, and
TestCachedHttpClientFunc_HasRedirectPolicy) with the repository-standard test
factory: call cmdutil.TestFactory(t, /* config or nil */) to obtain a *Factory
and use that for creating fn; this removes the hand-rolled &Factory{IOStreams:
&IOStreams{ErrOut: io.Discard}} instances and ensures test wiring and defaults
are consistent with future Factory changes.
cmd/build.go (1)

39-43: Verify terminal detection implementation with actual IOStreams usage context.

The current approach at line 39 infers IsTerminal only from in (stdin). Go best practices recommend checking each stream independently: stdin TTY status matters for interactive input, while stdout/stderr TTY status matters for rendering decisions (colors, progress spinners). Depending on how cmdutil.IOStreams.IsTerminal is actually used downstream, this single-stream check may cause incorrect behavior when streams have mixed TTY states (e.g., stdin is a TTY but output is redirected).

Before refactoring, confirm:

  1. How is IOStreams.IsTerminal used throughout the codebase?
  2. Is it for interactive prompts (use in TTY) or output rendering (use errOut TTY)?
  3. If the flag controls output behavior, consider deriving it from errOut instead.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/build.go` around lines 39 - 43, The terminal-detection currently sets
cmdutil.IOStreams.IsTerminal based only on `in`, which can be wrong for
output-rendering decisions; inspect how `IOStreams.IsTerminal` is used
(interactive prompts vs. output rendering) and then fix by checking TTY status
per stream (`in`, `out`, `errOut`) and either (A) derive `IsTerminal` from the
stream used for rendering (likely `errOut`) or (B) extend `cmdutil.IOStreams` to
expose separate flags such as `IsInTerminal`, `IsOutTerminal`, `IsErrTerminal`
and populate them using term.IsTerminal on each file descriptor
(`in.(*os.File).Fd()`, `out.(*os.File).Fd()`, `errOut.(*os.File).Fd()`) so
downstream code can make correct decisions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/build.go`:
- Around line 67-69: The loop over functional options can panic if a nil option
is present; update the code that iterates over opts (the for _, o := range opts
loop) to guard before invoking o(cfg) by checking that o != nil and either skip
nil entries or filter them out beforehand, ensuring cfg is only passed to
non-nil option functions (references: opts, o, cfg).

In `@cmd/root_integration_test.go`:
- Around line 138-141: The test currently assigns f.IOStreams after calling
cmdutil.NewDefault which lets factory construction (e.g., DefaultTokenProvider
wiring to f.IOStreams.ErrOut) write to real stderr; instead, create the test
buffers and an ioStreams value first and pass it into cmdutil.NewDefault via the
InvocationContext (so the call becomes cmdutil.NewDefault(nil,
cmdutil.InvocationContext{Profile: profile, IOStreams: ioStreams})), and remove
the subsequent f.IOStreams reassignment so all factory-internal logging uses the
test buffers.

In `@internal/cmdutil/factory_default.go`:
- Around line 35-43: NewDefault currently only replaces a nil streams pointer
with SystemIO(), so partially populated IOStreams (e.g., &IOStreams{Out: buf})
leave ErrOut or In nil and can panic; change NewDefault to normalize any non-nil
partial IOStreams by first obtaining a full default via SystemIO() and then
copy/override only the non-nil fields from the incoming streams into that
default before assigning to Factory.IOStreams (refer to NewDefault, IOStreams,
Factory, and f.IOStreams.ErrOut) so the Factory always holds a fully-initialized
IOStreams struct.

---

Nitpick comments:
In `@cmd/build.go`:
- Around line 39-43: The terminal-detection currently sets
cmdutil.IOStreams.IsTerminal based only on `in`, which can be wrong for
output-rendering decisions; inspect how `IOStreams.IsTerminal` is used
(interactive prompts vs. output rendering) and then fix by checking TTY status
per stream (`in`, `out`, `errOut`) and either (A) derive `IsTerminal` from the
stream used for rendering (likely `errOut`) or (B) extend `cmdutil.IOStreams` to
expose separate flags such as `IsInTerminal`, `IsOutTerminal`, `IsErrTerminal`
and populate them using term.IsTerminal on each file descriptor
(`in.(*os.File).Fd()`, `out.(*os.File).Fd()`, `errOut.(*os.File).Fd()`) so
downstream code can make correct decisions.

In `@internal/cmdutil/factory_http_test.go`:
- Around line 12-40: Replace the inline Factory literals in the tests that call
cachedHttpClientFunc (used in TestCachedHttpClientFunc,
TestCachedHttpClientFunc_HasTimeout, and
TestCachedHttpClientFunc_HasRedirectPolicy) with the repository-standard test
factory: call cmdutil.TestFactory(t, /* config or nil */) to obtain a *Factory
and use that for creating fn; this removes the hand-rolled &Factory{IOStreams:
&IOStreams{ErrOut: io.Discard}} instances and ensures test wiring and defaults
are consistent with future Factory changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c1353809-ae72-4972-a783-e77b28aa3964

📥 Commits

Reviewing files that changed from the base of the PR and between 619ec8c and 0bfc2e4.

📒 Files selected for processing (9)
  • cmd/build.go
  • cmd/root.go
  • cmd/root_integration_test.go
  • internal/cmdutil/factory_default.go
  • internal/cmdutil/factory_default_test.go
  • internal/cmdutil/factory_http_test.go
  • internal/cmdutil/iostreams.go
  • internal/credential/default_provider.go
  • internal/credential/integration_test.go

Comment thread cmd/build.go
Comment thread cmd/root_integration_test.go Outdated
Comment thread internal/cmdutil/factory_default.go
@tuxedomm tuxedomm force-pushed the refactor/build-execute-split branch from 0bfc2e4 to dd05477 Compare April 9, 2026 12:59
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (3)
cmd/build.go (2)

67-69: ⚠️ Potential issue | 🟡 Minor

Guard nil functional options before invocation.

Line 68 can panic if a nil option is present in opts.

Proposed fix
 	for _, o := range opts {
-		o(cfg)
+		if o != nil {
+			o(cfg)
+		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/build.go` around lines 67 - 69, The loop invoking functional options in
cmd/build.go should guard against nil entries to avoid a panic; in the block
that iterates over opts (the for _, o := range opts loop), add a nil-check and
only call o(cfg) when o != nil (i.e., skip nil option functions), ensuring that
any nil option in the slice won't cause a runtime panic.

37-44: ⚠️ Potential issue | 🟠 Major

Backfill nil WithIO arguments to keep streams always writable.

At Line 43, WithIO stores out/errOut as-is; passing nil values can break warning/progress/error output routing and runtime writes.

Proposed fix
 func WithIO(in io.Reader, out, errOut io.Writer) BuildOption {
 	return func(c *buildConfig) {
+		sys := cmdutil.SystemIO()
+		if in == nil {
+			in = sys.In
+		}
+		if out == nil {
+			out = sys.Out
+		}
+		if errOut == nil {
+			errOut = sys.ErrOut
+		}
 		isTerminal := false
 		if f, ok := in.(*os.File); ok {
 			isTerminal = term.IsTerminal(int(f.Fd()))
 		}
 		c.streams = &cmdutil.IOStreams{In: in, Out: out, ErrOut: errOut, IsTerminal: isTerminal}
 	}
 }

As per coding guidelines "**/*.go: Program output (JSON envelopes) must go to stdout; progress, warnings, and hints must go to stderr".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/build.go` around lines 37 - 44, The WithIO function should defensively
backfill nil I/O args so streams are always writable: in the WithIO closure
(function WithIO) ensure any nil in/out/errOut are replaced with sensible
defaults (os.Stdin, os.Stdout, os.Stderr) before computing isTerminal and
creating the cmdutil.IOStreams instance on buildConfig; update the IsTerminal
check to use the resolved input reader (not the original possibly-nil value) and
then assign c.streams = &cmdutil.IOStreams{In: resolvedIn, Out: resolvedOut,
ErrOut: resolvedErrOut, IsTerminal: isTerminal} so progress/warnings go to
stderr and program output can go to stdout reliably.
internal/cmdutil/factory_default.go (1)

35-43: ⚠️ Potential issue | 🟠 Major

Normalize partial IOStreams inputs before storing on Factory.

At Line 36, only a nil pointer is handled. A non-nil IOStreams with nil ErrOut/Out/In can still flow through and break warning/error writes later.

Proposed fix
 func NewDefault(streams *IOStreams, inv InvocationContext) *Factory {
-	if streams == nil {
-		streams = SystemIO()
-	}
+	sys := SystemIO()
+	if streams == nil {
+		streams = sys
+	} else {
+		if streams.In == nil {
+			streams.In = sys.In
+		}
+		if streams.Out == nil {
+			streams.Out = sys.Out
+		}
+		if streams.ErrOut == nil {
+			streams.ErrOut = sys.ErrOut
+		}
+	}
 	f := &Factory{
 		Keychain:   keychain.Default(),
 		Invocation: inv,
 		IOStreams:  streams,
 	}

As per coding guidelines "**/*.go: Program output (JSON envelopes) must go to stdout; progress, warnings, and hints must go to stderr".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/cmdutil/factory_default.go` around lines 35 - 43, NewDefault
currently only replaces a nil *IOStreams; if callers pass a non-nil IOStreams
with some nil fields (In/Out/ErrOut) those nils can break later writes. In
NewDefault, after the nil-check, obtain defaults := SystemIO() and for any nil
field on the incoming streams (streams.In, streams.Out, streams.ErrOut) assign
the corresponding defaults field so the Factory stores a fully-populated
IOStreams; keep using keychain.Default(), Invocation inv and the normalized
streams when constructing the Factory.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@cmd/build.go`:
- Around line 67-69: The loop invoking functional options in cmd/build.go should
guard against nil entries to avoid a panic; in the block that iterates over opts
(the for _, o := range opts loop), add a nil-check and only call o(cfg) when o
!= nil (i.e., skip nil option functions), ensuring that any nil option in the
slice won't cause a runtime panic.
- Around line 37-44: The WithIO function should defensively backfill nil I/O
args so streams are always writable: in the WithIO closure (function WithIO)
ensure any nil in/out/errOut are replaced with sensible defaults (os.Stdin,
os.Stdout, os.Stderr) before computing isTerminal and creating the
cmdutil.IOStreams instance on buildConfig; update the IsTerminal check to use
the resolved input reader (not the original possibly-nil value) and then assign
c.streams = &cmdutil.IOStreams{In: resolvedIn, Out: resolvedOut, ErrOut:
resolvedErrOut, IsTerminal: isTerminal} so progress/warnings go to stderr and
program output can go to stdout reliably.

In `@internal/cmdutil/factory_default.go`:
- Around line 35-43: NewDefault currently only replaces a nil *IOStreams; if
callers pass a non-nil IOStreams with some nil fields (In/Out/ErrOut) those nils
can break later writes. In NewDefault, after the nil-check, obtain defaults :=
SystemIO() and for any nil field on the incoming streams (streams.In,
streams.Out, streams.ErrOut) assign the corresponding defaults field so the
Factory stores a fully-populated IOStreams; keep using keychain.Default(),
Invocation inv and the normalized streams when constructing the Factory.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fa2aed7a-e6aa-4953-9965-85347239d904

📥 Commits

Reviewing files that changed from the base of the PR and between 0bfc2e4 and dd05477.

📒 Files selected for processing (10)
  • cmd/build.go
  • cmd/init.go
  • cmd/root.go
  • cmd/root_integration_test.go
  • internal/cmdutil/factory_default.go
  • internal/cmdutil/factory_default_test.go
  • internal/cmdutil/factory_http_test.go
  • internal/cmdutil/iostreams.go
  • internal/credential/default_provider.go
  • internal/credential/integration_test.go
✅ Files skipped from review due to trivial changes (2)
  • cmd/root_integration_test.go
  • internal/cmdutil/factory_default_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/credential/integration_test.go
  • internal/cmdutil/factory_http_test.go
  • internal/cmdutil/iostreams.go
  • cmd/root.go

@tuxedomm tuxedomm force-pushed the refactor/build-execute-split branch from 9412170 to 212fbae Compare April 9, 2026 14:24
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cmd/schema/schema.go (1)

448-453: ⚠️ Potential issue | 🟠 Major

Apply strict-mode filtering to schema <service> outputs too.

mode is resolved above, but this branch still renders the raw service spec. In strict mode, schema <service> --format pretty goes through printResourceList, and --format json returns spec unchanged, so methods hidden at the resource/method level are still discoverable one level up.

[suggested fix]

♻️ Minimal direction
 	if len(parts) == 1 {
+		filteredSpec := filterSpecByStrictMode(spec, mode)
 		if opts.Format == "pretty" {
-			printResourceList(out, spec)
+			printResourceList(out, filteredSpec)
 		} else {
-			output.PrintJson(out, spec)
+			output.PrintJson(out, filteredSpec)
 		}
 		return nil
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/schema/schema.go` around lines 448 - 453, The branch handling the
single-part service output currently prints the raw spec (using
printResourceList and output.PrintJson) and ignores the previously-resolved
mode; modify this branch to first compute a filtered spec according to mode
(e.g., filteredSpec := filterSpecByMode(spec, mode) or call the existing helper
that applies strict-mode filtering) and then pass filteredSpec to
printResourceList(out, filteredSpec) for opts.Format == "pretty" and to
output.PrintJson(out, filteredSpec) for JSON, ensuring hidden resources/methods
are removed before rendering.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@cmd/schema/schema.go`:
- Around line 448-453: The branch handling the single-part service output
currently prints the raw spec (using printResourceList and output.PrintJson) and
ignores the previously-resolved mode; modify this branch to first compute a
filtered spec according to mode (e.g., filteredSpec := filterSpecByMode(spec,
mode) or call the existing helper that applies strict-mode filtering) and then
pass filteredSpec to printResourceList(out, filteredSpec) for opts.Format ==
"pretty" and to output.PrintJson(out, filteredSpec) for JSON, ensuring hidden
resources/methods are removed before rendering.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b2ddd629-7fd7-460d-bcb1-35e95a607621

📥 Commits

Reviewing files that changed from the base of the PR and between dd05477 and 212fbae.

📒 Files selected for processing (1)
  • cmd/schema/schema.go

@tuxedomm tuxedomm force-pushed the refactor/build-execute-split branch 2 times, most recently from 0b3180b to 57db84e Compare April 9, 2026 14:32
@github-actions github-actions Bot added size/XL Architecture-level or global-impact change and removed size/L Large or sensitive change across domains or core paths labels Apr 9, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cmd/schema/schema.go (1)

448-453: ⚠️ Potential issue | 🟠 Major

Strict-mode filtering is skipped for schema <service> output.

When len(parts) == 1, both pretty and JSON branches return unfiltered service data. This is inconsistent with Line 473/Line 501 behavior and can surface methods that are later hidden/invalid under strict mode.

🛠️ Suggested direction
 if len(parts) == 1 {
+		view := spec
+		if mode.IsActive() {
+			view = filterSpecMethodsByStrictMode(spec, mode)
+		}
 		if opts.Format == "pretty" {
-			printResourceList(out, spec)
+			printResourceList(out, view)
 		} else {
-			output.PrintJson(out, spec)
+			output.PrintJson(out, view)
 		}
 		return nil
 }
// helper (outside this hunk)
func filterSpecMethodsByStrictMode(spec map[string]interface{}, mode core.StrictMode) map[string]interface{} {
	if !mode.IsActive() || spec == nil {
		return spec
	}
	out := make(map[string]interface{}, len(spec))
	for k, v := range spec {
		out[k] = v
	}
	resources, _ := spec["resources"].(map[string]interface{})
	if resources == nil {
		return out
	}
	resCopy := make(map[string]interface{}, len(resources))
	for rn, rv := range resources {
		rm, _ := rv.(map[string]interface{})
		if rm == nil {
			resCopy[rn] = rv
			continue
		}
		rmCopy := make(map[string]interface{}, len(rm))
		for k, v := range rm {
			rmCopy[k] = v
		}
		if methods, ok := rm["methods"].(map[string]interface{}); ok {
			rmCopy["methods"] = filterMethodsByStrictMode(methods, mode)
		}
		resCopy[rn] = rmCopy
	}
	out["resources"] = resCopy
	return out
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/schema/schema.go` around lines 448 - 453, The branch handling len(parts)
== 1 returns the unfiltered spec for both pretty and JSON outputs; update it to
apply strict-mode filtering before printing by calling a new helper like
filterSpecMethodsByStrictMode(spec, opts.StrictMode) (or the existing
filterMethodsByStrictMode inside that helper) and then pass the filtered spec to
printResourceList(out, filtered) or output.PrintJson(out, filtered)
respectively; ensure you add/import core.StrictMode if needed and place the
helper (filterSpecMethodsByStrictMode) alongside other helpers so
resources->methods are filtered consistently with the other branches.
🧹 Nitpick comments (1)
cmd/schema/schema.go (1)

429-435: Resolve strict mode only after the empty-path early return.

On Line 431, ResolveStrictMode runs even for lark-cli schema (no path), where strict filtering is not used. This adds avoidable account/credential work to a lightweight listing path.

♻️ Suggested change
 func schemaRun(opts *SchemaOptions) error {
 	out := opts.Factory.IOStreams.Out
-	mode := opts.Factory.ResolveStrictMode(opts.Ctx)

 	if opts.Path == "" {
 		printServices(out)
 		return nil
 	}
+	mode := opts.Factory.ResolveStrictMode(opts.Ctx)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/schema/schema.go` around lines 429 - 435, In schemaRun, avoid calling
ResolveStrictMode when opts.Path is empty; move the call to
opts.Factory.ResolveStrictMode(opts.Ctx) so it occurs after the early-return
branch that checks opts.Path == "" and calls printServices(out), ensuring
ResolveStrictMode is only executed for non-empty-path flows that need strict
filtering rather than for the lightweight listing path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@cmd/schema/schema.go`:
- Around line 448-453: The branch handling len(parts) == 1 returns the
unfiltered spec for both pretty and JSON outputs; update it to apply strict-mode
filtering before printing by calling a new helper like
filterSpecMethodsByStrictMode(spec, opts.StrictMode) (or the existing
filterMethodsByStrictMode inside that helper) and then pass the filtered spec to
printResourceList(out, filtered) or output.PrintJson(out, filtered)
respectively; ensure you add/import core.StrictMode if needed and place the
helper (filterSpecMethodsByStrictMode) alongside other helpers so
resources->methods are filtered consistently with the other branches.

---

Nitpick comments:
In `@cmd/schema/schema.go`:
- Around line 429-435: In schemaRun, avoid calling ResolveStrictMode when
opts.Path is empty; move the call to opts.Factory.ResolveStrictMode(opts.Ctx) so
it occurs after the early-return branch that checks opts.Path == "" and calls
printServices(out), ensuring ResolveStrictMode is only executed for
non-empty-path flows that need strict filtering rather than for the lightweight
listing path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4cd66490-d209-43d0-b4bf-c0e26e19f079

📥 Commits

Reviewing files that changed from the base of the PR and between 212fbae and 0b3180b.

📒 Files selected for processing (1)
  • cmd/schema/schema.go

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
cmd/schema/schema.go (2)

331-355: ⚠️ Potential issue | 🟡 Minor

Keep tab completion in sync with strict-mode filtering.

schemaRun now hides identity-incompatible resources/methods, but completeSchemaPath still completes against the unfiltered registry. In strict mode that means shell completion can suggest a path this same command later rejects as unknown. Please thread the factory/context into ValidArgsFunction and apply the same filtering there.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/schema/schema.go` around lines 331 - 355, ValidArgsFunction currently
calls completeSchemaPath against the unfiltered registry, causing suggestions
that schemaRun later rejects in strict mode; update NewCmdSchema to pass the
Factory/Context into the completion function by wrapping completeSchemaPath with
a closure that sets up the same SchemaOptions/ctx/filtering used by schemaRun
(reuse SchemaOptions and opts.Ctx or f.Context), so completion applies the same
identity-compatible resource/method filtering before returning completions;
reference NewCmdSchema, ValidArgsFunction, completeSchemaPath, SchemaOptions,
schemaRun and the provided Factory to locate where to thread the context and
apply the filter.

473-499: ⚠️ Potential issue | 🟠 Major

Return “unknown resource” when strict mode prunes every method.

After Lines 477/493 filter the methods, this branch still succeeds for a resource whose visible method set is empty. That diverges from filterSpecByStrictMode, which drops those resources entirely, so schema <service> hides the resource while schema <service>.<resource> still exposes an empty shell. Please short-circuit here and reuse the existing unknown-resource path once the filtered method set is empty.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/schema/schema.go` around lines 473 - 499, After filtering methods with
filterMethodsByStrictMode in the branch handling opts.Format == "pretty" and the
JSON branch, check whether the resulting methods map is empty and, if so, take
the same "unknown resource" code path used elsewhere (i.e., short-circuit and
print the unknown-resource message/exit) instead of continuing to render an
empty resource; update the pretty branch (after methods =
filterMethodsByStrictMode(...)) and the JSON branch (after assigning
filtered["methods"] = filterMethodsByStrictMode(...)) to detect len(methods)==0
(or len(filtered["methods"].(map[string]interface{}))==0) and reuse the existing
unknown-resource handling for consistency with filterSpecByStrictMode.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@cmd/schema/schema.go`:
- Around line 331-355: ValidArgsFunction currently calls completeSchemaPath
against the unfiltered registry, causing suggestions that schemaRun later
rejects in strict mode; update NewCmdSchema to pass the Factory/Context into the
completion function by wrapping completeSchemaPath with a closure that sets up
the same SchemaOptions/ctx/filtering used by schemaRun (reuse SchemaOptions and
opts.Ctx or f.Context), so completion applies the same identity-compatible
resource/method filtering before returning completions; reference NewCmdSchema,
ValidArgsFunction, completeSchemaPath, SchemaOptions, schemaRun and the provided
Factory to locate where to thread the context and apply the filter.
- Around line 473-499: After filtering methods with filterMethodsByStrictMode in
the branch handling opts.Format == "pretty" and the JSON branch, check whether
the resulting methods map is empty and, if so, take the same "unknown resource"
code path used elsewhere (i.e., short-circuit and print the unknown-resource
message/exit) instead of continuing to render an empty resource; update the
pretty branch (after methods = filterMethodsByStrictMode(...)) and the JSON
branch (after assigning filtered["methods"] = filterMethodsByStrictMode(...)) to
detect len(methods)==0 (or len(filtered["methods"].(map[string]interface{}))==0)
and reuse the existing unknown-resource handling for consistency with
filterSpecByStrictMode.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8971e18a-07ac-4db7-98e0-be4df87e3416

📥 Commits

Reviewing files that changed from the base of the PR and between 0b3180b and 57db84e.

📒 Files selected for processing (1)
  • cmd/schema/schema.go

@tuxedomm tuxedomm force-pushed the refactor/build-execute-split branch 2 times, most recently from 719073a to bcbc1bf Compare April 14, 2026 09:00
Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@tuxedomm tuxedomm force-pushed the refactor/build-execute-split branch 2 times, most recently from 1b0f335 to b1628db Compare April 21, 2026 04:12
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 21, 2026

Codecov Report

❌ Patch coverage is 64.42953% with 106 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.92%. Comparing base (ccc27ce) to head (06f552a).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
cmd/schema/schema.go 39.81% 56 Missing and 9 partials ⚠️
internal/cmdutil/iostreams.go 50.00% 10 Missing ⚠️
cmd/root.go 0.00% 9 Missing ⚠️
internal/cmdutil/identity_flag.go 72.41% 5 Missing and 3 partials ⚠️
cmd/build.go 91.83% 2 Missing and 2 partials ⚠️
cmd/service/service.go 76.92% 3 Missing ⚠️
internal/util/proxy.go 66.66% 3 Missing ⚠️
internal/credential/default_provider.go 50.00% 1 Missing and 1 partial ⚠️
internal/cmdutil/factory_default.go 90.00% 1 Missing ⚠️
internal/update/update.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #371      +/-   ##
==========================================
+ Coverage   60.74%   60.92%   +0.17%     
==========================================
  Files         394      399       +5     
  Lines       33910    34089     +179     
==========================================
+ Hits        20600    20770     +170     
+ Misses      11400    11395       -5     
- Partials     1910     1924      +14     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Introduce a public cmd.Build entry point so external consumers (cli-server,
MCP server, other embedders) can assemble the full CLI command tree without
going through os.Args or the platform keychain. Build takes an
InvocationContext plus functional BuildOptions:

  * WithIO(in, out, errOut) — inject custom streams; terminal detection
    is derived from the input's underlying *os.File when present.
  * WithKeychain(kc)        — swap the credential store.
  * HideProfile(bool)       — registered later in cmd.HideProfile.

The existing Execute() keeps using the internal buildInternal (which
still returns the Factory so error handling can attribute exit codes),
and SetDefaultFS replaces the global VFS implementation at startup.

Hardening applied up front:

  * cmdutil.NewIOStreams(in, out, errOut) centralizes terminal detection
    so SystemIO() and WithIO share one path.
  * cmdutil.NewDefault normalizes partial IOStreams — callers may pass
    &IOStreams{Out: buf} without tripping nil-writer panics in the
    RoundTripper warnings, Cobra, or the credential provider.
  * Build guards against nil functional options.
  * An API contract test (cmd/build_api_test.go) exercises Build +
    WithIO + WithKeychain + HideProfile + SetDefaultFS so the public
    surface is reachable by deadcode analysis.

Change-Id: I7c895e6019817401accbde2db3ef800da40ad319
When strict mode is active, schema output now excludes methods that
are incompatible with the forced identity. This applies to both
pretty and JSON output formats at the resource and method levels.

Change-Id: I39647d5578466c3e23dc545bfb917ae075203ad7
Change-Id: Iec11151c5002c2f58a8aa067d08747db2e4d2d8c
…egister shims

Thread a context.Context through RegisterShortcuts, RegisterServiceCommands,
and service.registerService/Resource/Method by introducing explicit
*WithContext variants. Pass that context into NewCmdServiceMethodWithContext
so shortcut and service command construction can honor cancellation and
strict-mode pruning consistently.

Also drop the context-less registerMethod and registerResource shims —
they became unreachable once the WithContext variants took over, and
were the source of new deadcode warnings. registerService is retained
because service_test.go still calls it directly.

Change-Id: I3fe5673aed663c7383bbbc5b0ae94d1f3491f22d
- GlobalOptions gains HideProfile; RegisterGlobalFlags stays pure and reads
  the policy off the struct. No boolean-trap parameter, one call per site.
- buildConfig holds GlobalOptions inline so HideProfile(bool) BuildOption
  mutates it directly. buildInternal stays a pure assembly function and
  requires callers to supply WithIO — no implicit os.Std* fallback.
- Add WithIO BuildOption (wrapping raw io.Reader/Writer with automatic
  *os.File TTY detection); Execute injects streams explicitly and decides
  profile visibility via HideProfile(isSingleAppMode()).
- installTipsHelpFunc force-shows hidden root flags while rendering the
  root command's own help, so single-app users still discover --profile
  via lark-cli --help without it polluting subcommand helps.

Change-Id: I7755387e993992ca969e0a4a6f54441cc1993eef
Two transport-layer changes bundled because both reshape the base
round-tripper contract used by the HTTP client, the Lark SDK client,
and the in-process updater.

1. Extension abort hook (PreRoundTripE).

   Extensions implementing exttransport.AbortableInterceptor can now
   return an error from PreRoundTripE to skip the built-in chain. The
   post hook still fires with (nil, reason) so extensions can unwind
   resources. extensionMiddleware captures the provider name so the
   returned *AbortError carries attribution.

2. Shared base transport to stop RPC leak.

   util.NewBaseTransport cloned http.DefaultTransport on every call, so
   each cmdutil.Factory produced a fresh *http.Transport whose
   persistConn readLoop/writeLoop goroutines lingered until
   IdleConnTimeout (~90s). Invisible in a single-process CLI, but the
   fork is consumed by cli-server where each RPC request constructs a
   new Factory, causing linear memory + goroutine growth under load.

   Replace NewBaseTransport with SharedTransport — returns
   http.DefaultTransport (the stdlib-wide singleton) by default, and
   a cached proxy-disabled clone only when LARK_CLI_NO_PROXY is set.
   Return type is http.RoundTripper to discourage in-place mutation of
   the shared instance. FallbackTransport is kept as a thin
   *http.Transport wrapper so existing callers in internal/auth and
   internal/cmdutil transport decorators (which were already on the
   singleton path) do not have to migrate.

   Leak-site migrations: factory_default.go (HTTP + SDK base) and
   update.go now call SharedTransport directly.

Change-Id: Ia82462134c5c5ee838be878b887860f41446a235
@tuxedomm tuxedomm force-pushed the refactor/build-execute-split branch from 9d86ea2 to fb1bc4d Compare April 21, 2026 06:04
Two regressions surfaced on refactor/build-execute-split:

1. cmd.Build(ctx, inv) without WithIO panicked at rootCmd.SetIn/Out/Err
   because cfg.streams stayed nil — NewDefault normalized internally
   but cmd/build.go never saw the normalized value. Default cfg.streams
   to cmdutil.SystemIO() before the root command wires them, and add a
   TestBuild_NoOptions regression guard.

2. sidecar/server-demo/main.go still called cmdutil.NewDefault(inv),
   so `go build -tags authsidecar_demo ./sidecar/server-demo` failed
   with "not enough arguments". Pass nil for the new streams parameter
   to preserve the prior behavior (NewDefault substitutes SystemIO).

Change-Id: I20227b2355cde7d19e22eba3eb841c6d8611e8a7
@liangshuo-1 liangshuo-1 merged commit fbed6be into main Apr 21, 2026
19 checks passed
@liangshuo-1 liangshuo-1 deleted the refactor/build-execute-split branch April 21, 2026 06:48
@liangshuo-1 liangshuo-1 mentioned this pull request Apr 21, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Architecture-level or global-impact change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants