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 @@
+
+
+ Runtime Console
+ Dashboard
+
+
+
+
+
+
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 @@
+
+
+
+ {{ session.name || session.id }}
+
+
+ {{ session.command }}
+ {{ session.status }}
+
+
+
+
+
+
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 @@
+
+
+ Dashboard
+
+
+
+
+
Total sessions: {{ store.sessions.length }}
+
Running: {{ runningCount }}
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+ {{ session.name }}
+ {{ session.command }}
+
+
+
+
+
+
+
+
+
+
+
+
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,
+ },
+ },
+ },
+})