diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee57cf6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Build outputs +bin/ + +# Frontend generated artifacts +web/node_modules/ +web/dist/ + +# Embedded frontend build output (keep lightweight placeholder only) +internal/static/dist/assets/ + +# OS/editor noise +.DS_Store +*.swp diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0bd3b19 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,48 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- `cmd/supervisor/`: Go entrypoint (`main.go`) for the single executable. +- `internal/`: backend application code. +- `internal/httpserver/`: HTTP routes, middleware, REST and WebSocket handlers. +- `internal/session/`: PTY process runtime, session lifecycle, buffering, and manager logic. +- `internal/static/`: embedded frontend assets (`go:embed` serves `internal/static/dist`). +- `web/`: Vue 3 + TypeScript frontend (Vite, Pinia, Vue Router, xterm). +- `scripts/`: build helpers for frontend and full build pipeline. +- `bin/`: compiled artifacts (generated). + +## Build, Test, and Development Commands +- `make frontend-build`: installs web dependencies, runs Vite production build, copies `web/dist` to `internal/static/dist`. +- `make backend-build`: builds Go binary at `bin/supervisor`. +- `make build`: full build (frontend + backend). +- `make run`: builds everything and starts the app. +- `cd web && npm run dev`: frontend dev server with proxy to backend (`:8080`). +- `go test ./...`: run backend unit tests (when tests are present). + +Do not edit `internal/static/dist` by hand; it is generated from `web/dist` by `make frontend-build`. + +## Coding Style & Naming Conventions +- Go: format with `gofmt`; package names lowercase; exported symbols in `CamelCase`; errors as `err` with wrapped context. +- Vue/TS: components in `PascalCase.vue`, stores and API modules in `camelCase.ts`. +- Keep handlers thin: validation + transport only; put lifecycle/process logic in `internal/session`. +- Prefer explicit JSON DTOs over ad-hoc maps except small error responses. +- Keep session state rules centralized in `internal/session/manager.go` (example: only non-running sessions can be deleted). + +## Testing Guidelines +- Backend tests use Go’s standard `testing` package. +- Place tests alongside code as `*_test.go` (example: `internal/session/manager_test.go`). +- Prioritize tests for session lifecycle (`create/start/input/resize/stop`) and WebSocket message handling. +- Frontend automated tests are not configured yet; if added, keep them under `web/src/**/__tests__`. + +## Commit & Pull Request Guidelines +- Existing history uses short, direct commit messages (often imperative). Keep subject concise, one logical change per commit. +- Recommended pattern: `area: action` (example: `session: handle resize messages`). +- PRs should include a summary of behavior changes. +- List impacted paths and manual verification commands. +- Add screenshots/GIFs for UI changes. +- Link the related issue/task when available. + +## Security & Configuration Tips +- Default bind is `:8080`; override with `SUPERVISOR_ADDR`. +- Do not commit secrets or environment-specific credentials. +- PTY commands are user-supplied; keep future changes mindful of command execution risk. +- Session deletion endpoint is `DELETE /api/sessions/{id}`; preserve current API semantics (`409` for running sessions, `204` on success). diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c0cbe91 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +.PHONY: frontend-build backend-build build run + +frontend-build: + ./scripts/build-frontend.sh + +backend-build: + go build -o ./bin/supervisor ./cmd/supervisor + +build: frontend-build backend-build + +run: build + ./bin/supervisor diff --git a/README.md b/README.md index 5f38860..f714026 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,123 @@ -# Supervisor +# Supervisor Prototype -Supervisor is a lightweight multi-agent orchestration tool for LLM workflows. +Supervisor je prvý funkčný prototyp ako jedna Go aplikácia s embednutým Vue frontendom. -It runs multiple AI agents under the control of a central supervisor that plans tasks, -delegates them to worker agents, and evaluates their results. The system provides -a web interface to observe, control, and intervene in agent workflows in real time. +## Čo projekt robí -Designed for development workflows, Supervisor allows agents to analyze code, -generate patches, run tools, review changes, and collaborate on complex tasks -while keeping a human in the loop when needed. +- spúšťa paralelné externé konzolové procesy v PTY session +- session bežia ďalej nezávisle od pripojeného browsera +- streamuje live terminál výstup cez WebSocket +- prijíma input z klávesnice (xterm.js) aj programový input cez HTTP API +- drží metadata a scrollback output v in-memory store +- podporuje odstránenie ukončených session (status `stopped`, `exited`, `error`) -The core is written in Go and intended to run inside an isolated environment -(such as a VM or container) with secure access through a web interface. \ No newline at end of file +## Architektúra + +- `cmd/supervisor/main.go`: entrypoint +- `internal/app`: zostavenie aplikácie, wiring komponentov +- `internal/httpserver`: čistý `net/http` router, middleware, API + WS handlery +- `internal/session`: PTY procesy, lifecycle session, output buffer, subscribe mechanizmus +- `internal/store/memory`: in-memory persistencia metadata session +- `internal/supervisor`: skeleton orchestration vrstvy pre budúce supervisor/worker rozšírenie +- `web/`: samostatný Vue 3 + Vite frontend +- `internal/static`: embedded statické assets servované zo zabudovaného FS + +## API endpointy + +- `GET /healthz` +- `GET /api/sessions` +- `POST /api/sessions` +- `GET /api/sessions/{id}` +- `POST /api/sessions/{id}/input` +- `POST /api/sessions/{id}/stop` +- `DELETE /api/sessions/{id}` +- `GET /ws/sessions/{id}` + +Poznámka k mazaniu: + +- bežiacu session (`running`) nie je možné odstrániť (`409 Conflict`) +- neexistujúca session vracia `404` +- úspešné odstránenie vracia `204 No Content` + +### Session model + +Každá session obsahuje: + +- `id` +- `name` +- `agentId` +- `command` +- `status` +- `createdAt` +- `startedAt` +- `exitedAt` +- `exitCode` + +## WebSocket protokol + +JSON envelope: + +```json +{ + "type": "terminal.input | terminal.output | terminal.resize | session.status | error", + "session": "session-id", + "payload": {} +} +``` + +Použité payloady: + +- `terminal.input`: `{ "data": "..." }` +- `terminal.output`: `{ "data": "..." }` +- `terminal.resize`: `{ "cols": 120, "rows": 30 }` +- `session.status`: `{ "status": "running", "exitCode": null }` +- `error`: `{ "message": "..." }` + +## Frontend poznámky + +- layout má globálny reset okrajov (`body { margin: 0; }`) +- v sidebare je pri ukončených session tlačidlo `Remove` +- pri odstránení aktuálne otvorenej session UI presmeruje na dashboard + +## Build + +Predpoklady: + +- Go (1.23+) +- Node.js + npm + +Frontend build do `web/dist` a následne sa skopíruje do `internal/static/dist` pre `go:embed`. + +```bash +make frontend-build +make backend-build +make build +``` + +## Spustenie + +```bash +make run +``` + +alebo: + +```bash +./bin/supervisor +``` + +Default adresa: `:8080`. + +Konfigurácia: + +- `SUPERVISOR_ADDR` (napr. `:9090`) + +## Budúce rozšírenie + +Štruktúra je pripravená na doplnenie: + +- supervisor orchestration manager +- worker agents +- workflow runs +- audit log +- persistent store (DB) diff --git a/cmd/supervisor/main.go b/cmd/supervisor/main.go new file mode 100644 index 0000000..37404ba --- /dev/null +++ b/cmd/supervisor/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "log" + "os/signal" + "syscall" + + "supervisor/internal/app" + "supervisor/internal/config" +) + +func main() { + cfg := config.Load() + + application, err := app.New(cfg) + if err != nil { + log.Fatalf("failed to initialize app: %v", err) + } + + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + if err := application.Run(ctx); err != nil { + log.Fatalf("application error: %v", err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8685457 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module supervisor + +go 1.23 + +require ( + github.com/creack/pty v1.1.24 + github.com/gorilla/websocket v1.5.3 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a15fd45 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..d2f7b73 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,75 @@ +package app + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "time" + + "supervisor/internal/config" + "supervisor/internal/httpserver" + "supervisor/internal/session" + "supervisor/internal/store/memory" + "supervisor/internal/supervisor" + "supervisor/internal/util" +) + +type App struct { + cfg config.Config + logger *log.Logger + httpServer *http.Server + SessionManager *session.Manager + Supervisor *supervisor.Manager +} + +func New(cfg config.Config) (*App, error) { + logger := util.NewLogger() + memStore := memory.NewStore() + sessionManager := session.NewManager(memStore, nil) + supervisorManager := supervisor.NewManager() + + router, err := httpserver.NewRouter(httpserver.Dependencies{ + Logger: logger, + Manager: sessionManager, + }) + if err != nil { + return nil, fmt.Errorf("build router: %w", err) + } + + srv := &http.Server{ + Addr: cfg.Addr, + Handler: router, + ReadHeaderTimeout: 10 * time.Second, + } + + return &App{ + cfg: cfg, + logger: logger, + httpServer: srv, + SessionManager: sessionManager, + Supervisor: supervisorManager, + }, nil +} + +func (a *App) Run(ctx context.Context) error { + errCh := make(chan error, 1) + go func() { + a.logger.Printf("HTTP server listening on %s", a.cfg.Addr) + errCh <- a.httpServer.ListenAndServe() + }() + + select { + case <-ctx.Done(): + shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + _ = a.httpServer.Shutdown(shutdownCtx) + return nil + case err := <-errCh: + if errors.Is(err, http.ErrServerClosed) { + return nil + } + return err + } +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..a3effe2 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,15 @@ +package config + +import "os" + +type Config struct { + Addr string +} + +func Load() Config { + addr := os.Getenv("SUPERVISOR_ADDR") + if addr == "" { + addr = ":8080" + } + return Config{Addr: addr} +} diff --git a/internal/domain/agent.go b/internal/domain/agent.go new file mode 100644 index 0000000..fd6b573 --- /dev/null +++ b/internal/domain/agent.go @@ -0,0 +1,10 @@ +package domain + +import "time" + +type Agent struct { + ID string `json:"id"` + Name string `json:"name"` + Kind string `json:"kind"` + CreatedAt time.Time `json:"createdAt"` +} diff --git a/internal/domain/event.go b/internal/domain/event.go new file mode 100644 index 0000000..554aa27 --- /dev/null +++ b/internal/domain/event.go @@ -0,0 +1,32 @@ +package domain + +import "time" + +type EventType string + +const ( + EventTerminalOutput EventType = "terminal.output" + EventSessionStatus EventType = "session.status" + EventError EventType = "error" +) + +type Event struct { + Type EventType `json:"type"` + SessionID string `json:"session"` + Payload any `json:"payload"` + At time.Time `json:"at"` +} + +// TerminalOutputEvent carries raw terminal bytes as UTF-8 text. +type TerminalOutputEvent struct { + Data string `json:"data"` +} + +type SessionStatusEvent struct { + Status SessionStatus `json:"status"` + ExitCode *int `json:"exitCode,omitempty"` +} + +type ErrorEvent struct { + Message string `json:"message"` +} diff --git a/internal/domain/run.go b/internal/domain/run.go new file mode 100644 index 0000000..b968c22 --- /dev/null +++ b/internal/domain/run.go @@ -0,0 +1,20 @@ +package domain + +import "time" + +type RunStatus string + +const ( + RunStatusPlanned RunStatus = "planned" + RunStatusRunning RunStatus = "running" + RunStatusDone RunStatus = "done" + RunStatusFailed RunStatus = "failed" +) + +type Run struct { + ID string `json:"id"` + WorkflowID string `json:"workflowId"` + RequestedBy string `json:"requestedBy"` + Status RunStatus `json:"status"` + CreatedAt time.Time `json:"createdAt"` +} diff --git a/internal/domain/session.go b/internal/domain/session.go new file mode 100644 index 0000000..706f650 --- /dev/null +++ b/internal/domain/session.go @@ -0,0 +1,25 @@ +package domain + +import "time" + +type SessionStatus string + +const ( + SessionStatusCreated SessionStatus = "created" + SessionStatusRunning SessionStatus = "running" + SessionStatusStopped SessionStatus = "stopped" + SessionStatusExited SessionStatus = "exited" + SessionStatusError SessionStatus = "error" +) + +type Session struct { + ID string `json:"id"` + Name string `json:"name"` + AgentID string `json:"agentId"` + Command string `json:"command"` + Status SessionStatus `json:"status"` + CreatedAt time.Time `json:"createdAt"` + StartedAt *time.Time `json:"startedAt,omitempty"` + ExitedAt *time.Time `json:"exitedAt,omitempty"` + ExitCode *int `json:"exitCode,omitempty"` +} diff --git a/internal/httpserver/handlers/health.go b/internal/httpserver/handlers/health.go new file mode 100644 index 0000000..356006c --- /dev/null +++ b/internal/httpserver/handlers/health.go @@ -0,0 +1,13 @@ +package handlers + +import ( + "encoding/json" + "net/http" +) + +type HealthHandler struct{} + +func (h HealthHandler) Handle(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) +} diff --git a/internal/httpserver/handlers/sessions.go b/internal/httpserver/handlers/sessions.go new file mode 100644 index 0000000..b787110 --- /dev/null +++ b/internal/httpserver/handlers/sessions.go @@ -0,0 +1,131 @@ +package handlers + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "supervisor/internal/domain" + "supervisor/internal/session" +) + +type SessionService interface { + CreateSession(ctx context.Context, params session.CreateSessionParams) (domain.Session, error) + StartSession(ctx context.Context, id string) error + StopSession(ctx context.Context, id string) error + DeleteSession(ctx context.Context, id string) error + ListSessions(ctx context.Context) ([]domain.Session, error) + GetSession(ctx context.Context, id string) (domain.Session, error) + WriteInput(ctx context.Context, id string, input string) error + Resize(ctx context.Context, id string, cols, rows int) error + Subscribe(id string) (<-chan domain.Event, func(), error) + Scrollback(id string) ([]byte, error) +} + +type SessionsHandler struct { + manager SessionService +} + +func NewSessionsHandler(manager SessionService) *SessionsHandler { + return &SessionsHandler{manager: manager} +} + +func (h *SessionsHandler) List(w http.ResponseWriter, r *http.Request) { + sessions, err := h.manager.ListSessions(r.Context()) + if err != nil { + writeError(w, http.StatusInternalServerError, err) + return + } + writeJSON(w, http.StatusOK, sessions) +} + +func (h *SessionsHandler) Create(w http.ResponseWriter, r *http.Request) { + var req session.CreateSessionParams + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + created, err := h.manager.CreateSession(r.Context(), req) + if err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + if err := h.manager.StartSession(r.Context(), created.ID); err != nil { + writeError(w, http.StatusInternalServerError, err) + return + } + current, err := h.manager.GetSession(r.Context(), created.ID) + if err != nil { + writeError(w, http.StatusInternalServerError, err) + return + } + writeJSON(w, http.StatusCreated, current) +} + +func (h *SessionsHandler) Get(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + item, err := h.manager.GetSession(r.Context(), id) + if err != nil { + if errors.Is(err, session.ErrSessionNotFound) { + writeError(w, http.StatusNotFound, err) + return + } + writeError(w, http.StatusInternalServerError, err) + return + } + writeJSON(w, http.StatusOK, item) +} + +func (h *SessionsHandler) Input(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + var req session.InputRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + if err := h.manager.WriteInput(r.Context(), id, req.Input); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + writeJSON(w, http.StatusOK, map[string]bool{"ok": true}) +} + +func (h *SessionsHandler) Stop(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + if err := h.manager.StopSession(r.Context(), id); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + item, err := h.manager.GetSession(r.Context(), id) + if err != nil { + writeError(w, http.StatusInternalServerError, err) + return + } + writeJSON(w, http.StatusOK, item) +} + +func (h *SessionsHandler) Delete(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + if err := h.manager.DeleteSession(r.Context(), id); err != nil { + switch { + case errors.Is(err, session.ErrSessionNotFound): + writeError(w, http.StatusNotFound, err) + case errors.Is(err, session.ErrSessionRunning): + writeError(w, http.StatusConflict, err) + default: + writeError(w, http.StatusBadRequest, err) + } + return + } + w.WriteHeader(http.StatusNoContent) +} + +func writeJSON(w http.ResponseWriter, code int, payload any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + _ = json.NewEncoder(w).Encode(payload) +} + +func writeError(w http.ResponseWriter, code int, err error) { + writeJSON(w, code, map[string]string{"error": err.Error()}) +} diff --git a/internal/httpserver/handlers/ws_terminal.go b/internal/httpserver/handlers/ws_terminal.go new file mode 100644 index 0000000..a319809 --- /dev/null +++ b/internal/httpserver/handlers/ws_terminal.go @@ -0,0 +1,155 @@ +package handlers + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/gorilla/websocket" + + "supervisor/internal/domain" + "supervisor/internal/ws" +) + +type WSTerminalHandler struct { + manager SessionService + upgrader websocket.Upgrader +} + +func NewWSTerminalHandler(manager SessionService) *WSTerminalHandler { + return &WSTerminalHandler{ + manager: manager, + upgrader: websocket.Upgrader{ + CheckOrigin: func(_ *http.Request) bool { return true }, + }, + } +} + +func (h *WSTerminalHandler) Handle(w http.ResponseWriter, r *http.Request) { + sessionID := r.PathValue("id") + current, err := h.manager.GetSession(r.Context(), sessionID) + if err != nil { + writeError(w, http.StatusNotFound, err) + return + } + + conn, err := h.upgrader.Upgrade(w, r, nil) + if err != nil { + return + } + defer conn.Close() + + events, cancel, err := h.manager.Subscribe(sessionID) + if err != nil { + _ = conn.WriteJSON(errorEnvelope(sessionID, err.Error())) + return + } + defer cancel() + + if err := conn.WriteJSON(map[string]any{ + "type": string(domain.EventSessionStatus), + "session": sessionID, + "payload": ws.SessionStatusPayload{Status: string(current.Status), ExitCode: current.ExitCode}, + }); err != nil { + return + } + + scrollback, _ := h.manager.Scrollback(sessionID) + if len(scrollback) > 0 { + if err := conn.WriteJSON(map[string]any{ + "type": string(domain.EventTerminalOutput), + "session": sessionID, + "payload": ws.TerminalOutputPayload{Data: string(scrollback)}, + }); err != nil { + return + } + } + + ctx, stop := context.WithCancel(r.Context()) + defer stop() + + readErr := make(chan error, 1) + go func() { + readErr <- h.readLoop(ctx, conn, sessionID) + }() + + for { + select { + case err := <-readErr: + if err == nil || websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { + return + } + _ = conn.WriteJSON(errorEnvelope(sessionID, err.Error())) + return + case event, ok := <-events: + if !ok { + return + } + if err := conn.WriteJSON(toEnvelope(event)); err != nil { + return + } + } + } +} + +func (h *WSTerminalHandler) readLoop(ctx context.Context, conn *websocket.Conn, sessionID string) error { + conn.SetReadLimit(1 << 20) + _ = conn.SetReadDeadline(time.Time{}) + + for { + select { + case <-ctx.Done(): + return nil + default: + } + + _, msg, err := conn.ReadMessage() + if err != nil { + return err + } + + var envelope ws.Envelope + if err := json.Unmarshal(msg, &envelope); err != nil { + return err + } + + switch envelope.Type { + case "terminal.input": + var payload ws.TerminalInputPayload + if err := json.Unmarshal(envelope.Payload, &payload); err != nil { + return err + } + if err := h.manager.WriteInput(ctx, sessionID, payload.Data); err != nil { + return err + } + case "terminal.resize": + var payload ws.TerminalResizePayload + if err := json.Unmarshal(envelope.Payload, &payload); err != nil { + return err + } + if err := h.manager.Resize(ctx, sessionID, payload.Cols, payload.Rows); err != nil { + return err + } + default: + return fmt.Errorf("unsupported message type: %s", envelope.Type) + } + } +} + +func toEnvelope(event domain.Event) map[string]any { + return map[string]any{ + "type": string(event.Type), + "session": event.SessionID, + "payload": event.Payload, + } +} + +func errorEnvelope(sessionID string, message string) map[string]any { + return map[string]any{ + "type": "error", + "session": sessionID, + "payload": ws.ErrorPayload{Message: message}, + } +} diff --git a/internal/httpserver/middleware.go b/internal/httpserver/middleware.go new file mode 100644 index 0000000..05eac72 --- /dev/null +++ b/internal/httpserver/middleware.go @@ -0,0 +1,27 @@ +package httpserver + +import ( + "log" + "net/http" + "time" +) + +func LoggingMiddleware(logger *log.Logger, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + started := time.Now() + next.ServeHTTP(w, r) + logger.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(started)) + }) +} + +func RecoverMiddleware(logger *log.Logger, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if rec := recover(); rec != nil { + logger.Printf("panic: %v", rec) + http.Error(w, "internal server error", http.StatusInternalServerError) + } + }() + next.ServeHTTP(w, r) + }) +} diff --git a/internal/httpserver/router.go b/internal/httpserver/router.go new file mode 100644 index 0000000..47a088e --- /dev/null +++ b/internal/httpserver/router.go @@ -0,0 +1,42 @@ +package httpserver + +import ( + "log" + "net/http" + + "supervisor/internal/httpserver/handlers" + "supervisor/internal/static" +) + +type Dependencies struct { + Logger *log.Logger + Manager handlers.SessionService +} + +func NewRouter(deps Dependencies) (http.Handler, error) { + mux := http.NewServeMux() + + healthHandler := handlers.HealthHandler{} + sessions := handlers.NewSessionsHandler(deps.Manager) + wsHandler := handlers.NewWSTerminalHandler(deps.Manager) + + mux.HandleFunc("GET /healthz", healthHandler.Handle) + mux.HandleFunc("GET /api/sessions", sessions.List) + mux.HandleFunc("POST /api/sessions", sessions.Create) + mux.HandleFunc("GET /api/sessions/{id}", sessions.Get) + mux.HandleFunc("POST /api/sessions/{id}/input", sessions.Input) + mux.HandleFunc("POST /api/sessions/{id}/stop", sessions.Stop) + mux.HandleFunc("DELETE /api/sessions/{id}", sessions.Delete) + mux.HandleFunc("GET /ws/sessions/{id}", wsHandler.Handle) + + staticHandler, err := static.NewSPAHandler() + if err != nil { + return nil, err + } + mux.Handle("/", staticHandler) + + var root http.Handler = mux + root = RecoverMiddleware(deps.Logger, root) + root = LoggingMiddleware(deps.Logger, root) + return root, nil +} diff --git a/internal/session/manager.go b/internal/session/manager.go new file mode 100644 index 0000000..daab44e --- /dev/null +++ b/internal/session/manager.go @@ -0,0 +1,158 @@ +package session + +import ( + "context" + "errors" + "sync" + "time" + + "supervisor/internal/domain" + "supervisor/internal/store" + "supervisor/internal/util" +) + +var ErrSessionNotFound = errors.New("session not found") +var ErrSessionRunning = errors.New("session is running") + +type Manager struct { + mu sync.RWMutex + store store.SessionStore + factory PTYFactory + runtimes map[string]*Session + scrollbackLimit int +} + +func NewManager(store store.SessionStore, factory PTYFactory) *Manager { + if factory == nil { + factory = DefaultPTYFactory{} + } + return &Manager{ + store: store, + factory: factory, + runtimes: make(map[string]*Session), + scrollbackLimit: 512 * 1024, + } +} + +func (m *Manager) CreateSession(ctx context.Context, params CreateSessionParams) (domain.Session, error) { + command := params.Command + if command == "" { + command = "bash" + } + s := domain.Session{ + ID: util.NewID("sess"), + Name: params.Name, + AgentID: params.AgentID, + Command: command, + Status: domain.SessionStatusCreated, + CreatedAt: time.Now().UTC(), + } + if s.Name == "" { + s.Name = s.ID + } + + runtime := NewSession(s, m.scrollbackLimit, func(session domain.Session) { + _ = m.store.Upsert(context.Background(), session) + }) + + m.mu.Lock() + m.runtimes[s.ID] = runtime + m.mu.Unlock() + + if err := m.store.Upsert(ctx, s); err != nil { + return domain.Session{}, err + } + return s, nil +} + +func (m *Manager) StartSession(_ context.Context, id string) error { + runtime, err := m.runtimeByID(id) + if err != nil { + return err + } + return runtime.Start(m.factory) +} + +func (m *Manager) StopSession(_ context.Context, id string) error { + runtime, err := m.runtimeByID(id) + if err != nil { + return err + } + return runtime.Stop() +} + +func (m *Manager) ListSessions(ctx context.Context) ([]domain.Session, error) { + return m.store.List(ctx) +} + +func (m *Manager) GetSession(ctx context.Context, id string) (domain.Session, error) { + s, ok, err := m.store.Get(ctx, id) + if err != nil { + return domain.Session{}, err + } + if !ok { + return domain.Session{}, ErrSessionNotFound + } + return s, nil +} + +func (m *Manager) WriteInput(_ context.Context, id string, input string) error { + runtime, err := m.runtimeByID(id) + if err != nil { + return err + } + return runtime.WriteInput(input) +} + +func (m *Manager) Subscribe(id string) (<-chan domain.Event, func(), error) { + runtime, err := m.runtimeByID(id) + if err != nil { + return nil, nil, err + } + _, ch, cancel := runtime.Subscribe() + return ch, cancel, nil +} + +func (m *Manager) Resize(_ context.Context, id string, cols, rows int) error { + runtime, err := m.runtimeByID(id) + if err != nil { + return err + } + return runtime.Resize(cols, rows) +} + +func (m *Manager) Scrollback(id string) ([]byte, error) { + runtime, err := m.runtimeByID(id) + if err != nil { + return nil, err + } + return runtime.Scrollback(), nil +} + +func (m *Manager) DeleteSession(ctx context.Context, id string) error { + runtime, err := m.runtimeByID(id) + if err != nil { + return err + } + + snapshot := runtime.Snapshot() + if snapshot.Status == domain.SessionStatusRunning { + return ErrSessionRunning + } + + m.mu.Lock() + delete(m.runtimes, id) + m.mu.Unlock() + + return m.store.Delete(ctx, id) +} + +func (m *Manager) runtimeByID(id string) (*Session, error) { + m.mu.RLock() + defer m.mu.RUnlock() + runtime, ok := m.runtimes[id] + if !ok { + return nil, ErrSessionNotFound + } + return runtime, nil +} diff --git a/internal/session/models.go b/internal/session/models.go new file mode 100644 index 0000000..c3a5ab4 --- /dev/null +++ b/internal/session/models.go @@ -0,0 +1,20 @@ +package session + +import "supervisor/internal/domain" + +type CreateSessionParams struct { + Name string `json:"name"` + AgentID string `json:"agentId"` + Command string `json:"command"` +} + +type InputRequest struct { + Input string `json:"input"` +} + +type ResizeRequest struct { + Cols int `json:"cols"` + Rows int `json:"rows"` +} + +type SessionSummary = domain.Session diff --git a/internal/session/process.go b/internal/session/process.go new file mode 100644 index 0000000..095025d --- /dev/null +++ b/internal/session/process.go @@ -0,0 +1,53 @@ +package session + +import ( + "os" + "os/exec" + "syscall" + + "github.com/creack/pty" +) + +type DefaultPTYFactory struct{} + +type shellProcess struct { + cmd *exec.Cmd + pty *os.File +} + +func (f DefaultPTYFactory) Start(command string) (PTYProcess, error) { + cmd := exec.Command("bash", "-lc", command) + cmd.Env = os.Environ() + ptmx, err := pty.Start(cmd) + if err != nil { + return nil, err + } + return &shellProcess{cmd: cmd, pty: ptmx}, nil +} + +func (p *shellProcess) Read(b []byte) (int, error) { + return p.pty.Read(b) +} + +func (p *shellProcess) Write(b []byte) (int, error) { + return p.pty.Write(b) +} + +func (p *shellProcess) Close() error { + return p.pty.Close() +} + +func (p *shellProcess) Wait() error { + return p.cmd.Wait() +} + +func (p *shellProcess) Resize(cols, rows uint16) error { + return pty.Setsize(p.pty, &pty.Winsize{Cols: cols, Rows: rows}) +} + +func (p *shellProcess) SignalStop() error { + if p.cmd.Process == nil { + return nil + } + return p.cmd.Process.Signal(syscall.SIGTERM) +} diff --git a/internal/session/pty.go b/internal/session/pty.go new file mode 100644 index 0000000..e86a580 --- /dev/null +++ b/internal/session/pty.go @@ -0,0 +1,14 @@ +package session + +import "io" + +type PTYProcess interface { + io.ReadWriteCloser + Wait() error + Resize(cols, rows uint16) error + SignalStop() error +} + +type PTYFactory interface { + Start(command string) (PTYProcess, error) +} diff --git a/internal/session/session.go b/internal/session/session.go new file mode 100644 index 0000000..3f58178 --- /dev/null +++ b/internal/session/session.go @@ -0,0 +1,273 @@ +package session + +import ( + "errors" + "io" + "os/exec" + "sync" + "time" + + "supervisor/internal/domain" + "supervisor/internal/util" +) + +type StateChangeFn func(domain.Session) + +type Session struct { + mu sync.RWMutex + meta domain.Session + process PTYProcess + subscribers map[string]chan domain.Event + scrollback []byte + scrollbackSize int + onStateChange StateChangeFn +} + +func NewSession(meta domain.Session, scrollbackSize int, onStateChange StateChangeFn) *Session { + if scrollbackSize <= 0 { + scrollbackSize = 256 * 1024 + } + return &Session{ + meta: meta, + subscribers: make(map[string]chan domain.Event), + scrollbackSize: scrollbackSize, + onStateChange: onStateChange, + } +} + +func (s *Session) Snapshot() domain.Session { + s.mu.RLock() + defer s.mu.RUnlock() + return s.meta +} + +func (s *Session) Scrollback() []byte { + s.mu.RLock() + defer s.mu.RUnlock() + cpy := make([]byte, len(s.scrollback)) + copy(cpy, s.scrollback) + return cpy +} + +func (s *Session) Start(factory PTYFactory) error { + s.mu.Lock() + if s.meta.Status == domain.SessionStatusRunning { + s.mu.Unlock() + return nil + } + if s.meta.Command == "" { + s.mu.Unlock() + return errors.New("empty command") + } + proc, err := factory.Start(s.meta.Command) + if err != nil { + now := time.Now().UTC() + s.meta.Status = domain.SessionStatusError + s.meta.ExitedAt = &now + s.mu.Unlock() + s.emitStateChange() + s.publish(domain.Event{ + Type: domain.EventError, + SessionID: s.meta.ID, + Payload: domain.ErrorEvent{ + Message: err.Error(), + }, + At: time.Now().UTC(), + }) + return err + } + now := time.Now().UTC() + s.process = proc + s.meta.Status = domain.SessionStatusRunning + s.meta.StartedAt = &now + s.meta.ExitedAt = nil + s.meta.ExitCode = nil + s.mu.Unlock() + + s.emitStateChange() + s.publishStatus() + + go s.readLoop(proc) + go s.waitLoop(proc) + return nil +} + +func (s *Session) Stop() error { + s.mu.Lock() + if s.process == nil { + s.mu.Unlock() + return nil + } + proc := s.process + if s.meta.Status == domain.SessionStatusRunning { + s.meta.Status = domain.SessionStatusStopped + } + s.mu.Unlock() + + s.emitStateChange() + s.publishStatus() + return proc.SignalStop() +} + +func (s *Session) WriteInput(input string) error { + s.mu.RLock() + proc := s.process + status := s.meta.Status + s.mu.RUnlock() + + if proc == nil || status != domain.SessionStatusRunning { + return errors.New("session is not running") + } + _, err := proc.Write([]byte(input)) + return err +} + +func (s *Session) Resize(cols, rows int) error { + if cols <= 0 || rows <= 0 { + return errors.New("invalid terminal size") + } + s.mu.RLock() + proc := s.process + s.mu.RUnlock() + if proc == nil { + return errors.New("session has no process") + } + return proc.Resize(uint16(cols), uint16(rows)) +} + +func (s *Session) Subscribe() (string, <-chan domain.Event, func()) { + id := util.NewID("sub") + ch := make(chan domain.Event, 128) + s.mu.Lock() + s.subscribers[id] = ch + s.mu.Unlock() + cancel := func() { + s.mu.Lock() + sub, ok := s.subscribers[id] + if ok { + delete(s.subscribers, id) + close(sub) + } + s.mu.Unlock() + } + return id, ch, cancel +} + +func (s *Session) readLoop(proc PTYProcess) { + buf := make([]byte, 4096) + for { + n, err := proc.Read(buf) + if n > 0 { + chunk := append([]byte(nil), buf[:n]...) + s.appendScrollback(chunk) + s.publish(domain.Event{ + Type: domain.EventTerminalOutput, + SessionID: s.Snapshot().ID, + Payload: domain.TerminalOutputEvent{ + Data: string(chunk), + }, + At: time.Now().UTC(), + }) + } + if err != nil { + if !errors.Is(err, io.EOF) { + s.publish(domain.Event{ + Type: domain.EventError, + SessionID: s.Snapshot().ID, + Payload: domain.ErrorEvent{Message: err.Error()}, + At: time.Now().UTC(), + }) + } + return + } + } +} + +func (s *Session) waitLoop(proc PTYProcess) { + err := proc.Wait() + _ = proc.Close() + + var exitCode *int + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + code := exitErr.ExitCode() + exitCode = &code + } + } + if err == nil { + code := 0 + exitCode = &code + } + + now := time.Now().UTC() + s.mu.Lock() + if s.meta.Status != domain.SessionStatusError { + s.meta.Status = domain.SessionStatusExited + } + s.meta.ExitedAt = &now + s.meta.ExitCode = exitCode + s.process = nil + s.mu.Unlock() + + s.emitStateChange() + s.publishStatus() + + if err != nil { + s.publish(domain.Event{ + Type: domain.EventError, + SessionID: s.Snapshot().ID, + Payload: domain.ErrorEvent{Message: err.Error()}, + At: time.Now().UTC(), + }) + } +} + +func (s *Session) appendScrollback(data []byte) { + s.mu.Lock() + defer s.mu.Unlock() + if len(data) >= s.scrollbackSize { + s.scrollback = append([]byte(nil), data[len(data)-s.scrollbackSize:]...) + return + } + s.scrollback = append(s.scrollback, data...) + if len(s.scrollback) > s.scrollbackSize { + extra := len(s.scrollback) - s.scrollbackSize + s.scrollback = append([]byte(nil), s.scrollback[extra:]...) + } +} + +func (s *Session) publishStatus() { + snap := s.Snapshot() + s.publish(domain.Event{ + Type: domain.EventSessionStatus, + SessionID: snap.ID, + Payload: domain.SessionStatusEvent{ + Status: snap.Status, + ExitCode: snap.ExitCode, + }, + At: time.Now().UTC(), + }) +} + +func (s *Session) publish(event domain.Event) { + s.mu.RLock() + subs := make([]chan domain.Event, 0, len(s.subscribers)) + for _, ch := range s.subscribers { + subs = append(subs, ch) + } + s.mu.RUnlock() + + for _, ch := range subs { + select { + case ch <- event: + default: + } + } +} + +func (s *Session) emitStateChange() { + if s.onStateChange == nil { + return + } + s.onStateChange(s.Snapshot()) +} diff --git a/internal/static/dist/index.html b/internal/static/dist/index.html new file mode 100644 index 0000000..02754ef --- /dev/null +++ b/internal/static/dist/index.html @@ -0,0 +1,10 @@ + + + + + Supervisor + + +

Frontend not built yet. Run make frontend-build.

+ + diff --git a/internal/static/embed.go b/internal/static/embed.go new file mode 100644 index 0000000..447a66b --- /dev/null +++ b/internal/static/embed.go @@ -0,0 +1,7 @@ +package static + +import "embed" + +// DistFS contains built frontend assets copied into internal/static/dist. +//go:embed dist dist/* +var DistFS embed.FS diff --git a/internal/static/serve.go b/internal/static/serve.go new file mode 100644 index 0000000..f812190 --- /dev/null +++ b/internal/static/serve.go @@ -0,0 +1,52 @@ +package static + +import ( + "io/fs" + "net/http" + "path" +) + +type SPAHandler struct { + assets fs.FS + files http.Handler +} + +func NewSPAHandler() (http.Handler, error) { + sub, err := fs.Sub(DistFS, "dist") + if err != nil { + return nil, err + } + return &SPAHandler{ + assets: sub, + files: http.FileServer(http.FS(sub)), + }, nil +} + +func (h *SPAHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + requested := path.Clean(r.URL.Path) + if requested == "." || requested == "/" { + h.serveIndex(w, r) + return + } + candidate := requested[1:] + if candidate == "" { + h.serveIndex(w, r) + return + } + if file, err := h.assets.Open(candidate); err == nil { + _ = file.Close() + h.files.ServeHTTP(w, r) + return + } + h.serveIndex(w, r) +} + +func (h *SPAHandler) serveIndex(w http.ResponseWriter, r *http.Request) { + index, err := fs.ReadFile(h.assets, "index.html") + if err != nil { + http.Error(w, "index.html not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _, _ = w.Write(index) +} diff --git a/internal/store/memory/store.go b/internal/store/memory/store.go new file mode 100644 index 0000000..c72b58a --- /dev/null +++ b/internal/store/memory/store.go @@ -0,0 +1,51 @@ +package memory + +import ( + "context" + "sort" + "supervisor/internal/domain" + "sync" +) + +type Store struct { + mu sync.RWMutex + sessions map[string]domain.Session +} + +func NewStore() *Store { + return &Store{sessions: make(map[string]domain.Session)} +} + +func (s *Store) Upsert(_ context.Context, session domain.Session) error { + s.mu.Lock() + defer s.mu.Unlock() + s.sessions[session.ID] = session + return nil +} + +func (s *Store) Get(_ context.Context, id string) (domain.Session, bool, error) { + s.mu.RLock() + defer s.mu.RUnlock() + session, ok := s.sessions[id] + return session, ok, nil +} + +func (s *Store) List(_ context.Context) ([]domain.Session, error) { + s.mu.RLock() + defer s.mu.RUnlock() + items := make([]domain.Session, 0, len(s.sessions)) + for _, session := range s.sessions { + items = append(items, session) + } + sort.Slice(items, func(i, j int) bool { + return items[i].CreatedAt.After(items[j].CreatedAt) + }) + return items, nil +} + +func (s *Store) Delete(_ context.Context, id string) error { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.sessions, id) + return nil +} diff --git a/internal/store/types.go b/internal/store/types.go new file mode 100644 index 0000000..e5c01df --- /dev/null +++ b/internal/store/types.go @@ -0,0 +1,13 @@ +package store + +import ( + "context" + "supervisor/internal/domain" +) + +type SessionStore interface { + Upsert(ctx context.Context, session domain.Session) error + Get(ctx context.Context, id string) (domain.Session, bool, error) + List(ctx context.Context) ([]domain.Session, error) + Delete(ctx context.Context, id string) error +} diff --git a/internal/supervisor/manager.go b/internal/supervisor/manager.go new file mode 100644 index 0000000..ca3790a --- /dev/null +++ b/internal/supervisor/manager.go @@ -0,0 +1,13 @@ +package supervisor + +import "sync" + +// Manager is a future extension point for high-level orchestration. +type Manager struct { + mu sync.RWMutex + assignments map[string]AgentAssignment +} + +func NewManager() *Manager { + return &Manager{assignments: make(map[string]AgentAssignment)} +} diff --git a/internal/supervisor/models.go b/internal/supervisor/models.go new file mode 100644 index 0000000..c429ffe --- /dev/null +++ b/internal/supervisor/models.go @@ -0,0 +1,9 @@ +package supervisor + +import "time" + +type AgentAssignment struct { + AgentID string `json:"agentId"` + SessionID string `json:"sessionId"` + AssignedAt time.Time `json:"assignedAt"` +} diff --git a/internal/util/ids.go b/internal/util/ids.go new file mode 100644 index 0000000..ff00917 --- /dev/null +++ b/internal/util/ids.go @@ -0,0 +1,12 @@ +package util + +import ( + "crypto/rand" + "encoding/hex" +) + +func NewID(prefix string) string { + buf := make([]byte, 8) + _, _ = rand.Read(buf) + return prefix + "_" + hex.EncodeToString(buf) +} diff --git a/internal/util/logger.go b/internal/util/logger.go new file mode 100644 index 0000000..f286f8a --- /dev/null +++ b/internal/util/logger.go @@ -0,0 +1,10 @@ +package util + +import ( + "log" + "os" +) + +func NewLogger() *log.Logger { + return log.New(os.Stdout, "[supervisor] ", log.LstdFlags|log.Lmicroseconds) +} diff --git a/internal/ws/messages.go b/internal/ws/messages.go new file mode 100644 index 0000000..719bd90 --- /dev/null +++ b/internal/ws/messages.go @@ -0,0 +1,31 @@ +package ws + +import "encoding/json" + +type Envelope struct { + Type string `json:"type"` + Session string `json:"session"` + Payload json.RawMessage `json:"payload"` +} + +type TerminalInputPayload struct { + Data string `json:"data"` +} + +type TerminalOutputPayload struct { + Data string `json:"data"` +} + +type TerminalResizePayload struct { + Cols int `json:"cols"` + Rows int `json:"rows"` +} + +type SessionStatusPayload struct { + Status string `json:"status"` + ExitCode *int `json:"exitCode,omitempty"` +} + +type ErrorPayload struct { + Message string `json:"message"` +} diff --git a/scripts/build-all.sh b/scripts/build-all.sh new file mode 100755 index 0000000..8f7e663 --- /dev/null +++ b/scripts/build-all.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +"$ROOT_DIR/scripts/build-frontend.sh" + +mkdir -p "$ROOT_DIR/bin" +cd "$ROOT_DIR" +go build -o "$ROOT_DIR/bin/supervisor" ./cmd/supervisor + +echo "Built binary: $ROOT_DIR/bin/supervisor" diff --git a/scripts/build-frontend.sh b/scripts/build-frontend.sh new file mode 100755 index 0000000..3c8a544 --- /dev/null +++ b/scripts/build-frontend.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +WEB_DIR="$ROOT_DIR/web" +EMBED_DIST_DIR="$ROOT_DIR/internal/static/dist" + +cd "$WEB_DIR" +npm install +npm run build + +rm -rf "$EMBED_DIST_DIR" +mkdir -p "$EMBED_DIST_DIR" +cp -R "$WEB_DIR/dist/." "$EMBED_DIST_DIR/" + +echo "Frontend built into $WEB_DIR/dist and copied to $EMBED_DIST_DIR" diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..9963490 --- /dev/null +++ b/web/index.html @@ -0,0 +1,23 @@ + + + + + + Supervisor + + + +
+ + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..7f1f80b --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,1591 @@ +{ + "name": "supervisor-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "supervisor-web", + "version": "0.1.0", + "dependencies": { + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", + "axios": "^1.8.4", + "pinia": "^2.3.1", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "^5.7.3", + "vite": "^6.2.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", + "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.30", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", + "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "dependencies": { + "@vue/compiler-core": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", + "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.30", + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", + "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", + "dependencies": { + "@vue/compiler-dom": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", + "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", + "dependencies": { + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", + "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", + "dependencies": { + "@vue/reactivity": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", + "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", + "dependencies": { + "@vue/reactivity": "3.5.30", + "@vue/runtime-core": "3.5.30", + "@vue/shared": "3.5.30", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", + "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", + "dependencies": { + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30" + }, + "peerDependencies": { + "vue": "3.5.30" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz", + "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==" + }, + "node_modules/@xterm/addon-fit": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==" + }, + "node_modules/@xterm/xterm": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", + "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", + "dependencies": { + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-sfc": "3.5.30", + "@vue/runtime-dom": "3.5.30", + "@vue/server-renderer": "3.5.30", + "@vue/shared": "3.5.30" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..631dd9f --- /dev/null +++ b/web/package.json @@ -0,0 +1,24 @@ +{ + "name": "supervisor-web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", + "axios": "^1.8.4", + "pinia": "^2.3.1", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "typescript": "^5.7.3", + "vite": "^6.2.0" + } +} diff --git a/web/src/App.vue b/web/src/App.vue new file mode 100644 index 0000000..0d3daf3 --- /dev/null +++ b/web/src/App.vue @@ -0,0 +1,7 @@ + + + diff --git a/web/src/api/http.ts b/web/src/api/http.ts new file mode 100644 index 0000000..c883df4 --- /dev/null +++ b/web/src/api/http.ts @@ -0,0 +1,6 @@ +import axios from 'axios' + +export const http = axios.create({ + baseURL: '/api', + timeout: 15000, +}) diff --git a/web/src/api/sessions.ts b/web/src/api/sessions.ts new file mode 100644 index 0000000..8635463 --- /dev/null +++ b/web/src/api/sessions.ts @@ -0,0 +1,47 @@ +import { http } from './http' + +export interface SessionDto { + id: string + name: string + agentId: string + command: string + status: string + createdAt: string + startedAt?: string + exitedAt?: string + exitCode?: number +} + +export interface CreateSessionRequest { + name: string + agentId: string + command: string +} + +export async function listSessions(): Promise { + const { data } = await http.get('/sessions') + return data +} + +export async function createSession(payload: CreateSessionRequest): Promise { + const { data } = await http.post('/sessions', payload) + return data +} + +export async function getSession(id: string): Promise { + const { data } = await http.get(`/sessions/${id}`) + return data +} + +export async function postSessionInput(id: string, input: string): Promise { + await http.post(`/sessions/${id}/input`, { input }) +} + +export async function stopSession(id: string): Promise { + const { data } = await http.post(`/sessions/${id}/stop`) + return data +} + +export async function deleteSession(id: string): Promise { + await http.delete(`/sessions/${id}`) +} diff --git a/web/src/api/ws.ts b/web/src/api/ws.ts new file mode 100644 index 0000000..deb973e --- /dev/null +++ b/web/src/api/ws.ts @@ -0,0 +1,59 @@ +export interface WSEnvelope { + type: string + session: string + payload: T +} + +export class TerminalSocket { + private ws: WebSocket | null = null + + constructor( + private readonly sessionId: string, + private readonly onMessage: (msg: WSEnvelope) => void, + private readonly onError?: (error: Event) => void, + private readonly onClose?: () => void, + ) {} + + connect(): void { + const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws' + const url = `${protocol}://${window.location.host}/ws/sessions/${this.sessionId}` + this.ws = new WebSocket(url) + this.ws.onmessage = (event) => { + try { + this.onMessage(JSON.parse(event.data) as WSEnvelope) + } catch { + // ignore malformed payloads + } + } + this.ws.onerror = (event) => this.onError?.(event) + this.ws.onclose = () => this.onClose?.() + } + + sendInput(data: string): void { + this.send({ + type: 'terminal.input', + session: this.sessionId, + payload: { data }, + }) + } + + sendResize(cols: number, rows: number): void { + this.send({ + type: 'terminal.resize', + session: this.sessionId, + payload: { cols, rows }, + }) + } + + close(): void { + this.ws?.close() + this.ws = null + } + + private send(payload: WSEnvelope): void { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { + return + } + this.ws.send(JSON.stringify(payload)) + } +} diff --git a/web/src/components/layout/AppShell.vue b/web/src/components/layout/AppShell.vue new file mode 100644 index 0000000..9e40186 --- /dev/null +++ b/web/src/components/layout/AppShell.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/web/src/components/layout/Sidebar.vue b/web/src/components/layout/Sidebar.vue new file mode 100644 index 0000000..9881e5a --- /dev/null +++ b/web/src/components/layout/Sidebar.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/web/src/components/layout/Topbar.vue b/web/src/components/layout/Topbar.vue new file mode 100644 index 0000000..6bc29f7 --- /dev/null +++ b/web/src/components/layout/Topbar.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/web/src/components/sessions/SessionCard.vue b/web/src/components/sessions/SessionCard.vue new file mode 100644 index 0000000..6067395 --- /dev/null +++ b/web/src/components/sessions/SessionCard.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/web/src/components/sessions/SessionList.vue b/web/src/components/sessions/SessionList.vue new file mode 100644 index 0000000..a3ea8b9 --- /dev/null +++ b/web/src/components/sessions/SessionList.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/web/src/components/terminals/TerminalTabs.vue b/web/src/components/terminals/TerminalTabs.vue new file mode 100644 index 0000000..a87f11d --- /dev/null +++ b/web/src/components/terminals/TerminalTabs.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/web/src/components/terminals/XTermView.vue b/web/src/components/terminals/XTermView.vue new file mode 100644 index 0000000..c58bb33 --- /dev/null +++ b/web/src/components/terminals/XTermView.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/web/src/main.ts b/web/src/main.ts new file mode 100644 index 0000000..4e90daa --- /dev/null +++ b/web/src/main.ts @@ -0,0 +1,10 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import App from './App.vue' +import router from './router' +import '@xterm/xterm/css/xterm.css' + +const app = createApp(App) +app.use(createPinia()) +app.use(router) +app.mount('#app') diff --git a/web/src/router/index.ts b/web/src/router/index.ts new file mode 100644 index 0000000..c3b9020 --- /dev/null +++ b/web/src/router/index.ts @@ -0,0 +1,22 @@ +import { createRouter, createWebHistory } from 'vue-router' +import DashboardView from '../views/DashboardView.vue' +import SessionView from '../views/SessionView.vue' + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + name: 'dashboard', + component: DashboardView, + }, + { + path: '/sessions/:id', + name: 'session', + component: SessionView, + props: true, + }, + ], +}) + +export default router diff --git a/web/src/stores/sessions.ts b/web/src/stores/sessions.ts new file mode 100644 index 0000000..9a7dc74 --- /dev/null +++ b/web/src/stores/sessions.ts @@ -0,0 +1,91 @@ +import { defineStore } from 'pinia' +import { + createSession, + deleteSession, + getSession, + listSessions, + postSessionInput, + stopSession, + type CreateSessionRequest, + type SessionDto, +} from '../api/sessions' + +interface SessionsState { + sessions: SessionDto[] + activeSessionId: string | null + loading: boolean + error: string | null +} + +export const useSessionsStore = defineStore('sessions', { + state: (): SessionsState => ({ + sessions: [], + activeSessionId: null, + loading: false, + error: null, + }), + getters: { + activeSession(state): SessionDto | undefined { + return state.sessions.find((item) => item.id === state.activeSessionId) + }, + }, + actions: { + async fetchSessions() { + this.loading = true + this.error = null + try { + this.sessions = await listSessions() + } catch (err) { + this.error = String(err) + } finally { + this.loading = false + } + }, + async createSession(payload: CreateSessionRequest) { + const created = await createSession(payload) + this.sessions = [created, ...this.sessions.filter((item) => item.id !== created.id)] + this.activeSessionId = created.id + return created + }, + async loadSession(id: string) { + const current = await getSession(id) + const idx = this.sessions.findIndex((item) => item.id === id) + if (idx >= 0) { + this.sessions.splice(idx, 1, current) + } else { + this.sessions.unshift(current) + } + this.activeSessionId = id + return current + }, + setActiveSession(id: string) { + this.activeSessionId = id + }, + async stopSession(id: string) { + const updated = await stopSession(id) + const idx = this.sessions.findIndex((item) => item.id === id) + if (idx >= 0) { + this.sessions.splice(idx, 1, updated) + } + return updated + }, + async sendInput(id: string, input: string) { + await postSessionInput(id, input) + }, + async removeSession(id: string) { + await deleteSession(id) + this.sessions = this.sessions.filter((item) => item.id !== id) + if (this.activeSessionId === id) { + this.activeSessionId = null + } + }, + upsertSession(session: SessionDto) { + const idx = this.sessions.findIndex((item) => item.id === session.id) + if (idx >= 0) { + this.sessions.splice(idx, 1, { ...this.sessions[idx], ...session }) + } else { + this.sessions.unshift(session) + } + }, + }, +}) diff --git a/web/src/views/DashboardView.vue b/web/src/views/DashboardView.vue new file mode 100644 index 0000000..ba91cde --- /dev/null +++ b/web/src/views/DashboardView.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/web/src/views/SessionView.vue b/web/src/views/SessionView.vue new file mode 100644 index 0000000..4e8f8f0 --- /dev/null +++ b/web/src/views/SessionView.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..e2874a0 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "jsx": "preserve", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + "skipLibCheck": true + }, + "include": ["src/**/*.ts", "src/**/*.vue"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..1951be3 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8080', + }, + '/ws': { + target: 'ws://localhost:8080', + ws: true, + }, + }, + }, +})