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
+21 -5
View File
@@ -26,6 +26,7 @@ type State struct {
Ahead int `json:"ahead"`
Behind int `json:"behind"`
Remotes []string `json:"remotes"`
Forge string `json:"forge,omitempty"` // forge this project belongs to (derived from its remote)
UpdatedAt time.Time `json:"updatedAt"`
Error string `json:"error,omitempty"` // set if refreshing this repo failed
}
@@ -69,6 +70,9 @@ type Config struct {
MaxDepth int
Ignore []string
Fetch bool
// ForgeFor maps a repo's origin remote URL to a display label for the forge it
// belongs to (a configured forge's name, else the bare host, else ""). May be nil.
ForgeFor func(remoteURL string) string
}
// ConfigFunc supplies the current scan configuration.
@@ -123,7 +127,7 @@ func (s *Scanner) Refresh(ctx context.Context) {
return
default:
}
s.Index.set(s.refreshOne(ctx, p, cfg.Fetch))
s.Index.set(s.refreshOne(ctx, p, cfg.Fetch, cfg.ForgeFor))
}
}
@@ -131,10 +135,11 @@ func (s *Scanner) Refresh(ctx context.Context) {
// mutating action so the UI reflects the new state without waiting for the next
// full scan. It never fetches (network) — it only re-reads local state.
func (s *Scanner) RefreshRepo(ctx context.Context, path string) {
s.Index.set(s.refreshOne(ctx, path, false))
cfg := s.cfgFn(ctx)
s.Index.set(s.refreshOne(ctx, path, false, cfg.ForgeFor))
}
func (s *Scanner) refreshOne(ctx context.Context, path string, fetch bool) State {
func (s *Scanner) refreshOne(ctx context.Context, path string, fetch bool, forgeFor func(string) string) State {
rctx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
@@ -161,8 +166,19 @@ func (s *Scanner) refreshOne(ctx context.Context, path string, fetch bool) State
st.Ahead, st.Behind, _ = s.git.AheadBehind(rctx, path)
if remotes, err := s.git.Remotes(rctx, path); err == nil {
st.Remotes = remotes
// Remote names, plus the origin URL used to derive which forge this project
// belongs to (§ "get it from git").
if details, err := s.git.RemoteDetails(rctx, path); err == nil {
var originURL string
for _, rm := range details {
st.Remotes = append(st.Remotes, rm.Name)
if rm.Name == "origin" || originURL == "" {
originURL = rm.URL
}
}
if forgeFor != nil && originURL != "" {
st.Forge = forgeFor(originURL)
}
}
return st