Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

281 lines
7.8 KiB
Go

package server
import (
"context"
"database/sql"
"errors"
"net/http"
"strconv"
"strings"
"go.kenn.io/agentsview/internal/db"
"go.kenn.io/agentsview/internal/parser"
)
func (s *Server) registerSettingsRoutes() {
group := newRouteGroup(s.api, "/api/v1/settings", "Settings")
get(s, group, "", "Get settings", s.humaGetSettings)
put(s, group, "", "Update settings", s.humaUpdateSettings)
get(s, group, "/worktree-mappings", "List worktree mappings", s.humaListWorktreeMappings)
post(s, group, "/worktree-mappings", "Create worktree mapping", s.humaCreateWorktreeMapping)
put(s, group, "/worktree-mappings/{id}", "Update worktree mapping", s.humaUpdateWorktreeMapping)
deleteRoute(s, group, "/worktree-mappings/{id}", "Delete worktree mapping", s.humaDeleteWorktreeMapping)
post(s, group, "/worktree-mappings/apply", "Apply worktree mappings", s.humaApplyWorktreeMappings)
}
type settingsInput struct {
Body settingsUpdateRequest
}
type worktreeMappingCreateInput struct {
Body worktreeMappingRequest
}
type worktreeMappingUpdateInput struct {
ID string `path:"id" required:"true" doc:"Mapping ID"`
Body worktreeMappingRequest
}
type worktreeMappingPathInput struct {
ID string `path:"id" required:"true" doc:"Mapping ID"`
}
func (s *Server) humaGetSettings(
ctx context.Context,
_ *emptyInput,
) (*jsonOutput[settingsResponse], error) {
s.mu.RLock()
dirs := make(map[string][]string)
for _, def := range parser.Registry {
if !def.FileBased && def.EnvVar == "" {
continue
}
d := s.cfg.AgentDirs[def.Type]
if d == nil {
d = []string{}
}
dirs[string(def.Type)] = d
}
tc := s.cfg.Terminal
if tc.Mode == "" {
tc.Mode = string(terminalModeAuto)
}
githubToken := s.cfg.GithubToken
resp := settingsResponse{
AgentDirs: dirs,
Terminal: terminalResponse{
Mode: tc.Mode,
CustomBin: tc.CustomBin,
CustomArgs: tc.CustomArgs,
},
Host: s.cfg.Host,
Port: s.cfg.Port,
RequireAuth: s.cfg.RequireAuth,
ReadOnly: s.db.ReadOnly(),
}
if isLocalhostContext(ctx) {
resp.AuthToken = s.cfg.AuthToken
}
s.mu.RUnlock()
resp.GithubConfigured = resolveGitHubToken(ctx, githubToken) != ""
return &jsonOutput[settingsResponse]{Body: resp}, nil
}
func (s *Server) humaUpdateSettings(
ctx context.Context,
in *settingsInput,
) (*jsonOutput[settingsResponse], error) {
if s.db.ReadOnly() {
return nil, apiError(http.StatusNotImplemented,
"settings cannot be modified in read-only mode")
}
if in.Body.Terminal != nil {
return nil, apiError(http.StatusBadRequest,
"terminal config must be updated via POST /api/v1/config/terminal")
}
patch := make(map[string]any)
if in.Body.AuthToken != nil {
patch["auth_token"] = *in.Body.AuthToken
}
if in.Body.RequireAuth != nil {
patch["require_auth"] = *in.Body.RequireAuth
}
if len(patch) > 0 {
s.mu.Lock()
err := s.cfg.SaveSettings(patch)
if err == nil && s.cfg.RequireAuth {
err = s.cfg.EnsureAuthToken()
}
s.mu.Unlock()
if err != nil {
return nil, internalError("save settings", err)
}
}
return s.humaGetSettings(ctx, &emptyInput{})
}
func (s *Server) localWorktreeMappingHumaDB() (*db.DB, string, error) {
localDB, ok := s.db.(*db.DB)
if !ok || localDB == nil || localDB.ReadOnly() || s.engine == nil {
return nil, "", apiError(http.StatusNotImplemented, "not available in remote mode")
}
machine := strings.TrimSpace(s.engine.Machine())
if machine == "" {
machine = s.cfg.LocalMachineName
}
return localDB, machine, nil
}
func (s *Server) humaListWorktreeMappings(
ctx context.Context,
_ *emptyInput,
) (*jsonOutput[worktreeMappingsResponse], error) {
localDB, machine, err := s.localWorktreeMappingHumaDB()
if err != nil {
return nil, err
}
mappings, err := localDB.ListWorktreeProjectMappings(ctx, machine)
if err != nil {
return nil, internalError("list worktree mappings", err)
}
return &jsonOutput[worktreeMappingsResponse]{
Body: worktreeMappingsResponse{Machine: machine, Mappings: mappings},
}, nil
}
func (s *Server) humaCreateWorktreeMapping(
ctx context.Context,
in *worktreeMappingCreateInput,
) (*createdOutput[db.WorktreeProjectMapping], error) {
localDB, machine, err := s.localWorktreeMappingHumaDB()
if err != nil {
return nil, err
}
if in.Body.PathPrefix == nil {
return nil, apiError(http.StatusBadRequest, "path_prefix is required")
}
enabled := true
if in.Body.Enabled != nil {
enabled = *in.Body.Enabled
}
mapping, err := localDB.CreateWorktreeProjectMapping(ctx, db.WorktreeProjectMapping{
Machine: machine,
PathPrefix: *in.Body.PathPrefix,
Layout: valueOrDefault(in.Body.Layout, db.WorktreeMappingLayoutExplicit),
Project: stringValueOrEmpty(in.Body.Project),
Enabled: enabled,
})
if err != nil {
return nil, humaWorktreeMappingError(err)
}
return &createdOutput[db.WorktreeProjectMapping]{Status: http.StatusCreated, Body: mapping}, nil
}
func (s *Server) humaUpdateWorktreeMapping(
ctx context.Context,
in *worktreeMappingUpdateInput,
) (*jsonOutput[db.WorktreeProjectMapping], error) {
localDB, machine, err := s.localWorktreeMappingHumaDB()
if err != nil {
return nil, err
}
id, err := parseWorktreeMappingHumaID(in.ID)
if err != nil {
return nil, err
}
if in.Body.PathPrefix == nil || in.Body.Enabled == nil {
return nil, apiError(http.StatusBadRequest,
"path_prefix and enabled are required")
}
mapping, err := localDB.UpdateWorktreeProjectMapping(ctx, machine, id, db.WorktreeProjectMapping{
PathPrefix: *in.Body.PathPrefix,
Layout: valueOrDefault(in.Body.Layout, db.WorktreeMappingLayoutExplicit),
Project: stringValueOrEmpty(in.Body.Project),
Enabled: *in.Body.Enabled,
})
if err != nil {
return nil, humaWorktreeMappingError(err)
}
return &jsonOutput[db.WorktreeProjectMapping]{Body: mapping}, nil
}
func (s *Server) humaDeleteWorktreeMapping(
ctx context.Context,
in *worktreeMappingPathInput,
) (*noContentOutput, error) {
localDB, machine, err := s.localWorktreeMappingHumaDB()
if err != nil {
return nil, err
}
id, err := parseWorktreeMappingHumaID(in.ID)
if err != nil {
return nil, err
}
err = localDB.DeleteWorktreeProjectMapping(ctx, machine, id)
if errors.Is(err, sql.ErrNoRows) {
return nil, apiError(http.StatusNotFound, "mapping not found")
}
if err != nil {
return nil, internalError("delete worktree mapping", err)
}
return &noContentOutput{Status: http.StatusNoContent}, nil
}
func parseWorktreeMappingHumaID(raw string) (int64, error) {
id, err := strconv.ParseInt(raw, 10, 64)
if err != nil || id <= 0 {
return 0, apiError(http.StatusNotFound, "mapping not found")
}
return id, nil
}
func (s *Server) humaApplyWorktreeMappings(
ctx context.Context,
_ *emptyInput,
) (*jsonOutput[applyWorktreeMappingsResponse], error) {
localDB, machine, err := s.localWorktreeMappingHumaDB()
if err != nil {
return nil, err
}
result, err := localDB.ApplyWorktreeProjectMappings(ctx, machine)
if err != nil {
return nil, internalError("apply worktree mappings", err)
}
return &jsonOutput[applyWorktreeMappingsResponse]{
Body: applyWorktreeMappingsResponse{
Machine: machine,
ApplyWorktreeProjectMappingsResult: result,
},
}, nil
}
func humaWorktreeMappingError(err error) error {
switch {
case errors.Is(err, db.ErrWorktreeMappingInvalid):
return apiError(http.StatusBadRequest, err.Error())
case strings.Contains(err.Error(), "required"):
return apiError(http.StatusBadRequest, err.Error())
case errors.Is(err, db.ErrWorktreeMappingDuplicate):
return apiError(http.StatusConflict, "worktree mapping already exists")
case errors.Is(err, sql.ErrNoRows):
return apiError(http.StatusNotFound, "mapping not found")
default:
return internalError("worktree mapping write", err)
}
}
func valueOrDefault(value *string, fallback string) string {
if value == nil {
return fallback
}
return *value
}
func stringValueOrEmpty(value *string) string {
if value == nil {
return ""
}
return *value
}