Skip to content

Conversation

@jordanstephens
Copy link
Member

@jordanstephens jordanstephens commented Dec 10, 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.

  • Use agentTools.NewProviderPreparer and elicitations controller with survey to implement stack selection during defang subcommand invocation. This allows us to unify the codepaths for stack selection across agent tool calls and subcommand invocations
  • load project before instantiating provider
  • when instantiating provider, prompt for stack selection
    • 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
    • show a list of stacks with the timestamp at which they were most recently deployed
    • if the stack list includes "beta", show a message introducing users to stacks and to the implicit "beta" stack
  • when prompting the user for aws credentials
    • automatically show a list of profiles if profiles can be found
    • if profiles cannot be found, prompt for access key id and secret access key
  • Only allow reading and writing stack files when --project-name is not being used in a sub command invocation and when ProjectName is not being used by an agent tool call. 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.
  • Use the same default stackname when creating new stacks (provider + region) with non alphanumeric characters stripped

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

    • Unified stack selection merging remote deployments with local stacks; improved stack creation/import UX and a new stacks manager.
  • Bug Fixes

    • Provider validation message no longer lists the deprecated "auto" option.
  • Documentation

    • Removed "auto" from provider docs and updated macOS FAQ link.
  • Refactor

    • Streamlined project-name resolution and consistent provider setup across CLI flows.
  • Tests

    • Added comprehensive provider-setup tests and updated many existing tests.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 10, 2025

Walkthrough

Project-name resolution is moved earlier; provider setup now takes projectName and useWkDir. A StacksManager and interactive stack selection/import flow were added. Many CLI and agent tool interfaces switched to FabricClient and LoadProjectName. Documentation removed the "auto" DEFANG_PROVIDER option and updated a macOS FAQ link.

Changes

Cohort / File(s) Summary
Documentation
README.md, pkgs/npm/README.md, src/README.md, src/.goreleaser.yml
Removed auto from DEFANG_PROVIDER documentation and updated macOS FAQ link.
CLI commands & wiring
src/cmd/cli/command/...
src/cmd/cli/command/cd.go, src/cmd/cli/command/commands.go, src/cmd/cli/command/compose.go, src/cmd/cli/command/globals.go, src/cmd/cli/command/globals_test.go, src/cmd/cli/command/commands_test.go
Centralized and changed provider creation timing; replaced loader-based provider flows with signatures using projectName and useWkDir; updated call sites, flags handling, imports and tests.
Agent CLI interfaces & implementations
src/pkg/agent/tools/interfaces.go, src/pkg/agent/tools/default_tool_cli.go
Replaced LoadProjectNameWithFallback with LoadProjectName; switched many method params/returns from *GrpcClient to FabricClient; adjusted signatures and delegations.
Provider preparer & stack handling
src/pkg/agent/tools/provider.go, src/pkg/agent/tools/deploy.go, src/pkg/agent/tools/destroy.go, src/pkg/agent/tools/listConfig.go, src/pkg/agent/tools/removeConfig.go, src/pkg/agent/tools/services.go, src/pkg/agent/tools/setConfig.go, src/pkg/agent/tools/logs.go
Expanded SetupProvider to accept projectName and useWkDir; added StacksManager, stack collection/selection/create/import/load orchestration, new types (StackOption, ExistingStack), and reordered logic to load project name before provider setup; simplified AWS profile/auth flows.
Agent tools tests & new provider tests
src/pkg/agent/tools/*_test.go, src/pkg/agent/tools/provider_test.go
Updated many mocks/tests to use LoadProjectName and FabricClient; renamed error fields/call logs; removed some elicitation entries; added comprehensive provider-setup unit tests covering stack selection/import and provider instantiation.
Provider ID validation
src/pkg/cli/client/provider_id.go
Invalid-provider error message now lists selectable providers excluding auto.
Stacks utilities
src/pkg/stacks/stacks.go
Added paramsToMap helper, refactored Marshal to use it, and added LoadParameters to populate env from StackParameters.
Misc / small docs & tests
src/pkg/migrate/heroku.go
Minor doc-comment formatting change.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant CLI as CLI Command
    participant Loader as Loader
    participant PP as ProviderPreparer
    participant StackMgr as StacksManager
    participant API as Fabric API

    CLI->>Loader: LoadProjectName(ctx)
    Loader-->>CLI: projectName
    CLI->>PP: SetupProvider(ctx, projectName, stack?, useWkDir)
    PP->>StackMgr: collectPreviouslyDeployedStacks(projectName)
    StackMgr->>API: ListDeployments(projectName)
    API-->>StackMgr: deployments
    StackMgr->>StackMgr: merge local & remote stacks
    alt selected remote stack not present locally
        StackMgr->>StackMgr: import/create local stack from remote
    end
    StackMgr-->>PP: selected/imported stack parameters
    PP->>API: Initialize provider using stack parameters
    API-->>PP: providerID, provider
    PP-->>CLI: provider (or error)
    CLI->>API: Execute command using provider
    API-->>CLI: result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Pay special attention to:
    • src/pkg/agent/tools/provider.go — new StacksManager, stack orchestration, select/create/import logic, and AWS profile handling.
    • src/cmd/cli/command/commands.go — centralized provider initialization and updated function signatures.
    • src/pkg/agent/tools/provider_test.go and other updated tests — extensive new mocks and altered expectations.

Possibly related PRs

Suggested reviewers

  • lionello

Poem

🐰 I hopped through stacks both near and far,

I nudged the name — then fetched the star.
Local, remote, I gathered them well,
Docs trimmed neat, the flows now tell.
🥕 A tiny hop — and off I fell.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.46% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Stacks onboarding with support for "remote" stacks' accurately summarizes the main change—adding stack selection/onboarding features including remote stack support.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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/stacks-default-name

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

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

🧹 Nitpick comments (8)
src/cmd/cli/command/compose.go (1)

78-81: Consider aligning saveStacksToWkDir behavior for compose up with other commands

compose up now always calls newProviderChecked(ctx, project.Name, true), while compose down, ps, and logs derive saveStacksToWkDir from the --project-name flag. If the boolean controls persisting stack selection to .defang, you may want up to respect an explicit --project-name in the same way for ephemeral project names, or at least document that up always persists while the others don’t.

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

79-83: Avoid double-wrapping the “failed to fetch logs” error

err is first wrapped with fmt.Errorf("failed to fetch logs: %w", err) and then wrapped again in the return, producing messages like failed to fetch logs: failed to fetch logs: <root>. You can simplify to a single wrap and reuse that for both logging and returning:

-	if err != nil {
-		err = fmt.Errorf("failed to fetch logs: %w", err)
-		term.Error("Failed to fetch logs", "error", err)
-		return "", fmt.Errorf("failed to fetch logs: %w", err)
-	}
+	if err != nil {
+		wrapped := fmt.Errorf("failed to fetch logs: %w", err)
+		term.Error("Failed to fetch logs", "error", wrapped)
+		return "", wrapped
+	}
src/pkg/agent/tools/listConfig.go (1)

23-23: Minor: Inconsistent error message casing.

Line 23 uses "Could not connect" (capitalized) while lines 28 and 34 use lowercase ("failed to load...", "failed to setup..."). Consider using consistent casing across error messages for uniformity.

-		return "", fmt.Errorf("Could not connect: %w", err)
+		return "", fmt.Errorf("could not connect: %w", err)
src/pkg/agent/tools/provider.go (3)

127-128: Consider more robust argument handling.

Using os.Args[1] directly to construct the help message could be fragile if the argument structure changes (e.g., with global flags, subcommands). Consider using the command name from context if available.

-	executable, _ := os.Executable()
-	term.Infof("To skip this prompt, run %s %s --stack=%s", filepath.Base(executable), os.Args[1], "<stack_name>")
+	executable, _ := os.Executable()
+	cmdName := "command"
+	if len(os.Args) > 1 {
+		cmdName = os.Args[1]
+	}
+	term.Infof("To skip this prompt, run %s %s --stack=%s", filepath.Base(executable), cmdName, "<stack_name>")

148-170: Verify robustness of remote stack name parsing.

The code assumes remote stacks are formatted as "name (last deployed...)" and parses by finding parentheses. This approach could be fragile if:

  1. Stack names themselves contain parentheses
  2. The format changes
  3. The timestamp format varies

Consider adding validation or using a more structured approach to distinguish remote from local stacks.

A more robust approach could use a separate data structure to track whether each option is remote:

-	stackNames := make([]string, 0, len(stackMap)+1)
+	type stackChoice struct {
+		displayName string
+		actualName  string
+		isRemote    bool
+	}
+	stackChoices := make([]stackChoice, 0, len(stackMap)+1)
 	for _, stackOption := range stackMap {
-		name := stackOption.Name
+		displayName := stackOption.Name
 		if !stackOption.Local {
-			name += fmt.Sprintf(" (last deployed %s)", stackOption.LastDeployedAt.Local().Format(time.RFC822))
+			displayName += fmt.Sprintf(" (last deployed %s)", stackOption.LastDeployedAt.Local().Format(time.RFC822))
 		}
-		stackNames = append(stackNames, name)
+		stackChoices = append(stackChoices, stackChoice{
+			displayName: displayName,
+			actualName:  stackOption.Name,
+			isRemote:    !stackOption.Local,
+		})
 	}

Then look up the choice by display name to get the actual name and remote status, avoiding string parsing.


336-356: Minor redundancy: AWS_PROFILE check already performed.

Line 336 checks if os.Getenv("AWS_PROFILE") == "", but this condition is already guaranteed by the early return at lines 327-329. The nested check is unnecessary.

-	// TODO: add support for aws sso strategy
-	if os.Getenv("AWS_PROFILE") == "" {
-		knownProfiles, err := listAWSProfiles()
-		if err != nil {
-			return fmt.Errorf("failed to list AWS profiles: %w", err)
-		}
-		if len(knownProfiles) > 0 {
-			const useAccessKeysOption = "Use Access Key ID and Secret Access Key"
-			knownProfiles = append(knownProfiles, useAccessKeysOption)
-			profile, err := pp.ec.RequestEnum(ctx, "Select your profile", "profile_name", knownProfiles)
-			if err != nil {
-				return fmt.Errorf("failed to elicit AWS Profile Name: %w", err)
-			}
-			if profile != useAccessKeysOption {
-				err := os.Setenv("AWS_PROFILE", profile)
-				if err != nil {
-					return fmt.Errorf("failed to set AWS_PROFILE environment variable: %w", err)
-				}
-				return nil
-			}
+	// TODO: add support for aws sso strategy
+	knownProfiles, err := listAWSProfiles()
+	if err != nil {
+		return fmt.Errorf("failed to list AWS profiles: %w", err)
+	}
+	if len(knownProfiles) > 0 {
+		const useAccessKeysOption = "Use Access Key ID and Secret Access Key"
+		knownProfiles = append(knownProfiles, useAccessKeysOption)
+		profile, err := pp.ec.RequestEnum(ctx, "Select your profile", "profile_name", knownProfiles)
+		if err != nil {
+			return fmt.Errorf("failed to elicit AWS Profile Name: %w", err)
+		}
+		if profile != useAccessKeysOption {
+			err := os.Setenv("AWS_PROFILE", profile)
+			if err != nil {
+				return fmt.Errorf("failed to set AWS_PROFILE environment variable: %w", err)
+			}
+			return nil
 		}
 	}
src/cmd/cli/command/commands.go (2)

438-439: TODO noted: Remote stack loading enhancement.

This TODO aligns with the PR objectives. Would you like me to open an issue to track this enhancement for loading stacks from remote after connecting to fabric?


959-964: Provider result is discarded but function called for side effects.

The provider returned from newProviderChecked is assigned to _ (unused), suggesting it's called only for its side effects (setting up global.ProviderID and global.Stack). Consider either using the returned provider or extracting the setup logic into a separate function that makes the intent clearer.

-	_, err = newProviderChecked(ctx, project.Name, saveStacksToWkDir)
+	// Setup provider to initialize global.ProviderID and global.Stack
+	_, err = newProviderChecked(ctx, project.Name, saveStacksToWkDir)

Alternatively, if debug.NewDebugger could accept the provider directly, that would be cleaner.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0c267bd and 4b0ef16.

📒 Files selected for processing (29)
  • README.md (1 hunks)
  • pkgs/npm/README.md (1 hunks)
  • src/.goreleaser.yml (1 hunks)
  • src/README.md (1 hunks)
  • src/cmd/cli/command/cd.go (5 hunks)
  • src/cmd/cli/command/commands.go (14 hunks)
  • src/cmd/cli/command/commands_test.go (6 hunks)
  • src/cmd/cli/command/compose.go (5 hunks)
  • src/cmd/cli/command/globals.go (0 hunks)
  • src/cmd/cli/command/globals_test.go (0 hunks)
  • src/pkg/agent/tools/default_tool_cli.go (1 hunks)
  • src/pkg/agent/tools/deploy.go (1 hunks)
  • src/pkg/agent/tools/deploy_test.go (0 hunks)
  • src/pkg/agent/tools/destroy.go (1 hunks)
  • src/pkg/agent/tools/destroy_test.go (4 hunks)
  • src/pkg/agent/tools/estimate_test.go (1 hunks)
  • src/pkg/agent/tools/interfaces.go (1 hunks)
  • src/pkg/agent/tools/listConfig.go (1 hunks)
  • src/pkg/agent/tools/listConfig_test.go (2 hunks)
  • src/pkg/agent/tools/logs.go (1 hunks)
  • src/pkg/agent/tools/provider.go (5 hunks)
  • src/pkg/agent/tools/removeConfig.go (1 hunks)
  • src/pkg/agent/tools/removeConfig_test.go (2 hunks)
  • src/pkg/agent/tools/services.go (1 hunks)
  • src/pkg/agent/tools/services_test.go (3 hunks)
  • src/pkg/agent/tools/setConfig.go (1 hunks)
  • src/pkg/agent/tools/setConfig_test.go (2 hunks)
  • src/pkg/cli/client/provider_id.go (1 hunks)
  • src/pkg/migrate/heroku.go (1 hunks)
💤 Files with no reviewable changes (3)
  • src/cmd/cli/command/globals.go
  • src/pkg/agent/tools/deploy_test.go
  • src/cmd/cli/command/globals_test.go
🧰 Additional context used
🧬 Code graph analysis (7)
src/pkg/agent/tools/services.go (1)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (33-39)
src/pkg/agent/tools/services_test.go (3)
src/pkg/cli/client/grpc.go (1)
  • GrpcClient (19-24)
src/pkg/agent/tools/setConfig_test.go (1)
  • MockProvider (57-59)
src/pkg/cli/client/playground.go (1)
  • PlaygroundProvider (17-21)
src/pkg/agent/tools/destroy.go (2)
src/pkg/term/colorizer.go (1)
  • Errorf (322-324)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (33-39)
src/pkg/agent/tools/provider.go (2)
src/pkg/stacks/stacks.go (6)
  • StackParameters (17-22)
  • List (85-118)
  • Create (33-76)
  • Read (180-195)
  • Load (197-208)
  • StackListItem (78-83)
src/pkg/elicitations/elicitations.go (1)
  • Controller (8-12)
src/cmd/cli/command/commands_test.go (1)
src/protos/io/defang/v1/fabric.pb.go (9)
  • ListDeploymentsRequest (3197-3204)
  • ListDeploymentsRequest (3217-3217)
  • ListDeploymentsRequest (3232-3234)
  • ListDeploymentsResponse (3257-3262)
  • ListDeploymentsResponse (3275-3275)
  • ListDeploymentsResponse (3290-3292)
  • Deployment (3035-3050)
  • Deployment (3063-3063)
  • Deployment (3078-3080)
src/pkg/agent/tools/listConfig.go (2)
src/pkg/term/colorizer.go (2)
  • Errorf (322-324)
  • Debug (294-296)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (33-39)
src/cmd/cli/command/commands.go (5)
src/pkg/cli/client/provider.go (1)
  • Provider (43-69)
src/pkg/cli/connect.go (1)
  • NewProvider (35-49)
src/pkg/elicitations/elicitations.go (2)
  • Client (14-16)
  • NewController (32-36)
src/pkg/elicitations/survey.go (1)
  • NewSurveyClient (19-25)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (33-39)
⏰ 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 (52)
src/pkg/migrate/heroku.go (1)

336-342: Doc comment formatting improves clarity.

The blank lines separating the purpose, implementation details, and return/error information make the documentation more readable and scannable.

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

70-79: Error message now correctly reflects user-selectable providers

Using AllProviders() in the error keeps the message aligned with the non-auto providers exposed to users, while preserving the existing matching logic in Set. This is a clean, behaviorally safe UX improvement.

src/.goreleaser.yml (1)

91-91: No action needed—the FAQ link update is isolated and requires no consistency fixes.

The codebase contains only one FAQ documentation link (in this file at line 91), which is already using the updated structure /docs/intro/faq/questions. No other outdated FAQ links exist elsewhere in the codebase, so there are no consistency issues to address.

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

124-124: Test expectation correctly reflects implementation changes.

The error message update is accurate—AllProviders() explicitly excludes "auto" from the returned list by slicing from index 1, so the error message will properly display provider not one of [defang aws digitalocean gcp] as expected by the test.

README.md (1)

175-175: DEFANG_PROVIDER docs aligned with supported providers

Limiting documented values to aws, digitalocean, gcp, and defang (dropping auto) matches the refactor and avoids implying unsupported behavior.

pkgs/npm/README.md (1)

45-45: Keep npm package docs consistent with core README

This update mirrors the root README and correctly documents the allowed DEFANG_PROVIDER values.

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

321-331: Project-name-first flow for compose down looks sound

Resolving projectName via the loader before instantiating the provider and then gating saveStacksToWkDir on whether --project-name was supplied gives clearer error paths and avoids unnecessary provider setup when the project name can’t be determined.


475-483: compose ps provider initialization now matches down semantics

Using LoadProjectName plus the project-name flag to control saveStacksToWkDir keeps ps consistent with down and should make stack resolution and any .defang writes predictable across commands.


600-608: Logs command now participates in the unified provider/project-name flow

Having logs resolve projectName through the loader and call newProviderChecked with the same saveStacksToWkDir heuristic as down/ps ensures log tailing uses the same stack selection and provider onboarding behavior as deployment commands.

src/README.md (1)

45-45: Internal README correctly mirrors provider options

The updated DEFANG_PROVIDER description is consistent with the other READMEs and the removal of the auto option.

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

48-56: Logs tool now correctly reuses shared project-name and provider setup flow

Loading the project name via cli.LoadProjectName and delegating provider selection to NewProviderPreparer.SetupProvider brings the logs tool in line with the unified onboarding/stack-selection behavior used elsewhere, and the debug log on the resolved project name should help when troubleshooting.

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

30-30: CLIInterface exposes the simplified LoadProjectName contract

Adding LoadProjectName(ctx, loader) to CLIInterface matches the refactor to resolve the project name before provider creation and aligns with the new DefaultToolCLI and tests.

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

41-47: Tests correctly exercise the new LoadProjectName-first flow

Mocking LoadProjectName and asserting the call sequence Connect → LoadProjectName → NewProvider → ListConfig ensures the list-config tool uses the updated project-name resolution before provider creation, matching the new production behavior.

Also applies to: 154-161

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

90-92: DefaultToolCLI’s LoadProjectName delegation is straightforward and correct

Delegating to loader.LoadProjectName(ctx) cleanly implements the new CLIInterface method without adding extra behavior, keeping project-name resolution in the loader layer.

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

47-51: LGTM!

The updated SetupProvider call correctly passes project.Name obtained from the prior LoadProject call, and the true flag for useWkDir is appropriate for deploy operations that need working directory context.

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

26-34: LGTM!

The refactored flow correctly separates project name loading from provider setup. Using useWkDir=false is appropriate for destroy operations that don't need local compose file context.

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

65-74: LGTM!

The mock correctly implements the new LoadProjectName signature that takes only ctx and loader parameters, aligning with the interface changes.


176-177: Correct test expectation update.

Setting expectedProviderCalls: false when project name loading fails is correct—the new flow should not attempt provider setup if project name resolution fails.

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

26-35: LGTM!

The refactored flow correctly loads the project name first, then sets up the provider with useWkDir=false appropriate for config listing operations.

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

37-41: LGTM!

The provider setup flow is consistent with other files in this PR, correctly using useWkDir=false for service listing operations.

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

25-29: LGTM! Mock field renamed to match new API.

The field name change from LoadProjectNameWithFallbackError to LoadProjectNameError correctly reflects the migration to the simpler LoadProjectName method.


49-57: LGTM! Method signature correctly updated.

The mock method signature now matches the new LoadProjectName(ctx, loader) API, removing the provider parameter that is no longer needed.

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

27-35: LGTM! Provider setup flow correctly refactored.

The new sequence correctly:

  1. Loads the project name explicitly via LoadProjectName
  2. Sets up the provider with the loaded project name using SetupProvider(ctx, projectName, sc.Stack, false)

Error messages are clear and distinguish between project name loading and provider setup failures.

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

50-56: LGTM! Mock method updated to match new API.

The method signature change correctly reflects the removal of the provider parameter from LoadProjectName.


166-173: LGTM! Test expectations correctly updated.

The expected call log now includes LoadProjectName instead of LoadProjectNameWithFallback, matching the new API flow.

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

28-39: LGTM! Provider setup correctly refactored.

The new flow:

  1. Loads project name when not provided in params
  2. Sets up provider with explicit project name and useWkDir=false

This aligns with the PR's goal of loading project names before provider instantiation.

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

42-48: LGTM! Mock method correctly updated.

The signature change removes the provider parameter, aligning with the new LoadProjectName(ctx, loader) API.


173-180: LGTM! Test expectations updated correctly.

The expected call sequence now includes LoadProjectName instead of LoadProjectNameWithFallback.

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

82-86: LGTM! Mock method added to support new stack flow.

The ListDeployments mock returns an empty deployments list, which is appropriate for these tests. This supports the new multi-source stack discovery logic.


279-388: LGTM! Test provider initialization updated.

All test cases now use newProvider(ctx) without the loader parameter, reflecting the refactored provider initialization flow where project name is determined before provider creation.

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

40-50: LGTM! Bootstrap command correctly refactored.

The command now:

  1. Loads the project name from the loader when args are empty
  2. Creates the provider with the explicit project name using newProviderChecked(ctx, args[0], false)

This aligns with the PR's goal of loading project information before provider instantiation.


157-162: LGTM! Stack saving logic correctly implemented.

The saveStacksToWkDir flag is derived from the presence of the project-name flag: when the flag is empty, stacks are saved to the working directory. This makes sense as it allows the project directory to maintain its own stack configurations.

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

41-74: LGTM! Provider setup flow well-structured.

The new SetupProvider signature accepts explicit projectName and useWkDir parameters, enabling proper stack selection before provider creation. The logic correctly:

  1. Validates stackName is not nil
  2. Loads stack if name is provided, otherwise selects/creates one
  3. Sets up provider authentication after stack resolution

117-126: LGTM! Helpful beta stack migration messaging.

The special handling for the "beta" stack provides clear guidance for users migrating from the implicit beta stack to the new stacks feature. The messaging appropriately explains the situation and provides a link to documentation.


190-193: LGTM! Beta stack fallback correctly implemented.

Defaulting to "beta" when the Stack field is empty correctly handles legacy deployments that predate the stacks feature, as described in the PR objectives.


232-258: LGTM! Stack loading with remote fallback.

The loadStack method correctly:

  1. Attempts to read the stack locally first
  2. Falls back to fetching from remote deployments if not found locally
  3. Creates a local copy of remote stacks for future use
  4. Loads the stack environment variables

This aligns with the PR's multi-source stack handling objective.


260-312: LGTM! New stack creation flow well-implemented.

The createNewStack method:

  1. Prompts for provider, region, and stack name
  2. Uses sensible defaults (default region, generated name)
  3. Only creates local stack file when useWkDir is true

This provides flexibility for both agent and CLI usage patterns.


326-376: LGTM! AWS authentication flow improved.

The refactored authentication correctly:

  1. Auto-detects existing AWS_PROFILE or access keys
  2. Lists available profiles and includes an option to use access keys instead
  3. Falls back to prompting for access keys when no profiles are available

This implements the PR objective of automatically showing profiles when available.


180-213: Verify and document the sort order assumption for ListDeployments.

The code at line 197 assumes deployments are returned sorted by deployed_at descending, but this sort order is not specified in the API contract (proto file). If the backend doesn't guarantee this ordering, the logic to avoid overwriting stacks could fail silently. Either update the API documentation to explicitly specify the sort order, or make the code independent of API ordering by sorting the results client-side.

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

19-19: LGTM!

New imports are correctly added and properly used in the newProviderChecked function for the unified provider setup flow.

Also applies to: 28-28


91-98: LGTM!

The placeholder "<name>" and useWkDir=false are appropriate here since this code path is only used to retrieve the remote project name for an error hint message.


531-535: LGTM!

Setting NonInteractive=true before calling newProviderChecked ensures the simplified newProvider path is taken, making the empty projectName and false for useWkDir irrelevant.


582-587: LGTM!

The logic saveStacksToWkDir = projectNameFlag == "" correctly determines whether to persist stack selection to the working directory based on whether the project name was explicitly provided vs. derived from compose file.


756-765: LGTM!

Correctly separates project name loading from provider creation, following the new pattern where the project is loaded first and then passed to the provider setup.


888-897: LGTM!

Consistent application of the project-loading-first pattern.


922-932: LGTM!

Consistent with the refactored provider creation pattern.


1005-1014: LGTM!

Consistent application of the refactored provider creation pattern.


1252-1299: LGTM!

The simplified updateProviderID now handles only environment-driven defaulting. The removal of the loader parameter aligns with the separation of concerns where project loading happens before provider setup. The warnings for detected cloud credentials when defaulting to Defang playground are helpful for users who may have forgotten to specify their provider.


1301-1308: LGTM!

Simplified newProvider for the non-interactive code path, removing the loader dependency.


1310-1314: LGTM!

Clean adapter pattern implementing the ProviderCreator interface to bridge the CLI's provider creation with the agent tools' ProviderPreparer.


1336-1372: > Likely an incorrect or invalid review comment.


1316-1330: No action needed. The first return value from SetupProvider is intentionally discarded because newProviderChecked doesn't require the ProviderID—it only returns provider and error. Other functions that don't need the ProviderID (removeConfig, setConfig, services, logs, listConfig, destroy) similarly discard it. Only deploy.go captures it because that function specifically uses it in a CanIUseProvider call. This pattern is intentional and correct.

@jordanstephens jordanstephens force-pushed the jordan/stacks-default-name branch from 4b0ef16 to cfa44f7 Compare December 11, 2025 00:01
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

🧹 Nitpick comments (2)
src/pkg/agent/tools/provider.go (1)

335-385: Clean up redundant AWS_PROFILE check.

The function checks AWS_PROFILE at line 336 and returns early if set, making the check at line 345 redundant.

Apply this diff:

 func (pp *providerPreparer) SetupAWSAuthentication(ctx context.Context) error {
 	if os.Getenv("AWS_PROFILE") != "" {
 		return nil
 	}

 	if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" {
 		return nil
 	}

 	// TODO: add support for aws sso strategy
-	if os.Getenv("AWS_PROFILE") == "" {
-		knownProfiles, err := listAWSProfiles()
-		if err != nil {
-			return fmt.Errorf("failed to list AWS profiles: %w", err)
-		}
-		if len(knownProfiles) > 0 {
-			const useAccessKeysOption = "Use Access Key ID and Secret Access Key"
-			knownProfiles = append(knownProfiles, useAccessKeysOption)
+	knownProfiles, err := listAWSProfiles()
+	if err != nil {
+		return fmt.Errorf("failed to list AWS profiles: %w", err)
+	}
+	if len(knownProfiles) > 0 {
+		const useAccessKeysOption = "Use Access Key ID and Secret Access Key"
+		knownProfiles = append(knownProfiles, useAccessKeysOption)
 		profile, err := pp.ec.RequestEnum(ctx, "Select your profile", "profile_name", knownProfiles)
 		if err != nil {
 			return fmt.Errorf("failed to elicit AWS Profile Name: %w", err)
 		}
-			if profile != useAccessKeysOption {
-				err := os.Setenv("AWS_PROFILE", profile)
-				if err != nil {
-					return fmt.Errorf("failed to set AWS_PROFILE environment variable: %w", err)
-				}
-				return nil
+		if profile != useAccessKeysOption {
+			err := os.Setenv("AWS_PROFILE", profile)
+			if err != nil {
+				return fmt.Errorf("failed to set AWS_PROFILE environment variable: %w", err)
 			}
+			return nil
 		}
 	}
 	if os.Getenv("AWS_ACCESS_KEY_ID") == "" {
 		accessKeyID, err := pp.ec.RequestString(ctx, "Enter your AWS Access Key ID:", "access_key_id")
 		if err != nil {
 			return fmt.Errorf("failed to elicit AWS Access Key ID: %w", err)
 		}
 		if err := os.Setenv("AWS_ACCESS_KEY_ID", accessKeyID); err != nil {
 			return fmt.Errorf("failed to set AWS_ACCESS_KEY_ID environment variable: %w", err)
 		}
 	}
 	if os.Getenv("AWS_SECRET_ACCESS_KEY") == "" {
 		accessKeySecret, err := pp.ec.RequestString(ctx, "Enter your AWS Secret Access Key:", "access_key_secret")
 		if err != nil {
 			return fmt.Errorf("failed to elicit AWS Secret Access Key: %w", err)
 		}
 		if err := os.Setenv("AWS_SECRET_ACCESS_KEY", accessKeySecret); err != nil {
 			return fmt.Errorf("failed to set AWS_SECRET_ACCESS_KEY environment variable: %w", err)
 		}
 	}
 	return nil
 }
src/cmd/cli/command/commands.go (1)

89-99: Improve error handling for maximum projects exceeded.

The placeholder projectName := "<name>" at line 90 could result in confusing error messages if newProviderChecked fails before reaching the RemoteProjectName call.

Consider handling this case more gracefully:

 		if strings.Contains(err.Error(), "maximum number of projects") {
-			projectName := "<name>"
-			provider, err := newProviderChecked(ctx, projectName, false)
-			if err != nil {
-				return err
+			projectName := "your-project"
+			provider, provErr := newProviderChecked(ctx, "", false)
+			if provErr != nil {
+				term.Debug("Failed to get provider for hint:", provErr)
+				printDefangHint("To deactivate a project, do:", "compose down --project-name <name>")
+				return err
 			}
 			if resp, err := provider.RemoteProjectName(ctx); err == nil {
 				projectName = resp
 			}
 			printDefangHint("To deactivate a project, do:", "compose down --project-name "+projectName)
 		}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4b0ef16 and cfa44f7.

📒 Files selected for processing (2)
  • src/cmd/cli/command/commands.go (14 hunks)
  • src/pkg/agent/tools/provider.go (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/cmd/cli/command/commands.go (5)
src/pkg/cli/client/provider_id.go (2)
  • ProviderID (10-10)
  • ProviderDefang (14-14)
src/pkg/cli/connect.go (1)
  • NewProvider (35-49)
src/pkg/elicitations/elicitations.go (2)
  • Client (14-16)
  • NewController (32-36)
src/pkg/elicitations/survey.go (1)
  • NewSurveyClient (19-25)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (33-39)
src/pkg/agent/tools/provider.go (3)
src/pkg/cli/client/provider_id.go (1)
  • ProviderID (10-10)
src/pkg/stacks/stacks.go (6)
  • StackParameters (17-22)
  • List (85-118)
  • Create (33-76)
  • Read (180-195)
  • Load (197-208)
  • StackListItem (78-83)
src/pkg/elicitations/elicitations.go (1)
  • Controller (8-12)
⏰ 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). (3)
  • GitHub Check: nix-shell-test
  • GitHub Check: go-playground-test
  • GitHub Check: Analyze (go)
🔇 Additional comments (6)
src/pkg/agent/tools/provider.go (3)

41-74: LGTM! Clear separation of concerns in provider setup.

The function properly handles both explicit stack names and interactive selection flows. The error handling is appropriate and the flow is easy to follow.


181-214: LGTM! Good handling of legacy deployments.

The function correctly handles the migration from implicit "beta" stacks by defaulting empty stack names to "beta". The deduplication logic using a map is efficient and the reliance on pre-sorted deployments is clearly documented.


268-321: LGTM! Conditional persistence is well-designed.

The function appropriately only persists stacks to disk when useWkDir is true, allowing for transient stack creation in other scenarios. The use of MakeDefaultName ensures consistent stack naming conventions.

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

438-447: LGTM! Appropriate error handling for missing stack files.

The code correctly ignores only os.ErrNotExist errors, allowing the remote import flow to handle missing local stacks while still propagating other errors (e.g., permission issues, corrupt files).


585-597: The saveStacksToWkDir pattern is consistently applied across all commands, but reconsider the logic.

The pattern saveStacksToWkDir := projectNameFlag == "" is used uniformly across 10 occurrences spanning cd.go, compose.go, and commands.go. The implementation is consistent—it assumes that providing an explicit project name flag means users don't want local stack persistence.

However, the underlying assumption itself warrants review: Is this inverse relationship intentional and correct? Users might expect stack persistence to be independent of whether they specify a project name, or this behavior should be explicitly documented as a design decision rather than an implicit assumption.


1319-1333: The original concern is incorrect. The newProvider function exists and properly handles the --stack flag in non-interactive mode. Both code paths pass global.Stack to the provider initialization:

  • Non-interactive: newProvider(ctx)cli.NewProvider(ctx, global.ProviderID, global.Client, global.Stack)
  • Interactive: pp.SetupProvider(ctx, projectName, &global.Stack, useWkDir)

The global.Stack variable is populated from the --stack command-line flag, so stack selection works identically in both modes. No changes are required.

@jordanstephens jordanstephens force-pushed the jordan/stacks-default-name branch from 66805ca to 64334ac Compare December 11, 2025 00:13
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 (1)
src/pkg/agent/tools/setConfig.go (1)

27-39: LGTM! Provider setup correctly deferred until project name is available.

The refactored flow properly handles both scenarios: when params.ProjectName is provided by the caller and when it needs to be loaded. Provider setup correctly uses the resolved project name.

The boolean parameter false in SetupProvider on line 36 would benefit from a named constant for clarity, but this is consistent with other files in this PR.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cfa44f7 and 66805ca.

📒 Files selected for processing (8)
  • src/pkg/agent/tools/listConfig.go (1 hunks)
  • src/pkg/agent/tools/listConfig_test.go (3 hunks)
  • src/pkg/agent/tools/logs.go (1 hunks)
  • src/pkg/agent/tools/removeConfig.go (1 hunks)
  • src/pkg/agent/tools/removeConfig_test.go (3 hunks)
  • src/pkg/agent/tools/services.go (1 hunks)
  • src/pkg/agent/tools/setConfig.go (1 hunks)
  • src/pkg/agent/tools/setConfig_test.go (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/pkg/agent/tools/listConfig.go
🧰 Additional context used
🧬 Code graph analysis (4)
src/pkg/agent/tools/services.go (2)
src/pkg/term/colorizer.go (3)
  • Error (318-320)
  • Errorf (322-324)
  • Debugf (298-300)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (33-39)
src/pkg/agent/tools/removeConfig.go (2)
src/pkg/term/colorizer.go (1)
  • Errorf (322-324)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (33-39)
src/pkg/agent/tools/logs.go (1)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (33-39)
src/pkg/agent/tools/setConfig.go (2)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (33-39)
src/pkg/clouds/driver.go (1)
  • ProjectName (8-8)
⏰ 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). (3)
  • GitHub Check: nix-shell-test
  • GitHub Check: go-playground-test
  • GitHub Check: Analyze (go)
🔇 Additional comments (9)
src/pkg/agent/tools/logs.go (1)

48-58: LGTM! Provider initialization flow correctly refactored.

The changes properly sequence project name loading before provider setup, with accurate error messages for each step. The debug log on line 52 provides useful visibility into the loaded project name.

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

24-35: LGTM! Consistent with the refactored provider initialization flow.

The changes align with the broader refactoring pattern: load project name first, then setup provider. Error messages are accurate and the lowercase "could not connect" maintains consistency across the codebase.

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

41-47: LGTM! Mock updated to match new interface.

The mock method signature correctly reflects the removal of the provider parameter from LoadProjectName, aligning with the refactored implementation.


154-161: LGTM! Test expectations updated correctly.

The expected call sequence now reflects the new flow: ConnectLoadProjectNameNewProviderListConfig, which matches the refactored implementation.

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

65-74: LGTM! Mock updated to match new interface.

The mock method signature correctly reflects the removal of the provider parameter from LoadProjectName.


167-178: LGTM! Test expectation correctly reflects new sequencing.

Setting expectedProviderCalls: false on line 176 when project name loading fails is correct, since provider setup now occurs after project name loading in the refactored flow.

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

42-48: LGTM! Mock updated to match new interface.

The mock method signature correctly removes the provider parameter from LoadProjectName, consistent with the refactored implementation.


173-181: LGTM! Test expectations correctly updated.

The expected call sequence now reflects the new flow: ConnectLoadProjectNameNewProviderConfigDelete.

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

29-41: LGTM! Debug log correctly placed after error check.

The refactored flow properly loads the project name first, then sets up the provider. The debug log on line 36 is correctly positioned after the error handling (lines 30-35), ensuring it only logs valid project names. This addresses the previously identified issue.

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 (3)
src/pkg/agent/tools/provider.go (1)

267-320: createNewStack cannot be used without .defang when combined with selectOrCreateStack.

When useWkDir is false, createNewStack intentionally skips stacks.Create. However, selectOrCreateStack always calls pp.loadStack at the end, which in turn expects a local .defang/<name> file or imports from remote.

For useWkDir == false and no prior deployments for projectName:

  • selectStack returns CreateNewStack.
  • createNewStack returns a StackListItem but does not create a file.
  • loadStack fails with “stack %q does not exist locally or remotely”.

This means interactive flows that pass useWkDir=false (e.g., agent tools and CLI subcommands with --project-name) cannot successfully create and use a new stack.

You may want selectOrCreateStack to return *stacks.StackParameters (constructed from the provider/region prompts) and:

  • Only go through loadStack + stacks.Load when useWkDir is true, and
  • Use the in-memory parameters directly when useWkDir is false.
src/cmd/cli/command/commands.go (2)

89-99: newProviderChecked in the “maximum number of projects” error path will likely fail or prompt unnecessarily.

In this hint path you call:

projectName := "<name>"
provider, err := newProviderChecked(ctx, projectName, false)

newProviderChecked’s interactive branch now:

  • Runs full stack selection/creation via SetupProvider(ctx, projectName, &global.Stack, useWkDir=false), and
  • Ultimately relies on loadStack, which expects an existing local or remote stack for the supplied projectName.

For a placeholder project name "<name>" with no deployments, this will generally fail with a stack-not-found error before you can call provider.RemoteProjectName(ctx), and it may also pop up a stack-selection/credentials prompt in an error-handling path.

Given you only need a provider to resolve RemoteProjectName, consider:

  • Calling newProvider(ctx) here instead (using the same provider as the original command), or
  • Passing the actual project name that triggered the “maximum number of projects” error, if available, and ensuring useWkDir is appropriate.

1255-1301: newProviderChecked should propagate the selected provider ID into global.ProviderID for consistency.

In interactive mode, newProviderChecked now does:

pc := &providerCreator{}
ec := elicitations.NewController(...)
pp := agentTools.NewProviderPreparer(pc, ec, global.Client)
_, provider, err := pp.SetupProvider(ctx, projectName, &global.Stack, useWkDir)
...
_, err = provider.AccountInfo(ctx)
return provider, err

Two concerns:

  1. The ProviderID chosen by SetupProvider is never written back to global.ProviderID. As a result:

    • track.Cmd logs (which include P("provider", global.ProviderID)) may record ProviderAuto or a stale value even though the actual provider used is AWS/DO/GCP.
    • Callers like debug.NewDebugger(ctx, getCluster(), &global.ProviderID, &global.Stack) may see a provider ID that does not match the instantiated provider.
  2. In contrast, the non-interactive branch calls newProvider(ctx), which does run updateProviderID(ctx) and ensures global.ProviderID is in sync.

To keep global state consistent, consider:

id, provider, err := pp.SetupProvider(ctx, projectName, &global.Stack, useWkDir)
if err != nil { ... }
global.ProviderID = *id

before returning.

Also applies to: 1303-1332

♻️ Duplicate comments (1)
src/pkg/agent/tools/provider.go (1)

82-114: Remote stack detection/import is still based on display-name parentheses and ignores useWkDir.

Two related issues here:

  1. The “remote vs local” decision is inferred from the rendered label (strings.Contains(selectedStackName, "(")), which is fragile and was already flagged in a previous review. This can break if:

    • Label formatting changes.
    • Stack names or translations ever include parentheses.
    • You later add more metadata to the label.

    A more robust pattern is to keep a map from display name to a StackOption that explicitly carries whether it is local and the corresponding StackParameters, e.g.:

    type StackOption struct {
        Name           string
        Local          bool
        LastDeployedAt time.Time
        Params         *stacks.StackParameters // nil for pure-local if you want
    }

    Then build displayToOption := map[string]StackOption{} and retrieve the option directly instead of parsing parentheses.

  2. stacks.Create(*remoteStackParameters) is invoked unconditionally for remote selections, even when useWkDir is false. That means:

    • CLI subcommands invoked with --project-name (which pass useWkDir=false) will still create .defang/<stack> files in the current working directory when importing a remote stack.
    • Agent tools (which also call SetupProvider with useWkDir=false) can likewise write stack files into whatever directory happens to be current.

    This undermines the PR goal of “restrict reading and writing stack files when --project-name is used or when ProjectName is provided by an agent tool call” and can cause cross-project stack files to appear in unrelated directories.

Consider:

  • Using explicit metadata on StackOption (or a dedicated map/set of remote names) instead of parsing labels, and
  • Respecting useWkDir here (and/or in loadStack) so that remote stacks can be used without writing .defang files when useWkDir is false (e.g., by returning parameters directly to SetupProvider instead of round-tripping through the filesystem).

Also applies to: 148-171

🧹 Nitpick comments (2)
src/pkg/agent/tools/provider.go (1)

181-213: Consider deterministic ordering and provider ID error handling in collectExistingStacks.

Minor, but two improvements would make this more robust:

  • The final slice of stacks is built from a map without sorting, so the interactive menu order is effectively random. Sorting by DeployedAt (desc) or by name would produce a stable UX.
  • providerID.SetValue(deployment.GetProvider()) ignores errors; if a newer Fabric adds a provider enum the CLI doesn’t know about, this could silently yield a zero-value provider. At least logging or failing fast here would make such incompatibilities visible.
src/cmd/cli/command/commands.go (1)

757-766: Config and delete commands correctly respect --project-name vs working directory, but there is some duplication.

For config create, config rm, config ls, and delete you now:

  • Load the project name via loader.LoadProjectName.
  • Compute saveStacksToWkDir := (projectNameFlag == "").
  • Call newProviderChecked(ctx, projectName, saveStacksToWkDir).

This aligns with the PR intent:

  • When --project-name is omitted, stacks in the current working directory are considered and new ones can be created.
  • When --project-name is set, local stacks shouldn’t be enumerated/created (modulo the separate .defang read concern in PersistentPreRunE).

The pattern is repeated across several commands; if this spreads further, it might be worth extracting a helper like getProjectAndProvider(cmd) to reduce duplication, but that’s not urgent.

Also applies to: 890-898, 924-933, 1007-1015

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 66805ca and 64334ac.

📒 Files selected for processing (10)
  • src/cmd/cli/command/commands.go (14 hunks)
  • src/pkg/agent/tools/listConfig.go (1 hunks)
  • src/pkg/agent/tools/listConfig_test.go (3 hunks)
  • src/pkg/agent/tools/logs.go (1 hunks)
  • src/pkg/agent/tools/provider.go (5 hunks)
  • src/pkg/agent/tools/removeConfig.go (1 hunks)
  • src/pkg/agent/tools/removeConfig_test.go (3 hunks)
  • src/pkg/agent/tools/services.go (1 hunks)
  • src/pkg/agent/tools/setConfig.go (1 hunks)
  • src/pkg/agent/tools/setConfig_test.go (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/pkg/agent/tools/listConfig_test.go
  • src/pkg/agent/tools/listConfig.go
  • src/pkg/agent/tools/logs.go
  • src/pkg/agent/tools/services.go
🧰 Additional context used
🧬 Code graph analysis (4)
src/pkg/agent/tools/setConfig.go (3)
src/pkg/term/colorizer.go (1)
  • Errorf (322-324)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (33-39)
src/pkg/clouds/driver.go (1)
  • ProjectName (8-8)
src/pkg/agent/tools/removeConfig.go (1)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (33-39)
src/cmd/cli/command/commands.go (8)
src/pkg/term/colorizer.go (2)
  • Warn (310-312)
  • Errorf (322-324)
src/pkg/cli/client/provider_id.go (1)
  • ProviderID (10-10)
src/protos/io/defang/v1/fabric.pb.go (4)
  • Provider (28-28)
  • Provider (66-68)
  • Provider (70-72)
  • Provider (79-81)
src/pkg/cli/client/provider.go (1)
  • Provider (43-69)
src/pkg/cli/connect.go (1)
  • NewProvider (35-49)
src/pkg/elicitations/elicitations.go (2)
  • Client (14-16)
  • NewController (32-36)
src/pkg/elicitations/survey.go (1)
  • NewSurveyClient (19-25)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (33-39)
src/pkg/agent/tools/provider.go (3)
src/pkg/cli/client/provider_id.go (1)
  • ProviderID (10-10)
src/pkg/stacks/stacks.go (6)
  • StackParameters (17-22)
  • List (85-118)
  • Create (33-76)
  • Read (180-195)
  • Load (197-208)
  • StackListItem (78-83)
src/pkg/elicitations/elicitations.go (1)
  • Controller (8-12)
⏰ 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). (3)
  • GitHub Check: nix-shell-test
  • GitHub Check: go-playground-test
  • GitHub Check: Analyze (go)
🔇 Additional comments (6)
src/pkg/agent/tools/provider.go (2)

41-59: Ensure stackName is never nil at call sites and consider documenting this contract.

SetupProvider now hard-fails when stackName == nil. CLI paths use &global.Stack, but agent tool paths pass sc.Stack directly; if any caller ever leaves StackConfig.Stack nil, provider setup will return "stackName cannot be nil" instead of falling back to selection/creation.

Either:

  • Guarantee and document that StackConfig.Stack is always non-nil (even when empty), or
  • Change the API to accept nil and internally treat it as “no stack selected yet”.

334-383: AWS profile/key prompting behavior looks solid and matches stated goals.

The new SetupAWSAuthentication flow:

  • Respects existing AWS_PROFILE or access keys without prompting.
  • Enumerates profiles from ~/.aws/{credentials,config} and offers a “Use Access Key ID and Secret Access Key” fallback.
  • Only prompts for each missing credential exactly once.

listAWSProfiles correctly extracts profile names from both files and returns a sorted, duplicate-free list.

No issues from my side here.

Also applies to: 432-471

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

534-565: whoami’s use of newProviderChecked is fine and remains non-interactive.

Setting global.NonInteractive = true before calling:

provider, err := newProviderChecked(cmd.Context(), "", false)

ensures the non-interactive branch (newProvider(ctx)) is used, preserving the previous behavior (no stack selection survey, provider still derived from flags/env via updateProviderID).

No changes needed here.

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

65-75: Tests correctly reflect the new project-name loading and error flow.

Updating MockSetConfigCLI to expose LoadProjectName(ctx, loader) and expecting:

  • "could not connect: connection failed" for connect errors, and
  • No provider creation (expectedProviderCalls: false) when LoadProjectName fails,

matches the new HandleSetConfig behavior (connect → load project name → provider setup). The adjustments to error messages and call expectations look consistent.

Also applies to: 158-177

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

20-39: Project-name loading is correct, but useWkDir=false inherits the same stack-creation/import constraints.

The new flow:

client, err := cli.Connect(...)
pp := NewProviderPreparer(cli, ec, client)
if params.ProjectName == "" {
    projectName, err := cli.LoadProjectName(ctx, loader)
    ...
    params.ProjectName = projectName
}
_, provider, err := pp.SetupProvider(ctx, params.ProjectName, sc.Stack, false)

nicely ensures you always have a concrete ProjectName before setting up the provider, which is a good improvement over the previous fallback-based path.

However, because useWkDir is hard-coded to false here, HandleSetConfig is subject to the same limitations noted in provider.go:

  • It cannot successfully create a brand new stack for a project with no existing deployments or local stacks.
  • It may still create .defang/<stack> files when importing remote stacks, despite useWkDir=false.

Once the underlying SetupProvider behavior is adjusted (as suggested in provider.go), this call site should benefit automatically; no changes needed here beyond that.

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

42-48: Tests accurately track the new project-name loading and call ordering.

The mock now:

  • Implements LoadProjectName(ctx, loader) and logs "LoadProjectName".
  • Expects "could not connect: connection failed" on connect error.
  • Verifies the successful path calls Connect, then LoadProjectName, then NewProvider, then ConfigDelete.

This aligns with the updated HandleRemoveConfigTool behavior and the introduction of LoadProjectName + SetupProvider. Looks good.

Also applies to: 68-76, 172-181

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 (1)
src/pkg/agent/tools/provider.go (1)

140-177: Fragile remote stack detection still relies on string parsing.

The implementation checks for "(last deployed " in the selected name to determine if it's a remote stack. While more specific than checking for just "(", this approach is still fragile—a local stack name containing this substring would cause incorrect behavior.

Consider using a display-to-actual name mapping as previously suggested:

-	// Convert map back to slice
-	stackNames := make([]string, 0, len(stackOptions)+1)
-	for _, stackOption := range stackOptions {
-		name := stackOption.Name
-		if !stackOption.Local {
-			name += fmt.Sprintf(" (last deployed %s)", stackOption.LastDeployedAt.Local().Format(time.RFC822))
-		}
-		stackNames = append(stackNames, name)
-	}
-	if useWkDir {
-		stackNames = append(stackNames, CreateNewStack)
-	}
-
-	selectedStackName, err := ec.RequestEnum(ctx, "Select a stack", "stack", stackNames)
-	if err != nil {
-		return "", fmt.Errorf("failed to elicit stack choice: %w", err)
-	}
-
-	// check if the selected stack has parentheses indicating it's remote
-	if strings.Contains(selectedStackName, "(last deployed ") && strings.Contains(selectedStackName, ")") {
-		selectedStackName = strings.TrimSpace(selectedStackName[:strings.Index(selectedStackName, "(")])
-
-		// find the stack parameters from the remoteStackList
-		stackOption := stackOptions[selectedStackName]
-		if stackOption.Local {
-			return selectedStackName, nil
-		}
-
-		if stackOption.Parameters == nil {
-			return "", fmt.Errorf("stack parameters for remote stack %q are nil", selectedStackName)
-		}
-
-		term.Debugf("Importing stack %s from remote", selectedStackName)
-		_, err := stacks.Create(*stackOption.Parameters)
-		if err != nil {
-			return "", fmt.Errorf("failed to create local stack from remote: %w", err)
-		}
-	}
+	// Convert map to display names with mapping back to actual names
+	displayNames := make([]string, 0, len(stackOptions)+1)
+	displayToActual := make(map[string]string)
+	for _, stackOption := range stackOptions {
+		displayName := stackOption.Name
+		if !stackOption.Local {
+			displayName += fmt.Sprintf(" (last deployed %s)", stackOption.LastDeployedAt.Local().Format(time.RFC822))
+		}
+		displayNames = append(displayNames, displayName)
+		displayToActual[displayName] = stackOption.Name
+	}
+	if useWkDir {
+		displayNames = append(displayNames, CreateNewStack)
+		displayToActual[CreateNewStack] = CreateNewStack
+	}
+
+	selectedDisplayName, err := ec.RequestEnum(ctx, "Select a stack", "stack", displayNames)
+	if err != nil {
+		return "", fmt.Errorf("failed to elicit stack choice: %w", err)
+	}
+
+	selectedStackName := displayToActual[selectedDisplayName]
+	stackOption := stackOptions[selectedStackName]
+
+	// Import remote stack if it's not local and not the create-new option
+	if !stackOption.Local && selectedStackName != CreateNewStack {
+		if stackOption.Parameters == nil {
+			return "", fmt.Errorf("stack parameters for remote stack %q are nil", selectedStackName)
+		}
+
+		term.Debugf("Importing stack %s from remote", selectedStackName)
+		_, err := stacks.Create(*stackOption.Parameters)
+		if err != nil {
+			return "", fmt.Errorf("failed to create local stack from remote: %w", err)
+		}
+	}
🧹 Nitpick comments (2)
src/pkg/agent/tools/provider.go (2)

350-370: Redundant AWS_PROFILE check.

The condition if os.Getenv("AWS_PROFILE") == "" at line 350 is always true at this point since line 341 already returned early if AWS_PROFILE was set. The check can be removed to simplify the code.

-	// TODO: add support for aws sso strategy
-	if os.Getenv("AWS_PROFILE") == "" {
-		knownProfiles, err := listAWSProfiles()
-		if err != nil {
-			return fmt.Errorf("failed to list AWS profiles: %w", err)
-		}
-		if len(knownProfiles) > 0 {
-			const useAccessKeysOption = "Use Access Key ID and Secret Access Key"
-			knownProfiles = append(knownProfiles, useAccessKeysOption)
-			profile, err := pp.ec.RequestEnum(ctx, "Select your profile", "profile_name", knownProfiles)
-			if err != nil {
-				return fmt.Errorf("failed to elicit AWS Profile Name: %w", err)
-			}
-			if profile != useAccessKeysOption {
-				err := os.Setenv("AWS_PROFILE", profile)
-				if err != nil {
-					return fmt.Errorf("failed to set AWS_PROFILE environment variable: %w", err)
-				}
-				return nil
-			}
-		}
-	}
+	// TODO: add support for aws sso strategy
+	knownProfiles, err := listAWSProfiles()
+	if err != nil {
+		return fmt.Errorf("failed to list AWS profiles: %w", err)
+	}
+	if len(knownProfiles) > 0 {
+		const useAccessKeysOption = "Use Access Key ID and Secret Access Key"
+		knownProfiles = append(knownProfiles, useAccessKeysOption)
+		profile, err := pp.ec.RequestEnum(ctx, "Select your profile", "profile_name", knownProfiles)
+		if err != nil {
+			return fmt.Errorf("failed to elicit AWS Profile Name: %w", err)
+		}
+		if profile != useAccessKeysOption {
+			err := os.Setenv("AWS_PROFILE", profile)
+			if err != nil {
+				return fmt.Errorf("failed to set AWS_PROFILE environment variable: %w", err)
+			}
+			return nil
+		}
+	}

458-469: Missing scanner error check.

After the scanner.Scan() loop, scanner.Err() should be checked to detect any I/O errors that occurred during scanning. Without this check, partial reads due to errors would be silently ignored.

 		for scanner.Scan() {
 			line := scanner.Text()
 			line = strings.TrimSpace(line)
 			if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
 				section = strings.Trim(line, "[]")
 				// In config, profiles are named "profile NAME"
 				section = strings.TrimPrefix(section, "profile ")
 				profiles[section] = struct{}{}
 			}
 		}
+		if err := scanner.Err(); err != nil {
+			f.Close()
+			return nil, fmt.Errorf("error reading %s: %w", file, err)
+		}
 		f.Close()
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 64334ac and eca8b33.

📒 Files selected for processing (1)
  • src/pkg/agent/tools/provider.go (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/pkg/agent/tools/provider.go (2)
src/pkg/cli/client/provider_id.go (1)
  • ProviderID (10-10)
src/pkg/stacks/stacks.go (6)
  • StackParameters (17-22)
  • List (85-118)
  • Create (33-76)
  • Read (180-195)
  • Load (197-208)
  • StackListItem (78-83)
⏰ 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 (8)
src/pkg/agent/tools/provider.go (8)

41-74: LGTM!

The updated SetupProvider function cleanly orchestrates the provider setup flow with proper branching for explicit stack names vs. interactive selection, and appropriate error handling throughout.


76-81: LGTM!

The StackOption struct cleanly captures both local and remote stack metadata with appropriate fields for the display and import logic.


83-116: LGTM!

The function correctly merges remote and local stacks with local taking precedence, which aligns with the expected behavior where existing local stack files should be used instead of re-importing from remote.


182-185: LGTM!

Clean struct definition that appropriately extends StackParameters with deployment timestamp for remote stack tracking.


222-237: LGTM!

Clean orchestration function that delegates to the appropriate helpers based on user selection.


239-271: LGTM!

The function correctly implements the fallback logic from local to remote stacks with appropriate error handling. The previous variable shadowing concern has been addressed.


313-319: Conditional stack file creation aligns with requirements.

The useWkDir guard correctly prevents stack file creation when invoked outside a working directory context (e.g., agent tool calls). The function still returns the stack item for in-memory use downstream.


196-214: The Mode field is intentionally optional and defaults correctly.

The StackParameters struct includes the Mode field, but it's not required to be set during construction. The Marshal() function explicitly handles the case where Mode is unspecified (the zero value) by omitting it from the stack file. This is the correct behavior for remote stacks where deployment mode information is not available from the API response.

@jordanstephens jordanstephens force-pushed the jordan/stacks-default-name branch from eca8b33 to 88a4ad7 Compare December 11, 2025 01:47
@jordanstephens jordanstephens force-pushed the jordan/stacks-default-name branch from 88a4ad7 to dc0b8ea Compare December 11, 2025 02:00
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 (1)
src/pkg/agent/tools/provider.go (1)

72-90: New-stack flow breaks when useWkDir=false (no stackfile is ever readable)

For interactive provider setup you now have:

  • SetupProviderselectOrCreateStack(ctx, projectName, useWkDir)
  • selectOrCreateStack:
    • Calls selectStack, which can return CreateNewStack when there are no existing local/remote stacks.
    • On CreateNewStack, calls createNewStack(ctx, useWkDir) and then always calls pp.loadStack(ctx, projectName, selectedStackName).
  • createNewStack only calls pp.sm.Create(params) when useWkDir is true.
  • loadStack always starts with pp.sm.Read(stackName) and falls back to remote deployments if the file doesn’t exist.

For useWkDir=false and a new stack:

  • createNewStack never writes a .defang/<stack> file.
  • loadStack will call Read, see os.ErrNotExist, then collectPreviouslyDeployedStacks, which won’t find the brand-new stack (no deployments yet), and finally error with stack %q does not exist locally or remotely.

This contradicts the test expectations in provider_test.go for the “create stack without stackfile” cases and makes the useWkDir=false new‑stack path unusable.

You probably want one of:

  1. Disallow new stacks when useWkDir=false: In selectStack / selectOrCreateStack, if !useWkDir and there are no local/remote stacks, return a clear error like “no stacks exist; run from a project directory or omit --project-name” instead of CreateNewStack.

  2. Support true ephemeral stacks: When useWkDir=false and you create a new stack:

    • Don’t call loadStack at all.
    • Use the freshly built stacks.StackParameters from createNewStack to set providerID and call NewProvider, without touching StacksManager.Read/Load.
    • Optionally skip stacks.Create entirely so no local .defang is written.

Right now the code guarantees a failure in that scenario even though the tests assume it will work, so this is worth fixing before release.

 func (pp *providerPreparer) selectOrCreateStack(ctx context.Context, projectName string, useWkDir bool) (*stacks.StackParameters, error) {
-    selectedStackName, err := pp.selectStack(ctx, projectName, useWkDir)
+    selectedStackName, err := pp.selectStack(ctx, projectName, useWkDir)
     ...
-    if selectedStackName == CreateNewStack {
-        newStack, err := pp.createNewStack(ctx, useWkDir)
+    if selectedStackName == CreateNewStack {
+        if !useWkDir {
+            return nil, fmt.Errorf("cannot create a new stack when --project-name is used; run from the project directory instead")
+        }
+        newStack, err := pp.createNewStack(ctx, useWkDir)
         ...
     }
-    return pp.loadStack(ctx, projectName, selectedStackName)
+    return pp.loadStack(ctx, projectName, selectedStackName)
 }

Also applies to: 256-305, 307-360

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

1257-1303: Provider auto-detection path still warns but always picks Defang playground

updateProviderID does:

  • Honor --provider over DEFANG_PROVIDER.
  • When ProviderAuto, warn if AWS/DO/GCP env vars are present, then unconditionally set global.ProviderID = cliClient.ProviderDefang.

This matches the current behavior (auto = playground), but the warnings may still confuse users who expect “auto” to actually use those env vars.

If the intention is that ProviderAuto always means “Defang playground, env vars ignored”, consider:

  • Updating any user-facing docs/help text to say so explicitly.
  • Potentially downgrading the term.Warn calls to a single concise info log, or removing them entirely, to avoid implying that the CLI could have auto-selected AWS/DO/GCP.

The helper newProvider and the non-interactive branch in newProviderChecked correctly call updateProviderID before instantiating the provider, so the code path is coherent; this is mainly a UX/semantics refinement.

Also applies to: 1305-1313, 1320-1334

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eca8b33 and 88a4ad7.

📒 Files selected for processing (5)
  • src/cmd/cli/command/commands.go (14 hunks)
  • src/cmd/cli/command/globals.go (2 hunks)
  • src/cmd/cli/command/globals_test.go (3 hunks)
  • src/pkg/agent/tools/provider.go (5 hunks)
  • src/pkg/agent/tools/provider_test.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/cmd/cli/command/commands.go (5)
src/pkg/cli/client/provider.go (1)
  • Provider (43-69)
src/pkg/cli/connect.go (1)
  • NewProvider (35-49)
src/pkg/elicitations/elicitations.go (2)
  • Client (14-16)
  • NewController (32-36)
src/pkg/elicitations/survey.go (1)
  • NewSurveyClient (19-25)
src/pkg/agent/tools/provider.go (1)
  • NewProviderPreparer (63-70)
src/pkg/agent/tools/provider.go (4)
src/pkg/stacks/stacks.go (6)
  • Create (33-76)
  • StackParameters (17-22)
  • Read (180-195)
  • Load (197-208)
  • List (85-118)
  • StackListItem (78-83)
src/pkg/elicitations/elicitations.go (1)
  • Controller (8-12)
src/pkg/cli/client/client.go (1)
  • FabricClient (17-44)
src/pkg/cli/client/provider_id.go (1)
  • ProviderID (10-10)
⏰ 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 (5)
src/cmd/cli/command/globals_test.go (1)

21-40: Test updates correctly track the loadStackFile rename

Swapping loadDotDefang for loadStackFile keeps the existing behaviors under test:

  • OS env still wins over values from the stack file.
  • A non-existent stack name still returns an error at the helper level (even though the CLI now ignores os.ErrNotExist).

No issues from the test side here.

Also applies to: 316-323

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

362-424: Provider authentication flows look sound; nice AWS profile UX

The new authentication helpers:

  • Short-circuit when AWS/GCP/DO env vars are already present.
  • For AWS:
    • Prefer AWS_PROFILE if set.
    • Otherwise, list profiles from ~/.aws/{credentials,config} and offer a “Use Access Key ID and Secret Access Key” option before falling back to prompting for keys.
  • For GCP and DO:
    • Prompt for the missing env vars and set them via os.Setenv.

This matches typical provider SDK expectations and keeps behavior strictly additive (only filling in missing values). Given SetupProvider only calls these in interactive flows (non-interactive uses the old newProvider path), there’s no risk of unexpected prompts in CI.

No changes needed here from a correctness perspective.

Also applies to: 439-470, 472-512

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

23-33: I cannot verify this review comment because the repository cannot be cloned in the sandbox environment. Without access to the actual codebase, I cannot confirm:

  • The current implementation of loadStackFile at lines 23-33 and 244-272
  • What the documentation actually states about .defang file loading
  • Whether general .defang files are still loaded elsewhere in the codebase
  • The exact behavior changes described

Manual verification is required to validate the claims about:

  1. Whether loadStackFile truly skips general .defang files
  2. Whether the comments/docstrings still describe behavior that no longer exists
  3. Whether this represents a breaking change for users
src/cmd/cli/command/commands.go (2)

438-449: Project-name gating of stack-file loading is correct, but verification of remaining .defang references and GlobalConfig comments cannot be completed

The code change wrapping global.loadStackFile(global.getStackName(cmd.Flags())) in:

if !cmd.Flags().Changed("project-name") {
    ...
}

correctly prevents reading stack files from the current working directory when --project-name is explicitly supplied.

Combined with changes to loadStackFile that now only loads .defang/<stack> files and no longer loads a general .defang file, this ensures:

  • No stack RC is read when --project-name is supplied
  • Only stack-specific files are read for the working directory project

To complete the verification, the following still need to be checked:

  • Update GlobalConfig comments to explicitly document that only .defang/<stack> files are supported and general .defang is ignored
  • Verify no remaining call sites still expect .defang to be honored for legacy configurations

89-99: newProviderChecked wiring is consistent, but ProviderID global may be stale

All the updated commands now follow the same pattern:

  • Resolve projectName via configureLoader().LoadProjectName(ctx) or flags.
  • Compute saveStacksToWkDir := projectNameFlag == "".
  • Call newProviderChecked(ctx, projectName, saveStacksToWkDir) and use the returned provider for API calls.

This is good: it centralizes provider+stack selection and correctly disables working‑directory stack writes when --project-name is set.

However:

  • newProviderChecked in interactive mode constructs a providerPreparer, calls pp.SetupProvider(ctx, projectName, &global.Stack, useWkDir), and ignores the returned *cliClient.ProviderID.
  • It never writes that ProviderID back into global.ProviderID.

Some code still passes &global.ProviderID elsewhere, e.g.:

debugger, err := debug.NewDebugger(ctx, getCluster(), &global.ProviderID, &global.Stack)
...
debugConfig := debug.DebugConfig{
    ProviderID: &global.ProviderID,
    ...
}

If global.ProviderID is only set by flags/env (updateProviderID) and never updated by the interactive stack selection flow, there's a risk that:

  • The provider actually used for config/debug/delete operations (via provider) is different from global.ProviderID, and
  • Any downstream code that uses global.ProviderID (debugger, agent, etc.) will see the wrong provider.

Consider either:

  • Writing the selected providerID back into global.ProviderID inside newProviderChecked (or SetupProvider), or
  • Refactoring downstream code (e.g. debug) to rely solely on the concrete provider instead of global.ProviderID.

At minimum this deserves a quick grep to see all remaining uses of global.ProviderID after newProviderChecked to ensure no stale state is relied upon.

Also applies to: 531-567, 587-599, 755-768, 893-900, 927-935, 959-967, 1009-1017

Comment on lines 107 to 162
type StackOption struct {
Name string
Local bool
LastDeployedAt time.Time
Parameters *stacks.StackParameters
}

func (pp *providerPreparer) collectStackOptions(ctx context.Context, projectName string, useWkDir bool) (map[string]StackOption, error) {
// Merge remote and local stacks into a single list of type StackOption,
// prefer local if both exist
stackMap := make(map[string]StackOption)
remoteStackList, err := pp.collectPreviouslyDeployedStacks(ctx, projectName)
if err != nil {
return "", fmt.Errorf("failed to list stacks: %w", err)
return nil, fmt.Errorf("failed to collect existing stacks: %w", err)
}
for _, remoteStack := range remoteStackList {
label := fmt.Sprintf("%s (last deployed %s)", remoteStack.Name, remoteStack.DeployedAt.Local().Format(time.RFC822))
stackMap[label] = StackOption{
Name: remoteStack.Name,
Local: false,
LastDeployedAt: remoteStack.DeployedAt,
Parameters: &remoteStack.StackParameters,
}
}

if len(stackList) == 0 {
if useWkDir {
localStackList, err := pp.sm.List()
if err != nil {
return nil, fmt.Errorf("failed to list stacks: %w", err)
}

for _, localStack := range localStackList {
stackMap[localStack.Name] = StackOption{
Name: localStack.Name,
Local: true,
LastDeployedAt: time.Time{},
}
}
}

return stackMap, 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

Beta-stack messaging and stack-option map keys don’t align with labels

In the stack selection flow:

  • collectStackOptions creates stackMap keyed as:
    • Remote stacks: label := fmt.Sprintf("%s (last deployed %s)", name, timestamp)stackMap[label] = StackOption{ Name: name, Local: false, ... }
    • Local stacks (when useWkDir): stackMap[localStack.Name] = StackOption{ Name: localStack.Name, Local: true }
  • printStacksInfoMessage then checks _, betaExists := stacks["beta"] to decide whether to show the “implicit beta stack” message.
  • selectStack passes stackLabels := keys(stackOptions) into RequestEnum and looks up selectedStackOption := stackOptions[selectedStackLabel].

Two concrete issues:

  1. Implicit beta deployments won’t trigger the message on first run

    When you only have previous deployments with empty Stack (normalized to "beta" in collectPreviouslyDeployedStacks), the only entry created is under the label "beta (last deployed ...)". There is no "beta" key in stackOptions, so betaExists is false and the beta-stacks onboarding message is never shown for the very case it’s meant to address.

    Fix: detect beta by the underlying StackOption.Name, not the map key:

    func printStacksInfoMessage(stacks map[string]StackOption) {
        betaExists := false
        for _, opt := range stacks {
            if opt.Name == "beta" {
                betaExists = true
                break
            }
        }
        ...
    }
  2. Comment vs behavior: “prefer local if both exist”

    The comment on collectStackOptions says “prefer local if both exist”, but because remote entries are keyed by label (with timestamp) and locals by bare name, you’ll see both:

    • "stack1" (local)
    • "stack1 (last deployed ...)" (remote)

    and the selection logic treats them as distinct options. If you really want to prefer local, consider keying by canonical name and maintaining a separate display‑label map for the prompt.

Additionally, printStacksInfoMessage calls os.Args[1] without checking len(os.Args) > 1:

executable, _ := os.Executable()
term.Infof("To skip this prompt, run %s %s --stack=%s", filepath.Base(executable), os.Args[1], "<stack_name>")

This will panic in edge cases (e.g., program invoked with no arguments, or reused as a library). A simple length guard with a fallback to just the binary name would make this more robust.

Also applies to: 150-164, 165-215, 216-254

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

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/provider.go (1)

79-90: loadStack unconditionally creates local stack files, ignoring useWkDir.

When a stack name is provided and the stack doesn't exist locally but does remotely, loadStack at line 80 will create the local stack file regardless of the useWkDir flag. This contradicts the PR objective of restricting stack file writes when --project-name is used.

Consider passing useWkDir to loadStack and conditionally creating the stack file:

-func (pp *providerPreparer) loadStack(ctx context.Context, projectName, stackName string) (*stacks.StackParameters, error) {
+func (pp *providerPreparer) loadStack(ctx context.Context, projectName, stackName string, useWkDir bool) (*stacks.StackParameters, error) {
 	stack, err := pp.sm.Read(stackName)
 	if err != nil {
 		if !errors.Is(err, os.ErrNotExist) {
 			return nil, fmt.Errorf("failed to read stack: %w", err)
 		}
 		// call collectExistingStacks to see if it exists remotely
 		existingStacks, err := pp.collectPreviouslyDeployedStacks(ctx, projectName)
 		// ...
 		term.Debugf("Importing stack %s from remote", stackName)
-		_, err = pp.sm.Create(*stack)
-		if err != nil {
-			return nil, fmt.Errorf("failed to create local stack from remote: %w", err)
+		if useWkDir {
+			_, err = pp.sm.Create(*stack)
+			if err != nil {
+				return nil, fmt.Errorf("failed to create local stack from remote: %w", err)
+			}
 		}
 	}
♻️ Duplicate comments (1)
src/cmd/cli/command/commands.go (1)

1272-1283: Past review comment still applies regarding environment variable warnings.

The warnings about detected AWS/DigitalOcean/GCP environment variables while defaulting to Defang playground may confuse users. Consider either removing these warnings or implementing auto-detection that uses the detected provider.

🧹 Nitpick comments (5)
src/pkg/agent/tools/provider_test.go (2)

523-529: Avoid mutating test case data during test execution.

Modifying tc.userStackSelection in-place (lines 526-528) is a test smell that makes the test harder to reason about and could cause issues if test cases are ever run in parallel or reused.

Consider computing the expected label separately:

 		if tc.hasOtherRemoteStacks {
 			label := "remotestack (last deployed " + deployTime.Local().Format(time.RFC822) + ")"
 			stackOptions = append(stackOptions, label)
-			// Update the expected selection to match the actual format
-			if strings.Contains(tc.userStackSelection, "remotestack") {
-				tc.userStackSelection = label
-			}
 		}
+		// Compute the actual expected selection
+		expectedSelection := tc.userStackSelection
+		if tc.hasOtherRemoteStacks && strings.Contains(tc.userStackSelection, "remotestack") {
+			expectedSelection = "remotestack (last deployed " + deployTime.Local().Format(time.RFC822) + ")"
+		}
 		if tc.useWkDir {
 			stackOptions = append(stackOptions, CreateNewStack)
 		}

 		selectedOption := tc.userStackSelection
+		if tc.hasOtherRemoteStacks && strings.Contains(tc.userStackSelection, "remotestack") {
+			selectedOption = expectedSelection
+		}
 		if tc.userSelectsCreateNew {
 			selectedOption = CreateNewStack
 		}
-		mockEC.On("RequestEnum", ctx, "Select a stack", "stack", stackOptions).Return(selectedOption, nil)
+		mockEC.On("RequestEnum", ctx, "Select a stack", "stack", stackOptions).Return(expectedSelection, nil)

613-621: Consider improving default name computation to match production code.

The default name for non-Defang Playground providers is hardcoded to "defang" with a comment noting region processing would be needed. This doesn't match the production behavior which uses stacks.MakeDefaultName(providerID, region). Consider using the same logic for test accuracy.

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

536-540: Consider the behavior when provider cannot be determined in whoami.

Setting global.NonInteractive = true at line 536 ensures no prompts are shown, but the error is silently logged at line 539. If the provider determination fails, whoami will still execute with a potentially nil or misconfigured provider, which might produce confusing results.

Consider returning early or providing more context to the user when provider setup fails:

 	global.NonInteractive = true // don't show provider prompt
 	provider, err := newProviderChecked(cmd.Context(), "", false)
 	if err != nil {
-		term.Debug("unable to get provider:", err)
+		term.Debug("Provider info unavailable:", err)
+		// Continue with nil provider - cli.Whoami handles this gracefully
 	}
src/pkg/agent/tools/provider.go (2)

63-70: Consider accepting StacksManager as a constructor parameter for better testability.

The current design requires tests to directly set pp.sm after construction. Accepting StacksManager as a parameter would make the dependency injection more explicit.

-func NewProviderPreparer(pc ProviderCreator, ec elicitations.Controller, fc cliClient.FabricClient) *providerPreparer {
+func NewProviderPreparer(pc ProviderCreator, ec elicitations.Controller, fc cliClient.FabricClient, sm StacksManager) *providerPreparer {
+	if sm == nil {
+		sm = NewStacksManager()
+	}
 	return &providerPreparer{
 		pc: pc,
 		ec: ec,
 		fc: fc,
-		sm: NewStacksManager(),
+		sm: sm,
 	}
 }

388-438: Good improvement to AWS authentication flow with minor redundancy.

The AWS authentication now properly offers profile selection before falling back to access keys. However, line 398 re-checks AWS_PROFILE which is redundant since line 389-391 already checked and returned early if it was set.

 func (pp *providerPreparer) SetupAWSAuthentication(ctx context.Context) error {
 	if os.Getenv("AWS_PROFILE") != "" {
 		return nil
 	}

 	if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" {
 		return nil
 	}

-	// TODO: add support for aws sso strategy
-	if os.Getenv("AWS_PROFILE") == "" {
-		knownProfiles, err := listAWSProfiles()
-		if err != nil {
-			return fmt.Errorf("failed to list AWS profiles: %w", err)
-		}
-		if len(knownProfiles) > 0 {
+	// TODO: add support for aws sso strategy
+	knownProfiles, err := listAWSProfiles()
+	if err != nil {
+		return fmt.Errorf("failed to list AWS profiles: %w", err)
+	}
+	if len(knownProfiles) > 0 {
 		// ... rest of profile selection logic
-		}
 	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eca8b33 and dc0b8ea.

📒 Files selected for processing (5)
  • src/cmd/cli/command/commands.go (14 hunks)
  • src/cmd/cli/command/globals.go (2 hunks)
  • src/cmd/cli/command/globals_test.go (3 hunks)
  • src/pkg/agent/tools/provider.go (5 hunks)
  • src/pkg/agent/tools/provider_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/cmd/cli/command/globals.go
  • src/cmd/cli/command/globals_test.go
🧰 Additional context used
🧬 Code graph analysis (2)
src/pkg/agent/tools/provider_test.go (5)
src/pkg/cli/connect.go (1)
  • NewProvider (35-49)
src/pkg/cli/client/provider_id.go (2)
  • ProviderID (10-10)
  • ProviderDefang (14-14)
src/pkg/stacks/stacks.go (4)
  • Create (33-76)
  • StackParameters (17-22)
  • List (85-118)
  • StackListItem (78-83)
src/protos/io/defang/v1/fabric.pb.go (2)
  • Provider_DEFANG (32-32)
  • Provider_value (47-53)
src/pkg/agent/tools/provider.go (1)
  • CreateNewStack (21-21)
src/pkg/agent/tools/provider.go (3)
src/pkg/stacks/stacks.go (6)
  • Create (33-76)
  • StackParameters (17-22)
  • Read (180-195)
  • Load (197-208)
  • List (85-118)
  • StackListItem (78-83)
src/pkg/elicitations/elicitations.go (1)
  • Controller (8-12)
src/pkg/cli/client/provider_id.go (1)
  • ProviderID (10-10)
🔇 Additional comments (11)
src/pkg/agent/tools/provider_test.go (3)

21-158: LGTM!

The mock implementations are well-structured and correctly use testify/mock patterns. The FabricClient stub methods appropriately return nil for unused methods while ListDeployments is properly mocked for testing.


220-248: LGTM!

The helper functions are well-designed for creating test data. The provider enum conversion correctly handles the "defang" special case and uses the proto-generated value map for other providers.


418-461: LGTM!

The test function is well-structured with proper isolation (fresh mocks per test), clear assertions, and appropriate conditional mock verification.

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

438-449: LGTM!

The conditional check at line 438 properly addresses the concern about loading local .defang stack files when --project-name is used. This prevents cross-project stack file access as intended by the PR objectives.


1314-1334: LGTM!

The new provider setup flow properly integrates the elicitations controller and provider preparer. The non-interactive mode fallback is appropriate, and the error wrapping provides clear context.


587-592: LGTM!

The pattern of deriving saveStacksToWkDir from whether project-name flag is set is consistently applied across commands. This correctly implements the PR objective of restricting stack file operations when --project-name is used.


926-941: LGTM!

The config list command follows the established pattern correctly, using LoadProjectName and deriving saveStacksToWkDir appropriately.


89-99: LGTM!

The placeholder project name "<name>" is appropriate for this error handling path where we're generating a hint message but don't have a specific project context.

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

27-54: LGTM!

The StacksManager interface provides a clean abstraction for stack operations, enabling proper unit testing with mocks. The implementation correctly delegates to the underlying stacks package.


235-268: LGTM!

The function correctly handles the implicit "beta" stack naming convention and safely converts proto deployments to internal types. The comment about sort order documents the assumption clearly.


287-319: LGTM on shadowing fix, but unconditional stack creation remains.

The variable shadowing issue from the past review has been properly addressed. The stack variable is now correctly updated within the same scope. However, the unconditional pp.sm.Create at line 308 should be gated by useWkDir as noted in the earlier comment.

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/agent/tools/provider.go (2)

217-225: Remote stack import should respect useWkDir.

When a user selects a remote stack, lines 221-225 unconditionally create a local stack file. However, when useWkDir is false (e.g., when --project-name is used), the PR objectives state that stack file access should be restricted to prevent cross-project pollution. The import should only write the local file when useWkDir is true.

Apply this diff:

 	if selectedStackOption.Parameters == nil {
 		return "", fmt.Errorf("stack parameters for remote stack %q are nil", selectedStackLabel)
 	}
 
-	term.Debugf("Importing stack %s from remote", selectedStackLabel)
-	_, err = pp.sm.Create(*selectedStackOption.Parameters)
-	if err != nil {
-		return "", fmt.Errorf("failed to create local stack from remote: %w", err)
+	if useWkDir {
+		term.Debugf("Importing stack %s from remote", selectedStackLabel)
+		_, err = pp.sm.Create(*selectedStackOption.Parameters)
+		if err != nil {
+			return "", fmt.Errorf("failed to create local stack from remote: %w", err)
+		}
 	}
 
 	return selectedStackOption.Name, nil

164-177: Beta detection logic is still incorrect.

The check on line 165 looks for a map key "beta", but when a beta stack exists only remotely (the primary use case this messaging is meant to address), the key in stacks will be "beta (last deployed ...)" instead of "beta". This means the onboarding message won't appear for users with implicit beta deployments.

Additionally, line 176 accesses os.Args[1] without checking len(os.Args) > 1, which can panic.

Apply this diff to fix both issues:

 func printStacksInfoMessage(stacks map[string]StackOption) {
-	_, betaExists := stacks["beta"]
+	betaExists := false
+	for _, opt := range stacks {
+		if opt.Name == "beta" {
+			betaExists = true
+			break
+		}
+	}
 	if betaExists {
 		infoLine := "This project was deployed with an implicit Stack called 'beta' before Stacks were introduced."
 		if len(stacks) == 1 {
 			infoLine += "\n   To update your existing deployment, select the 'beta' Stack.\n" +
 				"Creating a new Stack will result in a separate deployment instance."
 		}
 		infoLine += "\n   To learn more about Stacks, visit: https://docs.defang.io/docs/concepts/stacks"
 		term.Info(infoLine + "\n")
 	}
 	executable, _ := os.Executable()
-	term.Infof("To skip this prompt, run %s %s --stack=%s", filepath.Base(executable), os.Args[1], "<stack_name>")
+	subcommand := ""
+	if len(os.Args) > 1 {
+		subcommand = os.Args[1]
+	}
+	term.Infof("To skip this prompt, run %s %s --stack=%s", filepath.Base(executable), subcommand, "<stack_name>")
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dc0b8ea and 39a1469.

📒 Files selected for processing (1)
  • src/pkg/agent/tools/provider.go (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/pkg/agent/tools/provider.go (4)
src/pkg/stacks/stacks.go (6)
  • Create (33-76)
  • StackParameters (17-22)
  • Read (180-195)
  • Load (197-208)
  • List (85-118)
  • StackListItem (78-83)
src/pkg/elicitations/elicitations.go (1)
  • Controller (8-12)
src/pkg/cli/client/provider_id.go (1)
  • ProviderID (10-10)
src/pkg/timeutils/timeutils.go (1)
  • AsTime (39-44)
🔇 Additional comments (7)
src/pkg/agent/tools/provider.go (7)

27-54: LGTM!

The StacksManager abstraction cleanly wraps the stacks package, enabling testability through dependency injection. The implementation is straightforward and follows good separation of concerns.


114-162: LGTM!

The method correctly merges remote and local stacks, preferring local when both exist. The two-map approach (name-keyed stackMap for merging, label-keyed stackLabelMap for display) properly handles the presentation of timestamps while maintaining the preference for local stacks.


388-436: LGTM!

The AWS authentication flow correctly handles multiple scenarios: existing credentials, profile selection, and manual key entry. The logic properly prioritizes AWS_PROFILE, then access keys, then prompts for credentials with automatic profile discovery.


484-528: LGTM!

The function correctly parses AWS profile names from both ~/.aws/credentials and ~/.aws/config, properly handling the "profile " prefix in config files. Error handling and resource cleanup are appropriate.


72-105: Verify consistent useWkDir threading.

The signature correctly adds projectName and useWkDir parameters to support the PR's cross-project isolation goals. However, ensure that loadStack is updated to receive and respect useWkDir (see separate comment on that method).


321-374: LGTM!

The method correctly guards local stack file creation with useWkDir (line 361-367), properly implementing the cross-project isolation pattern. This is the correct approach that should be mirrored in selectStack and loadStack.


270-285: Update to pass useWkDir to loadStack.

Line 284 calls loadStack without the useWkDir parameter. Once loadStack is updated to accept this parameter (see separate comment), this call site needs to be updated as well.

@jordanstephens jordanstephens force-pushed the jordan/stacks-default-name branch from 39a1469 to 38bb177 Compare December 11, 2025 19:16
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 (4)
src/pkg/agent/tools/provider.go (3)

164-177: Beta-stack detection and skip-prompt hint don’t match actual labels / commands

Two issues here:

  1. Beta detection won’t fire for remote-only “beta” stacks

    printStacksInfoMessage checks stacks["beta"], but collectStackOptions keys the map by label ("beta (last deployed ...)"), not bare name. For implicit “beta” deployments that only exist remotely, the key is "beta (last deployed ...)", so the beta-onboarding message is never shown.

    Consider detecting beta by underlying StackOption.Name instead of the key:

    func printStacksInfoMessage(stacks map[string]StackOption) {
    -	_, betaExists := stacks["beta"]
    -	if betaExists {
    +	betaExists := false
    +	for _, opt := range stacks {
    +		if opt.Name == "beta" {
    +			betaExists = true
    +			break
    +		}
    +	}
    +	if betaExists {
    		infoLine := "This project was deployed with an implicit Stack called 'beta' before Stacks were introduced."
    		// ...
    	}
  2. Hard-coded up subcommand can be misleading

    The hint always says run %s up --stack=%s, but SetupProvider is used by multiple subcommands. That’s confusing when you’re in, say, logs or destroy.

    You can safely recover the actual subcommand without risking panics:

    	executable, _ := os.Executable()
    -	term.Infof("To skip this prompt, run %s up --stack=%s", filepath.Base(executable), "<stack_name>")
    +	binary := filepath.Base(executable)
    +	subcommand := "<command>"
    +	if len(os.Args) > 1 {
    +		subcommand = os.Args[1]
    +	}
    +	term.Infof("To skip this prompt, run %s %s --stack=%s", binary, subcommand, "<stack_name>")

179-228: Remote stack import in selectStack still writes stackfiles when useWkDir=false

When selecting a remote stack:

if selectedStackOption.Local {
    return selectedStackOption.Name, nil
}

if selectedStackOption.Parameters == nil { ... }

term.Debugf("Importing stack %s from remote", selectedStackLabel)
_, err = pp.sm.Create(*selectedStackOption.Parameters)

pp.sm.Create is called unconditionally, even when useWkDir is false. That:

  • Violates the PR goal of restricting stackfile writes when operating outside the working dir / with --project-name.
  • Can create .defang/<stack> files in the caller’s cwd even in “no wkdir”/agent flows.

You already guard writes in createNewStack and loadStack with useWkDir; this should be consistent:

-	if selectedStackOption.Parameters == nil {
-		return "", fmt.Errorf("stack parameters for remote stack %q are nil", selectedStackLabel)
-	}
-
-	term.Debugf("Importing stack %s from remote", selectedStackLabel)
-	_, err = pp.sm.Create(*selectedStackOption.Parameters)
-	if err != nil {
-		return "", fmt.Errorf("failed to create local stack from remote: %w", err)
-	}
-
-	return selectedStackOption.Name, nil
+	if selectedStackOption.Parameters == nil {
+		return "", fmt.Errorf("stack parameters for remote stack %q are nil", selectedStackLabel)
+	}
+
+	if useWkDir {
+		term.Debugf("Importing stack %s from remote", selectedStackLabel)
+		if _, err = pp.sm.Create(*selectedStackOption.Parameters); err != nil {
+			return "", fmt.Errorf("failed to create local stack from remote: %w", err)
+		}
+	}
+
+	return selectedStackOption.Name, nil

This keeps remote import behavior for working-dir flows while avoiding cross-project stackfile writes when useWkDir=false.


274-325: loadStack + selectOrCreateStack break “no-wkdir” new/remote stack flows with real StacksManager

For useWkDir=false flows, there are two problematic cases:

  1. New stacks (no local file, no remote)

    • selectOrCreateStack always calls loadStack even after createNewStack.

    • createNewStack deliberately does not write a stackfile when useWkDir=false.

    • loadStack immediately calls pp.sm.Read(stackName). With the real NewStacksManager, this is stacks.Read, which fails with os.ErrNotExist.

    • There are no remote deployments for a brand-new stack, so collectPreviouslyDeployedStacks finds nothing and you return:

      if stack == nil {
          return nil, fmt.Errorf("stack %q does not exist locally or remotely", stackName)
      }

    This contradicts the tests that expect an ephemeral, no‑wkdir new stack to succeed.

  2. Remote-only stacks with useWkDir=false

    • When a stack exists only in Fabric deployments, loadStack finds it via collectPreviouslyDeployedStacks but still unconditionally calls pp.sm.Load(stackName) afterward.
    • With useWkDir=false you never wrote .defang/<stackName>, so stacks.Load will fail (no such file or directory), and SetupProvider returns “failed to load stack”.

    In tests, mocks stub StacksManager.Read/Load to succeed for these scenarios, so the discrepancy is hidden.

This conflicts both with the PR’s “no cross‑project stackfile access” objective and with the behavior your tests document.

You likely want “ephemeral” semantics when useWkDir=false:

  • Don’t require a stackfile to exist.
  • Use remote deployment data or the parameters returned by createNewStack directly.
  • Avoid StacksManager.Read/Load entirely in this mode.

One possible direction (illustrative, not drop‑in):

func (pp *providerPreparer) selectOrCreateStack(ctx context.Context, projectName string, useWkDir bool) (*stacks.StackParameters, error) {
    selectedStackName, err := pp.selectStack(ctx, projectName, useWkDir)
    if err != nil {
        return nil, fmt.Errorf("failed to select stack: %w", err)
    }

    if selectedStackName == CreateNewStack {
-       newStack, err := pp.createNewStack(ctx, useWkDir)
+       newStack, err := pp.createNewStack(ctx, useWkDir) // ideally returns *stacks.StackParameters
        if err != nil {
            return nil, fmt.Errorf("failed to create new stack: %w", err)
        }
-       selectedStackName = newStack.Name
+       if !useWkDir {
+           // Ephemeral: don’t touch stackfiles at all.
+           return newStack, nil
+       }
+       selectedStackName = newStack.Name
    }

-   return pp.loadStack(ctx, projectName, selectedStackName, useWkDir)
+   return pp.loadStack(ctx, projectName, selectedStackName, useWkDir)
}

and in loadStack:

-func (pp *providerPreparer) loadStack(ctx context.Context, projectName, stackName string, useWkDir bool) (*stacks.StackParameters, error) {
-    stack, err := pp.sm.Read(stackName)
+func (pp *providerPreparer) loadStack(ctx context.Context, projectName, stackName string, useWkDir bool) (*stacks.StackParameters, error) {
+    if !useWkDir {
+        // Ephemeral mode: resolve from remote deployments only, no stackfile reads/writes.
+        existingStacks, err := pp.collectPreviouslyDeployedStacks(ctx, projectName)
+        if err != nil {
+            return nil, fmt.Errorf("failed to collect existing stacks: %w", err)
+        }
+        for _, existing := range existingStacks {
+            if existing.Name == stackName {
+                return &existing.StackParameters, nil
+            }
+        }
+        return nil, fmt.Errorf("stack %q does not exist remotely", stackName)
+    }
+
+    stack, err := pp.sm.Read(stackName)
     // ... existing local+remote+Create+Load logic ...

This would:

  • Make tests for “no wkdir” cases reflect real behavior.
  • Keep current behavior for working-dir flows, including stackfile creation and loading.
src/pkg/agent/tools/provider_test.go (1)

250-351: No-wkdir new/remote stack test cases still don’t match real StacksManager behavior

Several TestSetupProvider cases model ephemeral behavior for useWkDir=false that the concrete NewStacksManager does not provide:

  • “no stackname, no local stackfiles, no deployments, useWkDir false, create stack without stackfile”
  • “no stackname, local stackfile exists, useWkDir false, create new stack without stackfile”
  • “stackname provided, stackfile doesn’t exist locally, exists in previous deployments, useWkDir false, expect remote stack loaded but stackfile not written”

In these cases:

  • createNewStack / remote import deliberately avoid calling StacksManager.Create when useWkDir=false.
  • With the real implementation, StacksManager.Read/Load call through to stacks.Read/stacks.Load, which require .defang/<stack> to exist.
  • Yet the tests stub Read and Load to succeed (expectNewStackCreated and the remote useWkDir=false case), so SetupProvider appears to work even though production code would fail with “stack ... does not exist locally or remotely” or a Load error.

This masks the production issue called out in provider.go (load/Read behavior in no‑wkdir mode) and makes the tests assert an idealized behavior that doesn’t exist.

You should either:

  • Align production with tests: implement true “ephemeral” semantics when useWkDir=false (don’t depend on stackfiles; see the earlier suggestion in loadStack / selectOrCreateStack), and then keep these test expectations; or
  • Align tests with current behavior: for useWkDir=false new/remote cases, stop stubbing Read/Load to succeed and assert that SetupProvider fails (or adjust the table expectations), so tests don’t claim support that the CLI doesn’t actually have.

Right now the mocks hide the discrepancy instead of surfacing it.

Also applies to: 463-560

🧹 Nitpick comments (3)
src/pkg/agent/tools/provider.go (1)

395-442: AWS auth flow matches requirements; consider softening profile-read failures

The updated SetupAWSAuthentication and listAWSProfiles behavior is aligned with the PR goals:

  • Respect pre-set AWS_PROFILE or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY without prompting.
  • When profiles are available, prompt for a profile first, with a clear “Use Access Key ID and Secret Access Key” escape hatch.
  • When no profiles are found, fall back to prompting for keys.
  • listAWSProfiles now checks scanner.Err() and reports IO/parsing errors instead of silently ignoring them.

One behavioral nuance: any non‑nil error from listAWSProfiles now aborts the auth setup ("failed to list AWS profiles"), even though you could still recover by falling back to access keys.

If you want a more resilient UX, you could treat profile-read errors as a warning and continue:

	knownProfiles, err := listAWSProfiles()
	if err != nil {
-		return fmt.Errorf("failed to list AWS profiles: %w", err)
+		// Log at debug/warn level instead of failing; fall back to access keys.
+		// term.Debugf("failed to list AWS profiles, falling back to access keys: %v", err)
+		knownProfiles = nil
	}

The rest of the flow (including the scanner error handling in listAWSProfiles) looks good.

Also applies to: 490-534

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

488-539: Stack selection tests depend on map iteration order of options

In setupMocks you build stackOptions as:

stackOptions := []string{}
if tc.hasOtherLocalStacks && tc.useWkDir {
    stackOptions = append(stackOptions, "existingstack")
}
if tc.hasOtherRemoteStacks {
    label := "remotestack (last deployed " + deployTime.Local().Format(time.RFC822) + ")"
    stackOptions = append(stackOptions, label)
    // ...
}
if tc.useWkDir {
    stackOptions = append(stackOptions, CreateNewStack)
}
mockEC.On("RequestEnum", ctx, "Select a stack", "stack", stackOptions).Return(selectedOption, nil)

But production selectStack builds the options slice from a map:

stackLabels := make([]string, 0, len(stackOptions)+1)
for label := range stackOptions {
    stackLabels = append(stackLabels, label)
}
if useWkDir {
    stackLabels = append(stackLabels, CreateNewStack)
}

Map iteration order is non-deterministic in Go, so:

  • The concrete order of options presented to the user is arbitrary.
  • The order of the options []string argument passed into RequestEnum in production won’t necessarily match the stackOptions slice you use in the mock expectation, making the test brittle or outright incorrect.

I’d recommend:

  • Sorting stackLabels before calling RequestEnum in production for a stable, user-friendly order and
  • Mirroring that in tests (build stackOptions, then sort.Strings(stackOptions) before setting the expectation), or relaxing the expectation to use mock.AnythingOfType("[]string") if order truly doesn’t matter.

Without this, these tests rely on unspecified map order.


596-633: Default-name logic in tests diverges from createNewStack / stacks.MakeDefaultName

setupNewStackCreationMocks hard-codes:

var defaultName string
switch tc.userProviderChoice {
case "Defang Playground":
    defaultName = "defang"
default:
    // For other providers, would need region processing
    defaultName = "defang"
}
mockEC.On("RequestStringWithDefault", ..., defaultName).Return(tc.userStackNameChoice, nil)

But createNewStack now uses stacks.MakeDefaultName(providerID, region), which (per PR description) builds a provider+region-based name with non-alphanumerics stripped.

Today all your test cases use "Defang Playground", so this mismatch isn’t visible, but as soon as you add scenarios for AWS/GCP/DO, the default name in tests will no longer match production behavior.

To keep tests faithful:

  • Either call stacks.MakeDefaultName inside setupNewStackCreationMocks (using the same providerID/region mapping you expect in production), or
  • At least replicate its logic instead of hard-coding "defang" for non-Defang providers.

That will help keep tests aligned as the default-name rules evolve.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 39a1469 and 38bb177.

📒 Files selected for processing (2)
  • src/pkg/agent/tools/provider.go (6 hunks)
  • src/pkg/agent/tools/provider_test.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/pkg/agent/tools/provider_test.go (3)
src/pkg/cli/client/provider_id.go (2)
  • ProviderID (10-10)
  • ProviderDefang (14-14)
src/pkg/stacks/stacks.go (5)
  • Create (33-76)
  • StackParameters (17-22)
  • Load (197-208)
  • List (85-118)
  • StackListItem (78-83)
src/pkg/agent/tools/provider.go (1)
  • CreateNewStack (21-21)
src/pkg/agent/tools/provider.go (1)
src/pkg/stacks/stacks.go (6)
  • Create (33-76)
  • StackParameters (17-22)
  • Read (180-195)
  • Load (197-208)
  • List (85-118)
  • StackListItem (78-83)
⏰ 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/pkg/agent/tools/provider.go (2)

27-72: StacksManager abstraction and SetupProvider wiring look sound

The StacksManager interface and stacksManager wrapper cleanly encapsulate stacks.Create/Read/Load/List, and injecting it into providerPreparer makes testing straightforward. The updated SetupProvider signature (projectName, *stackName, useWkDir) and flow (nil check, load vs select/create, then auth and provider creation) are consistent and easy to follow.


114-162: Remote/local stack merging and timestamp handling are correct

The collectStackOptions / ExistingStack / collectPreviouslyDeployedStacks pipeline correctly:

  • Normalizes empty remote stack names to "beta".
  • Avoids overwriting newer entries by using a per‑name map.
  • Preserves LastDeployedAt across local/remote merges while preferring local when both exist.
  • Safely handles possibly‑nil deployment timestamps before calling AsTime().

This addresses the earlier nil‑timestamp panic risk and matches the “prefer local if both exist” comment.

Also applies to: 230-272

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

21-219: Mocks for providers, fabric client, stacks manager, and elicitations are appropriate

The mock types (mockProviderCreator, mockElicitationsController, mockFabricClient, mockStacksManager, mockProvider) cover exactly the methods exercised in SetupProvider and keep everything in-package, which makes the tests focused and maintainable. The generic return handling (type assertions and nil‑safe fallbacks) is reasonable for these tests.

@jordanstephens jordanstephens force-pushed the jordan/stacks-default-name branch from 38bb177 to ff6d7e6 Compare December 11, 2025 19:28
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 (2)
src/pkg/agent/tools/provider.go (1)

114-162: Beta-stack info message never triggers for remote “beta” stacks and CLI hint hardcodes up.

collectStackOptions now returns a map keyed by display labels like "beta (last deployed ...)", but printStacksInfoMessage still checks stacks["beta"]. This means users with only implicit-beta remote deployments never see the beta-onboarding message, which undermines the PR’s goal.

Also, the skip-hint currently assumes the up subcommand, while SetupProvider is reused by other subcommands.

You can fix both by iterating values and decoupling the message from a specific subcommand:

-func printStacksInfoMessage(stacks map[string]StackOption) {
-	_, betaExists := stacks["beta"]
-	if betaExists {
-		infoLine := "This project was deployed with an implicit Stack called 'beta' before Stacks were introduced."
-		if len(stacks) == 1 {
+func printStacksInfoMessage(stackOptions map[string]StackOption) {
+	betaExists := false
+	onlyBeta := true
+	for _, opt := range stackOptions {
+		if opt.Name == "beta" {
+			betaExists = true
+		} else {
+			onlyBeta = false
+		}
+	}
+	if betaExists {
+		infoLine := "This project was deployed with an implicit Stack called 'beta' before Stacks were introduced."
+		if onlyBeta {
 			infoLine += "\n   To update your existing deployment, select the 'beta' Stack.\n" +
 				"Creating a new Stack will result in a separate deployment instance."
 		}
 		infoLine += "\n   To learn more about Stacks, visit: https://docs.defang.io/docs/concepts/stacks"
 		term.Info(infoLine + "\n")
 	}
 	executable, _ := os.Executable()
-	term.Infof("To skip this prompt, run %s up --stack=%s", filepath.Base(executable), "<stack_name>")
+	term.Infof("To skip this prompt, run %s <subcommand> --stack=%s", filepath.Base(executable), "<stack_name>")
 }

If you know the actual subcommand at the call site, you could pass it in instead of using <subcommand>.

Also applies to: 164-177

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

250-351: No-wkdir stack tests assume behavior that the real StacksManager doesn’t provide.

Several TestSetupProvider cases with useWkDir=false and either remote-only or new stacks:

  • “stackfile doesn't exist locally, exists in previous deployments, useWkDir false, expect remote stack loaded but stackfile not written”
  • “no stackname, no local stackfiles, no deployments, useWkDir false, create stack without stackfile”
  • “no stackname, local stackfile exists, useWkDir false, create new stack without stackfile”
  • “no stackname, previous deployment exists, useWkDir false, user selects existing deployment”

set expectNewStackCreated or remoteStackExists but expectStackFileWritten=false.

In setupMocks and helpers:

  • setupNewStackCreationMocks only stubs mockSM.Create when tc.useWkDir is true.
  • For expectNewStackCreated cases, you still stub mockSM.Read(tc.expectedStackName) and mockSM.Load(tc.expectedStackName) to succeed (lines 568–572), even when useWkDir=false.
  • For remote-only selections with expectStackFileWritten=false, setupExistingRemoteStackMocks stubs Read/Load without Create.

With the real NewStacksManager, Read and Load access .defang/<stack> on disk. When useWkDir=false, the production code doesn’t write these files (per the new guards in createNewStack/selectStack), so:

  • New-stack + no-wkdir cases will fail in loadStack with “stack %q does not exist locally or remotely”.
  • Remote-only + no-wkdir cases will eventually call stacks.Load on a non-existent file and error.

That means the tests are currently validating a desirable “ephemeral stack, no local file” behavior that the implementation doesn’t yet have, rather than catching the present mismatch.

I’d recommend either:

  1. Implement true ephemeral behavior (preferred)

    • Adjust loadStack (and possibly selectOrCreateStack) so that when useWkDir=false and a stack is resolved remotely or newly created, it does not call StacksManager.Load and does not require a local file (return just *stacks.StackParameters, as suggested in the provider.go comment).
    • Update mocks accordingly:
      • For no-wkdir+new/remote cases, stop stubbing Read/Load and instead only assert that Create is not called and that NewProvider receives the expected stack name/provider/region.
  2. Or, constrain the contract and tests

    • Decide that no-wkdir ephemeral stacks aren’t supported.
    • Change these test cases to expect an error (require.Error) instead of success, and remove the Read/Load stubs for scenarios where no stackfile should exist.

Right now, the mocking hides a real discrepancy between intended semantics and what NewStacksManager actually does.

Also applies to: 385-400, 478-575, 592-648

🧹 Nitpick comments (2)
src/pkg/agent/tools/provider.go (2)

179-200: Stack selection order is nondeterministic; sort stackLabels before prompting.

stackLabels is built from a map, so order is random. This can be confusing in the UI and makes strict mocking of RequestEnum brittle.

Consider sorting the labels before appending the "Create new stack" option:

-	// Convert map back to slice
-	stackLabels := make([]string, 0, len(stackOptions)+1)
-	for label := range stackOptions {
-		stackLabels = append(stackLabels, label)
-	}
+	// Convert map back to a deterministically ordered slice
+	stackLabels := make([]string, 0, len(stackOptions)+1)
+	for label := range stackOptions {
+		stackLabels = append(stackLabels, label)
+	}
+	sort.Strings(stackLabels)
 	if useWkDir {
 		stackLabels = append(stackLabels, CreateNewStack)
 	}

This keeps the UX stable and avoids order-sensitive test issues.


396-444: AWS auth/profile discovery flow looks solid; minor behavior note on profile listing errors.

The new AWS setup flow correctly:

  • Short-circuits when AWS_PROFILE or both AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY are already set.
  • Offers a profile picker when profiles are found, with a clear “Use Access Key ID and Secret Access Key” escape hatch.
  • Falls back to prompting for access keys only when needed.
  • Handles missing ~/.aws/{credentials,config} files gracefully in listAWSProfiles.

One behavioral nuance: failures in listAWSProfiles (e.g., unreadable files or scanner errors) currently abort provider setup instead of falling back to manual key entry. If you want a more resilient UX, consider treating those errors like “no profiles found” and continuing to the key prompts instead of returning an error.

Also applies to: 492-536

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 38bb177 and ff6d7e6.

📒 Files selected for processing (2)
  • src/pkg/agent/tools/provider.go (6 hunks)
  • src/pkg/agent/tools/provider_test.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/pkg/agent/tools/provider.go (3)
src/pkg/stacks/stacks.go (5)
  • StackParameters (17-22)
  • Read (180-195)
  • Load (197-208)
  • List (85-118)
  • StackListItem (78-83)
src/pkg/elicitations/elicitations.go (1)
  • Controller (8-12)
src/pkg/cli/client/provider_id.go (1)
  • ProviderID (10-10)
⏰ 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

to load a stack without a file
this required updating a bunch of methods to use the FabricClient
interface instead of the GrpcClient struct so it could be mocked

: (
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

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/provider.go (1)

399-459: AWS auth flow: listing profiles shouldn’t hard-fail the whole setup

SetupAWSAuthentication now treats any error from listAWSProfiles() as fatal:

knownProfiles, err := listAWSProfiles()
if err != nil {
    return fmt.Errorf("failed to list AWS profiles: %w", err)
}

but listAWSProfiles can fail for environmental reasons (e.g., os.UserHomeDir problems, unexpected I/O errors) even though the user could still proceed by manually entering access keys. This is a behavior regression versus a simple “prompt for keys” flow.

Consider downgrading profile‑listing errors to a warning and falling back to the access‑key prompts, so AWS auth remains possible even when profile files are unreadable.

-	knownProfiles, err := listAWSProfiles()
-	if err != nil {
-		return fmt.Errorf("failed to list AWS profiles: %w", err)
-	}
+	knownProfiles, err := listAWSProfiles()
+	if err != nil {
+		term.Warnf("Unable to list AWS profiles, falling back to access keys: %v", err)
+		knownProfiles = nil
+	}
@@
-	if len(knownProfiles) > 0 {
+	if len(knownProfiles) > 0 {
 		const useAccessKeysOption = "Use Access Key ID and Secret Access Key"

This preserves the new, nicer profile‑selection UX without blocking users in edge environments.

Also applies to: 507-551

♻️ Duplicate comments (1)
src/pkg/agent/tools/provider.go (1)

173-186: Beta-stack onboarding message still keyed off labels, so implicit beta deployments are missed

printStacksInfoMessage checks for "beta" in the map keys:

_, betaExists := stacks["beta"]

but the map passed here (stackOptions from collectStackOptions) is keyed by display labels like "beta (last deployed 02 Jan 06 15:04 MST)" whenever a LastDeployedAt is set. For the common case of existing remote deployments with an empty Stack (normalized to "beta" in collectPreviouslyDeployedStacks), you’ll only have a "beta (last deployed ...)" key, so betaExists is false and the beta‑onboarding message is never shown.

Instead, detection should be based on the underlying StackOption.Name, not the label key. This matches the earlier review that pointed out the same issue.

-func printStacksInfoMessage(stacks map[string]StackOption) {
-	_, betaExists := stacks["beta"]
-	if betaExists {
+func printStacksInfoMessage(stacks map[string]StackOption) {
+	betaExists := false
+	for _, opt := range stacks {
+		if opt.Name == "beta" {
+			betaExists = true
+			break
+		}
+	}
+	if betaExists {
 		infoLine := "This project was deployed with an implicit Stack called 'beta' before Stacks were introduced."
@@
-	term.Infof("To skip this prompt, run %s up --stack=%s", filepath.Base(executable), "<stack_name>")
+	term.Infof("To skip this prompt, run %s up --stack=%s", filepath.Base(executable), "<stack_name>")
 }

(You can keep or further refine the “up” subcommand hint depending on where this helper is used.)

🧹 Nitpick comments (7)
src/pkg/agent/tools/deploy_test.go (1)

20-51: Align MockGrpcClient with FabricClient and have Connect actually use it

Right now Connect returns a bare *client.GrpcClient even in the error case, and the MockGrpcClient type (with the comment about handling Track safely) is not used. That’s a bit misleading and misses the chance to isolate tests from any real Track behavior.

You can tighten this up by:

  • Making MockGrpcClient.Track match the FabricClient signature (...client.Property).
  • Returning nil on error from Connect.
  • Returning a *MockGrpcClient on success.

For example:

 type MockGrpcClient struct {
 	*client.GrpcClient
 }
 
-func (m *MockGrpcClient) Track(event string, props ...interface{}) error {
+func (m *MockGrpcClient) Track(event string, props ...client.Property) error {
 	// Mock implementation that doesn't panic
 	return nil
 }
@@
 func (m *MockDeployCLI) Connect(ctx context.Context, cluster string) (client.FabricClient, error) {
 	m.CallLog = append(m.CallLog, fmt.Sprintf("Connect(%s)", cluster))
 	if m.ConnectError != nil {
-		return &client.GrpcClient{}, m.ConnectError
+		return nil, m.ConnectError
 	}
-	// Return a base GrpcClient - we need to handle Track method differently
-	return &client.GrpcClient{}, nil
+	// Return a mock FabricClient that safely no-ops Track
+	return &MockGrpcClient{GrpcClient: &client.GrpcClient{}}, nil
 }

This keeps the tests robust against future uses of Track and makes the mock behavior closer to the real API surface.

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

147-165: Env population helper and LoadParameters look correct; consider minor defensive tweaks

paramsToMap and LoadParameters mirror the previous godotenv-based behavior and keep provider/region/mode handling centralized; the logic is sound and aligns with the new stacks manager usage.

Two small optional improvements:

  • Add a nil-guard in LoadParameters (and/or paramsToMap) to avoid panics if a caller ever passes nil.
  • In LoadParameters, you could use strings.Cut(rawEnvLine, "=") instead of strings.Split for slightly clearer intent and less allocation.

These are non-blocking; current implementation is functionally fine.

Also applies to: 215-230

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

228-247: Tidy up test case descriptions and default-name behavior for clarity

The new TestSetupProvider coverage and mocking strategy look solid and, importantly, now reflect the intended useWkDir=false “no local stackfile writes” behavior.

There are a couple of minor clarity issues you may want to address:

  • The test case named "stackname provided, stackfile exists locally, no previous deployments, useWkDir false, expect stack to be loaded" sets remoteStackExists: true (Line 288), which implies there are previous deployments. Either flip that flag or adjust the test name to avoid confusion.
  • In setupNewStackCreationMocks, defaultName is always "defang" even for non-Defang providers (Lines 661‑668). The production flow now uses a provider+region-based default stack name; updating this helper to mirror that (or to call the same helper used in provider.go) would keep the tests aligned with the actual UX, even though the current tests don’t assert on the default value.

These are non-blocking, but cleaning them up will make the tests easier to maintain and reason about.

Also applies to: 249-430, 644-681

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

19-32: Destroy‑tool tests correctly reflect the new flow; consider avoiding shared global mockFC

The changes to:

  • return client.FabricClient from Connect,
  • run LoadProjectName before provider setup,
  • pass FabricClient into ComposeDown and CanIUseProvider, and
  • stub mockFC.ListDeployments for stack resolution,

all match the updated interfaces and the intended “load project name, then set up provider for the selected stack” behavior. The successful path’s call sequence assertion is especially useful.

One structural nit: mockFC is a shared package-level variable reused across this and other test files. It’s currently safe (no t.Parallel and each top-level test resets it), but it does couple tests together. If you ever parallelize or add additional uses, consider injecting a FabricClient into your CLI mocks directly (e.g., as a field on MockDestroyCLI / MockRemoveConfigCLI / etc.) instead of going through a shared global.

This is a nice-to-have; the current tests are functionally fine.

Also applies to: 33-39, 46-60, 62-68, 70-72, 153-165, 182-191

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

4-15: CLIInterface refactor to FabricClient and LoadProjectName looks consistent; imports can be simplified

The interface changes:

  • switching all relevant methods to cliClient.FabricClient (Connect, ComposeUp/Down, CanIUseProvider, InteractiveLoginMCP, RunEstimate, CreatePlaygroundProvider), and
  • replacing LoadProjectNameWithFallback with LoadProjectName(ctx, loader),

line up with the updated mocks and tool flows in the tests you added/changed. This centralizes the FabricClient usage nicely.

One minor cleanup opportunity: you import github.com/DefangLabs/defang/src/pkg/cli twice under different names (cli and cliTypes). Since both refer to the same package, you can drop one and use a single alias consistently (e.g., just cli for both ComposeUpParams and TailOptions/ServiceStates) to reduce confusion.

Functionally everything here looks good.

Also applies to: 17-36

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

32-42: DefaultToolCLI FabricClient wiring and LoadProjectName passthrough look good

The migration from *GrpcClient to cliClient.FabricClient across CanIUseProvider, RunEstimate, Connect, ComposeUp, ComposeDown, CreatePlaygroundProvider, and InteractiveLoginMCP is coherent and preserves behavior. The new LoadProjectName delegating to loader.LoadProjectName(ctx) aligns with the refactor to resolve project names before provider setup. The remaining // TODO: add stack in CanIUseProvider is a reasonable follow‑up rather than a blocker.

Also applies to: 49-55, 61-63, 86-92, 94-100, 107-109

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

246-276: Optional: handle invalid provider IDs from deployments more defensively

In collectPreviouslyDeployedStacks, any error from providerID.SetValue(deployment.GetProvider()) is ignored:

var providerID cliClient.ProviderID
providerID.SetValue(deployment.GetProvider())

If the backend ever returns an unexpected provider string, you’ll silently record a zero‑value provider in StackParameters, which might later cause confusing behavior when setting up the provider.

Not critical today, but consider checking the error and either skipping that deployment or surfacing a clearer error if it’s invalid.

-		var providerID cliClient.ProviderID
-		providerID.SetValue(deployment.GetProvider())
+		var providerID cliClient.ProviderID
+		if err := providerID.SetValue(deployment.GetProvider()); err != nil {
+			term.Warnf("Skipping deployment with unknown provider %q: %v", deployment.GetProvider(), err)
+			continue
+		}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ff6d7e6 and a616520.

📒 Files selected for processing (12)
  • src/pkg/agent/tools/default_tool_cli.go (4 hunks)
  • src/pkg/agent/tools/deploy_test.go (3 hunks)
  • src/pkg/agent/tools/destroy_test.go (5 hunks)
  • src/pkg/agent/tools/estimate_test.go (4 hunks)
  • src/pkg/agent/tools/interfaces.go (1 hunks)
  • src/pkg/agent/tools/listConfig_test.go (6 hunks)
  • src/pkg/agent/tools/provider.go (6 hunks)
  • src/pkg/agent/tools/provider_test.go (1 hunks)
  • src/pkg/agent/tools/removeConfig_test.go (6 hunks)
  • src/pkg/agent/tools/services_test.go (13 hunks)
  • src/pkg/agent/tools/setConfig_test.go (7 hunks)
  • src/pkg/stacks/stacks.go (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (11)
src/pkg/agent/tools/provider_test.go (3)
src/pkg/cli/client/provider_id.go (2)
  • ProviderID (10-10)
  • ProviderDefang (14-14)
src/pkg/stacks/stacks.go (6)
  • Create (33-76)
  • StackParameters (17-22)
  • Read (185-200)
  • LoadParameters (215-230)
  • List (85-118)
  • StackListItem (78-83)
src/pkg/agent/tools/provider.go (1)
  • CreateNewStack (21-21)
src/pkg/agent/tools/removeConfig_test.go (3)
src/pkg/cli/connect.go (1)
  • Connect (16-33)
src/pkg/cli/client/client.go (1)
  • FabricClient (17-44)
src/protos/io/defang/v1/fabric.pb.go (1)
  • Provider_AWS (33-33)
src/pkg/agent/tools/setConfig_test.go (2)
src/pkg/cli/connect.go (1)
  • Connect (16-33)
src/pkg/cli/client/client.go (1)
  • FabricClient (17-44)
src/pkg/agent/tools/destroy_test.go (4)
src/pkg/agent/tools/interfaces.go (1)
  • CLIInterface (17-36)
src/pkg/cli/client/client.go (1)
  • FabricClient (17-44)
src/pkg/cli/client/provider_id.go (1)
  • ProviderID (10-10)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/agent/tools/listConfig_test.go (3)
src/pkg/cli/connect.go (2)
  • Connect (16-33)
  • NewProvider (35-49)
src/pkg/cli/client/client.go (1)
  • FabricClient (17-44)
src/protos/io/defang/v1/fabric.pb.go (1)
  • Provider_AWS (33-33)
src/pkg/agent/tools/services_test.go (2)
src/pkg/agent/tools/interfaces.go (1)
  • CLIInterface (17-36)
src/pkg/cli/client/client.go (1)
  • FabricClient (17-44)
src/pkg/agent/tools/estimate_test.go (2)
src/pkg/cli/client/client.go (1)
  • FabricClient (17-44)
src/pkg/cli/estimate.go (1)
  • RunEstimate (23-41)
src/pkg/agent/tools/interfaces.go (2)
src/pkg/cli/client/client.go (1)
  • FabricClient (17-44)
src/pkg/cli/connect.go (1)
  • NewProvider (35-49)
src/pkg/agent/tools/provider.go (3)
src/pkg/stacks/stacks.go (6)
  • Create (33-76)
  • StackParameters (17-22)
  • Read (185-200)
  • LoadParameters (215-230)
  • List (85-118)
  • StackListItem (78-83)
src/pkg/elicitations/elicitations.go (1)
  • Controller (8-12)
src/pkg/cli/client/provider_id.go (1)
  • ProviderID (10-10)
src/pkg/agent/tools/deploy_test.go (2)
src/pkg/cli/client/client.go (1)
  • FabricClient (17-44)
src/pkg/cli/composeUp.go (2)
  • ComposeUp (33-178)
  • ComposeUpParams (25-30)
src/pkg/agent/tools/default_tool_cli.go (4)
src/pkg/cli/client/caniuse.go (1)
  • CanIUseProvider (10-43)
src/pkg/cli/client/client.go (1)
  • FabricClient (17-44)
src/pkg/cli/estimate.go (1)
  • RunEstimate (23-41)
src/pkg/cli/connect.go (1)
  • Connect (16-33)
⏰ 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 (8)
src/pkg/agent/tools/deploy_test.go (3)

58-61: InteractiveLoginMCP FabricClient arg is ignored, which is fine for this test double

The updated signature correctly accepts a client.FabricClient and preserves the existing behavior of just logging and returning the configured error. That’s sufficient for these tests, since they only care about the error path and call ordering, not the client itself.


63-69: ComposeUp mock correctly tracks FabricClient-based signature

The ComposeUp mock now accepts a client.FabricClient plus cli.ComposeUpParams and still controls behavior solely via ComposeUpError, ComposeUpResponse, and Project. That matches the new interface while keeping the tests focused on deploy flow rather than FabricClient internals.


84-87: CanIUseProvider mock matches new signature and keeps tests focused on call order

Adapting CanIUseProvider to take a client.FabricClient, projectName, and serviceCount while just logging and returning nil is appropriate here, since the TestHandleDeployTool assertions only verify that the method was invoked (via CallLog) in the expected sequence.

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

31-37: Estimate tool tests correctly adapted to FabricClient and provider validation changes

The MockEstimateCLI updates (FabricClient return from Connect, FabricClient params for RunEstimate/CreatePlaygroundProvider) are consistent with the new interfaces, and the expectation "provider not one of [defang aws digitalocean gcp]" matches the removal of the auto option.

The tests still exercise the same behavior and call order and look good as-is.

Also applies to: 47-60, 117-125

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

31-37: Remove‑config tests align with FabricClient + LoadProjectName flow

The refactor to:

  • return client.FabricClient from Connect,
  • introduce LoadProjectName(ctx, loader) and log it in the successful path, and
  • stub mockFC.ListDeployments for stack/provider resolution,

matches the new CLIInterface and provider-preparer behavior. The updated error text casing (“could not connect…”) and the expected call sequence (ConnectLoadProjectNameNewProviderConfigDelete) look correct.

No issues from a correctness standpoint.

Also applies to: 44-50, 60-62, 156-167, 187-195

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

29-35: List‑config tests correctly reflect the new provider/setup sequence

The test updates:

  • returning client.FabricClient from Connect,
  • using LoadProjectName(ctx, loader) instead of the previous fallback variant, and
  • adding a ListDeployments mock on mockFC for stack resolution,

are consistent with the revised interfaces and flow. The expected call order including "LoadProjectName" in the successful case is a good regression guard.

Everything here looks sound.

Also applies to: 42-48, 58-60, 137-149, 169-175

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

35-41: Set‑config tests are in sync with FabricClient and LoadProjectName semantics

The migration to client.FabricClient in Connect, plus the new LoadProjectName(ctx, loader) method and its associated flags (LoadProjectNameCalled, expectedProviderCalls toggling to false on load failure), all line up with the updated tool flow.

The added mockFC.ListDeployments stub ensures provider/stack inference can run without touching real services, and the adjusted "could not connect: …" expectation matches the new error casing. The boolean expectations on which CLI methods should be called in each scenario look consistent with the intended control flow.

I don’t see any behavioral regressions here.

Also applies to: 62-71, 82-84, 156-163, 166-176, 247-259, 275-279

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

26-31: FabricClient-based mocks and services tool test wiring look correct

MockCLI’s updated methods (Connect, LoadProjectName, ComposeUp/Down, CreatePlaygroundProvider, InteractiveLoginMCP, RunEstimate) now match the FabricClient-based CLIInterface, and TestHandleServicesToolWithMockCLI correctly injects mockFC and stubs ListDeployments to exercise the new stack-selection path. The expectations around GetServicesCalled and GetServicesProject align with the intended behavior.

Also applies to: 39-44, 50-58, 70-76, 86-88, 94-96, 110-112, 154-283

@jordanstephens jordanstephens changed the title Stacks Onboarding Stacks onboarding with support for "remote" stacks Dec 11, 2025
@jordanstephens
Copy link
Member Author

Closing this in favour of #1721

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