Skip to content

Conversation

@jordanstephens
Copy link
Member

@jordanstephens jordanstephens commented Dec 16, 2025

Description

This PR improves the onboarding experience for stacks. Special care is given to users with existing deployments to the implicit "beta" stack.

We are also care to support using stacks outside the working directory of the project with which the stack is associated.

  • We factor out a new stacks.Selector which uses the elicitations controller, and the stacks manager to implement stack with survey for defang subcommand invocation. This allows us to unify the codepaths for stack selection across agent tool calls and subcommand invocations
  • when instantiating provider, prompt for stack selection if a provider is not available through other means. It is important that we maintain support for existing flows.
  • use the project name to request previous deployments from fabric
  • use previous deployments to create a list of previously deployed stacks
  • if previous deployments don't have a Stack, assume the stack was "beta"
  • also read stacks out of the local .defang directory
  • if the stack list includes "beta", show a message introducing users to stacks and to the implicit "beta" stack
  • Only allow reading and writing stack files when the current working directory is equal to the target stacks directory. During cli invocations In these cases, it was possible to read and write stack files for project "foo" while in the directory for project "bar", leading to confusion. This possibility has been removed in this PR.
  • I think we can avoid reading the stack file at startup, and instead read it lazily. It's too difficult to reason about initialization order with all of the globals right now

Linked Issues

Checklist

  • I have performed a self-review of my code
  • I have added appropriate tests
  • I have updated the Defang CLI docs and/or README to reflect my changes, if necessary

Summary by CodeRabbit

  • New Features

    • Interactive stack selector with wizard-driven creation, stack import/promotion, and explicit stack-aware provider selection; "beta" remains the default.
  • Improvements

    • Provider resolution unified around stacks with warnings for deprecated flags/envs and clearer informational messages.
    • Stack listings merge remote and local entries and display deployment dates.
    • CLI flows consolidated for consistent non-interactive and interactive behavior.
  • Tests

    • Expanded mocks and coverage for stack selection, manager behaviors, provider flows, and remote listings.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 16, 2025

Walkthrough

Introduces stack-driven provider resolution: new stacks.Manager, stack selector/wizard, and elicitations Controller integration; adds getStack/importStack and updates newProvider to accept (ctx, ec, sm); rewires CLI, agent tools, loader/provider interfaces, many tests, and stack parameter serialization/loading.

Changes

Cohort / File(s) Summary
CLI commands & tests
src/cmd/cli/command/commands.go, src/cmd/cli/command/commands_test.go, src/cmd/cli/command/stack.go, src/cmd/cli/command/stack_test.go, src/cmd/cli/command/compose.go, src/cmd/cli/command/compose.go...
Add getStack, importStack, printProviderMismatchWarnings; change newProvider signature to accept elicitations.Controller and stacks.Manager; list stacks via manager; pass stack name into existing-deployments handling; tests updated with mock elicitations and mock stacks.
Stacks manager & tests
src/pkg/stacks/manager.go, src/pkg/stacks/manager_test.go, src/pkg/stacks/stacks.go
New Manager API: NewManager(fabric DeploymentLister, targetDir, projectName) (*manager, error); List(ctx) merges remote/local stacks; add ListLocal, ListRemote(ctx), RemoteStack, OutsideError, DeployedAt on StackListItem; add parameter load/save helpers; tests adapted.
Stack selector & tests
src/pkg/stacks/selector.go, src/pkg/stacks/selector_test.go
New stackSelector via NewSelector(ec, sm) with SelectStack(ctx) implementing elicitations-driven stack selection and "Create new stack" wizard flow; creation/loading routed through manager; extensive unit tests and test hooks (WizardInterface).
Provider & loader API
src/pkg/cli/client/provider.go, src/pkg/cli/client/caniuse.go, src/pkg/cli/client/byoc/baseclient.go, src/pkg/cli/client/playground.go
Add Provider.GetStackName() and Loader.OutsideWorkingDirectory() to interfaces; CanIUseProvider drops explicit stack param and uses provider.GetStackName(); BYOC/Playground expose stack accessor.
Loader refactor & mocks
src/pkg/cli/client/client.go, src/pkg/cli/client/mock.go, src/pkg/cli/compose/loader.go
Remove ProjectLoader type; Loader gains OutsideWorkingDirectory() heuristic; MockLoader implements OutsideWorkingDirectory().
Agent tools & call sites
src/pkg/agent/tools/*.go, src/pkg/agent/tools/*_test.go
Tool handlers switch from ProjectLoader to cliClient.Loader; construct stacks.NewManager(client, workingDir, projectName) (now returns error) and pass stack manager into provider preparer; CLIInterface.CanIUseProvider signature changed (provider first, stack removed); many call sites and tests updated.
Compose / estimate flows
src/cmd/cli/command/compose.go, src/cmd/cli/command/estimate.go
Use newProvider(ctx, ec, sm) and provider.GetStackName() in existing-deployments handling; add interactiveSelectProvider used when auto-detecting provider.
Globals, testdata & minor UX
src/cmd/cli/command/globals.go, src/testdata/sanity/.defang/beta, src/pkg/cli/client/byoc/gcp/byoc.go
global.Stack includes Name; testdata .defang/beta added; GCP error message augmented; deprecation warnings for --provider/env surfaced in stack flow.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI
    participant Elicit as Elicitations
    participant StackMgr as StacksManager
    participant Selector as StackSelector
    participant ProviderFactory as ProviderFactory

    User->>CLI: run command (e.g., compose up / estimate)
    CLI->>StackMgr: NewManager(fabricClient, targetDir, projectName)
    CLI->>Elicit: create controller
    CLI->>CLI: getStack(ctx, ec, sm)
    alt stack determinable (flag/env/file)
        CLI-->>CLI: return StackParameters, whence
    else interactive
        CLI->>Selector: SelectStack(ctx)
        Selector->>StackMgr: List(ctx)
        Selector->>Elicit: RequestEnum(stackOptions)
        User->>Elicit: choose existing or "Create new stack"
        alt create new
            Selector->>Elicit: collect wizard params
            Selector->>StackMgr: Create(params)
            Selector->>StackMgr: Load(createdName)
        else choose existing
            Selector->>StackMgr: Load(selectedName)
        end
        Selector-->>CLI: selected StackParameters
    end
    CLI->>ProviderFactory: newProvider(ctx, ec, sm)
    ProviderFactory-->>CLI: Provider (uses provider.GetStackName())
    CLI->>ProviderFactory: proceed with operation
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas to focus:
    • stacks.NewManager, List/ListRemote(ctx) merging and OutsideError handling (src/pkg/stacks/manager.go, manager_test.go).
    • Selector mapping of labels ↔ stack names, wizard creation and error paths (src/pkg/stacks/selector.go, selector_test.go).
    • Widespread signature/interface changes across agent tools and loader/provider APIs (src/pkg/agent/tools/*, src/pkg/cli/client/*).
    • CLI provider resolution, getStack/newProvider changes and deprecation messaging (src/cmd/cli/command/commands.go, compose.go, estimate.go).

Possibly related PRs

Suggested reviewers

  • lionello
  • KevyVo

Poem

🐰 I dug where "beta" used to hide,

I nudged a prompt and hopped inside,
I wove a stack with wizard hands,
Now provider knows its proper lands,
Hop—deploys and tests all glide.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.76% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: introducing stacks onboarding with support for 'remote' stacks, which is the primary objective of this comprehensive PR.
Linked Issues check ✅ Passed The PR fulfills the objectives in #1719: it provides an ergonomic onboarding path for existing projects, surfaces the implicit 'beta' stack through the new stacks.Selector, and guides users on stack usage during onboarding.
Out of Scope Changes check ✅ Passed All changes align with stacks onboarding objectives [#1719]. Modifications to provider resolution, stack manager integration, and CLI command wiring are directly supporting the new stack selection flow and remote deployments support.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jordan/remote-stacks

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.5.0)

level=warning msg="[linters_context] running gomodguard failed: unable to read module file go.mod: current working directory must have a go.mod file: if you are not using go modules it is suggested to disable this linter"
level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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

@jordanstephens jordanstephens marked this pull request as ready for review December 17, 2025 00:32
Copy link
Contributor

@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: 4

Caution

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

⚠️ Outside diff range comments (1)
src/pkg/agent/tools/estimate.go (1)

23-26: Duplicate error wrapping on line 24-25.

The error is wrapped twice with the same message. Line 24 assigns the wrapped error to err (which is then unused), and line 25 wraps it again.

 	if err != nil {
-		err = fmt.Errorf("failed to parse compose file: %w", err)
 		return "", fmt.Errorf("failed to parse compose file: %w", err)
 	}
🧹 Nitpick comments (8)
src/pkg/stacks/selector.go (2)

30-32: Clarify error message terminology.

The error message references "mcp client," which may not be immediately clear to all users. Consider whether this is the correct user-facing terminology or if a more general term would be appropriate.

-		return nil, errors.New("your mcp client does not support elicitations, use the 'select_stack' tool to choose a stack")
+		return nil, errors.New("interactive prompts are not supported in this environment; use the 'select_stack' tool to choose a stack")

61-63: Consider informing users when auto-creating their first stack.

When no stacks exist, the code automatically returns CreateNewStack without showing any message to the user. Consider adding a brief informational message so users understand they're creating their first stack.

 	if len(stackList) == 0 {
+		term.Info("No stacks found. You'll be guided through creating your first stack.\n")
 		return CreateNewStack, nil
 	}
src/pkg/agent/tools/provider.go (1)

38-45: Variable shadowing: err is redeclared with := instead of assigned with =.

On line 40, err is declared with := which shadows the err declared on line 37. While this works correctly since the error is checked immediately, it's inconsistent with line 47 which uses = for assignment.

 	if stack.Name == "" {
 		selector := stacks.NewSelector(pp.ec, pp.sm)
-		newStack, err := selector.SelectStack(ctx)
+		var newStack *stacks.StackParameters
+		newStack, err = selector.SelectStack(ctx)
 		if err != nil {
 			return nil, nil, fmt.Errorf("failed to setup stack: %w", err)
 		}
 		*stack = *newStack
 	}
src/pkg/stacks/selector_test.go (2)

120-123: Consider moving WizardInterface to production code.

Defining WizardInterface in the test file limits its reusability and makes it invisible to production code. If this interface is needed for dependency injection in the production stackSelector, it should be defined in selector.go alongside the selector implementation.


142-196: Test double duplicates production logic.

The testableStackSelector duplicates the SelectStack and elicitStackSelection logic from production code. If the production stackSelector implementation changes, these test methods may drift out of sync, leading to passing tests but broken production code.

Consider either:

  1. Exposing a WithWizard option on the production selector for testing
  2. Using composition to inject only the wizard while reusing production logic
src/cmd/cli/command/commands_test.go (1)

250-274: Mock implementation uses panic for invalid inputs.

The mockElicitationsClient.Request method panics on invalid schema properties (line 257-258) and multi-field requests (line 264-266). While this is acceptable for controlled test scenarios, it could make debugging test failures harder.

Consider returning an error instead of panicking for better test diagnostics:

 func (m *mockElicitationsClient) Request(ctx context.Context, req elicitations.Request) (elicitations.Response, error) {
 	properties, ok := req.Schema["properties"].(map[string]any)
 	if !ok || len(properties) == 0 {
-		panic("invalid schema properties")
+		return elicitations.Response{}, fmt.Errorf("invalid schema properties")
 	}
 	fields := make([]string, 0)
 	for field := range properties {
 		fields = append(fields, field)
 	}
 
 	if len(fields) > 1 {
-		panic("mockElicitationsClient only supports single-field requests")
+		return elicitations.Response{}, fmt.Errorf("mockElicitationsClient only supports single-field requests")
 	}
src/pkg/stacks/manager.go (1)

73-113: Consider handling OutsideError from ListLocal gracefully in List().

When ListLocal() returns an OutsideError (because the working directory differs from the target directory), the List() method currently propagates this error. However, remote stacks should still be listable even when outside the working directory. Consider handling OutsideError as a non-fatal case where you proceed with only remote stacks.

 func (sm *manager) List(ctx context.Context) ([]StackListItem, error) {
 	remoteStacks, err := sm.ListRemote(ctx)
 	if err != nil {
 		return nil, fmt.Errorf("failed to list remote stacks: %w", err)
 	}
 	localStacks, err := sm.ListLocal()
 	if err != nil {
-		return nil, fmt.Errorf("failed to list local stacks: %w", err)
+		// If we're outside the working directory, just use remote stacks
+		var outsideErr *OutsideError
+		if !errors.As(err, &outsideErr) {
+			return nil, fmt.Errorf("failed to list local stacks: %w", err)
+		}
+		localStacks = nil // No local stacks available when outside
 	}
 	// Merge remote and local stacks into a single list of type StackOption,

You'll also need to add "errors" to the imports.

src/cmd/cli/command/commands.go (1)

1342-1354: Consider handling the case when SetSelectedProvider fails more gracefully.

Currently, if SetSelectedProvider fails, only a debug message is logged (line 1350), but the success message on line 1352 is still printed. This could be confusing since it prints that the provider "is now the default" even though the save failed.

 func saveSelectedProvider(ctx context.Context, projectName string, providerID cliClient.ProviderID) {
 	if projectName == "" {
 		return
 	}
 	if err := global.Client.SetSelectedProvider(ctx, &defangv1.SetSelectedProviderRequest{
 		Project:  projectName,
 		Provider: providerID.Value(),
 	}); err != nil {
 		term.Debugf("Unable to save selected provider to defang server: %v", err)
-	} else {
-		term.Printf("%v is now the default provider for project %v and will auto-select next time if no other provider is specified. Use --provider=auto to reselect.", global.Stack.Provider, projectName)
+		return
 	}
+	term.Printf("%v is now the default provider for project %v and will auto-select next time if no other provider is specified. Use --provider=auto to reselect.", global.Stack.Provider, projectName)
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dc909e4 and e7bed74.

📒 Files selected for processing (31)
  • src/cmd/cli/command/commands.go (5 hunks)
  • src/cmd/cli/command/commands_test.go (15 hunks)
  • src/cmd/cli/command/compose.go (5 hunks)
  • src/cmd/cli/command/estimate.go (1 hunks)
  • src/cmd/cli/command/globals.go (2 hunks)
  • src/pkg/agent/tools/default_tool_cli.go (1 hunks)
  • src/pkg/agent/tools/deploy.go (2 hunks)
  • src/pkg/agent/tools/deploy_test.go (1 hunks)
  • src/pkg/agent/tools/destroy.go (2 hunks)
  • src/pkg/agent/tools/destroy_test.go (1 hunks)
  • src/pkg/agent/tools/estimate.go (1 hunks)
  • src/pkg/agent/tools/interfaces.go (1 hunks)
  • src/pkg/agent/tools/listConfig.go (1 hunks)
  • src/pkg/agent/tools/logs.go (3 hunks)
  • src/pkg/agent/tools/provider.go (1 hunks)
  • src/pkg/agent/tools/removeConfig.go (1 hunks)
  • src/pkg/agent/tools/services.go (1 hunks)
  • src/pkg/agent/tools/setConfig.go (1 hunks)
  • src/pkg/cli/client/byoc/baseclient.go (1 hunks)
  • src/pkg/cli/client/caniuse.go (2 hunks)
  • src/pkg/cli/client/client.go (0 hunks)
  • src/pkg/cli/client/mock.go (1 hunks)
  • src/pkg/cli/client/playground.go (1 hunks)
  • src/pkg/cli/client/provider.go (1 hunks)
  • src/pkg/cli/compose/loader.go (1 hunks)
  • src/pkg/stacks/manager.go (1 hunks)
  • src/pkg/stacks/manager_test.go (12 hunks)
  • src/pkg/stacks/selector.go (1 hunks)
  • src/pkg/stacks/selector_test.go (1 hunks)
  • src/pkg/stacks/stacks.go (2 hunks)
  • src/testdata/sanity/.defang/beta (1 hunks)
💤 Files with no reviewable changes (1)
  • src/pkg/cli/client/client.go
🧰 Additional context used
🧬 Code graph analysis (22)
src/pkg/cli/compose/loader.go (2)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/clouds/driver.go (1)
  • ProjectName (8-8)
src/pkg/cli/client/playground.go (1)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/agent/tools/provider.go (1)
src/pkg/stacks/selector.go (1)
  • NewSelector (22-27)
src/pkg/agent/tools/estimate.go (2)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/agent/tools/interfaces.go (1)
  • CLIInterface (14-32)
src/pkg/agent/tools/interfaces.go (2)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/agent/tools/destroy_test.go (2)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/agent/tools/setConfig.go (2)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/stacks/manager.go (1)
  • NewManager (32-71)
src/pkg/agent/tools/deploy_test.go (3)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/cli/client/grpc.go (1)
  • GrpcClient (19-24)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/cmd/cli/command/estimate.go (1)
src/pkg/cli/client/provider_id.go (3)
  • ProviderID (10-10)
  • ProviderAWS (15-15)
  • ProviderGCP (17-17)
src/pkg/agent/tools/logs.go (5)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/agent/tools/default_tool_cli.go (1)
  • StackConfig (19-22)
src/pkg/mcp/tools/tools.go (1)
  • StackConfig (14-17)
src/pkg/stacks/manager.go (1)
  • NewManager (32-71)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/agent/tools/listConfig.go (4)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/agent/tools/interfaces.go (1)
  • CLIInterface (14-32)
src/pkg/stacks/manager.go (1)
  • NewManager (32-71)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (26-33)
src/pkg/agent/tools/removeConfig.go (2)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/stacks/manager.go (1)
  • NewManager (32-71)
src/pkg/agent/tools/destroy.go (3)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/agent/tools/interfaces.go (1)
  • CLIInterface (14-32)
src/pkg/stacks/manager.go (1)
  • NewManager (32-71)
src/pkg/agent/tools/default_tool_cli.go (1)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/cmd/cli/command/commands_test.go (4)
src/pkg/elicitations/elicitations.go (3)
  • Request (25-28)
  • Response (30-33)
  • NewController (35-40)
src/pkg/stacks/manager.go (1)
  • Manager (14-18)
src/pkg/cli/client/provider_id.go (4)
  • ProviderID (10-10)
  • ProviderAWS (15-15)
  • ProviderDO (16-16)
  • ProviderGCP (17-17)
src/pkg/stacks/stacks.go (5)
  • List (97-99)
  • StackListItem (87-95)
  • Load (227-229)
  • StackParameters (18-25)
  • Create (36-38)
src/pkg/cli/client/caniuse.go (2)
src/pkg/cli/client/client.go (1)
  • FabricClient (11-38)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/agent/tools/deploy.go (3)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/stacks/manager.go (1)
  • NewManager (32-71)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/agent/tools/services.go (3)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/stacks/manager.go (1)
  • NewManager (32-71)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (26-33)
src/cmd/cli/command/globals.go (3)
src/pkg/stacks/stacks.go (1)
  • StackParameters (18-25)
src/pkg/cli/client/provider_id.go (1)
  • ProviderAuto (13-13)
src/pkg/modes/modes.go (2)
  • Mode (12-12)
  • ModeUnspecified (15-15)
src/cmd/cli/command/commands.go (11)
src/pkg/elicitations/survey.go (1)
  • NewSurveyClient (19-25)
src/pkg/elicitations/elicitations.go (3)
  • NewController (35-40)
  • Client (16-18)
  • Controller (8-14)
src/pkg/stacks/manager.go (2)
  • NewManager (32-71)
  • Manager (14-18)
src/pkg/stacks/stacks.go (2)
  • StackParameters (18-25)
  • Load (227-229)
src/pkg/cli/client/provider.go (2)
  • Provider (43-70)
  • Loader (72-76)
src/protos/io/defang/v1/fabric.pb.go (10)
  • Provider (28-28)
  • Provider (66-68)
  • Provider (70-72)
  • Provider (79-81)
  • GetSelectedProviderRequest (631-636)
  • GetSelectedProviderRequest (649-649)
  • GetSelectedProviderRequest (664-666)
  • SetSelectedProviderRequest (736-742)
  • SetSelectedProviderRequest (755-755)
  • SetSelectedProviderRequest (770-772)
src/pkg/cli/client/provider_id.go (3)
  • ProviderAuto (13-13)
  • ProviderDefang (14-14)
  • ProviderID (10-10)
src/pkg/stacks/selector.go (1)
  • NewSelector (22-27)
src/pkg/cli/connect.go (1)
  • NewProvider (35-49)
src/pkg/cli/client/account_info.go (1)
  • AccountInfo (5-10)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/cmd/cli/command/compose.go (4)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/elicitations/survey.go (1)
  • NewSurveyClient (19-25)
src/pkg/elicitations/elicitations.go (2)
  • NewController (35-40)
  • Client (16-18)
src/pkg/stacks/manager.go (1)
  • NewManager (32-71)
src/pkg/stacks/manager.go (1)
src/pkg/stacks/stacks.go (9)
  • List (97-99)
  • StackListItem (87-95)
  • Load (227-229)
  • StackParameters (18-25)
  • Create (36-38)
  • ListInDirectory (101-135)
  • ReadInDirectory (213-225)
  • LoadInDirectory (231-239)
  • CreateInDirectory (40-84)
🪛 GitHub Actions: Go package
src/cmd/cli/command/commands.go

[error] 1408-1408: Panic: runtime error: invalid memory address or nil pointer dereference

🔇 Additional comments (42)
src/testdata/sanity/.defang/beta (1)

1-2: Test data file format is correct and properly structured.

The key=value format matches the stack file parsing implementation. Godotenv loads environment variables from .env files, and the codebase's Parse() function uses godotenv.Parse() to handle the format. The test file correctly uses standard environment variable names (DEFANG_PROVIDER, AWS_REGION) that the stack loader expects, and the file location at .defang/beta follows the established naming convention. This test data properly supports stack loading scenarios.

src/pkg/stacks/selector.go (1)

84-88: LGTM! Clear onboarding message for beta stack.

The informational message about the implicit "beta" stack effectively guides users through the migration to explicit stacks. The conditional message when only the beta stack exists is particularly helpful.

src/pkg/cli/client/mock.go (1)

175-177: LGTM! Mock implementation is consistent.

The OutsideWorkingDirectory() method correctly returns false for the mock loader, which is appropriate for test scenarios.

src/cmd/cli/command/estimate.go (1)

30-38: LGTM! Provider selection flow is consistent.

The interactive provider selection correctly updates global.Stack.Provider and aligns with the new stack-centric provider resolution flow.

src/pkg/cli/client/provider.go (1)

69-69: LGTM! Interface extensions support stack-aware operations.

The new GetStackName() method on Provider and OutsideWorkingDirectory() on Loader cleanly enable stack-aware provider initialization and directory-context checks across the codebase.

Also applies to: 75-75

src/pkg/cli/client/playground.go (1)

21-21: LGTM! Default stack name provides backward compatibility.

The GetStackName() method returns "beta" as the default when PulumiStack is empty, which aligns with the PR's goal of surfacing the implicit "beta" stack for existing projects.

Also applies to: 26-31

src/pkg/cli/client/byoc/baseclient.go (1)

74-76: LGTM! Stack name accessor is consistent.

The GetStackName() method provides a clean accessor for the Pulumi stack name, consistent with the PlaygroundProvider implementation.

src/pkg/stacks/stacks.go (1)

94-94: Initialize DeployedAt field in ListInDirectory function.

The DeployedAt field in StackListItem struct is not explicitly initialized in ListInDirectory (lines 126-131). While local stacks have no deployment information, the field should be explicitly set to time.Time{} for consistency with how local stacks are handled in manager.go line 93, rather than relying on implicit zero value initialization.

src/pkg/cli/client/caniuse.go (1)

10-23: LGTM!

The signature change correctly centralizes stack name retrieval via provider.GetStackName(), eliminating the need for callers to pass the stack name separately. This aligns well with the stack-centric refactor and reduces the chance of inconsistency between the provider's internal stack and the passed parameter.

src/pkg/agent/tools/removeConfig.go (2)

21-21: LGTM on signature update.

The change from ProjectLoader to Loader aligns with the broader refactor to use the unified Loader interface.


28-31: No action needed. The code correctly handles an optional params.ProjectName. The stack manager can be created with an empty project name, and the actual project name is properly loaded via LoadProjectNameWithFallback before being used in operations. This pattern is consistent with other parts of the codebase (e.g., "list --remote" uses empty project names).

src/pkg/agent/tools/estimate.go (1)

20-20: Signature change aligns with the Loader interface migration.

The parameter type change from cliClient.ProjectLoader to cliClient.Loader is consistent with the broader refactoring across the codebase.

src/pkg/agent/tools/destroy_test.go (1)

60-66: Mock signature correctly updated to match the interface.

The CanIUseProvider signature change (removing stackName parameter) aligns with the updated CLIInterface in interfaces.go. The stack name is now retrieved via provider.GetStackName() in the actual implementation.

src/pkg/agent/tools/listConfig.go (2)

20-20: Signature change consistent with the loader migration.

The update from cliClient.ProjectLoader to cliClient.Loader aligns with the repository-wide interface consolidation.


27-31: Stack manager initialization with proper error handling.

The updated stacks.NewManager call correctly passes the client, working directory, and project name. Error handling is appropriately propagated.

src/cmd/cli/command/globals.go (2)

98-98: Explicit field initialization improves clarity.

Adding the explicit Name: "" initialization makes the default state clear and aligns with the updated StackParameters struct.


257-261: Use errors.Is(err, os.ErrNotExist) instead of os.IsNotExist to properly detect missing files.

os.IsNotExist does not unwrap wrapped errors. If godotenv.Read wraps the underlying file error, the current check will fail to detect missing files. Use errors.Is(err, os.ErrNotExist) instead, which properly unwraps and checks the error chain.

// Current code (unreliable with wrapped errors)
if os.IsNotExist(err) {
    return nil
}

// Recommended fix
if errors.Is(err, os.ErrNotExist) {
    return nil
}
src/pkg/agent/tools/setConfig.go (2)

21-21: Signature change aligns with the Loader migration.

The update from cliClient.ProjectLoader to cliClient.Loader is consistent with the other tool handlers in this PR.


28-31: Stack manager initialization follows established pattern.

The stacks.NewManager call and error handling are consistent with other tool handlers like listConfig.go and destroy.go.

src/pkg/agent/tools/destroy.go (3)

20-20: Signature change consistent with the Loader migration.

The parameter type change from cliClient.ProjectLoader to cliClient.Loader aligns with the broader interface consolidation.


27-35: Stack manager and provider setup follow established patterns.

The stacks.NewManager initialization with error handling, followed by NewProviderPreparer and SetupProvider, is consistent with other tool handlers in this PR.


42-42: CanIUseProvider call updated to match new interface.

The stackName parameter has been removed from the call, aligning with the updated CLIInterface where the stack name is now retrieved internally via provider.GetStackName().

src/pkg/stacks/selector_test.go (2)

50-72: Good nil-safe mock implementation.

The MockStacksManager.List and MockStacksManager.Load methods correctly handle nil returns with type assertions, preventing panics when tests return nil values. This is a robust pattern for testify mocks.


79-118: Comprehensive test coverage for stack selection.

The test suite thoroughly covers:

  • Existing stack selection
  • New stack creation via wizard
  • Auto-create when no stacks exist
  • Error handling for all failure points (elicitations disabled, list errors, elicitation errors, load errors, wizard errors, create errors)

The use of AssertExpectations and AssertNotCalled ensures mock interactions are verified.

src/cmd/cli/command/commands_test.go (3)

276-295: Mock implementation looks correct for test scenarios.

The mockStacksManager implements the required List, Load, and Create methods. The embedded stacks.Manager interface will cause panics if unimplemented methods are called, which is acceptable for these specific tests.


377-397: Test correctly exercises interactive stack/provider selection.

The test properly sets up mocks for the elicitations client and stacks manager, then verifies that newProvider uses them for interactive selection and saves the selected provider.


220-226: Good defensive nil check for map initialization.

The nil check before map assignment prevents panics when savedProvider hasn't been initialized, improving test reliability.

src/pkg/agent/tools/services.go (2)

30-34: Stack manager creation with proper error handling.

The two-step pattern (create manager, then provider preparer) with explicit error handling is clean and aligns with the updated NewManager signature that now returns an error.


23-23: Signature aligns with Loader interface refactor.

The change from ProjectLoader to Loader is consistent with the interface simplification across the codebase. The Loader interface provides the necessary methods (LoadProject, LoadProjectName, OutsideWorkingDirectory).

src/pkg/agent/tools/default_tool_cli.go (1)

30-32: Simplified CanIUseProvider signature removes redundant parameter.

The stackName parameter was removed because cliClient.CanIUseProvider now retrieves the stack name directly from provider.GetStackName(). This reduces parameter duplication and keeps stack information encapsulated within the provider.

src/pkg/agent/tools/logs.go (3)

48-56: Consistent stack manager and provider setup pattern.

The stack manager creation and provider setup follows the same pattern as other tools (services.go), maintaining consistency across the codebase.


65-68: CanIUseProvider call uses updated interface signature.

The call correctly uses the simplified signature without the stackName parameter. The DefaultToolCLI.CanIUseProvider wrapper handles the parameter reordering to match the underlying cliClient.CanIUseProvider function.


25-25: Signature aligns with Loader interface refactor.

Consistent with the changes in services.go and other tools.

src/cmd/cli/command/compose.go (2)

223-252: LGTM - handleExistingDeployments correctly handles stack name parameter.

The updated filtering logic properly compares dep.Provider with accountInfo.Provider.Value() and correctly defaults to "beta" when stackName is empty, aligning with the PR objective to surface the implicit "beta" stack for existing projects.


566-577: LGTM - Provider construction properly integrated with elicitations and stack manager.

The new flow correctly wires up the elicitation client, controller, and stack manager before calling newProvider. This aligns with the stack-centric provider resolution changes.

src/pkg/agent/tools/deploy.go (1)

47-57: LGTM - Stack manager integration and error handling look correct.

The stack manager is properly created with error handling, and the CanIUseProvider call uses the updated signature correctly.

src/pkg/stacks/manager.go (1)

127-164: LGTM - ListRemote correctly handles empty stack names and duplicate deployments.

The logic properly defaults empty stack names to "beta" (line 139) and correctly avoids overwriting existing entries since deployments are sorted by deployed_at desc (lines 143-144).

src/pkg/stacks/manager_test.go (3)

668-675: Test expects List() to fail when directories differ - verify this is intended behavior.

This test expects List() to fail when the working directory differs from the target directory. However, if List() should gracefully fall back to only remote stacks when ListLocal() fails with OutsideError, this test would need to be updated accordingly. Please verify the intended behavior.


18-31: LGTM - Mock client properly implements DeploymentLister interface.

The mock correctly returns configured deployments or an error for testing purposes.


340-391: LGTM - Comprehensive tests for ListRemote functionality.

Good coverage of the remote stack listing including timestamp verification.

src/cmd/cli/command/commands.go (2)

1258-1325: LGTM - Stack resolution logic with proper precedence.

The getStack function correctly implements the provider/stack precedence rules:

  1. Stack file (via --stack flag)
  2. Command line flag (--provider)
  3. Environment variable (DEFANG_PROVIDER)
  4. Previously used provider (stored preference)
  5. Interactive selection

The fallback to cliClient.ProviderDefang in non-interactive mode is appropriate.


531-546: LGTM - whoami command properly integrates with new provider flow.

The whoami command correctly creates the elicitation client, controller, and stack manager before calling newProvider.

Copy link
Contributor

@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: 2

Caution

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

⚠️ Outside diff range comments (2)
src/pkg/stacks/manager_test.go (2)

253-268: Test may not verify the intended behavior.

This test doesn't call t.Chdir(tmpDir), so the manager will have outside = true and Load will return an OutsideError rather than a "stack not found" error. If the intent is to test loading a nonexistent stack file, you need to change to the target directory first.

 func TestManager_LoadNonexistentStack(t *testing.T) {
 	// Create a temporary directory for testing
 	tmpDir := t.TempDir()

+	// Change to temp directory so working directory matches target directory
+	t.Chdir(tmpDir)
+
 	mockClient := &mockFabricClient{}
 	manager, err := NewManager(mockClient, tmpDir, "test-project")

270-305: Test may not verify the intended validation behavior.

Similar to TestManager_LoadNonexistentStack, this test is missing t.Chdir(tmpDir). The Create method will return OutsideError before reaching the stack name validation logic.

 func TestManager_CreateInvalidStackName(t *testing.T) {
 	// Create a temporary directory for testing
 	tmpDir := t.TempDir()

+	// Change to temp directory so working directory matches target directory
+	t.Chdir(tmpDir)
+
 	mockClient := &mockFabricClient{}
 	manager, err := NewManager(mockClient, tmpDir, "test-project")
♻️ Duplicate comments (2)
src/pkg/agent/tools/interfaces.go (1)

15-15: LGTM! Parameter order now matches implementation.

The updated signature resolves the inconsistency flagged in previous reviews. The interface now correctly matches the actual CanIUseProvider implementation in src/pkg/cli/client/caniuse.go, with provider before projectName and stackName removed.

src/pkg/agent/tools/deploy_test.go (1)

80-83: LGTM! Mock signature updated consistently.

The mock now correctly implements the updated CanIUseProvider interface with the proper parameter order.

🧹 Nitpick comments (6)
src/pkg/stacks/selector_test.go (1)

120-196: Consider moving WizardInterface and testableStackSelector to production code or use a different testing approach.

The testableStackSelector duplicates the entire SelectStack and elicitStackSelection logic from the production stackSelector. This creates maintenance burden since any changes to the production code must be manually synchronized to the test harness.

Consider one of these alternatives:

  1. Move WizardInterface to selector.go and make wizard injection a first-class feature of stackSelector
  2. Use a functional option pattern to inject the wizard during construction
  3. Test the production stackSelector directly by mocking only the external dependencies

Example refactor for option 1 in selector.go:

type WizardInterface interface {
    CollectParameters(ctx context.Context) (*StackParameters, error)
}

type stackSelector struct {
    ec     elicitations.Controller
    sm     Manager
    wizard WizardInterface
}

func NewSelector(ec elicitations.Controller, sm Manager, wizard WizardInterface) *stackSelector {
    return &stackSelector{
        ec:     ec,
        sm:     sm,
        wizard: wizard,
    }
}
src/pkg/stacks/manager.go (1)

108-112: Stack list order is non-deterministic due to map iteration.

The returned slice order depends on Go's random map iteration order. If consistent ordering is desired for UI display (e.g., alphabetical or by deployment time), consider sorting the result.

 	stackList := make([]StackListItem, 0, len(stackMap))
 	for _, stack := range stackMap {
 		stackList = append(stackList, stack)
 	}
+	// Sort by name for consistent ordering
+	sort.Slice(stackList, func(i, j int) bool {
+		return stackList[i].Name < stackList[j].Name
+	})
 	return stackList, nil
src/pkg/agent/tools/removeConfig.go (1)

28-31: Consider extracting stack manager creation into a helper.

The stack manager creation pattern stacks.NewManager(client, params.WorkingDirectory, params.ProjectName) with error handling is repeated across multiple tool files (removeConfig.go, listConfig.go, destroy.go). A shared helper function could reduce duplication and ensure consistency.

Example helper:

func createStackManager(client cliClient.FabricClient, workingDirectory, projectName string) (stacks.Manager, error) {
	sm, err := stacks.NewManager(client, workingDirectory, projectName)
	if err != nil {
		return nil, fmt.Errorf("failed to create stack manager: %w", err)
	}
	return sm, nil
}
src/cmd/cli/command/commands_test.go (1)

265-289: mockElicitationsClient implementation has intentional single-field limitation.

The panic on multi-field requests is acceptable for focused unit tests. Consider adding a brief comment explaining this is a test limitation.

 type mockElicitationsClient struct {
 	responses map[string]string
 }

+// Request implements elicitations.Client for testing.
+// Note: Only supports single-field schemas; panics otherwise.
 func (m *mockElicitationsClient) Request(ctx context.Context, req elicitations.Request) (elicitations.Response, error) {
src/cmd/cli/command/commands.go (2)

1380-1393: getPreviouslyUsedProvider error propagation may disrupt the flow.

When GetSelectedProvider fails (e.g., network issues), the error is returned and stops the entire provider selection. Consider whether a graceful fallback to interactive selection would be more user-friendly.

 func getPreviouslyUsedProvider(ctx context.Context, projectName string) (cliClient.ProviderID, error) {
 	if projectName == "" {
 		return cliClient.ProviderAuto, nil
 	}
 	resp, err := global.Client.GetSelectedProvider(ctx, &defangv1.GetSelectedProviderRequest{
 		Project: projectName,
 	})
 	if err != nil {
-		return cliClient.ProviderAuto, fmt.Errorf("unable to get selected provider: %w", err)
+		term.Debugf("Unable to get selected provider: %v", err)
+		return cliClient.ProviderAuto, nil // Fall back to interactive selection
 	}
 	var providerID cliClient.ProviderID
 	providerID.SetValue(resp.Provider)
 	return providerID, nil
 }

1473-1488: Redundant stacks.NewManager calls with different error handling paths.

Both branches (lines 1475 and 1484) create a NewManager but the err variable from the os.Getwd() call on line 1480 shadows the outer err, which is fine. However, the error from NewManager on line 1484 shadows err from line 1480, which is also fine but could be clearer.

Consider extracting the common error-handling pattern:

 	elicitationsClient := elicitations.NewSurveyClient(os.Stdin, os.Stdout, os.Stderr)
 	ec := elicitations.NewController(elicitationsClient)
-	var sm stacks.Manager
-	if outside {
-		sm, err = stacks.NewManager(global.Client, "", projectName)
-		if err != nil {
-			return nil, fmt.Errorf("failed to create stack manager: %w", err)
-		}
-	} else {
-		wd, err := os.Getwd()
-		if err != nil {
-			return nil, fmt.Errorf("failed to get working directory: %w", err)
-		}
-		sm, err = stacks.NewManager(global.Client, wd, projectName)
-		if err != nil {
-			return nil, fmt.Errorf("failed to create stack manager: %w", err)
-		}
-	}
+	targetDir := ""
+	if !outside {
+		targetDir, err = os.Getwd()
+		if err != nil {
+			return nil, fmt.Errorf("failed to get working directory: %w", err)
+		}
+	}
+	sm, err := stacks.NewManager(global.Client, targetDir, projectName)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create stack manager: %w", err)
+	}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4bdccea and d0e373f.

📒 Files selected for processing (31)
  • src/cmd/cli/command/commands.go (5 hunks)
  • src/cmd/cli/command/commands_test.go (15 hunks)
  • src/cmd/cli/command/compose.go (5 hunks)
  • src/cmd/cli/command/estimate.go (1 hunks)
  • src/cmd/cli/command/globals.go (2 hunks)
  • src/pkg/agent/tools/default_tool_cli.go (1 hunks)
  • src/pkg/agent/tools/deploy.go (2 hunks)
  • src/pkg/agent/tools/deploy_test.go (1 hunks)
  • src/pkg/agent/tools/destroy.go (2 hunks)
  • src/pkg/agent/tools/destroy_test.go (1 hunks)
  • src/pkg/agent/tools/estimate.go (1 hunks)
  • src/pkg/agent/tools/interfaces.go (1 hunks)
  • src/pkg/agent/tools/listConfig.go (1 hunks)
  • src/pkg/agent/tools/logs.go (3 hunks)
  • src/pkg/agent/tools/provider.go (1 hunks)
  • src/pkg/agent/tools/removeConfig.go (1 hunks)
  • src/pkg/agent/tools/services.go (1 hunks)
  • src/pkg/agent/tools/setConfig.go (1 hunks)
  • src/pkg/cli/client/byoc/baseclient.go (1 hunks)
  • src/pkg/cli/client/caniuse.go (2 hunks)
  • src/pkg/cli/client/client.go (0 hunks)
  • src/pkg/cli/client/mock.go (1 hunks)
  • src/pkg/cli/client/playground.go (1 hunks)
  • src/pkg/cli/client/provider.go (1 hunks)
  • src/pkg/cli/compose/loader.go (1 hunks)
  • src/pkg/stacks/manager.go (1 hunks)
  • src/pkg/stacks/manager_test.go (12 hunks)
  • src/pkg/stacks/selector.go (1 hunks)
  • src/pkg/stacks/selector_test.go (1 hunks)
  • src/pkg/stacks/stacks.go (2 hunks)
  • src/testdata/sanity/.defang/beta (1 hunks)
💤 Files with no reviewable changes (1)
  • src/pkg/cli/client/client.go
✅ Files skipped from review due to trivial changes (1)
  • src/testdata/sanity/.defang/beta
🚧 Files skipped from review as they are similar to previous changes (11)
  • src/pkg/cli/client/caniuse.go
  • src/pkg/agent/tools/services.go
  • src/pkg/agent/tools/estimate.go
  • src/pkg/cli/client/mock.go
  • src/cmd/cli/command/estimate.go
  • src/pkg/agent/tools/setConfig.go
  • src/pkg/stacks/selector.go
  • src/pkg/cli/client/provider.go
  • src/pkg/cli/compose/loader.go
  • src/pkg/cli/client/playground.go
  • src/pkg/stacks/stacks.go
🧰 Additional context used
🧬 Code graph analysis (15)
src/cmd/cli/command/globals.go (3)
src/pkg/stacks/stacks.go (1)
  • StackParameters (18-25)
src/pkg/cli/client/provider_id.go (1)
  • ProviderAuto (13-13)
src/pkg/modes/modes.go (2)
  • Mode (12-12)
  • ModeUnspecified (15-15)
src/pkg/agent/tools/deploy.go (6)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/agent/tools/default_tool_cli.go (1)
  • StackConfig (19-22)
src/pkg/mcp/mcp_server.go (1)
  • StackConfig (48-48)
src/pkg/stacks/manager.go (1)
  • NewManager (32-71)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (26-33)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/agent/tools/listConfig.go (3)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/stacks/manager.go (1)
  • NewManager (32-71)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (26-33)
src/pkg/agent/tools/provider.go (1)
src/pkg/stacks/selector.go (1)
  • NewSelector (22-27)
src/pkg/agent/tools/logs.go (2)
src/pkg/agent/tools/interfaces.go (1)
  • CLIInterface (14-32)
src/pkg/stacks/manager.go (1)
  • NewManager (32-71)
src/cmd/cli/command/compose.go (4)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/elicitations/survey.go (1)
  • NewSurveyClient (19-25)
src/pkg/elicitations/elicitations.go (2)
  • NewController (35-40)
  • Client (16-18)
src/pkg/stacks/manager.go (1)
  • NewManager (32-71)
src/pkg/stacks/manager.go (1)
src/pkg/stacks/stacks.go (9)
  • List (97-99)
  • StackListItem (87-95)
  • Load (227-229)
  • StackParameters (18-25)
  • Create (36-38)
  • ListInDirectory (101-135)
  • ReadInDirectory (213-225)
  • LoadInDirectory (231-239)
  • CreateInDirectory (40-84)
src/pkg/agent/tools/deploy_test.go (2)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/agent/tools/default_tool_cli.go (3)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/cli/client/grpc.go (1)
  • GrpcClient (19-26)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/stacks/manager_test.go (4)
src/pkg/stacks/manager.go (1)
  • NewManager (32-71)
src/pkg/stacks/stacks.go (5)
  • List (97-99)
  • Load (227-229)
  • StackParameters (18-25)
  • StackListItem (87-95)
  • Directory (29-29)
src/protos/io/defang/v1/fabric.pb.go (2)
  • Provider_AWS (33-33)
  • Provider_GCP (35-35)
src/pkg/cli/client/provider_id.go (1)
  • ProviderAWS (15-15)
src/cmd/cli/command/commands_test.go (8)
src/pkg/elicitations/elicitations.go (3)
  • Request (25-28)
  • Response (30-33)
  • NewController (35-40)
src/pkg/stacks/manager.go (1)
  • Manager (14-18)
src/pkg/cli/client/provider_id.go (4)
  • ProviderID (10-10)
  • ProviderAWS (15-15)
  • ProviderDO (16-16)
  • ProviderGCP (17-17)
src/pkg/stacks/stacks.go (5)
  • List (97-99)
  • StackListItem (87-95)
  • Load (227-229)
  • StackParameters (18-25)
  • Create (36-38)
src/pkg/clouds/aws/common.go (1)
  • Region (15-15)
src/pkg/crun/common.go (1)
  • Region (12-12)
src/pkg/cli/client/byoc/aws/byoc.go (1)
  • StsClient (57-57)
src/pkg/clouds/gcp/account.go (1)
  • FindGoogleDefaultCredentials (16-16)
src/cmd/cli/command/commands.go (7)
src/pkg/elicitations/survey.go (1)
  • NewSurveyClient (19-25)
src/pkg/elicitations/elicitations.go (3)
  • NewController (35-40)
  • Client (16-18)
  • Controller (8-14)
src/pkg/stacks/manager.go (2)
  • NewManager (32-71)
  • Manager (14-18)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/cli/client/provider_id.go (3)
  • ProviderAuto (13-13)
  • ProviderDefang (14-14)
  • ProviderID (10-10)
src/pkg/stacks/selector.go (1)
  • NewSelector (22-27)
src/pkg/cli/connect.go (1)
  • NewProvider (62-76)
src/pkg/agent/tools/interfaces.go (3)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/cli/client/grpc.go (1)
  • GrpcClient (19-26)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/stacks/selector_test.go (7)
src/pkg/stacks/stacks.go (5)
  • List (97-99)
  • StackListItem (87-95)
  • Load (227-229)
  • StackParameters (18-25)
  • Create (36-38)
src/pkg/stacks/selector.go (2)
  • CreateNewStack (15-15)
  • NewSelector (22-27)
src/pkg/agent/tools/provider.go (1)
  • CreateNewStack (13-13)
src/pkg/cli/client/provider_id.go (1)
  • ProviderAWS (15-15)
src/pkg/modes/modes.go (3)
  • Mode (12-12)
  • ModeBalanced (17-17)
  • ModeAffordable (16-16)
src/pkg/elicitations/elicitations.go (1)
  • Controller (8-14)
src/pkg/stacks/manager.go (1)
  • Manager (14-18)
src/pkg/agent/tools/destroy_test.go (3)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/cli/client/grpc.go (1)
  • GrpcClient (19-26)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Analyze (go)
  • GitHub Check: go-test
🔇 Additional comments (39)
src/pkg/stacks/selector_test.go (4)

79-118: LGTM!

Well-structured test for the existing stack selection flow with proper mock setup and assertion of expectations.


198-293: LGTM!

Good coverage of the stack creation flow, including the automatic creation path when no existing stacks are found.


295-497: LGTM!

Comprehensive error path coverage including elicitations not supported, list failures, elicitation errors, load errors, wizard errors, and creation errors.


499-536: LGTM!

Good use of subtests to verify both the multi-stack selection and empty stack list scenarios for elicitStackSelection.

src/pkg/stacks/manager.go (4)

115-120: LGTM!

Clean implementation with proper access control via OutsideError.


127-164: LGTM!

Good handling of the implicit "beta" stack for backwards compatibility with existing deployments, and proper deduplication logic that preserves the most recent deployment's data.


166-173: LGTM!

Well-designed custom error type that enables type-based error handling and provides clear context about the directory mismatch.


175-195: LGTM!

Consistent access control pattern across Load and Create methods with proper delegation to directory-specific implementations.

src/pkg/stacks/manager_test.go (3)

18-31: LGTM!

Clean mock implementation of DeploymentLister interface for testing remote stack listing functionality.


340-584: LGTM!

Comprehensive test coverage for remote stack listing including beta stack handling, duplicate deployment merging, and the merged local/remote list behavior.


586-721: LGTM!

Thorough testing of the directory constraint behavior, verifying that local operations work when directories match, fail with OutsideError when they differ, and that remote operations work regardless of directory state.

src/pkg/cli/client/byoc/baseclient.go (2)

74-76: LGTM!

Simple accessor method that exposes the PulumiStack value, aligning with the broader stack-name propagation pattern introduced in this PR.


116-122: Remove this comment—the concern is based on incorrect premise.

GetProjectDomain is newly added code in the latest commit, not a modification of existing logic. The method never included stack-name prefixing; the simple domain normalization is the intended design. For delegated/custom domains, infrastructure stack names should not be embedded.

Likely an incorrect or invalid review comment.

src/cmd/cli/command/globals.go (1)

98-98: LGTM!

Initialization of the Name field in StackParameters aligns with the struct definition in stacks.go.

src/pkg/agent/tools/provider.go (1)

38-45: LGTM! Clean refactor to selector-based stack selection.

The new flow properly delegates stack selection to the stacks.Selector when no stack name is provided, simplifying the provider preparer's responsibilities.

src/pkg/agent/tools/destroy_test.go (1)

60-66: LGTM! Mock signature aligned with interface.

The mock correctly implements the updated CanIUseProvider signature.

src/pkg/agent/tools/listConfig.go (1)

27-31: LGTM! Consistent with the stack-based initialization refactor.

The updated stack manager creation aligns with the broader PR changes. Note: This pattern is duplicated in removeConfig.go and destroy.go - see comment in removeConfig.go about a potential helper function.

src/pkg/agent/tools/destroy.go (1)

42-45: LGTM! Provider validation call updated correctly.

The CanIUseProvider call now uses the updated signature with the correct parameter order.

src/cmd/cli/command/compose.go (3)

223-229: LGTM! Improved deployment location tracking.

The updated signature parameterizes the stack name, and the provider consistency check at line 225-227 properly validates that deployments match both provider and location (account/region).


237-251: LGTM! Effective onboarding for implicit "beta" stack.

The automatic defaulting to "beta" and stack creation aligns well with the PR objectives to surface the implicit "beta" stack for existing projects. This provides a smooth onboarding path when deploying to a new location.


566-577: LGTM! Provider construction updated to new flow.

The refactored provider construction properly instantiates the elicitations controller and stack manager before calling newProvider, consistent with the stack-centric provider resolution introduced across the PR.

src/pkg/agent/tools/default_tool_cli.go (1)

30-32: Signature update aligns with the new stack-centric provider flow.

The removal of stackName parameter is correct since CanIUseProvider now retrieves the stack name via provider.GetStackName() internally, as shown in the caniuse.go snippet where Stack: provider.GetStackName() is used in the request.

src/pkg/agent/tools/logs.go (3)

25-25: Interface type change to cliClient.Loader is consistent with the refactored loader interface.

The Loader interface provides LoadProject, LoadProjectName, and OutsideWorkingDirectory methods, which aligns with the new stack-aware flow.


48-51: Stack manager initialization with proper error handling.

The updated NewManager signature correctly passes client, params.WorkingDirectory, and params.ProjectName. Error handling is appropriately added.


65-65: CanIUseProvider call updated to new signature.

The parameter order (ctx, client, provider, projectName, 0) correctly matches the updated interface.

src/pkg/agent/tools/deploy.go (3)

25-25: Interface type change is consistent with the refactor.


47-50: Stack manager initialization matches the new pattern.


47-57: Potential inconsistency: params.ProjectName vs project.Name.

The stack manager is created with params.ProjectName (line 47), but CanIUseProvider uses project.Name from the loaded compose file (line 57). If these differ, the stack context and the CanIUse check may reference different projects.

Consider verifying that params.ProjectName and project.Name are expected to be consistent, or whether the stack manager should be created after loading the project to use the authoritative project name:

+	// Note: project.Name from compose may override params.ProjectName
+	// Ensure consistency for stack manager and CanIUseProvider
src/cmd/cli/command/commands_test.go (5)

236-241: Defensive initialization of savedProvider map.

Good addition to prevent nil map panics when SetSelectedProvider is called before any provider is saved.


243-247: New ListDeployments mock supports the stack manager's remote listing functionality.


291-310: mockStacksManager implementation correctly satisfies the stacks.Manager interface.

The Load method returns a StackParameters with the expectedProvider, enabling provider-specific test scenarios.


339-339: Updated newProvider call signature with nil elicitations controller and stack manager.

When ec and sm are nil, the function should fall back to non-interactive defaults (playground provider). Ensure newProvider handles nil gracefully.


513-514: Clearing DIGITALOCEAN_TOKEN and manually setting global.Stack.Provider for test isolation.

These additions ensure environment variables from previous tests don't interfere. The manual global.Stack.Provider assignment simulates environment variable reflection.

Also applies to: 534-535, 555-556, 588-589

src/cmd/cli/command/commands.go (6)

558-573: whoamiCmd correctly initializes stack manager with working directory and project name.

The flow properly handles project name loading failures with a warning and continues execution.


1311-1378: getStack implements clear precedence for stack/provider resolution.

The precedence (stack flag → provider flag → env var → stored preference → interactive) is well-documented through the whence variable. The flow correctly returns early when a definitive provider is determined.


1395-1407: saveSelectedProvider silently ignores errors, which is appropriate for a best-effort save.

The debug log provides visibility without disrupting the user flow.


1460-1495: Nil loader handling addresses the previous review comment.

The fix correctly checks loader != nil before calling loader.LoadProjectName and loader.OutsideWorkingDirectory, preventing the nil pointer dereference that caused the pipeline failure.


1497-1499: canIUseProvider updated to the new signature without stackName.


1501-1537: interactiveSelectProvider now returns cliClient.ProviderID for type safety.

The conversion from string to ProviderID via Set ensures validation.

Copy link
Contributor

@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

♻️ Duplicate comments (1)
src/pkg/stacks/manager.go (1)

57-62: Shadowed error variable in symlink resolution.

The short variable declaration on line 57 creates a new err in the if-block scope. Line 58 then reuses this shadowed err, which can cause confusion. While the current code works, restructuring would improve clarity.

Consider the refactor suggested in the past review:

 		// For target directory, only resolve symlinks if the path exists
 		resolvedTargetDirectory := absTargetDirectory
 		if _, err := os.Stat(absTargetDirectory); err == nil {
-			resolvedTargetDirectory, err = filepath.EvalSymlinks(absTargetDirectory)
+			resolved, evalErr := filepath.EvalSymlinks(absTargetDirectory)
+			if evalErr != nil {
+				return nil, fmt.Errorf("failed to resolve symlinks in target directory: %w", evalErr)
+			}
+			resolvedTargetDirectory = resolved
-			if err != nil {
-				return nil, fmt.Errorf("failed to resolve symlinks in target directory: %w", err)
-			}
 		}
🧹 Nitpick comments (4)
src/pkg/agent/tools/deploy_test.go (2)

54-57: Consider returning a mock Provider instead of nil.

Returning nil from NewProvider makes the tests fragile. If HandleDeployTool calls any methods on the provider (e.g., GetStackName(), AccountInfo()), the tests would panic. Consider creating a minimal mock Provider that implements the client.Provider interface with no-op or simple stub methods to make tests more robust and realistic.

Example approach:

type MockProvider struct {
	client.Provider
	stackName string
}

func (p *MockProvider) GetStackName() string {
	return p.stackName
}

func (p *MockProvider) AccountInfo(ctx context.Context) (*client.AccountInfo, error) {
	return &client.AccountInfo{}, nil
}

// Add other methods as needed with no-op implementations

func (m *MockDeployCLI) NewProvider(ctx context.Context, providerId client.ProviderID, client client.FabricClient, stack string) client.Provider {
	m.CallLog = append(m.CallLog, fmt.Sprintf("NewProvider(%s)", providerId))
	return &MockProvider{stackName: stack}
}

200-209: Verify call log for both successful test cases.

The call log verification only runs for "successful_deploy_defang_provider" but not for "successful_deploy_aws_provider". Consider verifying the expected call sequence for both successful cases to ensure consistent behavior across providers.

Example:

 // For successful cases, verify CLI methods were called in order
-if tt.expectedError == "" && tt.name == "successful_deploy_defang_provider" {
+if tt.expectedError == "" && (tt.name == "successful_deploy_defang_provider" || tt.name == "successful_deploy_aws_provider") {
 	expectedCalls := []string{
 		"LoadProject",
 		"Connect(test-cluster)",
 		"NewProvider(aws)",
 		"CanIUseProvider",
 		"ComposeUp",
 	}
 	assert.Equal(t, expectedCalls, mockCLI.CallLog)
 }
src/cmd/cli/command/commands_test.go (1)

271-295: Consider returning errors instead of panicking in test mocks.

While panics work for catching test setup issues, returning errors would provide clearer test failure messages. However, this is acceptable for test code since it fails fast on misconfiguration.

 func (m *mockElicitationsClient) Request(ctx context.Context, req elicitations.Request) (elicitations.Response, error) {
 	properties, ok := req.Schema["properties"].(map[string]any)
 	if !ok || len(properties) == 0 {
-		panic("invalid schema properties")
+		return elicitations.Response{}, errors.New("invalid schema properties in test mock")
 	}
 	fields := make([]string, 0)
 	for field := range properties {
 		fields = append(fields, field)
 	}
 
 	if len(fields) > 1 {
-		panic("mockElicitationsClient only supports single-field requests")
+		return elicitations.Response{}, errors.New("mockElicitationsClient only supports single-field requests")
 	}
src/cmd/cli/command/commands.go (1)

1398-1411: Consider graceful degradation when fetching previously used provider fails.

If the RPC call to GetSelectedProvider fails (e.g., network issues), the error propagates up and blocks provider selection. Consider logging a warning and returning ProviderAuto to allow fallback to interactive selection.

 func getPreviouslyUsedProvider(ctx context.Context, projectName string) (cliClient.ProviderID, error) {
 	if projectName == "" {
 		return cliClient.ProviderAuto, nil
 	}
 	resp, err := global.Client.GetSelectedProvider(ctx, &defangv1.GetSelectedProviderRequest{
 		Project: projectName,
 	})
 	if err != nil {
-		return cliClient.ProviderAuto, fmt.Errorf("unable to get selected provider: %w", err)
+		term.Debugf("Unable to get previously selected provider: %v", err)
+		return cliClient.ProviderAuto, nil
 	}
 	var providerID cliClient.ProviderID
 	providerID.SetValue(resp.Provider)
 	return providerID, nil
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d0e373f and 0e9dac8.

📒 Files selected for processing (14)
  • src/cmd/cli/command/commands.go (5 hunks)
  • src/cmd/cli/command/commands_test.go (16 hunks)
  • src/cmd/cli/command/globals.go (1 hunks)
  • src/cmd/cli/command/workspace_test.go (2 hunks)
  • src/pkg/agent/tools/default_tool_cli.go (1 hunks)
  • src/pkg/agent/tools/deploy.go (2 hunks)
  • src/pkg/agent/tools/deploy_test.go (1 hunks)
  • src/pkg/agent/tools/destroy.go (2 hunks)
  • src/pkg/agent/tools/destroy_test.go (1 hunks)
  • src/pkg/agent/tools/interfaces.go (1 hunks)
  • src/pkg/agent/tools/logs.go (3 hunks)
  • src/pkg/stacks/manager.go (1 hunks)
  • src/pkg/stacks/manager_test.go (12 hunks)
  • src/pkg/stacks/selector.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/pkg/agent/tools/interfaces.go
  • src/cmd/cli/command/globals.go
🧰 Additional context used
🧬 Code graph analysis (10)
src/pkg/agent/tools/default_tool_cli.go (3)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/cli/client/grpc.go (1)
  • GrpcClient (19-26)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/agent/tools/destroy_test.go (3)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/cli/client/grpc.go (1)
  • GrpcClient (19-26)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/stacks/manager_test.go (2)
src/pkg/stacks/manager.go (1)
  • NewManager (33-72)
src/pkg/stacks/stacks.go (6)
  • List (97-99)
  • Create (36-38)
  • Load (227-229)
  • StackParameters (18-25)
  • StackListItem (87-95)
  • Directory (29-29)
src/pkg/stacks/selector.go (4)
src/pkg/elicitations/elicitations.go (1)
  • Controller (8-14)
src/pkg/stacks/manager.go (1)
  • Manager (15-19)
src/pkg/stacks/stacks.go (4)
  • StackParameters (18-25)
  • Create (36-38)
  • Load (227-229)
  • List (97-99)
src/pkg/stacks/wizard.go (1)
  • NewWizard (20-25)
src/pkg/agent/tools/deploy.go (5)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/agent/tools/interfaces.go (1)
  • CLIInterface (14-32)
src/pkg/stacks/manager.go (1)
  • NewManager (33-72)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (26-33)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/agent/tools/logs.go (3)
src/pkg/agent/tools/interfaces.go (1)
  • CLIInterface (14-32)
src/pkg/stacks/manager.go (1)
  • NewManager (33-72)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/cmd/cli/command/commands_test.go (5)
src/pkg/elicitations/elicitations.go (3)
  • Request (25-28)
  • Response (30-33)
  • NewController (35-40)
src/protos/io/defang/v1/fabric.pb.go (13)
  • ListDeploymentsRequest (2992-2999)
  • ListDeploymentsRequest (3012-3012)
  • ListDeploymentsRequest (3027-3029)
  • ListDeploymentsResponse (3052-3057)
  • ListDeploymentsResponse (3070-3070)
  • ListDeploymentsResponse (3085-3087)
  • Deployment (2822-2838)
  • Deployment (2851-2851)
  • Deployment (2866-2868)
  • Provider (28-28)
  • Provider (66-68)
  • Provider (70-72)
  • Provider (79-81)
src/pkg/stacks/manager.go (1)
  • Manager (15-19)
src/pkg/cli/client/provider_id.go (4)
  • ProviderID (10-10)
  • ProviderAWS (15-15)
  • ProviderDO (16-16)
  • ProviderGCP (17-17)
src/pkg/stacks/stacks.go (5)
  • List (97-99)
  • StackListItem (87-95)
  • Load (227-229)
  • StackParameters (18-25)
  • Create (36-38)
src/pkg/stacks/manager.go (8)
src/pkg/stacks/stacks.go (9)
  • List (97-99)
  • StackListItem (87-95)
  • Load (227-229)
  • StackParameters (18-25)
  • Create (36-38)
  • ListInDirectory (101-135)
  • ReadInDirectory (213-225)
  • LoadInDirectory (231-239)
  • CreateInDirectory (40-84)
src/pkg/term/colorizer.go (2)
  • Errorf (322-324)
  • Error (318-320)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/modes/modes.go (1)
  • Mode (12-12)
src/pkg/cli/compose/loader.go (1)
  • Project (25-25)
src/pkg/cli/client/provider_id.go (1)
  • ProviderID (10-10)
src/pkg/timeutils/timeutils.go (1)
  • AsTime (39-44)
src/pkg/cli/client/region.go (1)
  • GetRegion (5-16)
src/pkg/agent/tools/deploy_test.go (2)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/agent/tools/destroy.go (3)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/agent/tools/interfaces.go (1)
  • CLIInterface (14-32)
src/pkg/stacks/manager.go (1)
  • NewManager (33-72)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Analyze (go)
  • GitHub Check: go-test
🔇 Additional comments (24)
src/pkg/agent/tools/deploy_test.go (1)

80-83: Past issue resolved: parameter order now matches implementation.

The mock signature now correctly matches the real CanIUseProvider implementation with parameters in the order (ctx, client, provider, projectName, serviceCount). The previous parameter order mismatch has been fixed.

src/cmd/cli/command/workspace_test.go (1)

55-56: LGTM: Test isolation improved.

Resetting global.Stack.Name prevents unintended stack file loading during tests, ensuring deterministic test behavior.

Also applies to: 90-91

src/pkg/agent/tools/default_tool_cli.go (1)

30-31: LGTM: Signature updated to align with interface changes.

The stackName parameter is no longer needed since the provider now exposes GetStackName(). The updated signature matches the new interface definition.

src/pkg/agent/tools/destroy_test.go (1)

60-60: LGTM: Test mock updated to match interface.

The mock signature correctly reflects the updated CanIUseProvider interface.

src/pkg/agent/tools/destroy.go (1)

20-42: LGTM: Stack manager integration and error handling.

The loader type change, stack manager creation with error handling, and updated provider validation signature are all consistent with the broader refactoring. Error propagation is properly handled with contextual wrapping.

src/pkg/agent/tools/logs.go (1)

25-65: LGTM: Consistent refactoring applied.

The loader type change, stack manager creation, and provider validation updates mirror the pattern in other tool handlers. Error handling is appropriate.

src/pkg/stacks/selector.go (1)

29-53: LGTM: Stack selection flow is well-structured.

The logic correctly handles both creating new stacks via the wizard and loading existing stacks. Error handling is appropriate throughout the flow.

src/pkg/agent/tools/deploy.go (1)

25-57: LGTM: Consistent stack manager integration.

The loader type change, stack manager creation, and provider validation updates follow the established pattern across tool handlers. Error handling and propagation are appropriate.

src/pkg/stacks/manager.go (3)

74-117: LGTM: Local and remote stack merging logic.

The List method correctly merges local and remote stacks, preferring remote data (including DeployedAt timestamps) when both exist. Error handling properly distinguishes OutsideError from other failures.


131-168: LGTM: Remote deployment listing with implicit beta stack handling.

The ListRemote method correctly treats deployments with empty stack names as the implicit "beta" stack, aligning with the PR's onboarding objectives. The deduplication logic (preferring the most recent deployment per stack) is sound.


170-199: LGTM: Outside directory protection.

The OutsideError type and its usage in Load and Create methods correctly prevent cross-project stack file access, addressing the PR objective of restricting operations when the working directory differs from the target directory.

src/pkg/stacks/manager_test.go (5)

18-31: LGTM! Clean mock implementation.

The mockFabricClient correctly implements the DeploymentLister interface with configurable deployments and error injection for testing error paths.


46-133: LGTM! Comprehensive CRUD test coverage.

The test thoroughly validates stack creation, listing, and loading operations with appropriate assertions for all stack parameters including provider, region, and mode.


411-509: LGTM! Well-structured merge behavior test.

The test correctly validates that remote stacks take precedence over local stacks when names match, while both local-only and remote-only stacks are properly included in the merged result.


640-716: LGTM! Important security boundary test.

This test correctly validates the directory isolation behavior that prevents cross-project stack file access, ensuring Create and Load fail with appropriate error messages while List gracefully falls back to remote-only stacks.


33-44: Go version compatibility is already satisfied in go.mod.

The project specifies go 1.24.1, which fully supports the t.Chdir() method used throughout the test file. No action required.

src/cmd/cli/command/commands_test.go (4)

85-89: LGTM! Required interface method added.

The ListDeployments stub correctly returns an empty deployment list, enabling tests to work with the new stacks manager that depends on this method.


241-247: Good defensive fix for nil map panic.

The nil check before map assignment prevents a panic when SetSelectedProvider is called before any provider has been saved. This aligns with Go best practices for map initialization.


297-316: LGTM! Appropriate mock implementation.

The mockStacksManager correctly implements the stacks.Manager interface with configurable expectedProvider for testing different provider scenarios.


340-355: LGTM! Non-interactive path correctly tested.

Passing nil for the elicitations controller correctly tests the non-interactive code path where no user prompts are expected.

src/cmd/cli/command/commands.go (4)

558-573: LGTM! Proper stack manager initialization in whoami command.

The whoami command correctly initializes the elicitations controller and stack manager before calling newProvider, following the new stack-based provider resolution pattern.


1478-1513: LGTM! Nil loader check properly implemented.

The function now correctly handles the case when loader is nil by defaulting outside to true and skipping the LoadProjectName call, addressing the previously identified nil pointer dereference issue.


1427-1456: LGTM! Helpful user experience improvement.

The warning messages help users identify common configuration mismatches between selected provider and environment variables, preventing deployment issues.


1519-1556: LGTM! Improved type safety.

Changing the return type to cliClient.ProviderID provides better type safety and consistency with the rest of the codebase.

Copy link
Contributor

@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: 4

Caution

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

⚠️ Outside diff range comments (2)
src/cmd/cli/command/commands_test.go (2)

334-359: Missing AWS_REGION environment variable causing test failure.

The pipeline failure indicates this test fails with "missing AWS region". Unlike TestCommandGates (line 151), this test doesn't set the AWS_REGION environment variable before creating an AWS provider.

Apply this diff to fix the test:

 	t.Run("Should set cd image from canIUse response", func(t *testing.T) {
 		t.Chdir("../../../../src/testdata/sanity")
 		t.Setenv("DEFANG_STACK", "beta")
+		t.Setenv("AWS_REGION", "us-west-2")
 
 		// Set up RootCmd with required flags for getStack function

378-394: Missing AWS_REGION environment variable causing test failure.

Same issue as the previous test—this test also fails with "missing AWS region" and needs the environment variable set.

Apply this diff:

 	t.Run("Can override cd image from environment variable", func(t *testing.T) {
 		t.Chdir("../../../../src/testdata/sanity")
 		t.Setenv("DEFANG_STACK", "beta")
+		t.Setenv("AWS_REGION", "us-west-2")
 		sts := aws.StsClient
🧹 Nitpick comments (5)
src/cmd/cli/command/stack_test.go (2)

56-108: Test duplicates production command logic.

The inline stackListCmd definition duplicates most of the logic from makeStackListCmd() in stack.go. This creates maintenance burden and risks test/production divergence.

Consider refactoring to use the actual makeStackListCmd() function with any necessary test setup, or extract the shared logic into a testable helper:

-	// Create stackListCmd with manual RunE to avoid configureLoader call during test
-	stackListCmd := &cobra.Command{
-		Use:     "list",
-		...
-	}
+	// Use the actual command
+	stackListCmd := makeStackListCmd()

If configureLoader is the issue, consider making it injectable or using a test-specific configuration.


140-142: Consider testing with non-zero DeployedAt values.

The test uses zero-value DeployedAt timestamps. Consider adding a test case with actual deployment timestamps to verify the column formatting for realistic scenarios.

src/cmd/cli/command/commands.go (1)

1406-1419: Warning messages reference deprecated flags.

The warnings mention --provider and DEFANG_PROVIDER, but line 1351 indicates these are being deprecated in favor of --stack. Consider updating the warning messages for consistency.

 	if provider == cliClient.ProviderDefang {
 		if awsInEnv() {
-			term.Warn("AWS environment variables were detected; did you forget --provider=aws or DEFANG_PROVIDER=aws?")
+			term.Warn("AWS environment variables were detected; did you intend to use an AWS stack?")
 		}
 		if doInEnv() {
-			term.Warn("DIGITALOCEAN_TOKEN environment variable was detected; did you forget --provider=digitalocean or DEFANG_PROVIDER=digitalocean?")
+			term.Warn("DIGITALOCEAN_TOKEN environment variable was detected; did you intend to use a DigitalOcean stack?")
 		}
 		if gcpInEnv() {
-			term.Warn("GCP_PROJECT_ID/CLOUDSDK_CORE_PROJECT environment variable was detected; did you forget --provider=gcp or DEFANG_PROVIDER=gcp?")
+			term.Warn("GCP_PROJECT_ID/CLOUDSDK_CORE_PROJECT environment variable was detected; did you intend to use a GCP stack?")
 		}
 	}
src/cmd/cli/command/commands_test.go (2)

273-292: Consider consolidating or renaming duplicate mock types.

Two mock implementations of stacks.Manager exist: mockStacksManager (line 273) and mockStackManager (line 445). The names differ by only one letter ("Stacks" vs "Stack"), which is confusing. Consider either consolidating into a single configurable mock or renaming for clarity (e.g., simpleStackManagerMock vs configurableStackManagerMock).

Also applies to: 445-453


735-786: Remove dead code or implement warning capture.

The output buffer is declared but never used to capture output. The warning check at lines 778-784 doesn't actually verify warning output—it only checks if a flag was changed, which is already ensured by the test setup. Either remove this dead code or implement proper warning capture.

-		// Capture output to check for warnings
-		var output bytes.Buffer
-
 		// Call the function under test
 		stack, whence, err := getStack(ctx, ec, sm)
 ...
-		// Check warning expectations
-		if tc.expectWarning {
-			// Since we can't easily capture term.Warn output in tests, we just verify
-			// that the code path that would produce warnings was taken
-			if tc.providerFlag != "" && !testRootCmd.PersistentFlags().Changed("provider") {
-				t.Error("expected provider flag to be marked as changed for warning path")
-			}
-		}
-
-		_ = output // Suppress unused variable warning for now
 	})
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9c661b and 912087a.

📒 Files selected for processing (8)
  • src/cmd/cli/command/commands.go (4 hunks)
  • src/cmd/cli/command/commands_test.go (10 hunks)
  • src/cmd/cli/command/compose.go (5 hunks)
  • src/cmd/cli/command/estimate.go (3 hunks)
  • src/cmd/cli/command/stack.go (2 hunks)
  • src/cmd/cli/command/stack_test.go (3 hunks)
  • src/pkg/stacks/manager.go (1 hunks)
  • src/pkg/stacks/selector.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/pkg/stacks/selector.go
🧰 Additional context used
🧬 Code graph analysis (6)
src/cmd/cli/command/stack_test.go (5)
src/pkg/cli/compose/loader.go (1)
  • NewLoader (57-72)
src/pkg/stacks/manager.go (1)
  • NewManager (33-59)
src/pkg/stacks/stacks.go (1)
  • List (97-99)
src/pkg/term/colorizer.go (2)
  • Print (278-280)
  • Infof (306-308)
src/pkg/term/table.go (1)
  • Table (11-13)
src/cmd/cli/command/estimate.go (5)
src/pkg/cli/client/provider_id.go (4)
  • ProviderID (10-10)
  • ProviderAWS (15-15)
  • ProviderGCP (17-17)
  • ProviderDO (16-16)
src/pkg/track/track.go (1)
  • Evt (37-52)
src/cmd/cli/command/commands.go (1)
  • P (54-54)
src/pkg/cli/tail.go (1)
  • P (93-93)
src/pkg/debug/debug.go (1)
  • P (21-21)
src/pkg/stacks/manager.go (4)
src/pkg/stacks/stacks.go (4)
  • List (97-99)
  • StackListItem (87-95)
  • StackParameters (18-25)
  • ListInDirectory (101-135)
src/pkg/modes/modes.go (1)
  • Mode (12-12)
src/pkg/cli/client/provider_id.go (1)
  • ProviderID (10-10)
src/pkg/cli/client/region.go (1)
  • GetRegion (5-16)
src/cmd/cli/command/commands_test.go (4)
src/protos/io/defang/v1/fabric.pb.go (13)
  • ListDeploymentsRequest (2992-2999)
  • ListDeploymentsRequest (3012-3012)
  • ListDeploymentsRequest (3027-3029)
  • ListDeploymentsResponse (3052-3057)
  • ListDeploymentsResponse (3070-3070)
  • ListDeploymentsResponse (3085-3087)
  • Deployment (2822-2838)
  • Deployment (2851-2851)
  • Deployment (2866-2868)
  • Provider (28-28)
  • Provider (66-68)
  • Provider (70-72)
  • Provider (79-81)
src/pkg/stacks/manager.go (1)
  • Manager (15-19)
src/pkg/cli/client/provider_id.go (4)
  • ProviderID (10-10)
  • ProviderAWS (15-15)
  • ProviderAuto (13-13)
  • ProviderDefang (14-14)
src/pkg/stacks/stacks.go (5)
  • List (97-99)
  • StackListItem (87-95)
  • Load (227-229)
  • StackParameters (18-25)
  • Create (36-38)
src/cmd/cli/command/stack.go (3)
src/pkg/stacks/manager.go (1)
  • NewManager (33-59)
src/pkg/stacks/stacks.go (1)
  • List (97-99)
src/pkg/term/table.go (1)
  • Table (11-13)
src/cmd/cli/command/compose.go (4)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/elicitations/survey.go (1)
  • NewSurveyClient (19-25)
src/pkg/elicitations/elicitations.go (2)
  • NewController (35-40)
  • Client (16-18)
src/pkg/stacks/manager.go (1)
  • NewManager (33-59)
🪛 GitHub Actions: Go package
src/cmd/cli/command/stack_test.go

[error] 1-1: TestNewProvider/Should_set_cd_image_from_canIUse_response failed: CanIUseProvider() failed: missing AWS region: set AWS_REGION or edit your AWS profile at ~/.aws/config (https://docs.defang.io/docs/providers/aws#region); Expected cd image tag to be site/registry/repo:tag@sha256:digest, got:


[error] 1-1: TestNewProvider/Can_override_cd_image_from_environment_variable failed: CanIUseProvider() failed: missing AWS region: set AWS_REGION or edit your AWS profile at ~/.aws/config (https://docs.defang.io/docs/providers/aws#region); Expected cd image tag to be site/registry/repo:tag@sha256:digest, got:

src/cmd/cli/command/estimate.go

[error] 1-1: TestNewProvider/Should_set_cd_image_from_canIUse_response failed: CanIUseProvider() failed: missing AWS region: set AWS_REGION or edit your AWS profile at ~/.aws/config (https://docs.defang.io/docs/providers/aws#region); Expected cd image tag to be site/registry/repo:tag@sha256:digest, got:


[error] 1-1: TestNewProvider/Can_override_cd_image_from_environment_variable failed: CanIUseProvider() failed: missing AWS region: set AWS_REGION or edit your AWS profile at ~/.aws/config (https://docs.defang.io/docs/providers/aws#region); Expected cd image tag to be site/registry/repo:tag@sha256:digest, got:

src/cmd/cli/command/commands_test.go

[error] 1-1: TestNewProviderShould_set_cd_image_from_canIUse_response failed: missing AWS region: set AWS_REGION or edit your AWS profile; also expected cd image tag to be site/registry/repo:tag@sha256:digest but got an empty value.


[error] 1-1: TestNewProviderCan_override_cd_image_from_environment_variable failed: expected cd image tag to be site/registry/repo:tag@sha256:digest but got an empty value.

src/cmd/cli/command/stack.go

[error] 1-1: TestNewProvider/Should_set_cd_image_from_canIUse_response failed: CanIUseProvider() failed: missing AWS region: set AWS_REGION or edit your AWS profile at ~/.aws/config (https://docs.defang.io/docs/providers/aws#region); Expected cd image tag to be site/registry/repo:tag@sha256:digest, got:


[error] 1-1: TestNewProvider/Can_override_cd_image_from_environment_variable failed: CanIUseProvider() failed: missing AWS region: set AWS_REGION or edit your AWS profile at ~/.aws/config (https://docs.defang.io/docs/providers/aws#region); Expected cd image tag to be site/registry/repo:tag@sha256:digest, got:

src/cmd/cli/command/commands.go

[error] 1-1: TestNewProvider/Should_set_cd_image_from_canIUse_response failed: CanIUseProvider() failed: missing AWS region: set AWS_REGION or edit your AWS profile at ~/.aws/config (https://docs.defang.io/docs/providers/aws#region); Expected cd image tag to be site/registry/repo:tag@sha256:digest, got:


[error] 1-1: TestNewProvider/Can_override_cd_image_from_environment_variable failed: CanIUseProvider() failed: missing AWS region: set AWS_REGION or edit your AWS profile at ~/.aws/config (https://docs.defang.io/docs/providers/aws#region); Expected cd image tag to be site/registry/repo:tag@sha256:digest, got:

src/cmd/cli/command/compose.go

[error] 1-1: TestNewProvider/Should_set_cd_image_from_canIUse_response failed: CanIUseProvider() failed: missing AWS region: set AWS_REGION or edit your AWS profile at ~/.aws/config (https://docs.defang.io/docs/providers/aws#region); Expected cd image tag to be site/registry/repo:tag@sha256:digest, got:


[error] 1-1: TestNewProvider/Can_override_cd_image_from_environment_variable failed: CanIUseProvider() failed: missing AWS region: set AWS_REGION or edit your AWS profile at ~/.aws/config (https://docs.defang.io/docs/providers/aws#region); Expected cd image tag to be site/registry/repo:tag@sha256:digest, got:

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (18)
src/cmd/cli/command/estimate.go (1)

69-106: LGTM on the new interactive provider selection helper.

The function is well-structured with appropriate error handling, survey integration, and tracking. The panic for invalid input is acceptable as a programming contract violation.

src/cmd/cli/command/commands.go (3)

558-573: LGTM on whoami stack-aware provider flow.

The integration of elicitations controller and stack manager follows the new stack-driven pattern consistently with other commands.


1372-1386: LGTM on single stack auto-selection logic.

The auto-selection when exactly one stack matches the specified provider is a good UX improvement. The provider validation before assignment is appropriate.


1457-1492: LGTM on newProviderChecked with nil safety.

The nil check for loader is properly implemented, addressing the previous review concern. The stack manager creation correctly handles both inside and outside working directory cases.

src/cmd/cli/command/stack.go (3)

90-105: LGTM on manager-based stack listing.

The refactoring to use stacks.NewManager with proper context propagation and project name resolution follows the new architecture well.


97-100: Strict error handling for project name loading.

This returns an error if LoadProjectName fails, which is stricter than silently falling back to an empty project name. This is the correct production behavior.

Note: The test in stack_test.go handles this differently (swallows the error) - see earlier comment.


130-131: LGTM on DeployedAt column addition.

The new column provides useful deployment metadata to users when listing stacks.

src/cmd/cli/command/compose.go (3)

223-230: Good addition of provider consistency check.

The new check at lines 225-227 ensures deployments are from the same cloud provider before considering them as "same place". This prevents confusion when switching between providers.


237-250: LGTM on "beta" stack creation for legacy deployments.

This correctly implements the onboarding path for existing projects by surfacing the implicit "beta" stack, which aligns with the PR objectives from issue #1719.


566-577: LGTM on compose config command stack integration.

The elicitations and stack manager setup follows the established pattern from other commands, using project.Name for proper project context.

src/cmd/cli/command/commands_test.go (3)

82-91: LGTM!

The ListDeployments mock implementations correctly return test data that supports the new stacks onboarding flow. The server mock returns a "beta" stack with AWS provider, which aligns with testing the implicit beta stack behavior described in the PR objectives.

Also applies to: 251-255


414-443: LGTM!

The mockElicitationsController provides good flexibility for testing interactive vs non-interactive paths with configurable isSupported and enumChoice fields.


488-696: Comprehensive test coverage for stack selection.

Good table-driven test structure covering key scenarios: valid/invalid stacks, auto provider errors, provider flags with warnings, non-interactive defaults, single-stack matching, interactive selection, and error propagation.

src/pkg/stacks/manager.go (5)

61-104: LGTM!

The merge logic correctly handles the remote/local precedence—remote stacks overwrite local entries to show deployment timestamps. Silently ignoring OutsideError for local listing is appropriate since remote stacks should still be available when working outside the project directory.


127-149: API sort-order assumption should be verified.

Line 134 comments that "deployments are already sorted by deployed_at desc", but this relies on an undocumented API contract. If the API behavior changes or varies by endpoint version, the first-occurrence logic would capture incorrect timestamps.

Consider either:

  1. Documenting this API contract in the proto/API specification
  2. Adding explicit sorting after retrieval to be defensive:
// Sort deployments by timestamp descending to ensure most recent is first
sort.Slice(deployments, func(i, j int) bool {
    ti := deployments[i].GetTimestamp()
    tj := deployments[j].GetTimestamp()
    if ti == nil || tj == nil {
        return ti != nil
    }
    return ti.AsTime().After(tj.AsTime())
})

128-131: Good handling of implicit "beta" stack.

Treating empty stack names as "beta" aligns with the PR objective to surface the implicit stack used by existing projects during onboarding.


157-164: LGTM!

Clean custom error type with clear, informative message that will help users understand why an operation was blocked.


166-186: LGTM!

Both Load and Create correctly guard against cross-project file access by returning OutsideError when operating outside the working directory. This prevents accidental stack file modifications in unrelated projects.

Copy link
Contributor

@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: 2

♻️ Duplicate comments (2)
src/pkg/stacks/manager.go (1)

45-52: Symlink-relative paths may cause incorrect outside detection.

The comparison at line 51 uses raw absolute paths without resolving symlinks. If either workingDirectory or absTargetDirectory contains symlinks, two paths pointing to the same physical directory will compare as different, incorrectly setting outside = true.

Consider resolving symlinks before comparison:

 	} else {
 		// abs path for targetDirectory
 		var err error
 		absTargetDirectory, err = filepath.Abs(targetDirectory)
 		if err != nil {
 			return nil, fmt.Errorf("failed to get absolute path for target directory: %w", err)
 		}
+		// Resolve symlinks for accurate comparison
+		if resolved, err := filepath.EvalSymlinks(workingDirectory); err == nil {
+			workingDirectory = resolved
+		}
+		if resolved, err := filepath.EvalSymlinks(absTargetDirectory); err == nil {
+			absTargetDirectory = resolved
+		}
 		outside = workingDirectory != absTargetDirectory
 	}
src/cmd/cli/command/commands.go (1)

1350-1364: Warning says flag is "ignored" but value is still used.

The warning states --provider flag is being ignored but the code immediately parses and assigns the provider value to stack.Provider. This is confusing for users. The same applies to the DEFANG_PROVIDER environment variable warning.

Consider either:

  1. Actually ignoring the value (don't set stack.Provider), or
  2. Updating the warning to say the flag is "deprecated" rather than "ignored":
 	if RootCmd.PersistentFlags().Changed("provider") {
-		term.Warn("Warning: --provider flag is being ignored. Please use --stack instead. To learn about stacks, visit https://docs.defang.io/docs/concepts/stacks")
+		term.Warn("Warning: --provider flag is deprecated. Please use --stack instead. To learn about stacks, visit https://docs.defang.io/docs/concepts/stacks")
🧹 Nitpick comments (2)
src/pkg/stacks/manager.go (1)

104-114: Duplicate comment and verbose sort comparator.

Line 104-105 has a duplicate comment. The sort comparator can be simplified using strings.Compare:

-	// sort stacks by name asc
 	// sort stacks by name asc
 	slices.SortFunc(stackList, func(a, b StackListItem) int {
-		if a.Name < b.Name {
-			return -1
-		}
-		if a.Name > b.Name {
-			return 1
-		}
-		return 0
+		return strings.Compare(a.Name, b.Name)
 	})
src/cmd/cli/command/commands.go (1)

1476-1485: Variable shadowing with err in else branch.

The err variable on line 1478 shadows the outer err from line 1458, and then line 1481 redeclares err again with :=. While this works correctly, it's confusing. Consider using explicit assignments:

 	} else {
-		wd, err := os.Getwd()
-		if err != nil {
-			return nil, fmt.Errorf("failed to get working directory: %w", err)
+		wd, wdErr := os.Getwd()
+		if wdErr != nil {
+			return nil, fmt.Errorf("failed to get working directory: %w", wdErr)
 		}
-		sm, err = stacks.NewManager(global.Client, wd, projectName)
-		if err != nil {
-			return nil, fmt.Errorf("failed to create stack manager: %w", err)
+		sm, smErr := stacks.NewManager(global.Client, wd, projectName)
+		if smErr != nil {
+			return nil, fmt.Errorf("failed to create stack manager: %w", smErr)
 		}
+		sm = sm // or just use smErr check inline
 	}
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 912087a and 0a7eb9f.

📒 Files selected for processing (3)
  • src/cmd/cli/command/commands.go (4 hunks)
  • src/pkg/cli/client/byoc/gcp/byoc.go (1 hunks)
  • src/pkg/stacks/manager.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/pkg/stacks/manager.go (1)
src/pkg/stacks/stacks.go (9)
  • List (97-99)
  • StackListItem (87-95)
  • Load (227-229)
  • StackParameters (18-25)
  • Create (36-38)
  • ListInDirectory (101-135)
  • ReadInDirectory (213-225)
  • LoadInDirectory (231-239)
  • CreateInDirectory (40-84)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (5)
src/pkg/stacks/manager.go (3)

62-98: LGTM on the merge logic.

The merge correctly prioritizes remote stack data over local by processing local stacks first and then overwriting with remote entries. The DeployedAt.Local() conversion is appropriate for display purposes.


131-168: LGTM on ListRemote implementation.

The method correctly handles the implicit "beta" stack for deployments without an explicit stack name, and properly keeps only the most recent deployment per stack by leveraging the pre-sorted deployment list.


179-199: LGTM on Load and Create methods.

Both methods correctly enforce directory boundary checks via OutsideError and operate on targetDirectory as expected.

src/cmd/cli/command/commands.go (2)

558-573: LGTM on whoamiCmd changes.

The inline creation of the elicitations controller and stack manager is appropriate here since whoamiCmd needs graceful handling of provider resolution failures while still displaying user information.


1406-1435: LGTM on provider mismatch warnings.

The warnings provide helpful guidance when cloud credentials are detected but the playground is selected, and vice versa.

Copy link
Contributor

@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: 0

♻️ Duplicate comments (3)
src/cmd/cli/command/estimate.go (1)

80-83: Default option references unavailable provider.

The doInEnv() check sets defaultOption to ProviderDO, but the caller on lines 32-35 only passes ProviderAWS and ProviderGCP to this function. If DigitalOcean environment variables are detected, the survey will have a default that doesn't exist in the options list, which may cause unexpected behavior.

Apply the fix suggested in the previous review:

 	var defaultOption any // not string!
 	if awsInEnv() {
 		defaultOption = cliClient.ProviderAWS.String()
-	} else if doInEnv() {
-		defaultOption = cliClient.ProviderDO.String()
 	} else if gcpInEnv() {
 		defaultOption = cliClient.ProviderGCP.String()
 	}
src/cmd/cli/command/commands.go (2)

1351-1365: Deprecation warning is misleading.

The code warns that --provider flag and DEFANG_PROVIDER environment variable are "deprecated" but immediately uses their values to set stack.Provider. This contradicts the warning and confuses users—if they're deprecated and shouldn't be used, the code should not honor them.

Consider either:

  1. Actually deprecating them (warn but don't use the values), or
  2. Changing the warning to clarify they still work but stacks are preferred

If truly deprecating:

 	if RootCmd.PersistentFlags().Changed("provider") {
 		term.Warn("Warning: --provider flag is deprecated. Please use --stack instead. To learn about stacks, visit https://docs.defang.io/docs/concepts/stacks")
-		providerIDString := RootCmd.Flags().Lookup("provider").Value.String()
-		err := stack.Provider.Set(providerIDString)
-		if err != nil {
-			return nil, "", fmt.Errorf("invalid provider %q: %w", providerIDString, err)
-		}
 	} else if _, ok := os.LookupEnv("DEFANG_PROVIDER"); ok {
 		term.Warn("Warning: DEFANG_PROVIDER environment variable is deprecated. Please use --stack instead. To learn about stacks, visit https://docs.defang.io/docs/concepts/stacks")
-		providerIDString := os.Getenv("DEFANG_PROVIDER")
-		err := stack.Provider.Set(providerIDString)
-		if err != nil {
-			return nil, "", fmt.Errorf("invalid provider %q: %w", providerIDString, err)
-		}
 	}

Or if keeping them functional:

 	if RootCmd.PersistentFlags().Changed("provider") {
-		term.Warn("Warning: --provider flag is deprecated. Please use --stack instead. To learn about stacks, visit https://docs.defang.io/docs/concepts/stacks")
+		term.Warn("Warning: --provider flag is being used but stacks are preferred. Please use --stack instead. To learn about stacks, visit https://docs.defang.io/docs/concepts/stacks")

1373-1387: Auto-select logic won't trigger when provider is auto.

The condition on line 1374 compares knownStacks[0].Provider (e.g., "aws") to stack.Provider.String() which defaults to "auto" (line 1318). This comparison will never match when the provider is auto, preventing the single-stack auto-selection from working in the most common case.

Additionally, line 1383 assigns stack.Provider (the original ProviderAuto value) instead of the parsed providerID, which means even if the condition matched, the stack would have the wrong provider.

Apply this diff:

 	// if there is exactly one stack with that provider, use it
-	if len(knownStacks) == 1 && knownStacks[0].Provider == stack.Provider.String() {
+	if len(knownStacks) == 1 && (stack.Provider == cliClient.ProviderAuto || knownStacks[0].Provider == stack.Provider.String()) {
 		knownStack := knownStacks[0]
 		var providerID cliClient.ProviderID
 		err := providerID.Set(knownStack.Provider)
 		if err != nil {
 			return nil, "", fmt.Errorf("invalid provider %q in stack %q: %w", knownStack.Provider, knownStack.Name, err)
 		}
 		stack = &stacks.StackParameters{
 			Name:     knownStack.Name,
-			Provider: stack.Provider,
+			Provider: providerID,
 		}
 		whence = "only stack"
 		return stack, whence, nil
 	}
🧹 Nitpick comments (2)
src/cmd/cli/command/estimate.go (1)

70-72: Enhance panic message with context.

The panic message could be more helpful by including the actual count received.

Apply this diff:

 func interactiveSelectProvider(providers []cliClient.ProviderID) (cliClient.ProviderID, error) {
 	if len(providers) < 2 {
-		panic("interactiveSelectProvider called with less than 2 providers")
+		panic(fmt.Sprintf("interactiveSelectProvider requires at least 2 providers, got %d", len(providers)))
 	}
src/cmd/cli/command/commands.go (1)

1312-1405: Complex stack selection logic - consider simplifying.

The getStack function implements multiple precedence rules and decision paths. While the logic appears correct, the complexity and multiple exit points make it harder to maintain and reason about.

Consider refactoring to separate concerns:

  • Stack file loading (lines 1325-1341)
  • Known stacks enumeration (lines 1343-1350)
  • Deprecated flag handling (lines 1351-1365)
  • Auto-selection logic (lines 1373-1387)
  • Interactive selection (lines 1389-1404)

This could be split into helper functions like loadFromStackFile(), handleDeprecatedFlags(), and selectOrCreateStack() to improve clarity and testability.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a7eb9f and f573b94.

📒 Files selected for processing (4)
  • src/cmd/cli/command/commands.go (5 hunks)
  • src/cmd/cli/command/estimate.go (3 hunks)
  • src/cmd/cli/command/stack_test.go (4 hunks)
  • src/pkg/cli/client/byoc/gcp/byoc.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/pkg/cli/client/byoc/gcp/byoc.go
🧰 Additional context used
🧬 Code graph analysis (2)
src/cmd/cli/command/stack_test.go (3)
src/cmd/cli/command/commands.go (2)
  • RootCmd (412-517)
  • Execute (75-160)
src/pkg/cli/client/grpc.go (1)
  • GrpcClient (19-26)
src/cmd/cli/command/commands_test.go (1)
  • MockFabricControllerClient (223-227)
src/cmd/cli/command/estimate.go (3)
src/pkg/cli/client/provider_id.go (3)
  • ProviderID (10-10)
  • ProviderAWS (15-15)
  • ProviderGCP (17-17)
src/pkg/track/track.go (1)
  • Evt (37-52)
src/cmd/cli/command/commands.go (1)
  • P (54-54)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (3)
src/cmd/cli/command/stack_test.go (2)

31-58: Well-structured test isolation.

The test properly saves and restores global state, sets up a mock client, and creates a complete RootCmd with required flags. This enables testing the full command execution path rather than just the RunE handler.


95-102: Good defensive practice.

Creating the compose.yaml file prevents the stackListCmd from erroring due to missing input files, making the test more robust.

src/cmd/cli/command/commands.go (1)

559-574: Ensure whoamiCmd initialization handles outside-working-directory scenarios correctly.

The whoamiCmd uses direct initialization of elicitations controller and stack manager, differing from most other commands which use newProviderChecked. Critically, whoamiCmd:

  • Always passes the working directory to stacks.NewManager, without checking loader.OutsideWorkingDirectory() first. The newProviderChecked function correctly handles this by passing an empty string when outside the working directory.
  • Skips provider validation—newProviderChecked calls provider.AccountInfo(ctx) to validate, but whoamiCmd does not.

In remote or outside-working-directory execution contexts, this pattern could fail. Consider either using newProviderChecked directly or adopting its "outside" check logic before initializing the stack manager.

Copy link
Contributor

@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: 1

♻️ Duplicate comments (1)
src/cmd/cli/command/commands.go (1)

1373-1387: Auto-select logic may not trigger as intended when provider is unspecified.

When neither --provider nor DEFANG_PROVIDER is set, stack.Provider remains ProviderAuto. The condition on line 1374 compares knownStacks[0].Provider (e.g., "aws") against stack.Provider.String() (which would be "auto"), so this branch will only match if the user explicitly set --provider.

Additionally, on line 1383, when auto-selecting, the code uses Provider: stack.Provider which could still be ProviderAuto if the user happened to set --provider=auto. It should use the concrete provider from knownStack:

 	if len(knownStacks) == 1 && knownStacks[0].Provider == stack.Provider.String() {
 		knownStack := knownStacks[0]
 		var providerID cliClient.ProviderID
 		err := providerID.Set(knownStack.Provider)
 		if err != nil {
 			return nil, "", fmt.Errorf("invalid provider %q in stack %q: %w", knownStack.Provider, knownStack.Name, err)
 		}
 		stack = &stacks.StackParameters{
 			Name:     knownStack.Name,
-			Provider: stack.Provider,
+			Provider: providerID,
 		}
 		whence = "only stack"
 		return stack, whence, nil
 	}

If you intend for auto-selection to work when the user hasn't specified a provider, the condition should also handle the ProviderAuto case.

🧹 Nitpick comments (2)
src/cmd/cli/command/commands.go (2)

559-574: Consider extracting common setup logic.

This pattern of creating elicitationsClient, ec, and sm is duplicated here and in newProviderChecked (lines 1469-1486). Consider extracting this into a helper function to reduce duplication.

That said, the immediate implementation is correct and functional.


1407-1420: Consider updating warning messages to reference --stack instead of deprecated --provider.

The warning messages on lines 1412, 1415, and 1418 still reference --provider and DEFANG_PROVIDER which are deprecated. Consider updating them to guide users toward the new --stack approach:

-		term.Warn("AWS environment variables were detected; did you forget --provider=aws or DEFANG_PROVIDER=aws?")
+		term.Warn("AWS environment variables were detected; did you forget to use a stack with an AWS provider? See: defang stack create")

This is optional since the current messages still work during the deprecation transition period.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f573b94 and e4c335b.

📒 Files selected for processing (1)
  • src/cmd/cli/command/commands.go (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/cmd/cli/command/commands.go (7)
src/pkg/elicitations/survey.go (1)
  • NewSurveyClient (19-25)
src/pkg/elicitations/elicitations.go (3)
  • NewController (35-40)
  • Client (16-18)
  • Controller (8-14)
src/pkg/stacks/manager.go (2)
  • NewManager (34-60)
  • Manager (16-20)
src/pkg/stacks/stacks.go (3)
  • StackParameters (18-25)
  • Load (227-229)
  • List (97-99)
src/pkg/cli/client/provider.go (2)
  • Provider (43-70)
  • Loader (72-76)
src/pkg/cli/client/provider_id.go (3)
  • ProviderAuto (13-13)
  • ProviderDefang (14-14)
  • ProviderID (10-10)
src/pkg/cli/connect.go (1)
  • NewProvider (62-76)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (5)
src/cmd/cli/command/commands.go (5)

29-29: LGTM!

The elicitations package import is correctly added to support the new stack selection flow.


203-203: LGTM!

The --provider flag deprecation aligns with the stack-first approach introduced in this PR.


1438-1456: LGTM with acknowledgment of global state concern.

The function correctly implements the stack-driven provider resolution. The TODO comment on line 1444 appropriately flags the global state mutation that should be addressed in a future refactor.


1458-1493: LGTM!

The function correctly handles the nil loader case and properly integrates with the new stack-driven flow. The logic for determining outside and creating the appropriate stack manager is sound.


1495-1497: LGTM!

Clean delegation to cliClient.CanIUseProvider.

Copy link
Contributor

@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: 0

Caution

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

⚠️ Outside diff range comments (3)
src/pkg/agent/tools/estimate.go (1)

20-26: Redundant error assignment creates unused variable.

Line 24 assigns to err but then immediately returns on line 25 with a new error that wraps the original err (not the reassigned one). The assignment on line 24 is dead code.

Suggested fix
 func HandleEstimateTool(ctx context.Context, loader cliClient.Loader, params EstimateParams, cli CLIInterface, sc StackConfig) (string, error) {
 	term.Debug("Function invoked: loader.LoadProject")
 	project, err := cli.LoadProject(ctx, loader)
 	if err != nil {
-		err = fmt.Errorf("failed to parse compose file: %w", err)
 		return "", fmt.Errorf("failed to parse compose file: %w", err)
 	}
src/pkg/agent/tools/provider.go (2)

13-13: Remove duplicate constant - use stacks.CreateNewStack instead.

This constant duplicates stacks.CreateNewStack (defined at selector.go:14). Using the same constant from two places can lead to subtle bugs if they drift out of sync.

🔎 Apply this diff to remove the duplicate:
-const CreateNewStack = "Create new stack"

If this constant is used elsewhere in this file, replace usages with stacks.CreateNewStack.


35-45: Variable shadowing: err is declared twice in the same scope.

The err variable is declared with := on line 40, but it was already declared on line 37. While this works due to block scoping, it can cause confusion. The outer err (line 37) is never used.

🔎 Apply this diff to fix the shadowing:
 func (pp *providerPreparer) SetupProvider(ctx context.Context, stack *stacks.StackParameters) (*cliClient.ProviderID, cliClient.Provider, error) {
 	var providerID cliClient.ProviderID
-	var err error
 	if stack.Name == "" {
 		selector := stacks.NewSelector(pp.ec, pp.sm)
 		newStack, err := selector.SelectStack(ctx)
 		if err != nil {
 			return nil, nil, fmt.Errorf("failed to setup stack: %w", err)
 		}
 		*stack = *newStack
 	}

-	err = providerID.Set(stack.Provider.Name())
+	err := providerID.Set(stack.Provider.Name())
 	if err != nil {
 		return nil, nil, fmt.Errorf("failed to set provider ID: %w", err)
 	}
♻️ Duplicate comments (6)
src/pkg/cli/compose/loader.go (1)

94-97: Fix typo in comment: "provider" → "provided".

The comment still contains the typo. It should read "if --project-name is provided" not "if --project-name is provider".

Suggested fix
 func (l *Loader) OutsideWorkingDirectory() bool {
-	// if --project-name is provider, we assume we are outside the project's working directory
+	// if --project-name is provided, we assume we are outside the project's working directory
 	return l.options.ProjectName != ""
 }
src/pkg/stacks/manager.go (1)

44-52: Symlink-relative paths may cause incorrect outside detection.

The comparison at line 51 doesn't resolve symlinks. If either path contains symlinks, two paths pointing to the same physical directory may compare as different, incorrectly setting outside = true.

Suggested fix
 	} else {
 		// abs path for targetDirectory
-		var err error
-		absTargetDirectory, err = filepath.Abs(targetDirectory)
+		absTargetDirectory, err := filepath.Abs(targetDirectory)
 		if err != nil {
 			return nil, fmt.Errorf("failed to get absolute path for target directory: %w", err)
 		}
+		// Resolve symlinks for accurate comparison
+		if resolved, err := filepath.EvalSymlinks(workingDirectory); err == nil {
+			workingDirectory = resolved
+		}
+		if resolved, err := filepath.EvalSymlinks(absTargetDirectory); err == nil {
+			absTargetDirectory = resolved
+		}
 		outside = workingDirectory != absTargetDirectory
 	}
src/cmd/cli/command/estimate.go (1)

80-84: Default option still references unavailable provider.

As flagged in the previous review, the code sets defaultOption to ProviderDO when doInEnv() returns true (line 82), but DigitalOcean is not included in the options slice passed on lines 32-35. This mismatch may cause unexpected survey behavior or selection errors.

🔎 Apply the previously suggested fix
 	var defaultOption any // not string!
 	if awsInEnv() {
 		defaultOption = cliClient.ProviderAWS.String()
-	} else if doInEnv() {
-		defaultOption = cliClient.ProviderDO.String()
 	} else if gcpInEnv() {
 		defaultOption = cliClient.ProviderGCP.String()
 	}
src/cmd/cli/command/commands.go (3)

1347-1361: Warning message is misleading: says "deprecated" but value is still used.

The warning tells users to use --stack instead, but the code still reads and applies the provider value to stack.Provider. This is actually reasonable behavior for backwards compatibility during deprecation. Consider clarifying the warning:

-		term.Warn("Warning: --provider flag is deprecated. Please use --stack instead. To learn about stacks, visit https://docs.defang.io/docs/concepts/stacks")
+		term.Warn("Warning: --provider flag is deprecated and will be removed in a future release. Please migrate to --stack. To learn about stacks, visit https://docs.defang.io/docs/concepts/stacks")

This makes it clearer that the flag still works but will be removed later.


1369-1383: Auto-select only triggers when provider explicitly matches.

The condition knownStacks[0].Provider == stack.Provider.String() means auto-selection only occurs when:

  1. There's exactly one stack
  2. Its provider matches the explicitly set provider (or "auto" if nothing was set)

Since stack.Provider defaults to ProviderAuto, and knownStacks[0].Provider will be a concrete provider like "aws", this path will rarely match unless the user explicitly set --provider.

If the intent is to auto-select when there's exactly one stack regardless of provider, consider:

-	if len(knownStacks) == 1 && knownStacks[0].Provider == stack.Provider.String() {
+	if len(knownStacks) == 1 && (stack.Provider == cliClient.ProviderAuto || knownStacks[0].Provider == stack.Provider.String()) {

If the current behavior is intentional, the comment should clarify this limitation.


1318-1320: Typo: "RoomCmd" should be "RootCmd".

 	// This code unfortunately replicates the provider precedence rules in the
-	// RoomCmd's PersistentPreRunE func, I think we should avoid reading the
+	// RootCmd's PersistentPreRunE func, I think we should avoid reading the
 	// stack file during startup, and only read it here instead.
🧹 Nitpick comments (6)
src/pkg/stacks/manager.go (1)

104-114: Duplicate comment and verbose sorting.

Line 104-105 has a duplicate comment. The sort function can be simplified using strings.Compare.

Suggested fix
-	// sort stacks by name asc
 	// sort stacks by name asc
 	slices.SortFunc(stackList, func(a, b StackListItem) int {
-		if a.Name < b.Name {
-			return -1
-		}
-		if a.Name > b.Name {
-			return 1
-		}
-		return 0
+		return strings.Compare(a.Name, b.Name)
 	})

Note: Add "strings" to the imports.

src/pkg/stacks/selector_test.go (2)

120-123: Move WizardInterface to production code for better dependency injection.

The interface currently defined in the test file (selector_test.go) matches the contract implemented by the concrete Wizard type in production (wizard.go). Moving this interface to the main package and updating selector.go to depend on it rather than the concrete Wizard type would follow the dependency inversion principle and enable cleaner testing while supporting alternative implementations.


142-196: Consider injecting wizard dependency in production stackSelector to avoid duplication.

The testableStackSelector duplicates SelectStack logic specifically to inject a WizardInterface for testing. This duplication could be eliminated by refactoring the production stackSelector to accept wizard injection in its constructor or method, matching the pattern already used in tests.

The elicitStackSelection implementation difference is intentional—the test version correctly omits UI formatting logic (label formatting, deploy dates, info message) that belongs in production code.

src/cmd/cli/command/estimate.go (1)

70-72: Consider returning an error instead of panic.

The panic on line 71 will crash the CLI if called with fewer than 2 providers. While this appears to be an internal programming error rather than a user input issue, returning an error would be more graceful and testable.

🔎 View suggested fix
 func interactiveSelectProvider(providers []cliClient.ProviderID) (cliClient.ProviderID, error) {
 	if len(providers) < 2 {
-		panic("interactiveSelectProvider called with less than 2 providers")
+		return "", fmt.Errorf("interactiveSelectProvider requires at least 2 providers, got %d", len(providers))
 	}
src/cmd/cli/command/stack.go (1)

90-110: Consider graceful handling when project name cannot be loaded.

The current implementation returns an error if loader.LoadProjectName(ctx) fails. For stack list, users might want to see local stacks even without a valid compose file. Consider making project name loading optional:

 	projectName, err := loader.LoadProjectName(ctx)
 	if err != nil {
-		return err
+		term.Debugf("Unable to load project name: %v", err)
+		projectName = "" // Allow listing with empty project name
 	}

Alternatively, if the current behavior is intentional (require project context for remote deployment info), consider adding a clearer error message explaining why a compose file is needed.

src/cmd/cli/command/commands_test.go (1)

271-302: Side effect in mock: Load sets environment variables.

The mockStacksManager.Load method sets AWS_REGION and DEFANG_PROVIDER environment variables (lines 291-292). While t.Setenv is used (which restores values after the test), this couples the mock to specific test expectations and could cause confusion.

Consider making the mock more focused:

 func (m *mockStacksManager) Load(name string) (*stacks.StackParameters, error) {
-	m.t.Setenv("AWS_REGION", "us-west-2")
-	m.t.Setenv("DEFANG_PROVIDER", m.expectedProvider.String())
 	return &stacks.StackParameters{
 		Name:     name,
 		Provider: m.expectedProvider,
 		Region:   "us-west-2",
 	}, nil
 }

Let the test code set environment variables explicitly if needed, keeping the mock's responsibility limited to returning data.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e4c335b and bc99562.

📒 Files selected for processing (35)
  • src/cmd/cli/command/commands.go (5 hunks)
  • src/cmd/cli/command/commands_test.go (9 hunks)
  • src/cmd/cli/command/compose.go (5 hunks)
  • src/cmd/cli/command/estimate.go (3 hunks)
  • src/cmd/cli/command/globals.go (1 hunks)
  • src/cmd/cli/command/stack.go (2 hunks)
  • src/cmd/cli/command/stack_test.go (4 hunks)
  • src/cmd/cli/command/workspace_test.go (2 hunks)
  • src/pkg/agent/tools/default_tool_cli.go (1 hunks)
  • src/pkg/agent/tools/deploy.go (2 hunks)
  • src/pkg/agent/tools/deploy_test.go (1 hunks)
  • src/pkg/agent/tools/destroy.go (2 hunks)
  • src/pkg/agent/tools/destroy_test.go (1 hunks)
  • src/pkg/agent/tools/estimate.go (1 hunks)
  • src/pkg/agent/tools/interfaces.go (1 hunks)
  • src/pkg/agent/tools/listConfig.go (1 hunks)
  • src/pkg/agent/tools/logs.go (3 hunks)
  • src/pkg/agent/tools/provider.go (1 hunks)
  • src/pkg/agent/tools/removeConfig.go (1 hunks)
  • src/pkg/agent/tools/services.go (1 hunks)
  • src/pkg/agent/tools/setConfig.go (1 hunks)
  • src/pkg/cli/client/byoc/baseclient.go (1 hunks)
  • src/pkg/cli/client/byoc/gcp/byoc.go (1 hunks)
  • src/pkg/cli/client/caniuse.go (2 hunks)
  • src/pkg/cli/client/client.go (0 hunks)
  • src/pkg/cli/client/mock.go (1 hunks)
  • src/pkg/cli/client/playground.go (1 hunks)
  • src/pkg/cli/client/provider.go (1 hunks)
  • src/pkg/cli/compose/loader.go (1 hunks)
  • src/pkg/stacks/manager.go (1 hunks)
  • src/pkg/stacks/manager_test.go (12 hunks)
  • src/pkg/stacks/selector.go (1 hunks)
  • src/pkg/stacks/selector_test.go (1 hunks)
  • src/pkg/stacks/stacks.go (2 hunks)
  • src/testdata/sanity/.defang/beta (1 hunks)
💤 Files with no reviewable changes (1)
  • src/pkg/cli/client/client.go
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/pkg/cli/client/playground.go
  • src/pkg/cli/client/provider.go
  • src/pkg/stacks/stacks.go
  • src/pkg/agent/tools/listConfig.go
  • src/pkg/cli/client/mock.go
  • src/pkg/cli/client/caniuse.go
  • src/pkg/agent/tools/default_tool_cli.go
  • src/pkg/cli/client/byoc/gcp/byoc.go
  • src/pkg/stacks/selector.go
  • src/cmd/cli/command/stack_test.go
🧰 Additional context used
🧬 Code graph analysis (19)
src/pkg/stacks/selector_test.go (6)
src/pkg/stacks/stacks.go (5)
  • List (97-99)
  • StackListItem (87-95)
  • Load (227-229)
  • StackParameters (18-25)
  • Create (36-38)
src/pkg/stacks/selector.go (2)
  • CreateNewStack (15-15)
  • NewSelector (22-27)
src/pkg/cli/client/provider_id.go (1)
  • ProviderAWS (15-15)
src/pkg/modes/modes.go (3)
  • Mode (12-12)
  • ModeBalanced (17-17)
  • ModeAffordable (16-16)
src/pkg/elicitations/elicitations.go (1)
  • Controller (8-14)
src/pkg/stacks/manager.go (1)
  • Manager (16-20)
src/pkg/agent/tools/destroy.go (6)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/agent/tools/default_tool_cli.go (1)
  • StackConfig (19-22)
src/pkg/mcp/tools/tools.go (1)
  • StackConfig (14-17)
src/pkg/cli/connect.go (1)
  • Connect (17-19)
src/pkg/stacks/manager.go (1)
  • NewManager (34-60)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/agent/tools/removeConfig.go (5)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/agent/tools/interfaces.go (1)
  • CLIInterface (14-32)
src/pkg/agent/tools/default_tool_cli.go (1)
  • StackConfig (19-22)
src/pkg/cli/connect.go (1)
  • Connect (17-19)
src/pkg/stacks/manager.go (1)
  • NewManager (34-60)
src/pkg/agent/tools/provider.go (1)
src/pkg/stacks/selector.go (1)
  • NewSelector (22-27)
src/cmd/cli/command/globals.go (3)
src/pkg/stacks/stacks.go (1)
  • StackParameters (18-25)
src/pkg/cli/client/provider_id.go (1)
  • ProviderAuto (13-13)
src/pkg/modes/modes.go (2)
  • Mode (12-12)
  • ModeUnspecified (15-15)
src/pkg/stacks/manager.go (4)
src/pkg/stacks/stacks.go (9)
  • List (97-99)
  • StackListItem (87-95)
  • Load (227-229)
  • StackParameters (18-25)
  • Create (36-38)
  • ListInDirectory (101-135)
  • ReadInDirectory (213-225)
  • LoadInDirectory (231-239)
  • CreateInDirectory (40-84)
src/pkg/modes/modes.go (1)
  • Mode (12-12)
src/pkg/cli/client/provider_id.go (1)
  • ProviderID (10-10)
src/pkg/cli/client/region.go (1)
  • GetRegion (5-16)
src/cmd/cli/command/stack.go (2)
src/pkg/stacks/manager.go (1)
  • NewManager (34-60)
src/pkg/stacks/stacks.go (1)
  • List (97-99)
src/pkg/agent/tools/deploy_test.go (2)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/cmd/cli/command/compose.go (5)
src/pkg/cli/client/account_info.go (1)
  • AccountInfo (5-10)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/elicitations/survey.go (1)
  • NewSurveyClient (19-25)
src/pkg/elicitations/elicitations.go (2)
  • NewController (35-40)
  • Client (16-18)
src/pkg/stacks/manager.go (1)
  • NewManager (34-60)
src/pkg/agent/tools/services.go (3)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/stacks/manager.go (1)
  • NewManager (34-60)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (26-33)
src/cmd/cli/command/estimate.go (6)
src/pkg/cli/client/provider_id.go (3)
  • ProviderID (10-10)
  • ProviderAWS (15-15)
  • ProviderGCP (17-17)
src/pkg/term/colorizer.go (2)
  • Errorf (322-324)
  • DefaultTerm (27-27)
src/pkg/track/track.go (1)
  • Evt (37-52)
src/cmd/cli/command/commands.go (1)
  • P (54-54)
src/pkg/cli/tail.go (1)
  • P (93-93)
src/pkg/debug/debug.go (1)
  • P (21-21)
src/pkg/cli/compose/loader.go (2)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/clouds/driver.go (1)
  • ProjectName (8-8)
src/pkg/agent/tools/interfaces.go (2)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/agent/tools/estimate.go (2)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/agent/tools/interfaces.go (1)
  • CLIInterface (14-32)
src/pkg/agent/tools/destroy_test.go (2)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/agent/tools/setConfig.go (2)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/stacks/manager.go (1)
  • NewManager (34-60)
src/pkg/agent/tools/logs.go (3)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/stacks/manager.go (1)
  • NewManager (34-60)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/cmd/cli/command/commands_test.go (4)
src/pkg/stacks/manager.go (1)
  • Manager (16-20)
src/pkg/cli/client/provider_id.go (4)
  • ProviderID (10-10)
  • ProviderAWS (15-15)
  • ProviderAuto (13-13)
  • ProviderDefang (14-14)
src/pkg/stacks/stacks.go (5)
  • List (97-99)
  • StackListItem (87-95)
  • Load (227-229)
  • StackParameters (18-25)
  • Create (36-38)
src/pkg/modes/modes.go (2)
  • Mode (12-12)
  • ModeUnspecified (15-15)
src/pkg/stacks/manager_test.go (5)
src/pkg/stacks/manager.go (1)
  • NewManager (34-60)
src/pkg/stacks/stacks.go (5)
  • List (97-99)
  • Create (36-38)
  • StackParameters (18-25)
  • StackListItem (87-95)
  • Directory (29-29)
src/protos/io/defang/v1/fabric.pb.go (2)
  • Provider_AWS (33-33)
  • Provider_GCP (35-35)
src/pkg/cli/client/provider_id.go (1)
  • ProviderAWS (15-15)
src/pkg/modes/modes.go (2)
  • Mode (12-12)
  • ModeAffordable (16-16)
🪛 GitHub Actions: Go package
src/cmd/cli/command/workspace_test.go

[error] 1-1: Tests failed in package 'github.com/DefangLabs/defang/src/cmd/cli/command'.

src/cmd/cli/command/commands.go

[error] 1-1: Tests failed in package 'github.com/DefangLabs/defang/src/cmd/cli/command'.

src/cmd/cli/command/globals.go

[error] 1-1: Tests failed in package 'github.com/DefangLabs/defang/src/cmd/cli/command'.

src/cmd/cli/command/stack.go

[error] 1-1: Tests failed in package 'github.com/DefangLabs/defang/src/cmd/cli/command'.

src/cmd/cli/command/compose.go

[error] 1-1: Tests failed in package 'github.com/DefangLabs/defang/src/cmd/cli/command'.

src/cmd/cli/command/estimate.go

[error] 1-1: Tests failed in package 'github.com/DefangLabs/defang/src/cmd/cli/command'.

src/cmd/cli/command/commands_test.go

[error] 1-1: Tests failed in package 'github.com/DefangLabs/defang/src/cmd/cli/command'.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (61)
src/testdata/sanity/.defang/beta (1)

1-2: LGTM! Appropriate test data for stack configuration.

The environment variable format is standard and the values (AWS provider with us-west-2 region) are appropriate for testing the beta stack functionality introduced in this PR.

src/cmd/cli/command/globals.go (1)

98-98: LGTM!

The Name field initialization aligns correctly with the StackParameters struct definition. The empty string default is appropriate as the stack name will be determined later via flags or environment variables.

src/pkg/stacks/manager.go (1)

140-161: Good handling of implicit "beta" stack for legacy deployments.

The logic correctly treats deployments without an explicit stack name as the implicit "beta" stack (line 142-144), which aligns with the PR objective to surface the implicit beta stack for existing projects. The map-based deduplication with "first wins" semantics (line 148) correctly preserves the most recent deployment since deployments are pre-sorted by deployed_at desc.

src/pkg/agent/tools/removeConfig.go (1)

21-31: LGTM!

The function correctly adapts to the new stacks.NewManager signature and adds appropriate error handling. The loader type change from ProjectLoader to Loader aligns with the interface updates across the codebase.

src/pkg/stacks/selector_test.go (9)

16-43: LGTM! Well-structured mock implementations.

The mock implementations correctly implement all methods of the elicitations.Controller interface with proper type assertions and error handling.


45-77: LGTM! MockStacksManager handles nil returns correctly.

The mock properly handles nil return values with type assertions, preventing panics when simulating error scenarios.


79-118: LGTM! Comprehensive test for existing stack selection.

The test properly validates the complete flow: elicitations support check → list stacks → user selection → load stack parameters.


198-248: LGTM! Good test coverage for the "create new stack" flow.

The test validates the complete wizard-driven stack creation flow including parameter collection, stack creation, and subsequent loading.


250-293: LGTM! Tests the automatic stack creation when no stacks exist.

Good coverage for the edge case where elicitStackSelection returns CreateNewStack automatically due to empty stack list.


295-314: LGTM! Proper error handling test for unsupported elicitations.

The test correctly verifies that when elicitations are not supported, an appropriate error message is returned guiding users to use the select_stack tool.


316-405: LGTM! Comprehensive error path coverage.

The tests for ListStacksError, ElicitationError, and LoadStackError properly validate error propagation and message wrapping.


407-497: LGTM! Good coverage for wizard and creation error paths.

Tests properly validate that errors from wizard parameter collection and stack creation are correctly propagated with appropriate context.


499-536: LGTM! Sub-tests for elicitStackSelection behavior.

Good use of table-driven sub-tests to verify both the "multiple stacks" and "no stacks" scenarios for the elicitation helper.

src/pkg/stacks/manager_test.go (9)

18-31: LGTM! Clean mock implementation for FabricClient.

The mock properly simulates both success and error scenarios for ListDeployments.


46-66: LGTM! Test properly updated for new Manager API.

The test correctly uses t.Chdir() to set up the working directory match condition and passes context to List().


340-391: LGTM! Good test coverage for remote stack listing.

The test validates that remote deployments are correctly converted to StackListItem entries with proper field mapping including DeployedAt.


411-509: LGTM! Comprehensive test for merged local/remote stack lists.

The test validates the expected merge behavior:

  • Shared stacks prefer remote data (region, deployment time)
  • Remote-only stacks are included
  • Local-only stacks retain their properties without deployment time

511-543: LGTM! Good test for beta stack onboarding.

This test validates the core onboarding feature: empty Stack field in deployments is correctly interpreted as the implicit "beta" stack.


545-584: LGTM! Tests duplicate deployment handling.

The test confirms that when multiple deployments exist for the same stack name, only one entry is returned (using the first/most recent deployment).


586-638: LGTM! Tests local operations when directories match.

Good validation that Create, List, and Load all work correctly when the working directory matches the target directory.


640-716: Good test for cross-project access prevention.

This test validates the security feature that prevents local stack file access when operating outside the project directory, while still allowing remote stack listing.


718-752: LGTM! Confirms remote operations work regardless of directory.

Good validation that ListRemote is not subject to directory restrictions.

src/cmd/cli/command/workspace_test.go (2)

90-92: Same fix applied - ensure both tests pass together.

Both tests apply the same reset pattern. Verify this is sufficient by running the tests locally or checking if additional state needs to be cleared.


55-57: The fix is sufficient - tests properly isolate global state.

The tests save and restore the entire global state via cleanup, ensuring full restoration after execution. Resetting global.Stack.Name to "" before the test is intentional and targeted, preventing unwanted stack file loading during the test execution. This pattern provides complete test isolation without requiring other global fields to be explicitly reset.

src/pkg/cli/client/byoc/baseclient.go (2)

74-77: LGTM! Simple accessor implementing the Provider interface.

The GetStackName() method correctly returns the PulumiStack field, enabling stack-driven flows throughout the codebase.


116-122: The current implementation is correct. GetProjectDomain returns only the normalized zone, which is the intended behavior. Stack management is handled separately through a tagging system (zone tags like defang:stack), not by prefixing the domain. There is no evidence of a previous implementation that included the stack name in the domain, and this change does not affect existing deployments.

Likely an incorrect or invalid review comment.

src/pkg/agent/tools/provider.go (1)

39-44: LGTM! Clean integration with the new selector workflow.

The delegation to stacks.NewSelector and the in-place update via pointer assignment correctly integrates with the new stack selection flow while maintaining the caller's state.

src/cmd/cli/command/estimate.go (1)

31-40: Interactive provider selection integrated correctly.

The flow correctly prompts for provider selection when auto-detection is active and updates the global stack provider with the result. Error handling and context wrapping are appropriate.

src/pkg/agent/tools/interfaces.go (1)

15-15: LGTM!

The CanIUseProvider signature update correctly aligns with the implementation in src/pkg/cli/client/caniuse.go. The removal of stackName and reordering of provider before projectName is consistent with the stack-driven provider resolution flow introduced in this PR.

src/pkg/agent/tools/deploy_test.go (1)

80-83: LGTM!

The mock signature correctly matches the updated CLIInterface.CanIUseProvider method. The parameter order change (provider before projectName) and removal of stackName align with the interface definition and implementation.

src/pkg/agent/tools/destroy_test.go (1)

60-66: LGTM!

The mock signature correctly reflects the updated interface with provider before projectName and stackName removed. The implementation properly logs the call and returns the configured error.

src/pkg/agent/tools/services.go (2)

23-23: LGTM!

The signature update from cliClient.ProjectLoader to cliClient.Loader aligns with the broader refactor across tool handlers and the updated Loader interface definition.


30-34: LGTM!

The stack manager creation correctly uses the new 3-parameter signature stacks.NewManager(client, workingDirectory, projectName) with proper error handling. The error is appropriately wrapped with context and propagated.

src/pkg/agent/tools/setConfig.go (3)

21-21: LGTM!

The signature update to cliClient.Loader is consistent with the refactor across all tool handlers.


28-36: LGTM!

The stack manager creation and provider setup follow the standardized pattern introduced in this PR. Error handling is appropriate and errors are properly wrapped with context.


38-45: LGTM!

The project name loading flow correctly handles the case where params.ProjectName is empty by calling LoadProjectNameWithFallback and assigning the result. Error handling and wrapping are appropriate.

src/pkg/agent/tools/destroy.go (3)

20-20: LGTM!

The signature update to cliClient.Loader is consistent with the broader refactor across tool handlers.


27-35: LGTM!

The stack manager creation follows the new 3-parameter pattern with proper error handling, and the provider setup flow is correctly integrated. Error wrapping provides clear context.


42-45: LGTM!

The CanIUseProvider call correctly uses the updated signature with provider before projectName and without the stackName parameter. Error handling and wrapping are appropriate.

src/pkg/agent/tools/logs.go (3)

25-25: LGTM!

The signature update to cliClient.Loader aligns with the refactor applied consistently across all tool handlers in this PR.


48-56: LGTM!

The stack manager creation and provider setup follow the standardized pattern with proper error handling. The new 3-parameter NewManager signature is used correctly and errors are appropriately wrapped.


65-68: LGTM!

The CanIUseProvider call correctly uses the updated signature with provider positioned before projectName. Error handling is appropriate.

src/cmd/cli/command/compose.go (4)

8-8: LGTM: New imports support elicitations and stack management.

The added imports for os and elicitations are required for the new stack-driven provider resolution flow in makeComposeConfigCmd.

Also applies to: 22-22


117-117: LGTM: Passing stack name to handleExistingDeployments.

Correctly passes provider.GetStackName() to enable proper stack file creation when deploying to a new location.


223-252: LGTM: Updated signature and provider consistency check.

The function now:

  1. Accepts an explicit stackName parameter for stack awareness
  2. Compares dep.Provider against accountInfo.Provider.Value() for proper type matching
  3. Defaults to "beta" when stackName is empty, supporting the implicit beta stack onboarding

The logic correctly filters deployments by provider, account ID, and region.


566-577: LGTM: Elicitations and stack manager wiring in compose config.

The inline construction of elicitations client, controller, and stack manager follows the same pattern used elsewhere in the PR. The error handling for working directory and stack manager creation is appropriate.

src/pkg/agent/tools/deploy.go (2)

25-25: LGTM: Updated function signature.

The change from cliClient.ProjectLoader to cliClient.Loader aligns with the broader interface updates in this PR for stack-driven provider resolution.


47-57: LGTM: Stack manager creation and provider usage check.

The stack manager is correctly instantiated with client, params.WorkingDirectory, and params.ProjectName. Error propagation is properly handled. The CanIUseProvider call matches the updated signature used elsewhere in the codebase.

src/cmd/cli/command/stack.go (1)

130-131: LGTM: Extended table columns.

The addition of the "DeployedAt" column provides useful information about when stacks were last deployed.

src/cmd/cli/command/commands.go (6)

199-199: LGTM: Provider flag deprecation.

Marking --provider as deprecated guides users toward the new --stack approach.


555-570: LGTM: Whoami command updated for stack-driven flow.

The whoami command now properly creates elicitations client, controller, and stack manager to integrate with the new provider resolution path.


1403-1431: LGTM: Provider mismatch warnings extracted to helper.

Clean extraction of warning logic into a dedicated function improves readability and reusability.


1434-1452: LGTM: Refactored newProvider with stack-driven flow.

The function now correctly:

  1. Uses getStack for stack resolution
  2. Updates the global stack state (with TODO noting this should be removed)
  3. Prints informative message about stack usage
  4. Calls provider mismatch warnings

1454-1489: LGTM: newProviderChecked properly handles nil loader and outside working directory.

The function correctly:

  1. Handles nil loader case (addressed from past review)
  2. Uses loader.OutsideWorkingDirectory() to determine stack manager construction
  3. Creates appropriate stack manager based on context

1491-1492: LGTM: Simplified canIUseProvider.

Clean delegation to cliClient.CanIUseProvider with the updated signature.

src/cmd/cli/command/commands_test.go (6)

16-17: LGTM: New imports for test support.

The modes and stacks imports are required for the new test mocks and assertions.


82-91: LGTM: ListDeployments mock added to fabric service.

Returns a mock deployment for testing the new deployment-aware stack flow.


242-253: LGTM: MockFabricControllerClient enhanced.

Good addition of nil check for savedProvider map and ListDeployments implementation.


427-500: LGTM: Well-structured mock implementations.

Both mockElicitationsController and mockStackManager provide flexible mocking capabilities:

  • Support for default behaviors and specific overrides
  • Clean interface implementations
  • Appropriate field exposure for test configuration

501-802: Comprehensive test suite for getStack.

The TestGetStack function provides good coverage of:

  • Stack flag with valid/invalid stacks
  • Provider flag deprecation path
  • Environment variable handling
  • Non-interactive defaults
  • Single stack auto-selection
  • Interactive selection
  • Error propagation

Minor observation: The test at line 750 creates an output buffer that's never used for capturing warnings (as noted in the comment at line 793-798). This is acceptable since verifying term.Warn output in tests is complex.


345-388: Test setup correctly triggers production behavior.

The test properly tests the DEFANG_STACK environment variable path. Since os.Getenv("DEFANG_STACK") is set to "beta", the condition in getStack evaluates to true and the code executes as expected. The test does not test the --stack flag alternative path, but that limitation is outside the scope of this test case.

Copy link
Contributor

@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: 0

🧹 Nitpick comments (2)
src/cmd/cli/command/commands.go (2)

1347-1361: Clarify deprecated flag behavior to reduce user confusion.

The deprecation warnings state "please use --stack instead," but the code immediately reads and uses the deprecated flag/environment variable values. While technically correct (deprecated doesn't mean removed), users might expect these to be ignored based on the warning.

Consider either:

  1. Making the warning more explicit: "Warning: --provider flag is deprecated but still supported. Please migrate to --stack. See https://docs.defang.io/docs/concepts/stacks"
  2. Adding a brief code comment explaining that deprecated inputs are honored for backward compatibility

This would help users understand that their existing workflows still work while encouraging migration.


1369-1383: Consider adding a clarifying comment about auto-select conditions.

The auto-select logic at line 1370 only triggers when there's exactly one stack AND the provider matches. When stack.Provider is ProviderAuto (the default when no provider is specified), this condition won't match even if there's only one stack available.

This appears intentional based on past review discussions, but a brief comment explaining the behavior would improve maintainability:

// Auto-select only when provider was explicitly specified and matches the single known stack.
// When provider is Auto (not specified), we'll prompt the user even with one stack to increase awareness.
if len(knownStacks) == 1 && knownStacks[0].Provider == stack.Provider.String() {
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc99562 and d1f8d44.

📒 Files selected for processing (1)
  • src/cmd/cli/command/commands.go (5 hunks)
🔇 Additional comments (4)
src/cmd/cli/command/commands.go (4)

555-570: LGTM: whoami integration with stack-driven provider resolution.

The changes correctly integrate the whoami command with the new stack-driven provider resolution flow. Setting global.NonInteractive = true appropriately prevents prompts, and error handling allows the command to function even when project loading fails.


1403-1432: LGTM: helpful provider mismatch warnings.

This function provides valuable feedback to users when their environment variables don't match their selected provider, helping catch common configuration mistakes early.


1434-1452: LGTM: clean integration of stack-driven provider resolution.

The function properly delegates to getStack, provides clear user feedback about which stack and provider are being used, validates the configuration with mismatch warnings, and creates the provider. The TODO comment on line 1440 appropriately flags the global state mutation for future refactoring.


1454-1489: LGTM: robust nil handling and stack manager initialization.

The function correctly handles the nil loader case (lines 1458-1464) and properly determines whether to create a stack manager with or without a working directory based on the loader's location. This supports the "remote stacks" feature mentioned in the PR objectives. Error handling is comprehensive throughout.

jordanstephens and others added 11 commits December 18, 2025 13:48
This update allows the stack manager to list both local and remote stacks,
enabling deployment to previously deployed stacks without requiring local
stack files. Local stacks are read from the `.defang` directory while
remote stacks are fetched from deployed infrastructure.

Key changes:
- Enhanced Manager interface to accept context for remote API calls
- Extended NewManager to accept DeploymentLister for remote stack queries
- Added ListRemote method to fetch deployed stacks from infrastructure
- Updated List method to merge local and remote stacks with remote precedence
- Added DeployedAt timestamp to StackListItem for deployment tracking

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
The NewManager function signature was changed to return error for better error handling.
Updated all callers to properly handle the error return value and added appropriate
error checking with descriptive error messages.

Changes:
- Updated all agent tools to handle NewManager error return
- Fixed test cases to use proper error handling syntax
- Added error checks with consistent "failed to create stack manager" messages

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
The stack manager was updated to enforce working directory restrictions for local
operations, but the tests were failing due to symlink path comparison issues and
improper test setup. This commit addresses both the underlying symlink resolution
issue in the manager and updates tests to properly handle directory constraints.

Key fixes:
- Add symlink resolution in NewManager to handle macOS /var -> /private/var symlinks
- Update failing tests to use t.Chdir() when testing local operations
- Add comprehensive test coverage for both directory scenarios:
  * TestManager_WorkingDirectoryMatches: validates local operations work when directories match
  * TestManager_WorkingDirectoryDifferent: validates local operations are properly blocked
  * TestManager_RemoteOperationsWorkRegardlessOfDirectory: validates remote operations always work
- Add proper error message validation in directory mismatch tests

All tests now pass, providing clear validation of the manager's directory-based
access control while maintaining backwards compatibility for valid use cases.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Copy link
Contributor

@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: 6

♻️ Duplicate comments (5)
src/cmd/cli/command/commands.go (3)

1311-1313: Typo: "RoomCmd" should be "RootCmd".

This was noted in a previous review and appears to still be present.

 	// This code unfortunately replicates the provider precedence rules in the
-	// RoomCmd's PersistentPreRunE func, I think we should avoid reading the
+	// RootCmd's PersistentPreRunE func, I think we should avoid reading the
 	// stack file during startup, and only read it here instead.

1377-1382: Bug: stack.Provider should use the parsed providerID from the known stack.

On line 1366, knownStack.Provider is parsed into providerID, but on line 1379, stack.Provider uses the original value (which could be ProviderAuto) instead of the parsed providerID.

🔎 Proposed fix
 		stack = &stacks.StackParameters{
 			Name:     knownStack.Name,
-			Provider: stack.Provider,
+			Provider: providerID,
 			Region:   knownStack.Region,
 			Mode:     mode,
 		}

1362-1389: Auto-select condition doesn't work when provider is unspecified.

When the user doesn't specify --provider, stack.Provider remains ProviderAuto ("auto"), but real stacks have concrete providers like "aws" or "gcp". The condition on line 1363 compares knownStacks[0].Provider (e.g., "aws") against stack.Provider.String() ("auto"), which will never match.

Auto-selection only works when the user explicitly sets --provider to match the stack's provider. For a single stack with an unspecified provider, users are prompted to select instead of being auto-selected.

If the intent is to auto-select any single stack regardless of provider specification, apply the proposed fix:

 	// if there is exactly one stack with that provider, use it
-	if len(knownStacks) == 1 && knownStacks[0].Provider == stack.Provider.String() {
+	if len(knownStacks) == 1 && (stack.Provider == cliClient.ProviderAuto || knownStacks[0].Provider == stack.Provider.String()) {
src/pkg/stacks/selector.go (1)

101-115: Past review comment is incorrect; current implementation works correctly.

The past review comment claimed that the beta detection at line 103 would fail for deployed stacks because stackNames contains labels like "beta (deployed Jan 2)". However, examining the code at lines 66-77, stackNames is populated with s.Name (the raw stack name), while stackLabels receives the formatted version with deployment dates. The betaExists check at line 103 correctly uses stackNames, so it will detect "beta" regardless of deployment status.

src/pkg/stacks/manager.go (1)

42-52: Symlink-relative paths may cause incorrect outside detection.

The comparison at line 52 uses raw absolute paths without resolving symlinks. If either workingDirectory or absTargetDirectory contains symlinks, two paths pointing to the same physical directory may compare as different, incorrectly setting outside = true and blocking legitimate local operations (Create, Load, ListLocal).

🔎 Proposed fix to resolve symlinks before comparison
 	} else {
 		// abs path for targetDirectory
 		var err error
 		absTargetDirectory, err = filepath.Abs(targetDirectory)
 		if err != nil {
 			return nil, fmt.Errorf("failed to get absolute path for target directory: %w", err)
 		}
+		// Resolve symlinks for accurate comparison
+		resolvedWD, err := filepath.EvalSymlinks(workingDirectory)
+		if err != nil {
+			return nil, fmt.Errorf("failed to resolve symlinks in working directory: %w", err)
+		}
+		resolvedTarget, err := filepath.EvalSymlinks(absTargetDirectory)
+		if err != nil {
+			return nil, fmt.Errorf("failed to resolve symlinks in target directory: %w", err)
+		}
+		outside = resolvedWD != resolvedTarget
-		outside = workingDirectory != absTargetDirectory
 	}

Based on learnings from past review comments.

🧹 Nitpick comments (3)
src/pkg/stacks/stacks.go (1)

181-186: DeployedAt field is not populated for local stacks.

When listing stacks from the .defang directory, DeployedAt is initialized to its zero value. Per the AI summary, this is noted as "not yet populated in the existing code path." Consider adding a comment here to clarify that DeployedAt is populated only for remote/deployed stacks via the manager's List method.

src/pkg/stacks/selector_test.go (1)

147-201: Test double testableStackSelector duplicates production logic.

The testableStackSelector.SelectStack method duplicates the flow from the production stackSelector. This means:

  1. Tests for the "Create New Stack" path (lines 203-253, 255-298, 412-451, 453-502) test the test double, not the production code
  2. Changes to production stackSelector.SelectStack won't be caught by these tests

Consider either:

  • Refactoring production code to accept a WizardInterface dependency
  • Or adding integration tests that exercise the actual production path
src/pkg/stacks/selector.go (1)

52-52: Wrap Load error for consistency.

For consistency with error handling elsewhere in this function (lines 35, 42, 46), wrap the Load error with context.

🔎 Proposed fix
-	return ss.sm.Load(selectedStackName)
+	stack, err := ss.sm.Load(selectedStackName)
+	if err != nil {
+		return nil, fmt.Errorf("failed to load stack %q: %w", selectedStackName, err)
+	}
+	return stack, nil
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1f8d44 and 67cb5b5.

📒 Files selected for processing (36)
  • src/cmd/cli/command/commands.go (5 hunks)
  • src/cmd/cli/command/commands_test.go (9 hunks)
  • src/cmd/cli/command/compose.go (5 hunks)
  • src/cmd/cli/command/estimate.go (3 hunks)
  • src/cmd/cli/command/globals.go (1 hunks)
  • src/cmd/cli/command/stack.go (2 hunks)
  • src/cmd/cli/command/stack_test.go (5 hunks)
  • src/cmd/cli/command/workspace_test.go (2 hunks)
  • src/pkg/agent/tools/default_tool_cli.go (1 hunks)
  • src/pkg/agent/tools/deploy.go (2 hunks)
  • src/pkg/agent/tools/deploy_test.go (1 hunks)
  • src/pkg/agent/tools/destroy.go (2 hunks)
  • src/pkg/agent/tools/destroy_test.go (1 hunks)
  • src/pkg/agent/tools/estimate.go (1 hunks)
  • src/pkg/agent/tools/interfaces.go (1 hunks)
  • src/pkg/agent/tools/listConfig.go (1 hunks)
  • src/pkg/agent/tools/logs.go (3 hunks)
  • src/pkg/agent/tools/provider.go (1 hunks)
  • src/pkg/agent/tools/removeConfig.go (1 hunks)
  • src/pkg/agent/tools/services.go (1 hunks)
  • src/pkg/agent/tools/setConfig.go (1 hunks)
  • src/pkg/cli/client/byoc/baseclient.go (1 hunks)
  • src/pkg/cli/client/byoc/gcp/byoc.go (1 hunks)
  • src/pkg/cli/client/caniuse.go (2 hunks)
  • src/pkg/cli/client/client.go (0 hunks)
  • src/pkg/cli/client/mock.go (1 hunks)
  • src/pkg/cli/client/playground.go (1 hunks)
  • src/pkg/cli/client/provider.go (1 hunks)
  • src/pkg/cli/compose/loader.go (1 hunks)
  • src/pkg/stacks/manager.go (1 hunks)
  • src/pkg/stacks/manager_test.go (12 hunks)
  • src/pkg/stacks/selector.go (1 hunks)
  • src/pkg/stacks/selector_test.go (1 hunks)
  • src/pkg/stacks/stacks.go (5 hunks)
  • src/pkg/stacks/stacks_test.go (1 hunks)
  • src/testdata/sanity/.defang/beta (1 hunks)
💤 Files with no reviewable changes (1)
  • src/pkg/cli/client/client.go
✅ Files skipped from review due to trivial changes (1)
  • src/testdata/sanity/.defang/beta
🚧 Files skipped from review as they are similar to previous changes (11)
  • src/pkg/cli/client/playground.go
  • src/pkg/cli/client/provider.go
  • src/cmd/cli/command/workspace_test.go
  • src/pkg/cli/client/byoc/baseclient.go
  • src/pkg/cli/compose/loader.go
  • src/pkg/cli/client/byoc/gcp/byoc.go
  • src/pkg/agent/tools/default_tool_cli.go
  • src/cmd/cli/command/stack.go
  • src/pkg/agent/tools/deploy_test.go
  • src/pkg/agent/tools/interfaces.go
  • src/cmd/cli/command/globals.go
🧰 Additional context used
🧬 Code graph analysis (21)
src/pkg/agent/tools/deploy.go (5)
src/pkg/cli/compose/loader.go (2)
  • Loader (38-41)
  • Services (29-29)
src/pkg/agent/tools/interfaces.go (1)
  • CLIInterface (14-32)
src/pkg/stacks/manager.go (1)
  • NewManager (35-61)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (26-33)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/agent/tools/estimate.go (3)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/cli/compose/loader.go (1)
  • Loader (38-41)
src/pkg/agent/tools/interfaces.go (1)
  • CLIInterface (14-32)
src/pkg/agent/tools/provider.go (1)
src/pkg/stacks/selector.go (1)
  • NewSelector (22-27)
src/cmd/cli/command/estimate.go (5)
src/pkg/cli/client/provider_id.go (3)
  • ProviderID (10-10)
  • ProviderAWS (15-15)
  • ProviderGCP (17-17)
src/pkg/track/track.go (1)
  • Evt (37-52)
src/cmd/cli/command/commands.go (1)
  • P (54-54)
src/pkg/cli/tail.go (1)
  • P (93-93)
src/pkg/debug/debug.go (1)
  • P (21-21)
src/pkg/agent/tools/services.go (3)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/stacks/manager.go (1)
  • NewManager (35-61)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (26-33)
src/pkg/agent/tools/removeConfig.go (3)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/cli/connect.go (1)
  • Connect (17-19)
src/pkg/stacks/manager.go (1)
  • NewManager (35-61)
src/pkg/agent/tools/setConfig.go (2)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/stacks/manager.go (1)
  • NewManager (35-61)
src/pkg/agent/tools/destroy_test.go (2)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/stacks/manager_test.go (5)
src/pkg/stacks/manager.go (1)
  • NewManager (35-61)
src/pkg/stacks/stacks.go (6)
  • List (152-154)
  • Create (91-93)
  • Load (236-238)
  • StackParameters (18-25)
  • StackListItem (142-150)
  • Directory (84-84)
src/protos/io/defang/v1/fabric.pb.go (2)
  • Provider_AWS (33-33)
  • Provider_GCP (35-35)
src/pkg/cli/client/provider_id.go (1)
  • ProviderAWS (15-15)
src/pkg/modes/modes.go (2)
  • Mode (12-12)
  • ModeAffordable (16-16)
src/pkg/stacks/selector_test.go (6)
src/pkg/stacks/stacks.go (6)
  • List (152-154)
  • StackListItem (142-150)
  • Load (236-238)
  • StackParameters (18-25)
  • LoadParameters (266-284)
  • Create (91-93)
src/pkg/stacks/selector.go (2)
  • CreateNewStack (15-15)
  • NewSelector (22-27)
src/pkg/cli/client/provider_id.go (1)
  • ProviderAWS (15-15)
src/pkg/modes/modes.go (3)
  • Mode (12-12)
  • ModeBalanced (17-17)
  • ModeAffordable (16-16)
src/pkg/elicitations/elicitations.go (1)
  • Controller (8-14)
src/pkg/stacks/manager.go (1)
  • Manager (16-21)
src/pkg/cli/client/caniuse.go (2)
src/pkg/cli/client/client.go (1)
  • FabricClient (11-38)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/stacks/stacks.go (2)
src/pkg/cli/client/provider_id.go (2)
  • ProviderAWS (15-15)
  • ProviderGCP (17-17)
src/pkg/modes/modes.go (2)
  • Mode (12-12)
  • ModeUnspecified (15-15)
src/cmd/cli/command/stack_test.go (3)
src/pkg/cli/client/grpc.go (1)
  • GrpcClient (19-26)
src/cmd/cli/command/commands_test.go (1)
  • MockFabricControllerClient (228-232)
src/pkg/stacks/stacks.go (1)
  • LoadParameters (266-284)
src/cmd/cli/command/compose.go (5)
src/pkg/cli/client/account_info.go (1)
  • AccountInfo (5-10)
src/pkg/cli/client/provider.go (1)
  • Provider (43-70)
src/pkg/elicitations/survey.go (1)
  • NewSurveyClient (19-25)
src/pkg/elicitations/elicitations.go (2)
  • NewController (35-40)
  • Client (16-18)
src/pkg/stacks/manager.go (1)
  • NewManager (35-61)
src/cmd/cli/command/commands.go (8)
src/pkg/elicitations/survey.go (1)
  • NewSurveyClient (19-25)
src/pkg/elicitations/elicitations.go (3)
  • NewController (35-40)
  • Client (16-18)
  • Controller (8-14)
src/pkg/stacks/manager.go (2)
  • NewManager (35-61)
  • Manager (16-21)
src/pkg/stacks/stacks.go (3)
  • StackParameters (18-25)
  • Load (236-238)
  • List (152-154)
src/pkg/stacks/selector.go (1)
  • NewSelector (22-27)
src/pkg/cli/client/provider_id.go (3)
  • ProviderAuto (13-13)
  • ProviderDefang (14-14)
  • ProviderID (10-10)
src/pkg/utils.go (1)
  • Getenv (48-53)
src/pkg/cli/connect.go (1)
  • NewProvider (62-76)
src/pkg/agent/tools/destroy.go (3)
src/pkg/agent/tools/interfaces.go (1)
  • CLIInterface (14-32)
src/pkg/stacks/manager.go (1)
  • NewManager (35-61)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/agent/tools/logs.go (6)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/agent/tools/interfaces.go (1)
  • CLIInterface (14-32)
src/pkg/agent/tools/default_tool_cli.go (1)
  • StackConfig (19-22)
src/pkg/mcp/tools/tools.go (1)
  • StackConfig (14-17)
src/pkg/stacks/manager.go (1)
  • NewManager (35-61)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/stacks/manager.go (4)
src/pkg/modes/modes.go (1)
  • Mode (12-12)
src/pkg/cli/client/provider_id.go (1)
  • ProviderID (10-10)
src/pkg/timeutils/timeutils.go (1)
  • AsTime (39-44)
src/pkg/cli/client/region.go (1)
  • GetRegion (5-16)
src/pkg/agent/tools/listConfig.go (4)
src/pkg/cli/client/provider.go (1)
  • Loader (72-76)
src/pkg/cli/connect.go (1)
  • Connect (17-19)
src/pkg/stacks/manager.go (1)
  • NewManager (35-61)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (26-33)
src/cmd/cli/command/commands_test.go (4)
src/protos/io/defang/v1/fabric.pb.go (14)
  • ListDeploymentsRequest (2992-2999)
  • ListDeploymentsRequest (3012-3012)
  • ListDeploymentsRequest (3027-3029)
  • ListDeploymentsResponse (3052-3057)
  • ListDeploymentsResponse (3070-3070)
  • ListDeploymentsResponse (3085-3087)
  • Deployment (2822-2838)
  • Deployment (2851-2851)
  • Deployment (2866-2868)
  • Provider (28-28)
  • Provider (66-68)
  • Provider (70-72)
  • Provider (79-81)
  • DeploymentMode_DEVELOPMENT (87-87)
src/pkg/modes/modes.go (2)
  • Mode (12-12)
  • ModeAffordable (16-16)
src/pkg/cli/client/provider_id.go (4)
  • ProviderID (10-10)
  • ProviderAWS (15-15)
  • ProviderAuto (13-13)
  • ProviderDefang (14-14)
src/pkg/stacks/stacks.go (3)
  • StackListItem (142-150)
  • StackParameters (18-25)
  • List (152-154)
src/pkg/stacks/stacks_test.go (3)
src/pkg/stacks/stacks.go (2)
  • StackParameters (18-25)
  • ParamsFromMap (55-80)
src/pkg/cli/client/provider_id.go (2)
  • ProviderAWS (15-15)
  • ProviderGCP (17-17)
src/pkg/modes/modes.go (3)
  • Mode (12-12)
  • ModeAffordable (16-16)
  • ModeBalanced (17-17)
🪛 GitHub Actions: Go package
src/cmd/cli/command/estimate.go

[error] 1-1: Go test failed for package src/cmd/cli/command (likely TestCommandGates).

src/cmd/cli/command/stack_test.go

[error] 1-1: Go test failed for package src/cmd/cli/command (likely TestCommandGates).

src/cmd/cli/command/compose.go

[error] 1-1: Go test failed for package src/cmd/cli/command (likely TestCommandGates).

src/cmd/cli/command/commands.go

[error] 1-1: Go test failed for package src/cmd/cli/command (likely TestCommandGates).

src/cmd/cli/command/commands_test.go

[error] 1-1: Go test failed for package src/cmd/cli/command (likely TestCommandGates).

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (36)
src/pkg/cli/client/mock.go (1)

175-177: LGTM!

The mock implementation correctly implements the new OutsideWorkingDirectory() method from the Loader interface. Returning false is appropriate for a simple mock.

src/pkg/agent/tools/estimate.go (1)

20-20: LGTM!

The parameter type change from cliClient.ProjectLoader to cliClient.Loader aligns with the interface consolidation across the PR. The function continues to work correctly with the updated type.

src/pkg/stacks/stacks_test.go (2)

383-438: LGTM!

The test thoroughly validates the ToMap() conversion for both AWS and GCP stack parameters, checking all expected environment variable mappings.


440-491: LGTM!

The test properly validates ParamsFromMap() parsing for both GCP and AWS configurations, ensuring correct conversion from map to StackParameters.

src/pkg/cli/client/caniuse.go (1)

10-22: LGTM!

The refactor to remove the stack parameter and use provider.GetStackName() is clean and aligns with the stack-driven provider resolution pattern introduced in this PR. This simplifies the API by eliminating a redundant parameter.

src/pkg/agent/tools/removeConfig.go (2)

21-21: LGTM!

The parameter type change aligns with the broader Loader interface refactoring across agent tools.


28-31: LGTM!

The stack manager creation correctly uses the new three-parameter constructor and properly handles potential errors.

src/pkg/agent/tools/provider.go (1)

38-44: LGTM!

The refactor to use stacks.NewSelector and SelectStack cleanly separates stack selection logic and aligns with the PR's goal of unified stack selection. The in-place update pattern is correct.

src/pkg/agent/tools/deploy.go (3)

25-25: LGTM!

The parameter type change is consistent with the Loader interface consolidation across all agent tools.


47-50: LGTM!

Stack manager creation follows the updated constructor pattern with proper error handling.


57-57: LGTM!

The CanIUseProvider call correctly uses the updated signature without the stack parameter, consistent with the refactor in caniuse.go.

src/cmd/cli/command/estimate.go (2)

31-40: LGTM!

The interactive provider selection flow is clean and properly handles the auto-detection case with appropriate error wrapping.


69-104: LGTM!

The interactiveSelectProvider function is well-implemented with proper error handling, user prompt configuration, and telemetry tracking. The panic on line 71 is appropriate for an internal helper that should only be called with valid inputs.

src/pkg/agent/tools/destroy_test.go (1)

60-66: LGTM!

The CanIUseProvider mock signature correctly aligns with the updated interface, placing provider before projectName and removing the stackName parameter. The implementation properly logs the project name and returns the configured error.

src/pkg/agent/tools/services.go (1)

23-38: LGTM!

The signature update to use cliClient.Loader and the new three-argument stacks.NewManager call with proper error handling aligns with the broader refactor. The provider preparer correctly receives the stack manager.

src/pkg/stacks/stacks.go (3)

27-53: LGTM!

The ToMap() method correctly serializes StackParameters to environment variable format. The conditional handling of region variable names based on provider and the optional fields (Mode, AWSProfile, GCPProjectID) is well-structured.


55-80: LGTM!

The ParamsFromMap() function correctly handles the reverse mapping. Unknown keys are silently ignored, which is appropriate for forward compatibility when new environment variables are added.


264-284: LGTM!

The LoadParameters function correctly reimplements the godotenv Load/Overload logic for map inputs. The overload parameter properly controls whether existing environment variables should be overwritten.

src/pkg/agent/tools/setConfig.go (1)

21-36: LGTM!

The signature update and stack manager initialization follow the same pattern as other tool handlers, with proper error handling for the new stacks.NewManager call.

src/pkg/agent/tools/listConfig.go (1)

20-35: LGTM!

The updates to use cliClient.Loader and the three-argument stacks.NewManager with proper error handling are consistent with the refactoring pattern across other tool handlers.

src/pkg/agent/tools/logs.go (2)

25-56: LGTM!

The signature update to cliClient.Loader, the three-argument stacks.NewManager call with proper error handling, and the provider preparer setup are consistent with other tool handlers.


65-68: LGTM!

The CanIUseProvider call correctly uses the updated argument order with provider before projectName, matching the interface signature change across the codebase.

src/cmd/cli/command/stack_test.go (1)

30-45: The mock setup is valid and follows Go's standard embedding pattern.

MockFabricControllerClient correctly embeds the FabricControllerClient interface to satisfy the interface contract, and the test provides implementation fields (canIUseResponse, savedProvider) for test-specific behavior. The mockClient.SetClient(mockCtrl) pattern is the correct injection approach for this codebase. If test failures occur in the pipeline, they are likely due to other factors, not the mock setup structure.

Likely an incorrect or invalid review comment.

src/pkg/agent/tools/destroy.go (1)

20-48: LGTM! The destroy handler correctly integrates with the new stack-driven provider resolution.

The changes properly:

  • Create the stack manager with the correct parameters
  • Handle the error from NewManager
  • Use the updated CanIUseProvider signature that retrieves the stack name from the provider
src/pkg/stacks/selector_test.go (2)

84-123: Good test coverage for production stack selector.

This test correctly uses NewSelector to test the actual production code path for selecting an existing stack. The mock setup and assertions are well-structured.


300-319: Thorough error handling test.

Good coverage for the "elicitations not supported" error path, using the production NewSelector.

src/cmd/cli/command/compose.go (2)

223-252: LGTM! The handleExistingDeployments function properly handles the new stack-driven flow.

The logic correctly:

  • Filters deployments by provider first (line 225-227)
  • Matches location by account ID and region (line 229)
  • Creates a "beta" stack with appropriate parameters when deploying to a new location without an explicit stack

566-577: LGTM! Proper integration of elicitations and stack manager in compose config.

The code correctly initializes the elicitation controller and stack manager for interactive stack selection in the compose config command.

src/pkg/stacks/manager_test.go (4)

18-31: Well-structured mock for the fabric client.

The mockFabricClient properly implements the DeploymentLister interface with configurable responses and error injection for testing various scenarios.


340-391: Good coverage for remote stack listing.

The test properly verifies:

  • Remote stacks are retrieved from the fabric client
  • Deployment timestamps are correctly mapped to DeployedAt
  • Multiple remote stacks are handled

511-543: Important test for implicit "beta" stack handling.

This test verifies the core PR objective: deployments with empty stack names are treated as the implicit "beta" stack, enabling onboarding for existing projects.


640-716: Comprehensive test for cross-directory access restriction.

The test properly verifies that when the working directory differs from the target directory:

  • Create and Load operations are blocked with appropriate error messages
  • List still returns remote stacks (not blocked)
  • Remote operations (ListRemote) work regardless of directory

This aligns with the PR objective to restrict cross-project stack file access.

src/cmd/cli/command/commands.go (3)

1460-1495: LGTM! The newProviderChecked function properly handles nil loader and working directory logic.

The function correctly:

  • Handles nil loader case (lines 1464-1470)
  • Creates stack manager with appropriate target directory based on OutsideWorkingDirectory()
  • Validates the provider by calling AccountInfo

1340-1354: Good deprecation messaging.

The warning correctly indicates the flag is "deprecated" rather than "ignored", and provides guidance to use --stack instead. The flag value is still honored for backward compatibility.


1301-1407: Review comment appears incomplete and lacks substantiation.

The original review references bugs on lines 1363 and 1379 in the getStack function related to provider auto-selection, but provides no concrete explanation of what these bugs are or how they manifest. Additionally, the claimed connection to pipeline failures in TestCommandGates cannot be verified—this test name does not appear in the Defang CLI repository.

To properly evaluate this review:

  • Define which specific conditions are incorrect or problematic
  • Include reproducible test cases showing the failure
  • Verify whether TestCommandGates actually exists or provide the correct test name
  • Explain the concrete impact of each identified issue
src/cmd/cli/command/commands_test.go (1)

148-226: Unable to verify TestCommandGates pipeline failure without actual error output.

The test structure appears sound: t.Chdir() is correctly placed at the parent level before sequential subtests, avoiding parallel execution conflicts. Without the specific error message from the pipeline, I cannot confirm whether a failure exists or what the root cause is. If a failure does occur, run the test locally with verbose output to capture the actual error before proceeding with fixes.

Comment on lines +324 to +334
// If we have expected provider/region (from old NewMockStackManager usage), create default params
if m.expectedProvider != "" && m.expectedRegion != "" {
params := stacks.StackParameters{
Name: name,
Provider: m.expectedProvider,
Region: m.expectedRegion,
Mode: modes.ModeAffordable,
}
stacks.LoadParameters(params.ToMap(), true)
return &params, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for LoadParameters function definition
rg -n -B2 -A8 'func.*LoadParameters' --type=go src/

# Also search for how it's used in the test
rg -n 'LoadParameters' --type=go src/cmd/cli/command/

Repository: DefangLabs/defang

Length of output: 4315


🏁 Script executed:

#!/bin/bash
# Search for context around Load() calls in commands_test.go
rg -n -B5 -A10 'mockStackManager.*Load' src/cmd/cli/command/commands_test.go | head -80

# Also check if there are any t.Setenv or t.Cleanup patterns
rg -n 't\.Setenv|t\.Cleanup|defer.*Unsetenv' --type=go src/cmd/cli/command/commands_test.go

Repository: DefangLabs/defang

Length of output: 1392


Wrap LoadParameters calls with proper test isolation to prevent environment variable pollution between tests.

Line 332 calls stacks.LoadParameters(params.ToMap(), true), which sets environment variables using os.Setenv() without cleanup. These variables will persist across sequential test runs, potentially causing test interference. Use t.Setenv() and t.Cleanup() to properly isolate environment variable changes, consistent with the patterns used elsewhere in this test file.

🤖 Prompt for AI Agents
In src/cmd/cli/command/commands_test.go around lines 324 to 334, the call to
stacks.LoadParameters(params.ToMap(), true) sets process-level environment
variables via os.Setenv without cleanup; replace that with per-key t.Setenv(...)
calls (or iterate params.ToMap() and call t.Setenv(key, value) for each) so the
test runner will automatically restore environment variables, and remove the
direct LoadParameters call (or call it only with a map built from t.Getenv-safe
values) while ensuring any temporary state is reverted with t.Cleanup where
needed to avoid cross-test pollution.

term.Debugf("AccountInfo failed: %v", err)
} else if len(resp.Deployments) > 0 {
handleExistingDeployments(resp.Deployments, accountInfo, project.Name)
handleExistingDeployments(resp.Deployments, accountInfo, project.Name, provider.GetStackName())
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Return value from handleExistingDeployments is ignored.

The function returns an error, but the call site doesn't check it. If confirmDeploymentToNewLocation fails or the user cancels, the deployment will proceed anyway.

🔎 Proposed fix
 			} else if len(resp.Deployments) > 0 {
-				handleExistingDeployments(resp.Deployments, accountInfo, project.Name, provider.GetStackName())
+				if err := handleExistingDeployments(resp.Deployments, accountInfo, project.Name, provider.GetStackName()); err != nil {
+					return err
+				}
 			} else if global.Stack.Name == "" {
📝 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
handleExistingDeployments(resp.Deployments, accountInfo, project.Name, provider.GetStackName())
} else if len(resp.Deployments) > 0 {
if err := handleExistingDeployments(resp.Deployments, accountInfo, project.Name, provider.GetStackName()); err != nil {
return err
}
} else if global.Stack.Name == "" {
🤖 Prompt for AI Agents
In src/cmd/cli/command/compose.go around line 117, the call to
handleExistingDeployments(resp.Deployments, accountInfo, project.Name,
provider.GetStackName()) ignores its returned error; change the call to capture
the error, check it, and return or abort the current function on non-nil (e.g.,
if err := handleExistingDeployments(...); err != nil { return err } or otherwise
handle cancellation) so that failures from confirmDeploymentToNewLocation or
user cancellation stop the deployment from proceeding.

Comment on lines +96 to 105
os.WriteFile(
"compose.yaml",
[]byte(`services:
web:
image: nginx`),
os.FileMode(0644),
)
for _, stack := range tt.stacks {
stacks.Create(stack)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unchecked errors in test setup may cause silent failures.

The os.WriteFile and stacks.Create calls don't check for errors. If these fail, tests will proceed with incorrect state, potentially causing confusing failures or false passes.

🔎 Proposed fix
-			os.WriteFile(
+			err := os.WriteFile(
 				"compose.yaml",
 				[]byte(`services:
   web:
     image: nginx`),
 				os.FileMode(0644),
 			)
+			assert.NoError(t, err, "failed to create compose.yaml")
 			for _, stack := range tt.stacks {
-				stacks.Create(stack)
+				_, err := stacks.Create(stack)
+				assert.NoError(t, err, "failed to create stack")
 			}
🤖 Prompt for AI Agents
In src/cmd/cli/command/stack_test.go around lines 96 to 105, the test setup
calls os.WriteFile and stacks.Create without checking their return errors;
update the code to capture and handle those errors (e.g., if err :=
os.WriteFile(...); err != nil { t.Fatalf("write compose.yaml: %v", err) } and
for each stack call capture the error returned by stacks.Create and fail the
test on error (or use require.NoError/require.NoErrorf if using testify) so the
test stops immediately on setup failures.

Comment on lines 169 to 192
func TestLoadParameters(t *testing.T) {
params := map[string]string{
"DEFANG_PROVIDER": "aws",
"AWS_REGION": "us-west-2",
"AWS_PROFILE": "default",
"DEFANG_MODE": "AFFORDABLE",
}

// Clear any existing env vars that might interfere with the test
os.Unsetenv("DEFANG_PROVIDER")
os.Unsetenv("AWS_REGION")
os.Unsetenv("AWS_PROFILE")
os.Unsetenv("DEFANG_MODE")

err := stacks.LoadParameters(params, true)
if err != nil {
t.Fatalf("LoadParameters() error = %v", err)
}

assert.Equal(t, "aws", os.Getenv("DEFANG_PROVIDER"))
assert.Equal(t, "us-west-2", os.Getenv("AWS_REGION"))
assert.Equal(t, "default", os.Getenv("AWS_PROFILE"))
assert.Equal(t, "AFFORDABLE", os.Getenv("DEFANG_MODE"))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Test modifies environment variables without cleanup.

TestLoadParameters sets environment variables that persist after the test completes. This can cause test pollution and flaky behavior when tests run in parallel or in a specific order. Add cleanup to restore the original state.

🔎 Proposed fix
 func TestLoadParameters(t *testing.T) {
 	params := map[string]string{
 		"DEFANG_PROVIDER": "aws",
 		"AWS_REGION":      "us-west-2",
 		"AWS_PROFILE":     "default",
 		"DEFANG_MODE":     "AFFORDABLE",
 	}

 	// Clear any existing env vars that might interfere with the test
 	os.Unsetenv("DEFANG_PROVIDER")
 	os.Unsetenv("AWS_REGION")
 	os.Unsetenv("AWS_PROFILE")
 	os.Unsetenv("DEFANG_MODE")
+
+	// Restore env vars after test
+	t.Cleanup(func() {
+		os.Unsetenv("DEFANG_PROVIDER")
+		os.Unsetenv("AWS_REGION")
+		os.Unsetenv("AWS_PROFILE")
+		os.Unsetenv("DEFANG_MODE")
+	})

 	err := stacks.LoadParameters(params, true)
📝 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 TestLoadParameters(t *testing.T) {
params := map[string]string{
"DEFANG_PROVIDER": "aws",
"AWS_REGION": "us-west-2",
"AWS_PROFILE": "default",
"DEFANG_MODE": "AFFORDABLE",
}
// Clear any existing env vars that might interfere with the test
os.Unsetenv("DEFANG_PROVIDER")
os.Unsetenv("AWS_REGION")
os.Unsetenv("AWS_PROFILE")
os.Unsetenv("DEFANG_MODE")
err := stacks.LoadParameters(params, true)
if err != nil {
t.Fatalf("LoadParameters() error = %v", err)
}
assert.Equal(t, "aws", os.Getenv("DEFANG_PROVIDER"))
assert.Equal(t, "us-west-2", os.Getenv("AWS_REGION"))
assert.Equal(t, "default", os.Getenv("AWS_PROFILE"))
assert.Equal(t, "AFFORDABLE", os.Getenv("DEFANG_MODE"))
}
func TestLoadParameters(t *testing.T) {
params := map[string]string{
"DEFANG_PROVIDER": "aws",
"AWS_REGION": "us-west-2",
"AWS_PROFILE": "default",
"DEFANG_MODE": "AFFORDABLE",
}
// Clear any existing env vars that might interfere with the test
os.Unsetenv("DEFANG_PROVIDER")
os.Unsetenv("AWS_REGION")
os.Unsetenv("AWS_PROFILE")
os.Unsetenv("DEFANG_MODE")
// Restore env vars after test
t.Cleanup(func() {
os.Unsetenv("DEFANG_PROVIDER")
os.Unsetenv("AWS_REGION")
os.Unsetenv("AWS_PROFILE")
os.Unsetenv("DEFANG_MODE")
})
err := stacks.LoadParameters(params, true)
if err != nil {
t.Fatalf("LoadParameters() error = %v", err)
}
assert.Equal(t, "aws", os.Getenv("DEFANG_PROVIDER"))
assert.Equal(t, "us-west-2", os.Getenv("AWS_REGION"))
assert.Equal(t, "default", os.Getenv("AWS_PROFILE"))
assert.Equal(t, "AFFORDABLE", os.Getenv("DEFANG_MODE"))
}
🤖 Prompt for AI Agents
In src/cmd/cli/command/stack_test.go around lines 169 to 192, the test mutates
environment variables and never restores them, causing test pollution; capture
the original values of DEFANG_PROVIDER, AWS_REGION, AWS_PROFILE, and DEFANG_MODE
at the start of the test and defer restoring them (or use t.Setenv for each on
Go 1.17+ which handles cleanup) so the environment is returned to its previous
state after the test completes; ensure the defer restores even when
LoadParameters returns an error.

Comment on lines +125 to +128
// WizardInterface defines the interface for collecting stack parameters
type WizardInterface interface {
CollectParameters(ctx context.Context) (*StackParameters, error)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if WizardInterface or similar wizard abstraction exists in production code
rg -n "WizardInterface|CollectParameters" --type=go -g '!*_test.go' src/pkg/stacks/

Repository: DefangLabs/defang

Length of output: 249


🏁 Script executed:

#!/bin/bash
# Read the production selector.go to understand how it uses the wizard
echo "=== selector.go ===" 
cat -n src/pkg/stacks/selector.go | head -60

echo -e "\n=== wizard.go around line 34 ===" 
head -n 40 src/pkg/stacks/wizard.go | tail -n 15

echo -e "\n=== selector_test.go around WizardInterface (lines 120-140) ===" 
sed -n '120,150p' src/pkg/stacks/selector_test.go

Repository: DefangLabs/defang

Length of output: 3391


Move WizardInterface to production code and inject it into stackSelector.

The WizardInterface is currently defined only in the test file (lines 125-128), but production code in selector.go directly instantiates NewWizard() (line 39) and calls CollectParameters() on the concrete type. This couples production code to a concrete implementation, violating the dependency inversion principle.

Define the interface in production (stacks package) and accept it as a dependency in stackSelector (via constructor or method parameter) so that both production and tests depend on the abstraction rather than a concrete type. This eliminates the need for test workarounds like testableStackSelector and makes the code more flexible and testable.

🤖 Prompt for AI Agents
In src/pkg/stacks/selector_test.go lines 125-128, the WizardInterface is defined
only in the test, but selector.go instantiates NewWizard() directly (line ~39)
which couples production code to a concrete implementation; move the
WizardInterface definition into the production package (e.g.,
src/pkg/stacks/wizard.go or selector.go), change stackSelector to accept a
WizardInterface dependency (via its constructor or as a parameter) instead of
calling NewWizard(), update the selector constructor to take and store the
interface, update selector.go to call the injected interface's
CollectParameters, and update tests to provide a test double implementing the
interface (removing test-only workarounds like testableStackSelector).

Copy link
Contributor

@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: 0

♻️ Duplicate comments (5)
src/pkg/stacks/manager.go (1)

47-54: Symlink-relative paths may still cause incorrect outside detection.

The comparison workingDirectory != absTargetDirectory at line 53 doesn't resolve symlinks. If either path contains symlinks, two paths pointing to the same physical directory may compare as different, incorrectly setting outside = true and blocking local operations. This was flagged in a previous review.

src/cmd/cli/command/stack_test.go (1)

96-105: Unchecked errors in test setup may cause silent failures.

The os.WriteFile and stacks.Create calls don't check for errors. If these fail, tests will proceed with incorrect state, potentially causing confusing failures or false passes.

🔎 Proposed fix
-			os.WriteFile(
+			err := os.WriteFile(
 				"compose.yaml",
 				[]byte(`services:
   web:
     image: nginx`),
 				os.FileMode(0644),
 			)
+			if err != nil {
+				t.Fatalf("failed to create compose.yaml: %v", err)
+			}
 			for _, stack := range tt.stacks {
-				stacks.Create(stack)
+				_, err := stacks.Create(stack)
+				if err != nil {
+					t.Fatalf("failed to create stack: %v", err)
+				}
 			}
src/cmd/cli/command/commands_test.go (1)

324-337: mockStackManager.Load sets environment variables without cleanup.

Line 332 calls stacks.LoadParameters(params.ToMap(), true), which sets environment variables using os.Setenv() without cleanup. These variables will persist across sequential test runs, potentially causing test interference. Consider using t.Setenv() in the test setup instead of relying on LoadParameters in the mock.

src/cmd/cli/command/commands.go (2)

1311-1313: Typo in comment: "RoomCmd" should be "RootCmd".

The comment references "RoomCmd" but should reference "RootCmd".

🔎 Proposed fix
 	// This code unfortunately replicates the provider precedence rules in the
-	// RoomCmd's PersistentPreRunE func, I think we should avoid reading the
+	// RootCmd's PersistentPreRunE func, I think we should avoid reading the
 	// stack file during startup, and only read it here instead.

1340-1354: Deprecation warning followed by usage is appropriate for backwards compatibility.

The code warns that --provider flag is deprecated, then proceeds to use its value. While this was flagged in a previous review as "contradictory," this is actually the correct behavior for deprecation:

  1. Warn the user the flag is deprecated
  2. Still honor the flag value for backwards compatibility

The warning message now includes a link to documentation about stacks, which helps guide users to the new approach.

🧹 Nitpick comments (4)
src/pkg/stacks/manager.go (2)

106-107: Duplicate comment.

There's a duplicate comment on lines 106-107.

🔎 Proposed fix
 	// sort stacks by name asc
-	// sort stacks by name asc
 	slices.SortFunc(stackList, func(a, b StackListItem) int {

108-116: Consider using strings.Compare for cleaner sort logic.

The sort comparison can be simplified using the standard library's strings.Compare function.

🔎 Proposed fix
+	"strings"
 	slices.SortFunc(stackList, func(a, b StackListItem) int {
-		if a.Name < b.Name {
-			return -1
-		}
-		if a.Name > b.Name {
-			return 1
-		}
-		return 0
+		return strings.Compare(a.Name, b.Name)
 	})
src/cmd/cli/command/commands_test.go (2)

744-748: Environment variable cleanup may be incomplete.

When tc.envProvider is empty, os.Unsetenv("DEFANG_PROVIDER") is called, but this doesn't restore the original value if one existed before the test. Consider using t.Setenv which handles this automatically.

🔎 Proposed fix
 			// Set environment variable if provided
 			if tc.envProvider != "" {
 				t.Setenv("DEFANG_PROVIDER", tc.envProvider)
-			} else {
-				os.Unsetenv("DEFANG_PROVIDER")
 			}

Note: t.Setenv automatically restores the original value after the test, but it doesn't unset variables. If you need to ensure DEFANG_PROVIDER is unset for certain tests, consider setting up a parent test that uses t.Setenv("DEFANG_PROVIDER", "") or restructuring the test to not depend on the absence of this variable.


800-808: Warning verification is incomplete.

The comment acknowledges that warning output cannot be easily captured, but the verification logic on lines 803-805 checks if the provider flag was "changed" rather than actually verifying warnings were emitted. This test assertion will likely always pass since ParseFlags with --provider would mark it as changed.

Consider either removing this incomplete assertion or using a captured output buffer to verify warnings are actually printed.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 67cb5b5 and b24d150.

📒 Files selected for processing (4)
  • src/cmd/cli/command/commands.go (5 hunks)
  • src/cmd/cli/command/commands_test.go (9 hunks)
  • src/cmd/cli/command/stack_test.go (5 hunks)
  • src/pkg/stacks/manager.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/cmd/cli/command/stack_test.go (4)
src/cmd/cli/command/commands.go (2)
  • RootCmd (408-513)
  • Execute (71-156)
src/pkg/cli/client/grpc.go (1)
  • GrpcClient (19-26)
src/cmd/cli/command/commands_test.go (1)
  • MockFabricControllerClient (228-232)
src/pkg/stacks/stacks.go (1)
  • LoadParameters (266-284)
src/cmd/cli/command/commands_test.go (3)
src/pkg/modes/modes.go (3)
  • Mode (12-12)
  • ModeAffordable (16-16)
  • ModeUnspecified (15-15)
src/pkg/cli/client/provider_id.go (4)
  • ProviderID (10-10)
  • ProviderAWS (15-15)
  • ProviderAuto (13-13)
  • ProviderDefang (14-14)
src/pkg/stacks/stacks.go (1)
  • StackParameters (18-25)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Analyze (go)
  • GitHub Check: go-test
🔇 Additional comments (13)
src/pkg/stacks/manager.go (3)

155-164: Mode assignment is now included.

The Mode field is now being assigned using modes.Mode(deployment.GetMode()) at line 160, addressing the previous review comment about missing Mode assignment.


64-119: List method implementation looks correct.

The List method properly merges remote and local stacks, preferring remote entries when duplicates exist to show deployment timestamps. The error handling for OutsideError allows graceful degradation when operating outside the working directory.


197-199: LoadParameters delegates but ignores the receiver's context.

The LoadParameters method on the manager simply delegates to the package-level LoadParameters function without using the manager's targetDirectory or outside state. This is consistent with the design but may be surprising if callers expect directory-scoped behavior.

Consider whether this method should also check sm.outside like Load and Create do, or document that it operates on environment variables globally regardless of directory context.

src/cmd/cli/command/stack_test.go (2)

169-200: Test properly cleans up environment variables.

The TestLoadParameters test now includes proper cleanup of environment variables using a defer block. This addresses the previous review comment about test pollution.

Consider using t.Setenv instead, which automatically handles cleanup:

t.Setenv("DEFANG_PROVIDER", "aws")
t.Setenv("AWS_REGION", "us-west-2")
// etc.

30-58: Test setup properly saves and restores global state.

The test correctly saves RootCmd and global.Client and restores them in a defer block. The mock setup creates a minimal RootCmd with required flags for the stack list flow.

src/cmd/cli/command/commands_test.go (3)

249-254: Good defensive initialization of savedProvider map.

The SetSelectedProvider method now properly initializes the savedProvider map before use, preventing nil map assignment panics.


476-505: Mock elicitations controller implementation is well-structured.

The mockElicitationsController provides appropriate test doubles for the elicitations interface, with configurable behavior via enumChoice and isSupported fields.


507-718: Comprehensive test coverage for getStack.

TestGetStack provides good coverage of various scenarios including:

  • Stack flag with valid/invalid stacks
  • Auto provider handling
  • Provider flag deprecation warnings
  • Non-interactive defaults
  • Single stack auto-selection
  • Interactive selection
  • Error propagation
src/cmd/cli/command/commands.go (5)

1362-1381: Single stack auto-selection logic is now correct.

The condition on line 1363 now properly handles ProviderAuto by checking stack.Provider == cliClient.ProviderAuto || knownStacks[0].Provider == stack.Provider.String(). This addresses the previous review comment about auto-select not triggering when expected.

The fallback to importStack when the stack file cannot be loaded (lines 1366-1377) provides good resilience for stacks that exist remotely but not locally.


1401-1426: importStack properly converts StackListItem to StackParameters.

The function correctly:

  • Validates and converts the provider string to ProviderID
  • Handles optional Mode field
  • Loads parameters into the environment
  • Returns the constructed StackParameters

1459-1477: newProvider now properly integrates stack selection.

The refactored newProvider function:

  1. Calls getStack to resolve stack parameters
  2. Updates global.Stack with the resolved stack
  3. Prints informative messages about which stack is being used
  4. Warns about provider mismatches in environment

The informational message on line 1472 clearly indicates the stack name, provider, and source (whence), which helps with debugging and user understanding.


1479-1514: newProviderChecked properly handles nil loader.

The function now correctly handles loader == nil by defaulting outside = true and only calling loader methods when loader is non-nil. This addresses the previous review comment about nil pointer dereference.


557-572: whoami command creates stacks.Manager correctly.

The whoami command now properly initializes the elicitations controller and stacks manager before calling newProvider. The working directory is correctly obtained and passed to NewManager.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stacks onboarding for existing projects

2 participants