Files
kandev/apps/backend/internal/task/service/service_resources.go
T
2026-05-01 15:18:18 +01:00

823 lines
27 KiB
Go

package service
import (
"context"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/kandev/kandev/internal/events"
"github.com/kandev/kandev/internal/task/models"
"github.com/kandev/kandev/internal/worktree"
)
const spritesTokenEnvKey = "SPRITES_API_TOKEN"
// Workspace operations
// CreateWorkspace creates a new workspace
func (s *Service) CreateWorkspace(ctx context.Context, req *CreateWorkspaceRequest) (*models.Workspace, error) {
workspace := &models.Workspace{
ID: uuid.New().String(),
Name: req.Name,
Description: req.Description,
OwnerID: req.OwnerID,
DefaultExecutorID: normalizeOptionalID(req.DefaultExecutorID),
DefaultEnvironmentID: normalizeOptionalID(req.DefaultEnvironmentID),
DefaultAgentProfileID: normalizeOptionalID(req.DefaultAgentProfileID),
DefaultConfigAgentProfileID: normalizeOptionalID(req.DefaultConfigAgentProfileID),
}
if err := s.workspaces.CreateWorkspace(ctx, workspace); err != nil {
s.logger.Error("failed to create workspace", zap.Error(err))
return nil, err
}
s.publishWorkspaceEvent(ctx, events.WorkspaceCreated, workspace)
s.logger.Info("workspace created", zap.String("workspace_id", workspace.ID), zap.String("name", workspace.Name))
return workspace, nil
}
// GetWorkspace retrieves a workspace by ID
func (s *Service) GetWorkspace(ctx context.Context, id string) (*models.Workspace, error) {
return s.workspaces.GetWorkspace(ctx, id)
}
// UpdateWorkspace updates an existing workspace
func (s *Service) UpdateWorkspace(ctx context.Context, id string, req *UpdateWorkspaceRequest) (*models.Workspace, error) {
workspace, err := s.workspaces.GetWorkspace(ctx, id)
if err != nil {
return nil, err
}
if req.Name != nil {
workspace.Name = *req.Name
}
if req.Description != nil {
workspace.Description = *req.Description
}
if req.DefaultExecutorID != nil {
workspace.DefaultExecutorID = normalizeOptionalID(req.DefaultExecutorID)
}
if req.DefaultEnvironmentID != nil {
workspace.DefaultEnvironmentID = normalizeOptionalID(req.DefaultEnvironmentID)
}
if req.DefaultAgentProfileID != nil {
workspace.DefaultAgentProfileID = normalizeOptionalID(req.DefaultAgentProfileID)
}
if req.DefaultConfigAgentProfileID != nil {
workspace.DefaultConfigAgentProfileID = normalizeOptionalID(req.DefaultConfigAgentProfileID)
}
workspace.UpdatedAt = time.Now().UTC()
if err := s.workspaces.UpdateWorkspace(ctx, workspace); err != nil {
s.logger.Error("failed to update workspace", zap.String("workspace_id", id), zap.Error(err))
return nil, err
}
s.publishWorkspaceEvent(ctx, events.WorkspaceUpdated, workspace)
s.logger.Info("workspace updated", zap.String("workspace_id", workspace.ID))
return workspace, nil
}
// DeleteWorkspace deletes a workspace
func (s *Service) DeleteWorkspace(ctx context.Context, id string) error {
workspace, err := s.workspaces.GetWorkspace(ctx, id)
if err != nil {
return err
}
if err := s.workspaces.DeleteWorkspace(ctx, id); err != nil {
s.logger.Error("failed to delete workspace", zap.String("workspace_id", id), zap.Error(err))
return err
}
s.publishWorkspaceEvent(ctx, events.WorkspaceDeleted, workspace)
s.logger.Info("workspace deleted", zap.String("workspace_id", id))
return nil
}
func normalizeOptionalID(value *string) *string {
if value == nil {
return nil
}
trimmed := strings.TrimSpace(*value)
if trimmed == "" {
return nil
}
return &trimmed
}
// ListWorkspaces returns all workspaces
func (s *Service) ListWorkspaces(ctx context.Context) ([]*models.Workspace, error) {
return s.workspaces.ListWorkspaces(ctx)
}
// Workflow operations
// CreateWorkflow creates a new workflow
func (s *Service) CreateWorkflow(ctx context.Context, req *CreateWorkflowRequest) (*models.Workflow, error) {
workflow := &models.Workflow{
ID: uuid.New().String(),
WorkspaceID: req.WorkspaceID,
Name: req.Name,
Description: req.Description,
WorkflowTemplateID: req.WorkflowTemplateID,
Hidden: req.Hidden,
}
if err := s.workflows.CreateWorkflow(ctx, workflow); err != nil {
s.logger.Error("failed to create workflow", zap.Error(err))
return nil, err
}
// Create workflow steps from template if specified
if req.WorkflowTemplateID != nil && *req.WorkflowTemplateID != "" && s.workflowStepCreator != nil {
if err := s.workflowStepCreator.CreateStepsFromTemplate(ctx, workflow.ID, *req.WorkflowTemplateID); err != nil {
s.logger.Error("failed to create workflow steps from template",
zap.String("workflow_id", workflow.ID),
zap.String("template_id", *req.WorkflowTemplateID),
zap.Error(err))
// Don't fail workflow creation, just log the error
}
}
s.publishWorkflowEvent(ctx, events.WorkflowCreated, workflow)
s.logger.Info("workflow created", zap.String("workflow_id", workflow.ID), zap.String("name", workflow.Name))
return workflow, nil
}
// GetWorkflow retrieves a workflow by ID
func (s *Service) GetWorkflow(ctx context.Context, id string) (*models.Workflow, error) {
return s.workflows.GetWorkflow(ctx, id)
}
// UpdateWorkflow updates an existing workflow
func (s *Service) UpdateWorkflow(ctx context.Context, id string, req *UpdateWorkflowRequest) (*models.Workflow, error) {
workflow, err := s.workflows.GetWorkflow(ctx, id)
if err != nil {
return nil, err
}
if req.Name != nil {
workflow.Name = *req.Name
}
if req.Description != nil {
workflow.Description = *req.Description
}
if req.AgentProfileID != nil {
workflow.AgentProfileID = strings.TrimSpace(*req.AgentProfileID)
}
workflow.UpdatedAt = time.Now().UTC()
if err := s.workflows.UpdateWorkflow(ctx, workflow); err != nil {
s.logger.Error("failed to update workflow", zap.String("workflow_id", id), zap.Error(err))
return nil, err
}
s.publishWorkflowEvent(ctx, events.WorkflowUpdated, workflow)
s.logger.Info("workflow updated", zap.String("workflow_id", workflow.ID))
return workflow, nil
}
// DeleteWorkflow deletes a workflow
func (s *Service) DeleteWorkflow(ctx context.Context, id string) error {
workflow, err := s.workflows.GetWorkflow(ctx, id)
if err != nil {
return err
}
if err := s.workflows.DeleteWorkflow(ctx, id); err != nil {
s.logger.Error("failed to delete workflow", zap.String("workflow_id", id), zap.Error(err))
return err
}
s.publishWorkflowEvent(ctx, events.WorkflowDeleted, workflow)
s.logger.Info("workflow deleted", zap.String("workflow_id", id))
return nil
}
// ListWorkflows returns workflows for a workspace, excluding hidden ones by default.
// Pass includeHidden=true to include system-only flows like Improve Kandev.
func (s *Service) ListWorkflows(ctx context.Context, workspaceID string, includeHidden bool) ([]*models.Workflow, error) {
return s.workflows.ListWorkflows(ctx, workspaceID, includeHidden)
}
// ReorderWorkflows updates sort_order for workflows within a workspace.
func (s *Service) ReorderWorkflows(ctx context.Context, workspaceID string, workflowIDs []string) error {
if err := s.workflows.ReorderWorkflows(ctx, workspaceID, workflowIDs); err != nil {
s.logger.Error("failed to reorder workflows", zap.String("workspace_id", workspaceID), zap.Error(err))
return err
}
s.logger.Info("reordered workflows", zap.String("workspace_id", workspaceID), zap.Int("count", len(workflowIDs)))
return nil
}
// Repository operations
func (s *Service) CreateRepository(ctx context.Context, req *CreateRepositoryRequest) (*models.Repository, error) {
sourceType := req.SourceType
if sourceType == "" {
sourceType = "local"
}
prefix := strings.TrimSpace(req.WorktreeBranchPrefix)
if err := worktree.ValidateBranchPrefix(prefix); err != nil {
return nil, fmt.Errorf("%w: %s", ErrInvalidRepositorySettings, err)
}
if prefix == "" {
prefix = worktree.DefaultBranchPrefix
}
pullBeforeWorktree := true
if req.PullBeforeWorktree != nil {
pullBeforeWorktree = *req.PullBeforeWorktree
}
repository := &models.Repository{
ID: uuid.New().String(),
WorkspaceID: req.WorkspaceID,
Name: req.Name,
SourceType: sourceType,
LocalPath: req.LocalPath,
Provider: req.Provider,
ProviderRepoID: req.ProviderRepoID,
ProviderOwner: req.ProviderOwner,
ProviderName: req.ProviderName,
DefaultBranch: req.DefaultBranch,
WorktreeBranchPrefix: prefix,
PullBeforeWorktree: pullBeforeWorktree,
SetupScript: req.SetupScript,
CleanupScript: req.CleanupScript,
DevScript: req.DevScript,
}
// Auto-detect GitHub provider info from git remote if not provided
if repository.Provider == "" && repository.LocalPath != "" {
if p, o, n := ResolveGitRemoteProvider(repository.LocalPath); p != "" {
repository.Provider = p
repository.ProviderOwner = o
repository.ProviderName = n
}
}
if err := s.repoEntities.CreateRepository(ctx, repository); err != nil {
s.logger.Error("failed to create repository", zap.Error(err))
return nil, err
}
s.publishRepositoryEvent(ctx, events.RepositoryCreated, repository)
s.logger.Info("repository created", zap.String("repository_id", repository.ID))
return repository, nil
}
func (s *Service) GetRepository(ctx context.Context, id string) (*models.Repository, error) {
return s.repoEntities.GetRepository(ctx, id)
}
// GetRepositoryByProviderInfo looks up a repository by workspace and provider identity.
// Returns nil (with nil error) when no matching repository exists.
func (s *Service) GetRepositoryByProviderInfo(ctx context.Context, workspaceID, provider, owner, name string) (*models.Repository, error) {
return s.repoEntities.GetRepositoryByProviderInfo(ctx, workspaceID, provider, owner, name)
}
// FindOrCreateRepository looks up a repository by provider info, creating one if not found.
// If the repository exists but has no LocalPath and the request provides one, updates LocalPath.
func (s *Service) FindOrCreateRepository(ctx context.Context, req *FindOrCreateRepositoryRequest) (*models.Repository, error) {
existing, err := s.repoEntities.GetRepositoryByProviderInfo(ctx, req.WorkspaceID, req.Provider, req.ProviderOwner, req.ProviderName)
if err != nil {
return nil, fmt.Errorf("lookup repository: %w", err)
}
if existing != nil {
if existing.LocalPath == "" && req.LocalPath != "" {
existing.LocalPath = req.LocalPath
if updateErr := s.repoEntities.UpdateRepository(ctx, existing); updateErr != nil {
s.logger.Warn("failed to update repository local path",
zap.String("repository_id", existing.ID), zap.Error(updateErr))
}
}
return existing, nil
}
name := fmt.Sprintf("%s/%s", req.ProviderOwner, req.ProviderName)
return s.CreateRepository(ctx, &CreateRepositoryRequest{
WorkspaceID: req.WorkspaceID,
Name: name,
SourceType: "provider",
LocalPath: req.LocalPath,
Provider: req.Provider,
ProviderOwner: req.ProviderOwner,
ProviderName: req.ProviderName,
DefaultBranch: req.DefaultBranch,
})
}
func (s *Service) UpdateRepository(ctx context.Context, id string, req *UpdateRepositoryRequest) (*models.Repository, error) {
repository, err := s.repoEntities.GetRepository(ctx, id)
if err != nil {
return nil, err
}
if err := applyRepositoryUpdates(repository, req); err != nil {
return nil, err
}
repository.UpdatedAt = time.Now().UTC()
if err := s.repoEntities.UpdateRepository(ctx, repository); err != nil {
s.logger.Error("failed to update repository", zap.String("repository_id", id), zap.Error(err))
return nil, err
}
s.publishRepositoryEvent(ctx, events.RepositoryUpdated, repository)
s.logger.Info("repository updated", zap.String("repository_id", repository.ID))
return repository, nil
}
// applyRepositoryUpdates applies the non-nil fields from req onto repository.
// Returns an error if the WorktreeBranchPrefix is invalid.
func applyRepositoryUpdates(repository *models.Repository, req *UpdateRepositoryRequest) error {
if req.Name != nil {
repository.Name = *req.Name
}
if req.SourceType != nil {
repository.SourceType = *req.SourceType
}
if req.LocalPath != nil {
repository.LocalPath = *req.LocalPath
}
if req.Provider != nil {
repository.Provider = *req.Provider
}
if req.ProviderRepoID != nil {
repository.ProviderRepoID = *req.ProviderRepoID
}
if req.ProviderOwner != nil {
repository.ProviderOwner = *req.ProviderOwner
}
if req.ProviderName != nil {
repository.ProviderName = *req.ProviderName
}
if req.DefaultBranch != nil {
repository.DefaultBranch = *req.DefaultBranch
}
if req.WorktreeBranchPrefix != nil {
prefix := strings.TrimSpace(*req.WorktreeBranchPrefix)
if err := worktree.ValidateBranchPrefix(prefix); err != nil {
return fmt.Errorf("%w: %s", ErrInvalidRepositorySettings, err)
}
repository.WorktreeBranchPrefix = prefix
}
if req.PullBeforeWorktree != nil {
repository.PullBeforeWorktree = *req.PullBeforeWorktree
}
if req.SetupScript != nil {
repository.SetupScript = *req.SetupScript
}
if req.CleanupScript != nil {
repository.CleanupScript = *req.CleanupScript
}
if req.DevScript != nil {
repository.DevScript = *req.DevScript
}
return nil
}
func (s *Service) DeleteRepository(ctx context.Context, id string) error {
repository, err := s.repoEntities.GetRepository(ctx, id)
if err != nil {
return err
}
active, err := s.sessions.HasActiveTaskSessionsByRepository(ctx, id)
if err != nil {
s.logger.Error("failed to check active agent sessions for repository", zap.String("repository_id", id), zap.Error(err))
return err
}
if active {
return ErrActiveTaskSessions
}
if err := s.repoEntities.DeleteRepository(ctx, id); err != nil {
s.logger.Error("failed to delete repository", zap.String("repository_id", id), zap.Error(err))
return err
}
s.publishRepositoryEvent(ctx, events.RepositoryDeleted, repository)
s.logger.Info("repository deleted", zap.String("repository_id", id))
return nil
}
func (s *Service) ListRepositories(ctx context.Context, workspaceID string) ([]*models.Repository, error) {
return s.repoEntities.ListRepositories(ctx, workspaceID)
}
// Repository script operations
func (s *Service) CreateRepositoryScript(ctx context.Context, req *CreateRepositoryScriptRequest) (*models.RepositoryScript, error) {
script := &models.RepositoryScript{
ID: uuid.New().String(),
RepositoryID: req.RepositoryID,
Name: req.Name,
Command: req.Command,
Position: req.Position,
}
if err := s.repoEntities.CreateRepositoryScript(ctx, script); err != nil {
s.logger.Error("failed to create repository script", zap.Error(err))
return nil, err
}
s.publishRepositoryScriptEvent(ctx, events.RepositoryScriptCreated, script)
s.logger.Info("repository script created", zap.String("script_id", script.ID))
return script, nil
}
func (s *Service) GetRepositoryScript(ctx context.Context, id string) (*models.RepositoryScript, error) {
return s.repoEntities.GetRepositoryScript(ctx, id)
}
func (s *Service) UpdateRepositoryScript(ctx context.Context, id string, req *UpdateRepositoryScriptRequest) (*models.RepositoryScript, error) {
script, err := s.repoEntities.GetRepositoryScript(ctx, id)
if err != nil {
return nil, err
}
if req.Name != nil {
script.Name = *req.Name
}
if req.Command != nil {
script.Command = *req.Command
}
if req.Position != nil {
script.Position = *req.Position
}
script.UpdatedAt = time.Now().UTC()
if err := s.repoEntities.UpdateRepositoryScript(ctx, script); err != nil {
s.logger.Error("failed to update repository script", zap.String("script_id", id), zap.Error(err))
return nil, err
}
s.publishRepositoryScriptEvent(ctx, events.RepositoryScriptUpdated, script)
s.logger.Info("repository script updated", zap.String("script_id", script.ID))
return script, nil
}
func (s *Service) DeleteRepositoryScript(ctx context.Context, id string) error {
script, err := s.repoEntities.GetRepositoryScript(ctx, id)
if err != nil {
return err
}
if err := s.repoEntities.DeleteRepositoryScript(ctx, id); err != nil {
s.logger.Error("failed to delete repository script", zap.String("script_id", id), zap.Error(err))
return err
}
s.publishRepositoryScriptEvent(ctx, events.RepositoryScriptDeleted, script)
s.logger.Info("repository script deleted", zap.String("script_id", id))
return nil
}
func (s *Service) ListRepositoryScripts(ctx context.Context, repositoryID string) ([]*models.RepositoryScript, error) {
return s.repoEntities.ListRepositoryScripts(ctx, repositoryID)
}
// ListScriptsByRepositoryIDs returns scripts for multiple repositories in a single query.
func (s *Service) ListScriptsByRepositoryIDs(ctx context.Context, repoIDs []string) (map[string][]*models.RepositoryScript, error) {
return s.repoEntities.ListScriptsByRepositoryIDs(ctx, repoIDs)
}
// Executor operations
func (s *Service) CreateExecutor(ctx context.Context, req *CreateExecutorRequest) (*models.Executor, error) {
if err := validateExecutorConfig(req.Config); err != nil {
return nil, err
}
executor := &models.Executor{
ID: uuid.New().String(),
Name: req.Name,
Type: req.Type,
Status: req.Status,
IsSystem: req.IsSystem,
Resumable: req.Resumable,
Config: req.Config,
}
if err := s.executors.CreateExecutor(ctx, executor); err != nil {
return nil, err
}
s.publishExecutorEvent(ctx, events.ExecutorCreated, executor)
return executor, nil
}
func (s *Service) GetExecutor(ctx context.Context, id string) (*models.Executor, error) {
return s.executors.GetExecutor(ctx, id)
}
func (s *Service) UpdateExecutor(ctx context.Context, id string, req *UpdateExecutorRequest) (*models.Executor, error) {
executor, err := s.executors.GetExecutor(ctx, id)
if err != nil {
return nil, err
}
if err := validateExecutorUpdateRequest(executor, req); err != nil {
return nil, err
}
applyExecutorUpdates(executor, req)
executor.UpdatedAt = time.Now().UTC()
if err := s.executors.UpdateExecutor(ctx, executor); err != nil {
return nil, err
}
s.publishExecutorEvent(ctx, events.ExecutorUpdated, executor)
return executor, nil
}
// validateExecutorUpdateRequest validates config and system executor constraints.
func validateExecutorUpdateRequest(executor *models.Executor, req *UpdateExecutorRequest) error {
if req.Config != nil {
if err := validateExecutorConfig(req.Config); err != nil {
return err
}
}
if !executor.IsSystem {
return nil
}
if req.Name != nil && *req.Name != executor.Name {
return fmt.Errorf("system executors cannot be modified")
}
if req.Type != nil && *req.Type != executor.Type {
return fmt.Errorf("system executors cannot be modified")
}
if req.Status != nil && *req.Status != executor.Status {
return fmt.Errorf("system executors cannot be modified")
}
if req.Resumable != nil && *req.Resumable != executor.Resumable {
return fmt.Errorf("system executors cannot be modified")
}
return nil
}
// applyExecutorUpdates copies non-nil request fields onto the executor model.
func applyExecutorUpdates(executor *models.Executor, req *UpdateExecutorRequest) {
if req.Name != nil {
executor.Name = *req.Name
}
if req.Type != nil {
executor.Type = *req.Type
}
if req.Status != nil {
executor.Status = *req.Status
}
if req.Resumable != nil {
executor.Resumable = *req.Resumable
}
if req.Config != nil {
executor.Config = req.Config
}
}
func (s *Service) DeleteExecutor(ctx context.Context, id string) error {
executor, err := s.executors.GetExecutor(ctx, id)
if err != nil {
return err
}
if executor.IsSystem {
return fmt.Errorf("system executors cannot be deleted")
}
active, err := s.sessions.HasActiveTaskSessionsByExecutor(ctx, id)
if err != nil {
s.logger.Error("failed to check active agent sessions for executor", zap.String("executor_id", id), zap.Error(err))
return err
}
if active {
return ErrActiveTaskSessions
}
if err := s.executors.DeleteExecutor(ctx, id); err != nil {
return err
}
s.publishExecutorEvent(ctx, events.ExecutorDeleted, executor)
return nil
}
func (s *Service) ListExecutors(ctx context.Context) ([]*models.Executor, error) {
return s.executors.ListExecutors(ctx)
}
// Executor Profile operations
func (s *Service) CreateExecutorProfile(ctx context.Context, req *CreateExecutorProfileRequest) (*models.ExecutorProfile, error) {
if req.Name == "" {
return nil, fmt.Errorf("profile name is required")
}
if req.ExecutorID == "" {
return nil, fmt.Errorf("executor_id is required")
}
// Verify executor exists
executor, err := s.executors.GetExecutor(ctx, req.ExecutorID)
if err != nil {
return nil, fmt.Errorf("executor not found: %w", err)
}
if executor.Type == models.ExecutorTypeSprites && !hasSpritesToken(req.EnvVars) {
return nil, fmt.Errorf("sprites profiles require %s env var", spritesTokenEnvKey)
}
profile := &models.ExecutorProfile{
ExecutorID: req.ExecutorID,
Name: req.Name,
McpPolicy: req.McpPolicy,
Config: req.Config,
PrepareScript: req.PrepareScript,
CleanupScript: req.CleanupScript,
EnvVars: req.EnvVars,
}
if err := s.executors.CreateExecutorProfile(ctx, profile); err != nil {
return nil, err
}
s.publishExecutorProfileEvent(ctx, events.ExecutorProfileCreated, profile)
return profile, nil
}
func (s *Service) GetExecutorProfile(ctx context.Context, id string) (*models.ExecutorProfile, error) {
return s.executors.GetExecutorProfile(ctx, id)
}
func (s *Service) UpdateExecutorProfile(ctx context.Context, id string, req *UpdateExecutorProfileRequest) (*models.ExecutorProfile, error) {
profile, err := s.executors.GetExecutorProfile(ctx, id)
if err != nil {
return nil, err
}
executor, err := s.executors.GetExecutor(ctx, profile.ExecutorID)
if err != nil {
return nil, err
}
if req.Name != nil {
profile.Name = *req.Name
}
if req.McpPolicy != nil {
profile.McpPolicy = *req.McpPolicy
}
if req.Config != nil {
profile.Config = req.Config
}
if req.PrepareScript != nil {
profile.PrepareScript = *req.PrepareScript
}
if req.CleanupScript != nil {
profile.CleanupScript = *req.CleanupScript
}
if req.EnvVars != nil {
if executor.Type == models.ExecutorTypeSprites {
req.EnvVars = mergeSpritesTokenEnvVars(profile.EnvVars, req.EnvVars)
}
profile.EnvVars = req.EnvVars
}
if err := s.executors.UpdateExecutorProfile(ctx, profile); err != nil {
return nil, err
}
s.publishExecutorProfileEvent(ctx, events.ExecutorProfileUpdated, profile)
return profile, nil
}
func mergeSpritesTokenEnvVars(existing, incoming []models.ProfileEnvVar) []models.ProfileEnvVar {
merged := make([]models.ProfileEnvVar, 0, len(incoming)+1)
var existingToken models.ProfileEnvVar
hasExistingToken := false
hasIncomingToken := false
explicitRemove := false
for _, ev := range existing {
if strings.TrimSpace(ev.Key) != spritesTokenEnvKey {
continue
}
if strings.TrimSpace(ev.SecretID) == "" && strings.TrimSpace(ev.Value) == "" {
continue
}
existingToken = ev
hasExistingToken = true
break
}
for _, ev := range incoming {
if strings.TrimSpace(ev.Key) != spritesTokenEnvKey {
merged = append(merged, ev)
continue
}
if strings.TrimSpace(ev.SecretID) == "" && strings.TrimSpace(ev.Value) == "" {
explicitRemove = true
continue
}
hasIncomingToken = true
merged = append(merged, ev)
}
if !hasIncomingToken && !explicitRemove && hasExistingToken {
merged = append(merged, existingToken)
}
return merged
}
func hasSpritesToken(envVars []models.ProfileEnvVar) bool {
for _, ev := range envVars {
if strings.TrimSpace(ev.Key) != spritesTokenEnvKey {
continue
}
if strings.TrimSpace(ev.SecretID) != "" || strings.TrimSpace(ev.Value) != "" {
return true
}
}
return false
}
func (s *Service) DeleteExecutorProfile(ctx context.Context, id string) error {
profile, err := s.executors.GetExecutorProfile(ctx, id)
if err != nil {
return err
}
if err := s.executors.DeleteExecutorProfile(ctx, id); err != nil {
return err
}
s.publishExecutorProfileEvent(ctx, events.ExecutorProfileDeleted, profile)
return nil
}
func (s *Service) ListExecutorProfiles(ctx context.Context, executorID string) ([]*models.ExecutorProfile, error) {
return s.executors.ListExecutorProfiles(ctx, executorID)
}
func (s *Service) ListAllExecutorProfiles(ctx context.Context) ([]*models.ExecutorProfile, error) {
return s.executors.ListAllExecutorProfiles(ctx)
}
// Environment operations
func (s *Service) CreateEnvironment(ctx context.Context, req *CreateEnvironmentRequest) (*models.Environment, error) {
environment := &models.Environment{
ID: uuid.New().String(),
Name: req.Name,
Kind: req.Kind,
IsSystem: false,
WorktreeRoot: req.WorktreeRoot,
ImageTag: req.ImageTag,
Dockerfile: req.Dockerfile,
BuildConfig: req.BuildConfig,
}
if err := s.environments.CreateEnvironment(ctx, environment); err != nil {
return nil, err
}
s.publishEnvironmentEvent(ctx, events.EnvironmentCreated, environment)
return environment, nil
}
func (s *Service) GetEnvironment(ctx context.Context, id string) (*models.Environment, error) {
return s.environments.GetEnvironment(ctx, id)
}
func (s *Service) UpdateEnvironment(ctx context.Context, id string, req *UpdateEnvironmentRequest) (*models.Environment, error) {
environment, err := s.environments.GetEnvironment(ctx, id)
if err != nil {
return nil, err
}
if environment.IsSystem {
if req.Name != nil || req.Kind != nil || req.ImageTag != nil || req.Dockerfile != nil || req.BuildConfig != nil {
return nil, fmt.Errorf("system environments can only update the worktree root")
}
}
if req.Name != nil {
environment.Name = *req.Name
}
if req.Kind != nil {
environment.Kind = *req.Kind
}
if req.WorktreeRoot != nil {
environment.WorktreeRoot = *req.WorktreeRoot
}
if req.ImageTag != nil {
environment.ImageTag = *req.ImageTag
}
if req.Dockerfile != nil {
environment.Dockerfile = *req.Dockerfile
}
if req.BuildConfig != nil {
environment.BuildConfig = req.BuildConfig
}
environment.UpdatedAt = time.Now().UTC()
if err := s.environments.UpdateEnvironment(ctx, environment); err != nil {
return nil, err
}
s.publishEnvironmentEvent(ctx, events.EnvironmentUpdated, environment)
return environment, nil
}
func (s *Service) DeleteEnvironment(ctx context.Context, id string) error {
environment, err := s.environments.GetEnvironment(ctx, id)
if err != nil {
return err
}
if environment.IsSystem {
return fmt.Errorf("system environments cannot be deleted")
}
active, err := s.sessions.HasActiveTaskSessionsByEnvironment(ctx, id)
if err != nil {
s.logger.Error("failed to check active agent sessions for environment", zap.String("environment_id", id), zap.Error(err))
return err
}
if active {
return ErrActiveTaskSessions
}
if err := s.environments.DeleteEnvironment(ctx, id); err != nil {
return err
}
s.publishEnvironmentEvent(ctx, events.EnvironmentDeleted, environment)
return nil
}
func (s *Service) ListEnvironments(ctx context.Context) ([]*models.Environment, error) {
return s.environments.ListEnvironments(ctx)
}