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:
+17
-83
@@ -6,7 +6,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -30,36 +29,6 @@ import (
|
||||
"gitmanager/internal/store"
|
||||
)
|
||||
|
||||
// seedStore migrates config from .env into the store on first run only, so an
|
||||
// existing deployment keeps working after the switch to the DB (§1.3).
|
||||
func seedStore(ctx context.Context, st *store.Store, cfg config.Config, log *slog.Logger) {
|
||||
empty, err := st.IsEmpty(ctx)
|
||||
if err != nil {
|
||||
log.Warn("config store check failed", "err", err)
|
||||
return
|
||||
}
|
||||
if !empty {
|
||||
return
|
||||
}
|
||||
log.Info("seeding config store from environment (first run)")
|
||||
if cfg.GiteaURL != "" && cfg.GiteaToken != "" {
|
||||
if _, err := st.AddForge(ctx, store.Forge{Name: "Gitea", Kind: "gitea", BaseURL: strings.TrimRight(cfg.GiteaURL, "/"), Token: cfg.GiteaToken}); err != nil {
|
||||
log.Warn("seed forge failed", "err", err)
|
||||
}
|
||||
}
|
||||
for _, r := range cfg.RepoRoots {
|
||||
if _, err := st.AddProjectDir(ctx, r); err != nil {
|
||||
log.Warn("seed project dir failed", "dir", r, "err", err)
|
||||
}
|
||||
}
|
||||
if cfg.GitUserName != "" {
|
||||
_ = st.SetSetting(ctx, "git_user_name", cfg.GitUserName)
|
||||
}
|
||||
if cfg.GitUserEmail != "" {
|
||||
_ = st.SetSetting(ctx, "git_user_email", cfg.GitUserEmail)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
@@ -81,38 +50,40 @@ func main() {
|
||||
log.Info("git detected", "version", v)
|
||||
}
|
||||
|
||||
// Config store (forges, project dirs, git identity) on a private volume (§1.3).
|
||||
// Config store (forges + tokens, git identity) on a private volume (§1.3).
|
||||
st, err := store.Open(cfg.DBPath)
|
||||
if err != nil {
|
||||
log.Error("failed to open config store", "path", cfg.DBPath, "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer st.Close()
|
||||
seedStore(context.Background(), st, cfg, log)
|
||||
|
||||
// Coordination state: active project + activity feed (§8.2).
|
||||
feed := activity.New(log, 200)
|
||||
|
||||
// The read-only scanner reads its roots fresh from the store each cycle, so
|
||||
// project-directory changes take effect without a restart (§5).
|
||||
// The scanner walks the single projects root; each subdirectory is a project.
|
||||
// Each project's forge is derived from its git remote (svc.ForgeDisplay).
|
||||
var svc *service.Service
|
||||
scanCfg := func(ctx context.Context) repos.Config {
|
||||
roots, err := st.EnabledRoots(ctx)
|
||||
if err != nil {
|
||||
log.Warn("could not read project dirs", "err", err)
|
||||
return repos.Config{
|
||||
Roots: []string{cfg.ProjectsRoot},
|
||||
MaxDepth: cfg.ScanMaxDepth,
|
||||
Ignore: cfg.ScanIgnore,
|
||||
Fetch: cfg.ScanFetchEnabled,
|
||||
ForgeFor: func(remoteURL string) string { return svc.ForgeDisplay(ctx, remoteURL) },
|
||||
}
|
||||
return repos.Config{Roots: roots, MaxDepth: cfg.ScanMaxDepth, Ignore: cfg.ScanIgnore, Fetch: cfg.ScanFetchEnabled}
|
||||
}
|
||||
scanner := repos.NewScanner(g, log, scanCfg, cfg.ScanInterval)
|
||||
scanCtx, stopScan := context.WithCancel(context.Background())
|
||||
defer stopScan()
|
||||
go scanner.Run(scanCtx)
|
||||
log.Info("scanner started", "interval", cfg.ScanInterval.String())
|
||||
|
||||
// The one service layer both the HTTP API and the MCP server call (§1.7).
|
||||
// scanner.RefreshRepo lets a mutating action re-scan just that repo.
|
||||
svc := service.New(g, scanner.Index, feed, st, log, scanner.RefreshRepo)
|
||||
// Configure git (identity + per-forge auth) from the store.
|
||||
svc.ApplyGitConfig(context.Background())
|
||||
svc = service.New(g, scanner.Index, feed, st, log, scanner.RefreshRepo)
|
||||
svc.ApplyGitConfig(context.Background()) // git identity + per-forge auth from the store
|
||||
|
||||
scanCtx, stopScan := context.WithCancel(context.Background())
|
||||
defer stopScan()
|
||||
go scanner.Run(scanCtx) // started after svc is set, so ForgeFor is ready
|
||||
log.Info("scanner started", "root", cfg.ProjectsRoot, "interval", cfg.ScanInterval.String())
|
||||
|
||||
tmpl, err := render.New("web/templates")
|
||||
if err != nil {
|
||||
@@ -258,43 +229,6 @@ func main() {
|
||||
return c.JSON(http.StatusOK, map[string]bool{"ok": true})
|
||||
})
|
||||
|
||||
e.GET("/api/config/project-dirs", func(c echo.Context) error {
|
||||
dirs, err := svc.ProjectDirs(c.Request().Context())
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, dirs)
|
||||
})
|
||||
e.POST("/api/config/project-dirs", func(c echo.Context) error {
|
||||
var b struct{ Path string }
|
||||
if err := c.Bind(&b); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid body"})
|
||||
}
|
||||
d, err := svc.AddProjectDir(c.Request().Context(), b.Path)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, d)
|
||||
})
|
||||
e.PUT("/api/config/project-dirs/:id", func(c echo.Context) error {
|
||||
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
var b struct{ Enabled bool }
|
||||
if err := c.Bind(&b); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid body"})
|
||||
}
|
||||
if err := svc.SetProjectDirEnabled(c.Request().Context(), id, b.Enabled); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]bool{"ok": true})
|
||||
})
|
||||
e.DELETE("/api/config/project-dirs/:id", func(c echo.Context) error {
|
||||
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err := svc.DeleteProjectDir(c.Request().Context(), id); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]bool{"ok": true})
|
||||
})
|
||||
|
||||
e.GET("/api/config/identity", func(c echo.Context) error {
|
||||
name, email := svc.GitIdentity(c.Request().Context())
|
||||
return c.JSON(http.StatusOK, map[string]string{"name": name, "email": email})
|
||||
|
||||
Reference in New Issue
Block a user