package workspaceprovision import ( "context" "database/sql" "errors" "fmt" "os" "path/filepath" "strings" "inbox/internal/app/workspaceruntime" "inbox/internal/base/slug" "inbox/internal/domain/workspace" ) type store interface { GetProject(context.Context, string) (workspace.Project, error) GetProjectByRootPath(context.Context, string) (workspace.Project, error) GetProjectBySlug(context.Context, string) (workspace.Project, error) CreateProject(context.Context, workspace.Project) (workspace.Project, error) UpdateProjectDefaultBranch(context.Context, string, string) error GetWorkspace(context.Context, string) (workspace.Workspace, error) GetWorkspaceByProjectAndSlug(context.Context, string, string) (workspace.Workspace, error) CreateWorkspace(context.Context, workspace.Workspace) (workspace.Workspace, error) UpdateWorkspace(context.Context, workspace.Workspace) error } type Service struct { store store workspacesDir string runtime runtimeManager } type ProvisionRequest struct { ProjectDir string Name string } type runtimeManager interface { EnsureRepository(ctx context.Context, projectDir string) (string, error) Ensure(ctx context.Context, workspaceID string) (workspace.Workspace, workspaceruntime.Runtime, error) } func NewService(store store, workspacesDir string, runtime runtimeManager) *Service { return &Service{ store: store, workspacesDir: strings.TrimSpace(workspacesDir), runtime: runtime, } } func (s *Service) Provision(ctx context.Context, req ProvisionRequest) (workspace.Workspace, workspaceruntime.Runtime, error) { if s.runtime == nil { return workspace.Workspace{}, workspaceruntime.Runtime{}, fmt.Errorf("workspace runtime is not configured") } projectDir, err := filepath.Abs(strings.TrimSpace(req.ProjectDir)) if err != nil { return workspace.Workspace{}, workspaceruntime.Runtime{}, fmt.Errorf("resolve project dir: %w", err) } info, err := os.Stat(projectDir) if err != nil { return workspace.Workspace{}, workspaceruntime.Runtime{}, fmt.Errorf("stat project dir: %w", err) } if !info.IsDir() { return workspace.Workspace{}, workspaceruntime.Runtime{}, fmt.Errorf("project dir is not a directory: %s", projectDir) } projectName := filepath.Base(projectDir) projectSlug := slug.Normalize(projectName) if projectSlug == "" { projectSlug = "project" } workspaceSlug := slug.Normalize(firstNonEmpty(req.Name, projectName)) if workspaceSlug == "" { workspaceSlug = projectSlug } baseBranch, err := s.runtime.EnsureRepository(ctx, projectDir) if err != nil { return workspace.Workspace{}, workspaceruntime.Runtime{}, err } project, err := s.ensureProject(ctx, projectDir, projectName, projectSlug, baseBranch) if err != nil { return workspace.Workspace{}, workspaceruntime.Runtime{}, err } ws, err := s.store.GetWorkspaceByProjectAndSlug(ctx, project.ID, workspaceSlug) if err != nil { if !errors.Is(err, sql.ErrNoRows) { return workspace.Workspace{}, workspaceruntime.Runtime{}, err } ws = workspace.NormalizeWorkspaceForCreate(workspace.Workspace{ ProjectID: project.ID, Slug: workspaceSlug, Name: workspaceSlug, RootPath: filepath.Join(s.workspacesDir, workspaceSlug), BaseBranch: baseBranch, }) ws, err = s.store.CreateWorkspace(ctx, ws) if err != nil { return workspace.Workspace{}, workspaceruntime.Runtime{}, err } } ws = workspace.ApplyManagedRuntimeConfig(ws, s.workspacesDir, baseBranch) if err := s.store.UpdateWorkspace(ctx, ws); err != nil { return workspace.Workspace{}, workspaceruntime.Runtime{}, err } return s.runtime.Ensure(ctx, ws.ID) } func (s *Service) ensureProject(ctx context.Context, projectDir, projectName, projectSlug, baseBranch string) (workspace.Project, error) { project, err := s.store.GetProjectByRootPath(ctx, projectDir) if err != nil { if !errors.Is(err, sql.ErrNoRows) { return workspace.Project{}, err } project = workspace.NormalizeProjectForCreate(workspace.Project{ Slug: projectSlug, Name: projectName, RootPath: projectDir, DefaultBranch: baseBranch, }) created, createErr := s.store.CreateProject(ctx, project) if createErr == nil { return created, nil } if existing, lookupErr := s.store.GetProjectBySlug(ctx, projectSlug); lookupErr == nil && existing.RootPath == projectDir { project = existing } else { return workspace.Project{}, createErr } } if project.DefaultBranch != baseBranch { if err := s.store.UpdateProjectDefaultBranch(ctx, project.ID, baseBranch); err != nil { return workspace.Project{}, err } project.DefaultBranch = baseBranch } return project, nil } func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return value } } return "" }