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
281 lines
7.8 KiB
Go
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
|
|
}
|