Skip to content

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

Open
tuxedomm wants to merge 4 commits intomainfrom
refactor/build-execute-split
Open

refactor: split Execute into Build + Execute with explicit IO and keychain injection#371
tuxedomm wants to merge 4 commits intomainfrom
refactor/build-execute-split

Conversation

@tuxedomm
Copy link
Copy Markdown
Collaborator

@tuxedomm tuxedomm commented Apr 9, 2026

Summary

  • Split Execute() into Build() (public, returns *cobra.Command) + buildInternal() (private, also returns *Factory)
  • Add functional options: WithIO for IO streams, WithKeychain for secret storage backend
  • Make IO explicitly passed via SystemIO() constructor instead of hardcoding os.Stdin/Stdout/Stderr inside NewDefault
  • Enable keychain late binding via func() closure in credential provider, allowing callers to replace f.Keychain after Factory construction
  • Converge WarnIfProxied to use f.IOStreams.ErrOut instead of os.Stderr

Motivation

Prepare the public API for external consumers (e.g. server mode) that need to:

  1. Construct the cobra command tree without executing it
  2. Inject custom IO streams per invocation
  3. Replace the keychain backend at build time

Test plan

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

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 description covers summary, changes, motivation, and test plan sections, providing comprehensive documentation of the refactoring work.

✏️ 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

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

@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@719073aed5ff7f3bcccc3ff39d401dfc35faf8c0

🧩 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 on lines +67 to +69
for _, o := range opts {
o(cfg)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against nil functional options to avoid panic.

At Line 68, calling o(cfg) without a nil check can panic if a nil option is passed in opts.

Proposed fix
 	for _, o := range opts {
-		o(cfg)
+		if o != nil {
+			o(cfg)
+		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, o := range opts {
o(cfg)
}
for _, o := range opts {
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 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).

Comment on lines +138 to 141
f := cmdutil.NewDefault(nil, cmdutil.InvocationContext{Profile: profile})
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
f.IOStreams = &cmdutil.IOStreams{In: nil, Out: stdout, ErrOut: stderr}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Pass the test buffers into NewDefault directly.

Swapping f.IOStreams after Line 138 leaves anything captured during factory construction still pointing at the original system streams. In this PR, DefaultTokenProvider is wired with f.IOStreams.ErrOut inside NewDefault, so this helper can silently leak warnings/errors to the real stderr instead of the test buffer.

Suggested fix
-	f := cmdutil.NewDefault(nil, cmdutil.InvocationContext{Profile: profile})
 	stdout := &bytes.Buffer{}
 	stderr := &bytes.Buffer{}
-	f.IOStreams = &cmdutil.IOStreams{In: nil, Out: stdout, ErrOut: stderr}
+	f := cmdutil.NewDefault(&cmdutil.IOStreams{
+		In:     nil,
+		Out:    stdout,
+		ErrOut: stderr,
+	}, cmdutil.InvocationContext{Profile: profile})
 	return f, stdout, stderr
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
f := cmdutil.NewDefault(nil, cmdutil.InvocationContext{Profile: profile})
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
f.IOStreams = &cmdutil.IOStreams{In: nil, Out: stdout, ErrOut: stderr}
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
f := cmdutil.NewDefault(&cmdutil.IOStreams{
In: nil,
Out: stdout,
ErrOut: stderr,
}, cmdutil.InvocationContext{Profile: profile})
return f, stdout, stderr
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/root_integration_test.go` around lines 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.

Comment on lines +35 to 43
func NewDefault(streams *IOStreams, inv InvocationContext) *Factory {
if streams == nil {
streams = SystemIO()
}
f := &Factory{
Keychain: keychain.Default(),
Invocation: inv,
}
f.IOStreams = &IOStreams{
In: os.Stdin,
Out: os.Stdout,
ErrOut: os.Stderr,
IsTerminal: term.IsTerminal(int(os.Stdin.Fd())),
IOStreams: streams,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Normalize partial IOStreams inputs before storing them on the factory.

NewDefault only substitutes SystemIO() when streams == nil. With the new public IO injection path, callers can now legally pass a partially filled struct like &IOStreams{Out: buf}. The first warning/error write through f.IOStreams.ErrOut then panics on a nil writer, and Cobra can also inherit nil stdio handles.

Suggested 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,
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func NewDefault(streams *IOStreams, inv InvocationContext) *Factory {
if streams == nil {
streams = SystemIO()
}
f := &Factory{
Keychain: keychain.Default(),
Invocation: inv,
}
f.IOStreams = &IOStreams{
In: os.Stdin,
Out: os.Stdout,
ErrOut: os.Stderr,
IsTerminal: term.IsTerminal(int(os.Stdin.Fd())),
IOStreams: streams,
}
func NewDefault(streams *IOStreams, inv InvocationContext) *Factory {
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,
}
🤖 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 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.

@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

…mentation

Change-Id: If5c3e50e84859f9ac4ffceeb0ac3dc7b7330b274
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
Change-Id: I1d8da0dd4636384203cb748605682677ad1c4d92
@tuxedomm tuxedomm force-pushed the refactor/build-execute-split branch from 57db84e to 719073a Compare April 11, 2026 06:50
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.

1 participant