Config: .env = projects root only; derive each project's forge from git

Reduce .env to just REPOS_HOST_PATH (the projects root); runtime bootstrap moves to compose/defaults. Projects are the subdirectories of the single root — removed the project-directories feature (store table, service methods, /api/config/project-dirs, Settings section). Each project's forge is derived from its git remote (matched to a configured forge, else the bare host) and shown as a pill next to its name (State.Forge via scanner ForgeFor + svc.ForgeDisplay). Removed first-run .env seeding; forges + identity are managed in Settings. Added forge.HostOf. AGENT.md updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-22 05:52:03 -04:00
parent e30c3b632a
commit 32ae17cc9f
15 changed files with 175 additions and 434 deletions
+20 -47
View File
@@ -9,7 +9,6 @@ import (
"context"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"sync"
@@ -391,46 +390,6 @@ func (s *Service) DeleteForge(ctx context.Context, id int64) error {
return nil
}
// ProjectDirs lists configured project directories.
func (s *Service) ProjectDirs(ctx context.Context) ([]store.ProjectDir, error) {
return s.store.ListProjectDirs(ctx)
}
// AddProjectDir adds a directory to scan. The path must exist inside the
// container (i.e. be under a mounted base) and be a directory.
func (s *Service) AddProjectDir(ctx context.Context, path string) (store.ProjectDir, error) {
path = filepath.Clean(strings.TrimSpace(path))
if path == "" {
return store.ProjectDir{}, fmt.Errorf("a path is required")
}
info, err := os.Stat(path)
if err != nil {
return store.ProjectDir{}, fmt.Errorf("path not found in the container: %s (is it under a mounted directory?)", path)
}
if !info.IsDir() {
return store.ProjectDir{}, fmt.Errorf("not a directory: %s", path)
}
d, err := s.store.AddProjectDir(ctx, path)
if err != nil {
return store.ProjectDir{}, err
}
s.feed.Record(activity.ActorUser, "project-dir-added", path, "")
if s.refresh == nil {
return d, nil
}
return d, nil
}
// SetProjectDirEnabled toggles a project directory.
func (s *Service) SetProjectDirEnabled(ctx context.Context, id int64, enabled bool) error {
return s.store.SetProjectDirEnabled(ctx, id, enabled)
}
// DeleteProjectDir removes a project directory.
func (s *Service) DeleteProjectDir(ctx context.Context, id int64) error {
return s.store.DeleteProjectDir(ctx, id)
}
// GitIdentity returns the configured commit identity.
func (s *Service) GitIdentity(ctx context.Context) (name, email string) {
name, _ = s.store.GetSetting(ctx, "git_user_name", "")
@@ -450,13 +409,27 @@ func (s *Service) SetGitIdentity(ctx context.Context, name, email string) error
return nil
}
// ScanRoots returns the enabled project directories (for the scanner).
func (s *Service) ScanRoots(ctx context.Context) []string {
roots, err := s.store.EnabledRoots(ctx)
if err != nil && s.log != nil {
s.log.Warn("could not read project dirs", "err", err)
// ForgeDisplay maps a repo's origin remote URL to a label for the forge it
// belongs to: a configured forge's name (or its host) when the remote host
// matches one, otherwise the bare remote host, otherwise "". Used by the scanner
// to show each project's forge next to its name.
func (s *Service) ForgeDisplay(ctx context.Context, remoteURL string) string {
host, _, _, ok := forge.ParseRemote(remoteURL)
if !ok || host == "" {
return ""
}
return roots
forges, err := s.store.ListForges(ctx)
if err == nil {
for _, f := range forges {
if strings.EqualFold(forge.HostOf(f.BaseURL), host) {
if f.Name != "" {
return f.Name
}
return host
}
}
}
return host // not a configured forge, but still informative
}
// ApplyGitConfig writes the container's git global config from the store: a