diff --git a/src/cmd/cli/command/commands.go b/src/cmd/cli/command/commands.go index 82965bfd2..9d966ef05 100644 --- a/src/cmd/cli/command/commands.go +++ b/src/cmd/cli/command/commands.go @@ -32,6 +32,7 @@ import ( "github.com/DefangLabs/defang/src/pkg/setup" "github.com/DefangLabs/defang/src/pkg/surveyor" "github.com/DefangLabs/defang/src/pkg/term" + "github.com/DefangLabs/defang/src/pkg/timeutils" "github.com/DefangLabs/defang/src/pkg/track" "github.com/DefangLabs/defang/src/pkg/types" defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" @@ -857,11 +858,11 @@ var debugCmd = &cobra.Command{ } now := time.Now() - sinceTs, err := cli.ParseTimeOrDuration(since, now) + sinceTs, err := timeutils.ParseTimeOrDuration(since, now) if err != nil { return fmt.Errorf("invalid 'since' time: %w", err) } - untilTs, err := cli.ParseTimeOrDuration(until, now) + untilTs, err := timeutils.ParseTimeOrDuration(until, now) if err != nil { return fmt.Errorf("invalid 'until' time: %w", err) } diff --git a/src/cmd/cli/command/compose.go b/src/cmd/cli/command/compose.go index 1f40607d0..1419a701d 100644 --- a/src/cmd/cli/command/compose.go +++ b/src/cmd/cli/command/compose.go @@ -19,6 +19,7 @@ import ( "github.com/DefangLabs/defang/src/pkg/logs" "github.com/DefangLabs/defang/src/pkg/modes" "github.com/DefangLabs/defang/src/pkg/term" + "github.com/DefangLabs/defang/src/pkg/timeutils" "github.com/DefangLabs/defang/src/pkg/track" "github.com/DefangLabs/defang/src/pkg/types" defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" @@ -129,7 +130,11 @@ func makeComposeUpCmd() *cobra.Command { term.Warnf("Defang cannot monitor status of the following managed service(s): %v.\n To check if the managed service is up, check the status of the service which depends on it.", managedServices) } - deploy, project, err := cli.ComposeUp(ctx, project, client, provider, upload, mode) + deploy, project, err := cli.ComposeUp(ctx, client, provider, cli.ComposeUpParams{ + Project: project, + UploadMode: upload, + Mode: mode, + }) if err != nil { return handleComposeUpErr(ctx, err, project, provider) } @@ -452,7 +457,11 @@ func makeComposeConfigCmd() *cobra.Command { return err } - _, _, err = cli.ComposeUp(ctx, project, client, provider, compose.UploadModeIgnore, modes.ModeUnspecified) + _, _, err = cli.ComposeUp(ctx, client, provider, cli.ComposeUpParams{ + Project: project, + UploadMode: compose.UploadModeIgnore, + Mode: modes.ModeUnspecified, + }) if !errors.Is(err, dryrun.ErrDryRun) { return err } @@ -571,12 +580,12 @@ func handleLogsCmd(cmd *cobra.Command, args []string) error { } now := time.Now() - sinceTs, err := cli.ParseTimeOrDuration(since, now) + sinceTs, err := timeutils.ParseTimeOrDuration(since, now) if err != nil { return fmt.Errorf("invalid 'since' duration or time: %w", err) } sinceTs = sinceTs.UTC() - untilTs, err := cli.ParseTimeOrDuration(until, now) + untilTs, err := timeutils.ParseTimeOrDuration(until, now) if err != nil { return fmt.Errorf("invalid 'until' duration or time: %w", err) } diff --git a/src/cmd/cli/command/mcp.go b/src/cmd/cli/command/mcp.go index 2d51a9939..416b835c7 100644 --- a/src/cmd/cli/command/mcp.go +++ b/src/cmd/cli/command/mcp.go @@ -5,9 +5,9 @@ import ( "os" "path/filepath" + "github.com/DefangLabs/defang/src/pkg/agent/tools" cliClient "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/mcp" - "github.com/DefangLabs/defang/src/pkg/mcp/tools" "github.com/DefangLabs/defang/src/pkg/term" "github.com/mark3labs/mcp-go/server" "github.com/spf13/cobra" diff --git a/src/pkg/agent/agent.go b/src/pkg/agent/agent.go new file mode 100644 index 000000000..488315541 --- /dev/null +++ b/src/pkg/agent/agent.go @@ -0,0 +1 @@ +package agent diff --git a/src/pkg/mcp/common/common.go b/src/pkg/agent/common/common.go similarity index 100% rename from src/pkg/mcp/common/common.go rename to src/pkg/agent/common/common.go diff --git a/src/pkg/mcp/common/common_test.go b/src/pkg/agent/common/common_test.go similarity index 100% rename from src/pkg/mcp/common/common_test.go rename to src/pkg/agent/common/common_test.go diff --git a/src/pkg/mcp/tools/default_tool_cli.go b/src/pkg/agent/tools/default_tool_cli.go similarity index 92% rename from src/pkg/mcp/tools/default_tool_cli.go rename to src/pkg/agent/tools/default_tool_cli.go index 4f2f600e0..800278710 100644 --- a/src/pkg/mcp/tools/default_tool_cli.go +++ b/src/pkg/agent/tools/default_tool_cli.go @@ -6,11 +6,11 @@ import ( "os" "strconv" + "github.com/DefangLabs/defang/src/pkg/agent/common" "github.com/DefangLabs/defang/src/pkg/cli" cliClient "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/cli/compose" "github.com/DefangLabs/defang/src/pkg/login" - "github.com/DefangLabs/defang/src/pkg/mcp/common" "github.com/DefangLabs/defang/src/pkg/mcp/deployment_info" "github.com/DefangLabs/defang/src/pkg/modes" "github.com/DefangLabs/defang/src/pkg/term" @@ -45,8 +45,8 @@ func (DefaultToolCLI) Connect(ctx context.Context, cluster string) (*cliClient.G return cli.Connect(ctx, cluster) } -func (DefaultToolCLI) ComposeUp(ctx context.Context, project *compose.Project, client *cliClient.GrpcClient, provider cliClient.Provider, uploadMode compose.UploadMode, mode modes.Mode) (*defangv1.DeployResponse, *compose.Project, error) { - return cli.ComposeUp(ctx, project, client, provider, uploadMode, mode) +func (DefaultToolCLI) ComposeUp(ctx context.Context, client *cliClient.GrpcClient, provider cliClient.Provider, params cli.ComposeUpParams) (*defangv1.DeployResponse, *compose.Project, error) { + return cli.ComposeUp(ctx, client, provider, params) } func (DefaultToolCLI) Tail(ctx context.Context, provider cliClient.Provider, project *compose.Project, options cli.TailOptions) error { diff --git a/src/pkg/mcp/tools/deploy.go b/src/pkg/agent/tools/deploy.go similarity index 89% rename from src/pkg/mcp/tools/deploy.go rename to src/pkg/agent/tools/deploy.go index f813ea41c..6188ce0f4 100644 --- a/src/pkg/mcp/tools/deploy.go +++ b/src/pkg/agent/tools/deploy.go @@ -6,14 +6,15 @@ import ( "fmt" "strings" + "github.com/DefangLabs/defang/src/pkg/agent/common" + cliTypes "github.com/DefangLabs/defang/src/pkg/cli" cliClient "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/cli/compose" - "github.com/DefangLabs/defang/src/pkg/mcp/common" "github.com/DefangLabs/defang/src/pkg/modes" "github.com/DefangLabs/defang/src/pkg/term" ) -func handleDeployTool(ctx context.Context, loader cliClient.ProjectLoader, providerId *cliClient.ProviderID, cluster string, cli CLIInterface) (string, error) { +func HandleDeployTool(ctx context.Context, loader cliClient.ProjectLoader, providerId *cliClient.ProviderID, cluster string, cli CLIInterface) (string, error) { err := common.ProviderNotConfiguredError(*providerId) if err != nil { return "", err @@ -45,7 +46,11 @@ func handleDeployTool(ctx context.Context, loader cliClient.ProjectLoader, provi term.Debug("Function invoked: cli.ComposeUp") // Use ComposeUp to deploy the services - deployResp, project, err := cli.ComposeUp(ctx, project, client, provider, compose.UploadModeDigest, modes.ModeAffordable) + deployResp, project, err := cli.ComposeUp(ctx, client, provider, cliTypes.ComposeUpParams{ + Project: project, + UploadMode: compose.UploadModeDigest, + Mode: modes.ModeAffordable, + }) if err != nil { err = fmt.Errorf("failed to compose up services: %w", err) diff --git a/src/pkg/mcp/tools/deploy_test.go b/src/pkg/agent/tools/deploy_test.go similarity index 94% rename from src/pkg/mcp/tools/deploy_test.go rename to src/pkg/agent/tools/deploy_test.go index 196563169..82ae8fd35 100644 --- a/src/pkg/mcp/tools/deploy_test.go +++ b/src/pkg/agent/tools/deploy_test.go @@ -6,10 +6,10 @@ import ( "fmt" "testing" + "github.com/DefangLabs/defang/src/pkg/agent/common" + "github.com/DefangLabs/defang/src/pkg/cli" "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/cli/compose" - "github.com/DefangLabs/defang/src/pkg/mcp/common" - "github.com/DefangLabs/defang/src/pkg/modes" defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -52,7 +52,7 @@ func (m *MockDeployCLI) NewProvider(ctx context.Context, providerId client.Provi return nil } -func (m *MockDeployCLI) ComposeUp(ctx context.Context, project *compose.Project, grpcClient *client.GrpcClient, provider client.Provider, uploadMode compose.UploadMode, mode modes.Mode) (*defangv1.DeployResponse, *compose.Project, error) { +func (m *MockDeployCLI) ComposeUp(ctx context.Context, fabric *client.GrpcClient, provider client.Provider, params cli.ComposeUpParams) (*defangv1.DeployResponse, *compose.Project, error) { m.CallLog = append(m.CallLog, "ComposeUp") if m.ComposeUpError != nil { return nil, nil, m.ComposeUpError @@ -179,7 +179,7 @@ func TestHandleDeployTool(t *testing.T) { // Call the function loader := &client.MockLoader{} - result, err := handleDeployTool(t.Context(), loader, &tt.providerID, "test-cluster", mockCLI) + result, err := HandleDeployTool(t.Context(), loader, &tt.providerID, "test-cluster", mockCLI) // Verify error expectations if tt.expectedError != "" { diff --git a/src/pkg/mcp/tools/destroy.go b/src/pkg/agent/tools/destroy.go similarity index 93% rename from src/pkg/mcp/tools/destroy.go rename to src/pkg/agent/tools/destroy.go index a897603e3..efd7fd4f5 100644 --- a/src/pkg/mcp/tools/destroy.go +++ b/src/pkg/agent/tools/destroy.go @@ -5,13 +5,13 @@ import ( "errors" "fmt" + "github.com/DefangLabs/defang/src/pkg/agent/common" cliClient "github.com/DefangLabs/defang/src/pkg/cli/client" - "github.com/DefangLabs/defang/src/pkg/mcp/common" "github.com/DefangLabs/defang/src/pkg/term" "github.com/bufbuild/connect-go" ) -func handleDestroyTool(ctx context.Context, loader cliClient.ProjectLoader, providerId *cliClient.ProviderID, cluster string, cli CLIInterface) (string, error) { +func HandleDestroyTool(ctx context.Context, loader cliClient.ProjectLoader, providerId *cliClient.ProviderID, cluster string, cli CLIInterface) (string, error) { err := common.ProviderNotConfiguredError(*providerId) if err != nil { return "", err diff --git a/src/pkg/mcp/tools/destroy_test.go b/src/pkg/agent/tools/destroy_test.go similarity index 97% rename from src/pkg/mcp/tools/destroy_test.go rename to src/pkg/agent/tools/destroy_test.go index 27081a61a..8c975b676 100644 --- a/src/pkg/mcp/tools/destroy_test.go +++ b/src/pkg/agent/tools/destroy_test.go @@ -6,8 +6,8 @@ import ( "fmt" "testing" + "github.com/DefangLabs/defang/src/pkg/agent/common" "github.com/DefangLabs/defang/src/pkg/cli/client" - "github.com/DefangLabs/defang/src/pkg/mcp/common" "github.com/bufbuild/connect-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -137,7 +137,7 @@ func TestHandleDestroyTool(t *testing.T) { // Call the function loader := &client.MockLoader{} - result, err := handleDestroyTool(t.Context(), loader, &tt.providerID, "test-cluster", mockCLI) + result, err := HandleDestroyTool(t.Context(), loader, &tt.providerID, "test-cluster", mockCLI) // Verify error expectations if tt.expectedError != "" { diff --git a/src/pkg/mcp/tools/estimate.go b/src/pkg/agent/tools/estimate.go similarity index 95% rename from src/pkg/mcp/tools/estimate.go rename to src/pkg/agent/tools/estimate.go index f7d49a1c6..efbf7eedc 100644 --- a/src/pkg/mcp/tools/estimate.go +++ b/src/pkg/agent/tools/estimate.go @@ -17,7 +17,7 @@ type EstimateParams struct { Region string `json:"region"` } -func parseEstimateParams(request mcp.CallToolRequest, providerId *cliClient.ProviderID) (EstimateParams, error) { +func ParseEstimateParams(request mcp.CallToolRequest, providerId *cliClient.ProviderID) (EstimateParams, error) { modeString, err := request.RequireString("deployment_mode") if err != nil { modeString = "AFFORDABLE" // Default to AFFORDABLE if not provided @@ -50,7 +50,7 @@ func parseEstimateParams(request mcp.CallToolRequest, providerId *cliClient.Prov }, nil } -func handleEstimateTool(ctx context.Context, loader cliClient.ProjectLoader, params EstimateParams, cluster string, cli CLIInterface) (string, error) { +func HandleEstimateTool(ctx context.Context, loader cliClient.ProjectLoader, params EstimateParams, cluster string, cli CLIInterface) (string, error) { term.Debug("Function invoked: loader.LoadProject") project, err := cli.LoadProject(ctx, loader) if err != nil { diff --git a/src/pkg/mcp/tools/estimate_test.go b/src/pkg/agent/tools/estimate_test.go similarity index 98% rename from src/pkg/mcp/tools/estimate_test.go rename to src/pkg/agent/tools/estimate_test.go index 0aff317dc..7d5ce76c3 100644 --- a/src/pkg/mcp/tools/estimate_test.go +++ b/src/pkg/agent/tools/estimate_test.go @@ -203,7 +203,7 @@ func TestHandleEstimateTool(t *testing.T) { // Call the function loader := &client.MockLoader{} - params, err := parseEstimateParams(request, &providerID) + params, err := ParseEstimateParams(request, &providerID) if err != nil { // If parsing params fails, check if this was the expected error if tt.expectedError != "" { @@ -213,7 +213,7 @@ func TestHandleEstimateTool(t *testing.T) { require.NoError(t, err) } } - result, err := handleEstimateTool(t.Context(), loader, params, "test-cluster", mockCLI) + result, err := HandleEstimateTool(t.Context(), loader, params, "test-cluster", mockCLI) // Verify error expectations if tt.expectedError != "" { diff --git a/src/pkg/mcp/tools/interfaces.go b/src/pkg/agent/tools/interfaces.go similarity index 91% rename from src/pkg/mcp/tools/interfaces.go rename to src/pkg/agent/tools/interfaces.go index 670be7bd6..ee86a8709 100644 --- a/src/pkg/mcp/tools/interfaces.go +++ b/src/pkg/agent/tools/interfaces.go @@ -4,6 +4,7 @@ package tools import ( "context" + "github.com/DefangLabs/defang/src/pkg/cli" cliTypes "github.com/DefangLabs/defang/src/pkg/cli" cliClient "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/cli/compose" @@ -16,7 +17,7 @@ type CLIInterface interface { CanIUseProvider(ctx context.Context, client *cliClient.GrpcClient, providerId cliClient.ProviderID, projectName string, provider cliClient.Provider, serviceCount int) error CheckProviderConfigured(ctx context.Context, client *cliClient.GrpcClient, providerId cliClient.ProviderID, projectName, stack string, serviceCount int) (cliClient.Provider, error) ComposeDown(ctx context.Context, projectName string, client *cliClient.GrpcClient, provider cliClient.Provider) (string, error) - ComposeUp(ctx context.Context, project *compose.Project, client *cliClient.GrpcClient, provider cliClient.Provider, uploadMode compose.UploadMode, mode modes.Mode) (*defangv1.DeployResponse, *compose.Project, error) + ComposeUp(ctx context.Context, client *cliClient.GrpcClient, provider cliClient.Provider, params cli.ComposeUpParams) (*defangv1.DeployResponse, *compose.Project, error) ConfigDelete(ctx context.Context, projectName string, provider cliClient.Provider, name string) error ConfigSet(ctx context.Context, projectName string, provider cliClient.Provider, name, value string) error Connect(ctx context.Context, cluster string) (*cliClient.GrpcClient, error) diff --git a/src/pkg/mcp/tools/listConfig.go b/src/pkg/agent/tools/listConfig.go similarity index 89% rename from src/pkg/mcp/tools/listConfig.go rename to src/pkg/agent/tools/listConfig.go index e0349bf7e..375146388 100644 --- a/src/pkg/mcp/tools/listConfig.go +++ b/src/pkg/agent/tools/listConfig.go @@ -5,13 +5,13 @@ import ( "fmt" "strings" + "github.com/DefangLabs/defang/src/pkg/agent/common" cliClient "github.com/DefangLabs/defang/src/pkg/cli/client" - "github.com/DefangLabs/defang/src/pkg/mcp/common" "github.com/DefangLabs/defang/src/pkg/term" ) -// handleListConfigTool handles the list config tool logic -func handleListConfigTool(ctx context.Context, loader cliClient.ProjectLoader, providerId *cliClient.ProviderID, cluster string, cli CLIInterface) (string, error) { +// HandleListConfigTool handles the list config tool logic +func HandleListConfigTool(ctx context.Context, loader cliClient.ProjectLoader, providerId *cliClient.ProviderID, cluster string, cli CLIInterface) (string, error) { err := common.ProviderNotConfiguredError(*providerId) if err != nil { return "", err diff --git a/src/pkg/mcp/tools/listConfig_test.go b/src/pkg/agent/tools/listConfig_test.go similarity index 97% rename from src/pkg/mcp/tools/listConfig_test.go rename to src/pkg/agent/tools/listConfig_test.go index bf56bf08b..204812302 100644 --- a/src/pkg/mcp/tools/listConfig_test.go +++ b/src/pkg/agent/tools/listConfig_test.go @@ -6,8 +6,8 @@ import ( "fmt" "testing" + "github.com/DefangLabs/defang/src/pkg/agent/common" "github.com/DefangLabs/defang/src/pkg/cli/client" - "github.com/DefangLabs/defang/src/pkg/mcp/common" defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -126,7 +126,7 @@ func TestHandleListConfigTool(t *testing.T) { // Call the function loader := &client.MockLoader{} - result, err := handleListConfigTool(t.Context(), loader, &tt.providerID, "test-cluster", mockCLI) + result, err := HandleListConfigTool(t.Context(), loader, &tt.providerID, "test-cluster", mockCLI) // Verify error expectations if tt.expectedError != "" { diff --git a/src/pkg/mcp/tools/login.go b/src/pkg/agent/tools/login.go similarity index 80% rename from src/pkg/mcp/tools/login.go rename to src/pkg/agent/tools/login.go index 2fc283b09..bdfd9e28d 100644 --- a/src/pkg/mcp/tools/login.go +++ b/src/pkg/agent/tools/login.go @@ -4,13 +4,13 @@ import ( "context" "errors" + "github.com/DefangLabs/defang/src/pkg/agent/common" "github.com/DefangLabs/defang/src/pkg/auth" - "github.com/DefangLabs/defang/src/pkg/mcp/common" "github.com/DefangLabs/defang/src/pkg/term" ) -// handleLoginTool handles the login tool logic -func handleLoginTool(ctx context.Context, cluster string, cli CLIInterface) (string, error) { +// HandleLoginTool handles the login tool logic +func HandleLoginTool(ctx context.Context, cluster string, cli CLIInterface) (string, error) { term.Debug("Function invoked: cli.Connect") client, err := cli.Connect(ctx, cluster) if err != nil { diff --git a/src/pkg/mcp/tools/login_test.go b/src/pkg/agent/tools/login_test.go similarity index 98% rename from src/pkg/mcp/tools/login_test.go rename to src/pkg/agent/tools/login_test.go index d9782e80c..aebbc2dd6 100644 --- a/src/pkg/mcp/tools/login_test.go +++ b/src/pkg/agent/tools/login_test.go @@ -94,7 +94,7 @@ func TestHandleLoginTool(t *testing.T) { // Call the function var err error - result, err := handleLoginTool(context.Background(), tt.cluster, mockCLI) + result, err := HandleLoginTool(context.Background(), tt.cluster, mockCLI) if tt.expectedError != "" { assert.EqualError(t, err, tt.expectedError) } else { diff --git a/src/pkg/mcp/tools/logs.go b/src/pkg/agent/tools/logs.go similarity index 62% rename from src/pkg/mcp/tools/logs.go rename to src/pkg/agent/tools/logs.go index ef24d2e78..71cf2f41c 100644 --- a/src/pkg/mcp/tools/logs.go +++ b/src/pkg/agent/tools/logs.go @@ -8,46 +8,28 @@ import ( cliTypes "github.com/DefangLabs/defang/src/pkg/cli" cliClient "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/term" + "github.com/DefangLabs/defang/src/pkg/timeutils" "github.com/mark3labs/mcp-go/mcp" ) type LogsParams struct { DeploymentID string - Since time.Time - Until time.Time + Since string + Until string } -func parseLogsParams(request mcp.CallToolRequest) (LogsParams, error) { +func ParseLogsParams(request mcp.CallToolRequest) LogsParams { deploymentId := request.GetString("deployment_id", "") - since, err := request.RequireString("since") - if err != nil { - return LogsParams{}, fmt.Errorf("missing required parameter 'since': %w", err) - } - until, err := request.RequireString("until") - if err != nil { - return LogsParams{}, fmt.Errorf("missing required parameter 'until': %w", err) - } - var sinceTime, untilTime time.Time - if since != "" { - sinceTime, err = time.Parse(time.RFC3339, since) - if err != nil { - return LogsParams{}, fmt.Errorf("invalid parameter 'since', must be in RFC3339 format: %w", err) - } - } - if until != "" { - untilTime, err = time.Parse(time.RFC3339, until) - if err != nil { - return LogsParams{}, fmt.Errorf("invalid parameter 'until', must be in RFC3339 format: %w", err) - } - } + since := request.GetString("since", "") + until := request.GetString("until", "") return LogsParams{ DeploymentID: deploymentId, - Since: sinceTime, - Until: untilTime, - }, nil + Since: since, + Until: until, + } } -func handleLogsTool(ctx context.Context, loader cliClient.ProjectLoader, params LogsParams, cluster string, providerId *cliClient.ProviderID, cli CLIInterface) (string, error) { +func HandleLogsTool(ctx context.Context, loader cliClient.ProjectLoader, params LogsParams, cluster string, providerId *cliClient.ProviderID, cli CLIInterface) (string, error) { term.Debug("Function invoked: loader.LoadProject") project, err := cli.LoadProject(ctx, loader) if err != nil { @@ -70,10 +52,21 @@ func handleLogsTool(ctx context.Context, loader cliClient.ProjectLoader, params return "", fmt.Errorf("provider not configured correctly: %w", err) } + sinceTime, err := timeutils.ParseTimeOrDuration(params.Since, time.Now()) + if err != nil { + return "", fmt.Errorf("failed to parse 'since' parameter: %w", err) + } + + untilTime, err := timeutils.ParseTimeOrDuration(params.Until, time.Now()) + if err != nil { + return "", fmt.Errorf("failed to parse 'until' parameter: %w", err) + } + err = cli.Tail(ctx, provider, project, cliTypes.TailOptions{ Deployment: params.DeploymentID, - Since: params.Since, - Until: params.Until, + Since: sinceTime, + Until: untilTime, + Limit: 100, }) if err != nil { diff --git a/src/pkg/mcp/tools/removeConfig.go b/src/pkg/agent/tools/removeConfig.go similarity index 88% rename from src/pkg/mcp/tools/removeConfig.go rename to src/pkg/agent/tools/removeConfig.go index c60092f7b..ba82f9c60 100644 --- a/src/pkg/mcp/tools/removeConfig.go +++ b/src/pkg/agent/tools/removeConfig.go @@ -4,8 +4,8 @@ import ( "context" "fmt" + "github.com/DefangLabs/defang/src/pkg/agent/common" cliClient "github.com/DefangLabs/defang/src/pkg/cli/client" - "github.com/DefangLabs/defang/src/pkg/mcp/common" "github.com/DefangLabs/defang/src/pkg/term" "github.com/bufbuild/connect-go" "github.com/mark3labs/mcp-go/mcp" @@ -15,7 +15,7 @@ type RemoveConfigParams struct { Name string } -func parseRemoveConfigParams(request mcp.CallToolRequest) (RemoveConfigParams, error) { +func ParseRemoveConfigParams(request mcp.CallToolRequest) (RemoveConfigParams, error) { name, err := request.RequireString("name") if err != nil || name == "" { return RemoveConfigParams{}, fmt.Errorf("missing config `name`: %w", err) @@ -25,8 +25,8 @@ func parseRemoveConfigParams(request mcp.CallToolRequest) (RemoveConfigParams, e }, nil } -// handleRemoveConfigTool handles the remove config tool logic -func handleRemoveConfigTool(ctx context.Context, loader cliClient.ProjectLoader, params RemoveConfigParams, providerId *cliClient.ProviderID, cluster string, cli CLIInterface) (string, error) { +// HandleRemoveConfigTool handles the remove config tool logic +func HandleRemoveConfigTool(ctx context.Context, loader cliClient.ProjectLoader, params RemoveConfigParams, providerId *cliClient.ProviderID, cluster string, cli CLIInterface) (string, error) { err := common.ProviderNotConfiguredError(*providerId) if err != nil { return "", err diff --git a/src/pkg/mcp/tools/removeConfig_test.go b/src/pkg/agent/tools/removeConfig_test.go similarity index 97% rename from src/pkg/mcp/tools/removeConfig_test.go rename to src/pkg/agent/tools/removeConfig_test.go index 4985b9287..1ee1d71cc 100644 --- a/src/pkg/mcp/tools/removeConfig_test.go +++ b/src/pkg/agent/tools/removeConfig_test.go @@ -6,8 +6,8 @@ import ( "fmt" "testing" + "github.com/DefangLabs/defang/src/pkg/agent/common" "github.com/DefangLabs/defang/src/pkg/cli/client" - "github.com/DefangLabs/defang/src/pkg/mcp/common" "github.com/bufbuild/connect-go" "github.com/mark3labs/mcp-go/mcp" "github.com/stretchr/testify/assert" @@ -156,7 +156,7 @@ func TestHandleRemoveConfigTool(t *testing.T) { }, } - params, err := parseRemoveConfigParams(request) + params, err := ParseRemoveConfigParams(request) if err != nil { if tt.expectError { assert.EqualError(t, err, tt.expectedError) @@ -168,7 +168,7 @@ func TestHandleRemoveConfigTool(t *testing.T) { // Call the function loader := &client.MockLoader{} - result, err := handleRemoveConfigTool(t.Context(), loader, params, &tt.providerID, "test-cluster", mockCLI) + result, err := HandleRemoveConfigTool(t.Context(), loader, params, &tt.providerID, "test-cluster", mockCLI) // Verify error expectations if tt.expectError { diff --git a/src/pkg/mcp/tools/services.go b/src/pkg/agent/tools/services.go similarity index 95% rename from src/pkg/mcp/tools/services.go rename to src/pkg/agent/tools/services.go index 53558bc95..6c5db8f04 100644 --- a/src/pkg/mcp/tools/services.go +++ b/src/pkg/agent/tools/services.go @@ -7,14 +7,14 @@ import ( "fmt" "strings" + "github.com/DefangLabs/defang/src/pkg/agent/common" defangcli "github.com/DefangLabs/defang/src/pkg/cli" cliClient "github.com/DefangLabs/defang/src/pkg/cli/client" - "github.com/DefangLabs/defang/src/pkg/mcp/common" "github.com/DefangLabs/defang/src/pkg/term" "github.com/bufbuild/connect-go" ) -func handleServicesTool(ctx context.Context, loader cliClient.ProjectLoader, providerId *cliClient.ProviderID, cluster string, cli CLIInterface) (string, error) { +func HandleServicesTool(ctx context.Context, loader cliClient.ProjectLoader, providerId *cliClient.ProviderID, cluster string, cli CLIInterface) (string, error) { err := common.ProviderNotConfiguredError(*providerId) if err != nil { return "", err diff --git a/src/pkg/mcp/tools/services_test.go b/src/pkg/agent/tools/services_test.go similarity index 98% rename from src/pkg/mcp/tools/services_test.go rename to src/pkg/agent/tools/services_test.go index 67540b58c..1385c11f9 100644 --- a/src/pkg/mcp/tools/services_test.go +++ b/src/pkg/agent/tools/services_test.go @@ -5,9 +5,9 @@ import ( "errors" "testing" + "github.com/DefangLabs/defang/src/pkg/agent/common" defangcli "github.com/DefangLabs/defang/src/pkg/cli" "github.com/DefangLabs/defang/src/pkg/cli/client" - "github.com/DefangLabs/defang/src/pkg/mcp/common" "github.com/DefangLabs/defang/src/pkg/mcp/deployment_info" "github.com/bufbuild/connect-go" "github.com/stretchr/testify/assert" @@ -192,7 +192,7 @@ func TestHandleServicesToolWithMockCLI(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { loader := &client.MockLoader{} - result, err := handleServicesTool(ctx, loader, &tt.providerId, testCluster, tt.mockCLI) + result, err := HandleServicesTool(ctx, loader, &tt.providerId, testCluster, tt.mockCLI) // Check Go error expectation if tt.expectedError { diff --git a/src/pkg/mcp/tools/setAWSProvider.go b/src/pkg/agent/tools/setAWSProvider.go similarity index 88% rename from src/pkg/mcp/tools/setAWSProvider.go rename to src/pkg/agent/tools/setAWSProvider.go index 8dfea3495..a60251775 100644 --- a/src/pkg/mcp/tools/setAWSProvider.go +++ b/src/pkg/agent/tools/setAWSProvider.go @@ -9,8 +9,8 @@ import ( "github.com/mark3labs/mcp-go/mcp" ) -// handleSetAWSProvider handles the set AWS provider MCP tool request -func handleSetAWSProvider(ctx context.Context, request mcp.CallToolRequest, providerId *cliClient.ProviderID, cluster string) (string, error) { +// HandleSetAWSProvider handles the set AWS provider MCP tool request +func HandleSetAWSProvider(ctx context.Context, request mcp.CallToolRequest, providerId *cliClient.ProviderID, cluster string) (string, error) { awsId, err := request.RequireString("accessKeyId") if err != nil { return "", fmt.Errorf("Invalid AWS access key Id: %w", err) diff --git a/src/pkg/mcp/tools/setConfig.go b/src/pkg/agent/tools/setConfig.go similarity index 88% rename from src/pkg/mcp/tools/setConfig.go rename to src/pkg/agent/tools/setConfig.go index 2edd966b7..16608aba9 100644 --- a/src/pkg/mcp/tools/setConfig.go +++ b/src/pkg/agent/tools/setConfig.go @@ -5,8 +5,8 @@ import ( "fmt" "github.com/DefangLabs/defang/src/pkg" + "github.com/DefangLabs/defang/src/pkg/agent/common" cliClient "github.com/DefangLabs/defang/src/pkg/cli/client" - "github.com/DefangLabs/defang/src/pkg/mcp/common" "github.com/DefangLabs/defang/src/pkg/term" "github.com/mark3labs/mcp-go/mcp" ) @@ -16,7 +16,7 @@ type SetConfigParams struct { Value string } -func parseSetConfigParams(request mcp.CallToolRequest) (SetConfigParams, error) { +func ParseSetConfigParams(request mcp.CallToolRequest) (SetConfigParams, error) { name, err := request.RequireString("name") if err != nil || name == "" { return SetConfigParams{}, fmt.Errorf("missing 'name' parameter: %w", err) @@ -31,8 +31,8 @@ func parseSetConfigParams(request mcp.CallToolRequest) (SetConfigParams, error) }, nil } -// handleSetConfig handles the set config MCP tool request -func handleSetConfig(ctx context.Context, loader cliClient.ProjectLoader, params SetConfigParams, providerId *cliClient.ProviderID, cluster string, cli CLIInterface) (string, error) { +// HandleSetConfig handles the set config MCP tool request +func HandleSetConfig(ctx context.Context, loader cliClient.ProjectLoader, params SetConfigParams, providerId *cliClient.ProviderID, cluster string, cli CLIInterface) (string, error) { err := common.ProviderNotConfiguredError(*providerId) if err != nil { return "", err diff --git a/src/pkg/mcp/tools/setConfig_test.go b/src/pkg/agent/tools/setConfig_test.go similarity index 98% rename from src/pkg/mcp/tools/setConfig_test.go rename to src/pkg/agent/tools/setConfig_test.go index b68139c66..f3c633cb9 100644 --- a/src/pkg/mcp/tools/setConfig_test.go +++ b/src/pkg/agent/tools/setConfig_test.go @@ -219,7 +219,7 @@ func TestHandleSetConfig(t *testing.T) { t.Run(tt.name, func(t *testing.T) { request := createCallToolRequest(tt.requestArgs) loader := &client.MockLoader{} - params, err := parseSetConfigParams(request) + params, err := ParseSetConfigParams(request) if err != nil { if tt.expectedError { assert.EqualError(t, err, tt.errorMessage) @@ -228,7 +228,7 @@ func TestHandleSetConfig(t *testing.T) { require.NoError(t, err) } } - result, err := handleSetConfig(testContext, loader, params, &tt.providerId, tt.cluster, tt.mockCLI) + result, err := HandleSetConfig(testContext, loader, params, &tt.providerId, tt.cluster, tt.mockCLI) if tt.expectedError { assert.Error(t, err) diff --git a/src/pkg/mcp/tools/setGCPProvider.go b/src/pkg/agent/tools/setGCPProvider.go similarity index 83% rename from src/pkg/mcp/tools/setGCPProvider.go rename to src/pkg/agent/tools/setGCPProvider.go index 428076c56..a2e74932e 100644 --- a/src/pkg/mcp/tools/setGCPProvider.go +++ b/src/pkg/agent/tools/setGCPProvider.go @@ -9,8 +9,8 @@ import ( "github.com/mark3labs/mcp-go/mcp" ) -// handleSetGCPProvider handles the set GCP provider MCP tool request -func handleSetGCPProvider(ctx context.Context, request mcp.CallToolRequest, providerId *cliClient.ProviderID, cluster string) (string, error) { +// HandleSetGCPProvider handles the set GCP provider MCP tool request +func HandleSetGCPProvider(ctx context.Context, request mcp.CallToolRequest, providerId *cliClient.ProviderID, cluster string) (string, error) { gcpProjectID, err := request.RequireString("gcpProjectId") if err != nil { return "", fmt.Errorf("Invalid GCP project ID: %w", err) diff --git a/src/pkg/mcp/tools/setPlaygroundProvider.go b/src/pkg/agent/tools/setPlaygroundProvider.go similarity index 76% rename from src/pkg/mcp/tools/setPlaygroundProvider.go rename to src/pkg/agent/tools/setPlaygroundProvider.go index a39956678..8472cea98 100644 --- a/src/pkg/mcp/tools/setPlaygroundProvider.go +++ b/src/pkg/agent/tools/setPlaygroundProvider.go @@ -7,8 +7,8 @@ import ( "github.com/DefangLabs/defang/src/pkg/mcp/actions" ) -// handleSetPlaygroundProvider handles the set Playground provider MCP tool request -func handleSetPlaygroundProvider(providerId *cliClient.ProviderID) (string, error) { +// HandleSetPlaygroundProvider handles the set Playground provider MCP tool request +func HandleSetPlaygroundProvider(providerId *cliClient.ProviderID) (string, error) { if err := actions.SetPlaygroundProvider(providerId); err != nil { return "", fmt.Errorf("Failed to set Playground provider: %w", err) } diff --git a/src/pkg/mcp/tools/tools.go b/src/pkg/agent/tools/tools.go similarity index 89% rename from src/pkg/mcp/tools/tools.go rename to src/pkg/agent/tools/tools.go index d01ee7fb4..251aa51cf 100644 --- a/src/pkg/mcp/tools/tools.go +++ b/src/pkg/agent/tools/tools.go @@ -5,8 +5,8 @@ import ( "strings" "time" + "github.com/DefangLabs/defang/src/pkg/agent/common" "github.com/DefangLabs/defang/src/pkg/cli/client" - "github.com/DefangLabs/defang/src/pkg/mcp/common" "github.com/DefangLabs/defang/src/pkg/modes" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" @@ -29,7 +29,7 @@ func CollectTools(cluster string, providerId *client.ProviderID, cli CLIInterfac mcp.WithDescription("Login to Defang"), ), Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - output, err := handleLoginTool(ctx, cluster, cli) + output, err := HandleLoginTool(ctx, cluster, cli) if err != nil { return mcp.NewToolResultErrorFromErr("Failed to login", err), err } @@ -47,7 +47,7 @@ func CollectTools(cluster string, providerId *client.ProviderID, cli CLIInterfac if err != nil { return mcp.NewToolResultErrorFromErr("Failed to configure loader", err), err } - output, err := handleServicesTool(ctx, loader, providerId, cluster, cli) + output, err := HandleServicesTool(ctx, loader, providerId, cluster, cli) if err != nil { return mcp.NewToolResultErrorFromErr("Failed to list services", err), err } @@ -65,7 +65,7 @@ func CollectTools(cluster string, providerId *client.ProviderID, cli CLIInterfac if err != nil { return mcp.NewToolResultErrorFromErr("Failed to configure loader", err), err } - output, err := handleDeployTool(ctx, loader, providerId, cluster, cli) + output, err := HandleDeployTool(ctx, loader, providerId, cluster, cli) if err != nil { return mcp.NewToolResultErrorFromErr("Failed to deploy services", err), err } @@ -83,7 +83,7 @@ func CollectTools(cluster string, providerId *client.ProviderID, cli CLIInterfac if err != nil { return mcp.NewToolResultErrorFromErr("Failed to configure loader", err), err } - output, err := handleDestroyTool(ctx, loader, providerId, cluster, cli) + output, err := HandleDestroyTool(ctx, loader, providerId, cluster, cli) if err != nil { return mcp.NewToolResultErrorFromErr("Failed to destroy services", err), err } @@ -113,11 +113,8 @@ func CollectTools(cluster string, providerId *client.ProviderID, cli CLIInterfac if err != nil { return mcp.NewToolResultErrorFromErr("Failed to configure loader", err), err } - params, err := parseLogsParams(request) - if err != nil { - return mcp.NewToolResultErrorFromErr("Failed to parse logs parameters", err), err - } - output, err := handleLogsTool(ctx, loader, params, cluster, providerId, cli) + params := ParseLogsParams(request) + output, err := HandleLogsTool(ctx, loader, params, cluster, providerId, cli) if err != nil { return mcp.NewToolResultErrorFromErr("Failed to fetch logs", err), err } @@ -145,11 +142,11 @@ func CollectTools(cluster string, providerId *client.ProviderID, cli CLIInterfac if err != nil { return mcp.NewToolResultErrorFromErr("Failed to configure loader", err), err } - params, err := parseEstimateParams(request, providerId) + params, err := ParseEstimateParams(request, providerId) if err != nil { return mcp.NewToolResultErrorFromErr("Failed to parse estimate parameters", err), err } - output, err := handleEstimateTool(ctx, loader, params, cluster, cli) + output, err := HandleEstimateTool(ctx, loader, params, cluster, cli) if err != nil { return mcp.NewToolResultErrorFromErr("Failed to estimate costs", err), err } @@ -173,11 +170,11 @@ func CollectTools(cluster string, providerId *client.ProviderID, cli CLIInterfac if err != nil { return mcp.NewToolResultErrorFromErr("Failed to configure loader", err), err } - params, err := parseSetConfigParams(request) + params, err := ParseSetConfigParams(request) if err != nil { return mcp.NewToolResultErrorFromErr("Failed to parse set config parameters", err), err } - output, err := handleSetConfig(ctx, loader, params, providerId, cluster, cli) + output, err := HandleSetConfig(ctx, loader, params, providerId, cluster, cli) if err != nil { return mcp.NewToolResultErrorFromErr("Failed to set config", err), err } @@ -198,11 +195,11 @@ func CollectTools(cluster string, providerId *client.ProviderID, cli CLIInterfac if err != nil { return mcp.NewToolResultErrorFromErr("Failed to configure loader", err), err } - params, err := parseRemoveConfigParams(request) + params, err := ParseRemoveConfigParams(request) if err != nil { return mcp.NewToolResultErrorFromErr("Failed to parse remove config parameters", err), err } - output, err := handleRemoveConfigTool(ctx, loader, params, providerId, cluster, cli) + output, err := HandleRemoveConfigTool(ctx, loader, params, providerId, cluster, cli) if err != nil { return mcp.NewToolResultErrorFromErr("Failed to remove config", err), err } @@ -220,7 +217,7 @@ func CollectTools(cluster string, providerId *client.ProviderID, cli CLIInterfac if err != nil { return mcp.NewToolResultErrorFromErr("Failed to configure loader", err), err } - output, err := handleListConfigTool(ctx, loader, providerId, cluster, cli) + output, err := HandleListConfigTool(ctx, loader, providerId, cluster, cli) if err != nil { return mcp.NewToolResultErrorFromErr("Failed to list config", err), err } @@ -242,7 +239,7 @@ func CollectTools(cluster string, providerId *client.ProviderID, cli CLIInterfac ), ), Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - output, err := handleSetAWSProvider(ctx, request, providerId, cluster) + output, err := HandleSetAWSProvider(ctx, request, providerId, cluster) if err != nil { return mcp.NewToolResultErrorFromErr("Failed to set AWS provider", err), err } @@ -258,7 +255,7 @@ func CollectTools(cluster string, providerId *client.ProviderID, cli CLIInterfac ), ), Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - output, err := handleSetGCPProvider(ctx, request, providerId, cluster) + output, err := HandleSetGCPProvider(ctx, request, providerId, cluster) if err != nil { return mcp.NewToolResultErrorFromErr("Failed to set GCP provider", err), err } @@ -271,7 +268,7 @@ func CollectTools(cluster string, providerId *client.ProviderID, cli CLIInterfac workingDirectoryOption, ), Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - output, err := handleSetPlaygroundProvider(providerId) + output, err := HandleSetPlaygroundProvider(providerId) if err != nil { return mcp.NewToolResultErrorFromErr("Failed to set Playground provider", err), err } diff --git a/src/pkg/cli/composeUp.go b/src/pkg/cli/composeUp.go index e071811ef..26109a603 100644 --- a/src/pkg/cli/composeUp.go +++ b/src/pkg/cli/composeUp.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/DefangLabs/defang/src/pkg/cli/client" + cliClient "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/cli/compose" "github.com/DefangLabs/defang/src/pkg/dryrun" "github.com/DefangLabs/defang/src/pkg/modes" @@ -22,8 +23,18 @@ func (e ComposeError) Unwrap() error { return e.error } +type ComposeUpParams struct { + Project *compose.Project + UploadMode compose.UploadMode + Mode modes.Mode +} + // ComposeUp validates a compose project and uploads the services using the client -func ComposeUp(ctx context.Context, project *compose.Project, fabric client.FabricClient, provider client.Provider, upload compose.UploadMode, mode modes.Mode) (*defangv1.DeployResponse, *compose.Project, error) { +func ComposeUp(ctx context.Context, fabric client.FabricClient, provider cliClient.Provider, params ComposeUpParams) (*defangv1.DeployResponse, *compose.Project, error) { + upload := params.UploadMode + project := params.Project + mode := params.Mode + if dryrun.DoDryRun { upload = compose.UploadModeIgnore } diff --git a/src/pkg/cli/composeUp_test.go b/src/pkg/cli/composeUp_test.go index fb873faac..2d48b4daf 100644 --- a/src/pkg/cli/composeUp_test.go +++ b/src/pkg/cli/composeUp_test.go @@ -99,7 +99,11 @@ func TestComposeUp(t *testing.T) { mc := client.MockFabricClient{DelegateDomain: "example.com"} mp := &mockDeployProvider{MockProvider: client.MockProvider{UploadUrl: server.URL + "/"}} - d, project, err := ComposeUp(t.Context(), proj, mc, mp, compose.UploadModeDigest, modes.ModeAffordable) + d, project, err := ComposeUp(t.Context(), mc, mp, ComposeUpParams{ + Mode: modes.ModeAffordable, + Project: proj, + UploadMode: compose.UploadModeDigest, + }) if err != nil { t.Fatalf("ComposeUp() failed: %v", err) } @@ -283,7 +287,11 @@ func TestComposeUpStops(t *testing.T) { deploymentStatus: tt.cdStatus, } - resp, project, err := ComposeUp(ctx, project, fabric, provider, compose.UploadModeDigest, modes.ModeUnspecified) + resp, project, err := ComposeUp(ctx, fabric, provider, ComposeUpParams{ + Mode: modes.ModeUnspecified, + Project: project, + UploadMode: compose.UploadModeDigest, + }) if err != nil { t.Fatalf("ComposeUp() failed: %v", err) } diff --git a/src/pkg/cli/preview.go b/src/pkg/cli/preview.go index 95a55f5fa..40a600881 100644 --- a/src/pkg/cli/preview.go +++ b/src/pkg/cli/preview.go @@ -10,7 +10,11 @@ import ( ) func Preview(ctx context.Context, project *compose.Project, fabric cliClient.FabricClient, provider cliClient.Provider, mode modes.Mode) error { - resp, project, err := ComposeUp(ctx, project, fabric, provider, compose.UploadModePreview, mode) + resp, project, err := ComposeUp(ctx, fabric, provider, ComposeUpParams{ + Mode: mode, + Project: project, + UploadMode: compose.UploadModePreview, + }) if err != nil { return err } diff --git a/src/pkg/cli/tail.go b/src/pkg/cli/tail.go index 8fe6a2c3c..be7a258e6 100644 --- a/src/pkg/cli/tail.go +++ b/src/pkg/cli/tail.go @@ -97,35 +97,6 @@ func EnableUTCMode() { time.Local = time.UTC } -// ParseTimeOrDuration parses a time string or duration string (e.g. 1h30m) and returns a time.Time. -// At a minimum, this function supports RFC3339Nano, Go durations, and our own TimestampFormat (local). -func ParseTimeOrDuration(str string, now time.Time) (time.Time, error) { - if str == "" { - return time.Time{}, nil - } - if strings.ContainsAny(str, "TZ") { - return time.Parse(time.RFC3339Nano, str) - } - if strings.Contains(str, ":") { - local, err := time.ParseInLocation("15:04:05.999999", str, time.Local) - if err != nil { - return time.Time{}, err - } - // Replace the year, month, and day of t with today's date - now := now.Local() - sincet := time.Date(now.Year(), now.Month(), now.Day(), local.Hour(), local.Minute(), local.Second(), local.Nanosecond(), local.Location()) - if sincet.After(now) { - sincet = sincet.AddDate(0, 0, -1) // yesterday; subtract 1 day - } - return sincet, nil - } - dur, err := time.ParseDuration(str) - if err != nil { - return time.Time{}, err - } - return now.Add(-dur), nil // - because we want to go back in time -} - type CancelError struct { TailOptions ProjectName string diff --git a/src/pkg/cli/tail_test.go b/src/pkg/cli/tail_test.go index 6891366b9..84ddf933d 100644 --- a/src/pkg/cli/tail_test.go +++ b/src/pkg/cli/tail_test.go @@ -49,34 +49,6 @@ func TestIsProgressDot(t *testing.T) { } } -func TestParseTimeOrDuration(t *testing.T) { - now := time.Now() - tdt := []struct { - td string - want time.Time - }{ - {"", time.Time{}}, - {"1s", now.Add(-time.Second)}, - {"2m3s", now.Add(-2*time.Minute - 3*time.Second)}, - {"2024-01-01T00:00:00Z", time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)}, - {"2024-02-01T00:00:00.500Z", time.Date(2024, 2, 1, 0, 0, 0, 5e8, time.UTC)}, - {"2024-03-01T00:00:00+07:00", time.Date(2024, 3, 1, 0, 0, 0, 0, time.FixedZone("", 7*60*60))}, - {"00:01:02.040", time.Date(now.Year(), now.Month(), now.Day(), 0, 1, 2, 4e7, now.Location())}, // this test will fail if it's run at midnight UTC :( - } - for _, tt := range tdt { - t.Run(tt.td, func(t *testing.T) { - got, err := ParseTimeOrDuration(tt.td, now) - if err != nil { - t.Errorf("ParseTimeOrDuration() error = %v", err) - return - } - if !got.Equal(tt.want) { - t.Errorf("ParseTimeOrDuration() = %v, want %v", got, tt.want) - } - }) - } -} - type mockTailProvider struct { client.Provider ServerStreams []client.ServerStream[defangv1.TailResponse] diff --git a/src/pkg/mcp/actions/setAWSBYOCProvider.go b/src/pkg/mcp/actions/setAWSBYOCProvider.go index 161b2b530..152ec0d90 100644 --- a/src/pkg/mcp/actions/setAWSBYOCProvider.go +++ b/src/pkg/mcp/actions/setAWSBYOCProvider.go @@ -5,9 +5,9 @@ import ( "errors" "os" + "github.com/DefangLabs/defang/src/pkg/agent/common" "github.com/DefangLabs/defang/src/pkg/cli" "github.com/DefangLabs/defang/src/pkg/cli/client" - "github.com/DefangLabs/defang/src/pkg/mcp/common" ) func SetAWSByocProvider(ctx context.Context, providerId *client.ProviderID, cluster string, accessKeyId string, secretKey string, region string) error { diff --git a/src/pkg/mcp/actions/setGCPBYOCProvider.go b/src/pkg/mcp/actions/setGCPBYOCProvider.go index c063b7a24..8fdc9370a 100644 --- a/src/pkg/mcp/actions/setGCPBYOCProvider.go +++ b/src/pkg/mcp/actions/setGCPBYOCProvider.go @@ -4,9 +4,9 @@ import ( "context" "os" + "github.com/DefangLabs/defang/src/pkg/agent/common" "github.com/DefangLabs/defang/src/pkg/cli" "github.com/DefangLabs/defang/src/pkg/cli/client" - "github.com/DefangLabs/defang/src/pkg/mcp/common" ) func SetGCPByocProvider(ctx context.Context, providerId *client.ProviderID, cluster string, projectID string) error { diff --git a/src/pkg/mcp/mcp_server.go b/src/pkg/mcp/mcp_server.go index a61f1aebf..9b87ddd93 100644 --- a/src/pkg/mcp/mcp_server.go +++ b/src/pkg/mcp/mcp_server.go @@ -4,10 +4,10 @@ import ( "context" "fmt" - "github.com/DefangLabs/defang/src/pkg/mcp/common" + "github.com/DefangLabs/defang/src/pkg/agent/common" + "github.com/DefangLabs/defang/src/pkg/agent/tools" "github.com/DefangLabs/defang/src/pkg/mcp/prompts" "github.com/DefangLabs/defang/src/pkg/mcp/resources" - "github.com/DefangLabs/defang/src/pkg/mcp/tools" "github.com/DefangLabs/defang/src/pkg/term" "github.com/DefangLabs/defang/src/pkg/track" "github.com/mark3labs/mcp-go/mcp" diff --git a/src/pkg/mcp/prompts/awsBYOC.go b/src/pkg/mcp/prompts/awsBYOC.go index c81c7fa13..a9d3be260 100644 --- a/src/pkg/mcp/prompts/awsBYOC.go +++ b/src/pkg/mcp/prompts/awsBYOC.go @@ -3,9 +3,9 @@ package prompts import ( "context" + "github.com/DefangLabs/defang/src/pkg/agent/common" "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/mcp/actions" - "github.com/DefangLabs/defang/src/pkg/mcp/common" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) diff --git a/src/pkg/mcp/prompts/gcpBYOC.go b/src/pkg/mcp/prompts/gcpBYOC.go index f4a98c7a4..86090837e 100644 --- a/src/pkg/mcp/prompts/gcpBYOC.go +++ b/src/pkg/mcp/prompts/gcpBYOC.go @@ -3,9 +3,9 @@ package prompts import ( "context" + "github.com/DefangLabs/defang/src/pkg/agent/common" "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/mcp/actions" - "github.com/DefangLabs/defang/src/pkg/mcp/common" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) diff --git a/src/pkg/mcp/prompts/playgroundSetup.go b/src/pkg/mcp/prompts/playgroundSetup.go index 001363dff..894830a2b 100644 --- a/src/pkg/mcp/prompts/playgroundSetup.go +++ b/src/pkg/mcp/prompts/playgroundSetup.go @@ -3,9 +3,9 @@ package prompts import ( "context" + "github.com/DefangLabs/defang/src/pkg/agent/common" "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/mcp/actions" - "github.com/DefangLabs/defang/src/pkg/mcp/common" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) diff --git a/src/pkg/mcp/tests/client_test.go b/src/pkg/mcp/tests/client_test.go index 19ac49ba8..b8bfa3b64 100644 --- a/src/pkg/mcp/tests/client_test.go +++ b/src/pkg/mcp/tests/client_test.go @@ -14,9 +14,9 @@ import ( m3mcp "github.com/mark3labs/mcp-go/mcp" "google.golang.org/protobuf/types/known/emptypb" + "github.com/DefangLabs/defang/src/pkg/agent/tools" cliClient "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/mcp" - "github.com/DefangLabs/defang/src/pkg/mcp/tools" typepb "github.com/DefangLabs/defang/src/protos/google/type" defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" "github.com/DefangLabs/defang/src/protos/io/defang/v1/defangv1connect" diff --git a/src/pkg/timeutils/timeutils.go b/src/pkg/timeutils/timeutils.go new file mode 100644 index 000000000..53197a877 --- /dev/null +++ b/src/pkg/timeutils/timeutils.go @@ -0,0 +1,35 @@ +package timeutils + +import ( + "strings" + "time" +) + +// ParseTimeOrDuration parses a time string or duration string (e.g. 1h30m) and returns a time.Time. +// At a minimum, this function supports RFC3339Nano, Go durations, and our own TimestampFormat (local). +func ParseTimeOrDuration(str string, now time.Time) (time.Time, error) { + if str == "" { + return time.Time{}, nil + } + if strings.ContainsAny(str, "TZ") { + return time.Parse(time.RFC3339Nano, str) + } + if strings.Contains(str, ":") { + local, err := time.ParseInLocation("15:04:05.999999", str, time.Local) + if err != nil { + return time.Time{}, err + } + // Replace the year, month, and day of t with today's date + now := now.Local() + sincet := time.Date(now.Year(), now.Month(), now.Day(), local.Hour(), local.Minute(), local.Second(), local.Nanosecond(), local.Location()) + if sincet.After(now) { + sincet = sincet.AddDate(0, 0, -1) // yesterday; subtract 1 day + } + return sincet, nil + } + dur, err := time.ParseDuration(str) + if err != nil { + return time.Time{}, err + } + return now.Add(-dur), nil // - because we want to go back in time +} diff --git a/src/pkg/timeutils/timeutils_test.go b/src/pkg/timeutils/timeutils_test.go new file mode 100644 index 000000000..086c5f223 --- /dev/null +++ b/src/pkg/timeutils/timeutils_test.go @@ -0,0 +1,34 @@ +package timeutils + +import ( + "testing" + "time" +) + +func TestParseTimeOrDuration(t *testing.T) { + now := time.Now() + tdt := []struct { + td string + want time.Time + }{ + {"", time.Time{}}, + {"1s", now.Add(-time.Second)}, + {"2m3s", now.Add(-2*time.Minute - 3*time.Second)}, + {"2024-01-01T00:00:00Z", time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)}, + {"2024-02-01T00:00:00.500Z", time.Date(2024, 2, 1, 0, 0, 0, 5e8, time.UTC)}, + {"2024-03-01T00:00:00+07:00", time.Date(2024, 3, 1, 0, 0, 0, 0, time.FixedZone("", 7*60*60))}, + {"00:01:02.040", time.Date(now.Year(), now.Month(), now.Day(), 0, 1, 2, 4e7, now.Location())}, // this test will fail if it's run at midnight UTC :( + } + for _, tt := range tdt { + t.Run(tt.td, func(t *testing.T) { + got, err := ParseTimeOrDuration(tt.td, now) + if err != nil { + t.Errorf("ParseTimeOrDuration() error = %v", err) + return + } + if !got.Equal(tt.want) { + t.Errorf("ParseTimeOrDuration() = %v, want %v", got, tt.want) + } + }) + } +}