package api import ( "bytes" "context" "net/http" "os" "os/exec" "strings" "time" ) const scriptTimeout = 30 * time.Second // ScriptRegistry maps endpoint names to executable script paths. // Only registered scripts are exposed via POST /api/scripts/{name}. type ScriptRegistry map[string]string // SetScripts configures the script registry. The mux is not re-registered; // the handler looks up the registry at request time, so this may be called // after NewServer but before the first request. func (s *Server) SetScripts(r ScriptRegistry) { s.scripts = r } func (s *Server) handleScript(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") scriptPath, ok := s.scripts[name] if !ok { writeJSON(w, http.StatusNotFound, map[string]string{"error": "script not found: " + name}) return } ctx, cancel := context.WithTimeout(r.Context(), scriptTimeout) defer cancel() cmd := exec.CommandContext(ctx, scriptPath) cmd.Env = os.Environ() for k, v := range r.URL.Query() { if len(v) > 0 { cmd.Env = append(cmd.Env, "CLAUDOMATOR_"+strings.ToUpper(k)+"="+v[0]) } } var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr err := cmd.Run() exitCode := 0 if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { exitCode = exitErr.ExitCode() s.logger.Warn("script exited non-zero", "name", name, "exit_code", exitCode, "stderr", stderr.String()) } else { s.logger.Error("script execution failed", "name", name, "error", err, "path", scriptPath) writeJSON(w, http.StatusInternalServerError, map[string]string{ "error": "script execution failed", }) return } } writeJSON(w, http.StatusOK, map[string]interface{}{ "output": stdout.String(), "exit_code": exitCode, }) }