diff --git a/.gitignore b/.gitignore index 596f24d8..5b1ed1cd 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,6 @@ builder-debug.yml # playwright artifacts frontend/test-results/ + +# built daemon binary copied into the frontend bundle dir +frontend/daemon/ diff --git a/backend/internal/adapters/runtime/conpty/attach.go b/backend/internal/adapters/runtime/conpty/attach.go new file mode 100644 index 00000000..a8f11ef2 --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/attach.go @@ -0,0 +1,124 @@ +// attach.go - conpty Attach: a loopback Stream over the B3 pty-host. Unlike +// tmux/zellij, conpty does not spawn an attach CLI; it dials the session's +// loopback host and speaks the B1 framing protocol directly. The host replays +// the scrollback Snapshot as the first MsgTerminalData on connect, so a fresh +// Read naturally yields the repaint first. +package conpty + +import ( + "context" + "encoding/json" + "fmt" + "io" + "sync" + + "github.com/aoagents/agent-orchestrator/backend/internal/ports" +) + +var _ ports.Attacher = (*Runtime)(nil) + +// Attach opens a fresh attach Stream for the session by dialing its loopback +// pty-host. rows/cols size the host's PTY from birth when known (a MsgResize is +// sent right after connect). ctx cancellation closes the Stream. +func (r *Runtime) Attach(ctx context.Context, handle ports.RuntimeHandle, rows, cols uint16) (ports.Stream, error) { + sess := r.resolve(handle.ID) + if sess == nil { + return nil, fmt.Errorf("conpty: session %q not found", handle.ID) + } + conn, err := dialHost(sess.addr, dialTimeout) + if err != nil { + return nil, fmt.Errorf("conpty: dial host for %q: %w", handle.ID, err) + } + + pr, pw := io.Pipe() + s := &loopbackStream{conn: conn, pr: pr, pw: pw} + + // Pump host frames: MsgTerminalData payloads go into the pipe that Read + // drains. The first such frame is the scrollback snapshot, so the replay + // arrives before any live output. + go s.pump() + + // ctx cancellation must terminate the stream (mirrors the unix/windows + // spawn paths closing the PTY on ctx.Done). + go func() { + <-ctx.Done() + _ = s.Close() + }() + + if rows > 0 && cols > 0 { + if err := s.Resize(rows, cols); err != nil { + _ = s.Close() + return nil, err + } + } + return s, nil +} + +// loopbackStream is a ports.Stream backed by a single loopback connection to the +// pty-host. The pump goroutine reframes host output into an io.Pipe so Read +// presents a plain byte stream; Write/Resize encode client frames onto the conn. +type loopbackStream struct { + conn io.ReadWriteCloser + pr *io.PipeReader + pw *io.PipeWriter + + closeOnce sync.Once +} + +// pump reads framed host messages and writes MsgTerminalData payloads into the +// pipe. It closes the pipe when the connection ends so Read returns EOF. +func (s *loopbackStream) pump() { + parser := NewMessageParser(func(msgType byte, payload []byte) { + if msgType == MsgTerminalData { + // Write blocks until Read drains, preserving back-pressure and order. + _, _ = s.pw.Write(payload) + } + }) + buf := make([]byte, 4096) + for { + n, err := s.conn.Read(buf) + if n > 0 { + parser.Feed(buf[:n]) + } + if err != nil { + _ = s.pw.CloseWithError(err) + return + } + } +} + +func (s *loopbackStream) Read(p []byte) (int, error) { return s.pr.Read(p) } + +func (s *loopbackStream) Write(p []byte) (int, error) { + frame, err := EncodeMessage(MsgTerminalInput, p) + if err != nil { + return 0, err + } + if _, err := s.conn.Write(frame); err != nil { + return 0, err + } + return len(p), nil +} + +func (s *loopbackStream) Resize(rows, cols uint16) error { + payload, _ := json.Marshal(ResizePayload{Cols: int(cols), Rows: int(rows)}) + frame, err := EncodeMessage(MsgResize, payload) // small JSON payload, never overflows uint32 + if err != nil { + return err + } + _, err = s.conn.Write(frame) + return err +} + +// Close closes the conn and the pipe. Idempotent. Closing the conn unblocks +// pump's Read, which then closes the pipe-writer too; closing both here makes +// Close safe to call directly (e.g. on ctx cancel) without waiting for pump. +func (s *loopbackStream) Close() error { + var err error + s.closeOnce.Do(func() { + err = s.conn.Close() + _ = s.pw.Close() + _ = s.pr.Close() + }) + return err +} diff --git a/backend/internal/adapters/runtime/conpty/attach_test.go b/backend/internal/adapters/runtime/conpty/attach_test.go new file mode 100644 index 00000000..08b7ff89 --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/attach_test.go @@ -0,0 +1,151 @@ +package conpty + +import ( + "bytes" + "context" + "io" + "testing" + "time" + + "github.com/aoagents/agent-orchestrator/backend/internal/ports" +) + +// runtimeForFixture wires a conpty Runtime to a running serveFixture by stuffing +// the fixture's loopback addr into the session map under the given id, so Attach +// resolves it without a real Windows spawn. +func runtimeForFixture(id string, f *serveFixture) *Runtime { + r := New(Options{}) + r.mu.Lock() + r.sessions[id] = &hostSession{addr: f.addr, pid: f.pty.PID()} + r.mu.Unlock() + return r +} + +func readUntil(t *testing.T, s io.Reader, want string, timeout time.Duration) string { + t.Helper() + type res struct { + out string + } + done := make(chan res, 1) + go func() { + var buf []byte + tmp := make([]byte, 4096) + for { + n, err := s.Read(tmp) + if n > 0 { + buf = append(buf, tmp[:n]...) + if bytes.Contains(buf, []byte(want)) { + done <- res{string(buf)} + return + } + } + if err != nil { + done <- res{string(buf)} + return + } + } + }() + select { + case r := <-done: + return r.out + case <-time.After(timeout): + t.Fatalf("timed out reading for %q", want) + return "" + } +} + +// TestAttachReplaysScrollback: the host sends the ring snapshot as the first +// MsgTerminalData on connect, so a fresh Read on the Stream yields the replay. +func TestAttachReplaysScrollback(t *testing.T) { + f := startServe(t, 300) + defer f.cancel() + f.ring.Append([]byte("scrollback-line\n")) + + r := runtimeForFixture("sess", f) + s, err := r.Attach(context.Background(), nameHandle("sess"), 0, 0) + if err != nil { + t.Fatalf("Attach: %v", err) + } + defer s.Close() + + out := readUntil(t, s, "scrollback-line", 2*time.Second) + if !bytes.Contains([]byte(out), []byte("scrollback-line")) { + t.Fatalf("scrollback not replayed on Read; got %q", out) + } +} + +// TestAttachWriteReachesPTY: Write on the Stream sends MsgTerminalInput, which +// the host forwards to the fakePTY's input. +func TestAttachWriteReachesPTY(t *testing.T) { + f := startServe(t, 301) + defer f.cancel() + + r := runtimeForFixture("sess", f) + s, err := r.Attach(context.Background(), nameHandle("sess"), 0, 0) + if err != nil { + t.Fatalf("Attach: %v", err) + } + defer s.Close() + + keystrokes := []byte("ls -la\r") + if _, err := s.Write(keystrokes); err != nil { + t.Fatalf("Write: %v", err) + } + buf := make([]byte, len(keystrokes)) + if _, err := io.ReadFull(f.pty.inR, buf); err != nil { + t.Fatalf("read pty input: %v", err) + } + if string(buf) != string(keystrokes) { + t.Fatalf("pty input = %q, want %q", buf, keystrokes) + } +} + +// TestAttachResizeReachesPTY: an initial size on Attach plus a later Resize both +// reach the fakePTY.Resize via MsgResize frames. +func TestAttachResizeReachesPTY(t *testing.T) { + f := startServe(t, 302) + defer f.cancel() + + r := runtimeForFixture("sess", f) + // Attach with a birth size: the implementation sends an initial MsgResize. + s, err := r.Attach(context.Background(), nameHandle("sess"), 40, 132) + if err != nil { + t.Fatalf("Attach: %v", err) + } + defer s.Close() + + if err := s.Resize(50, 160); err != nil { + t.Fatalf("Resize: %v", err) + } + + // Poll for both resizes (birth + explicit) to arrive on the fakePTY. + deadline := time.Now().Add(2 * time.Second) + for time.Now().Before(deadline) { + f.pty.resizeMu.Lock() + n := len(f.pty.resizes) + var last ResizePayload + if n > 0 { + last = f.pty.resizes[n-1] + } + f.pty.resizeMu.Unlock() + if n >= 2 && last.Cols == 160 && last.Rows == 50 { + return + } + time.Sleep(10 * time.Millisecond) + } + f.pty.resizeMu.Lock() + defer f.pty.resizeMu.Unlock() + t.Fatalf("resizes did not reach pty as expected: %+v", f.pty.resizes) +} + +// TestAttachUnknownSession: Attach to a session with no resolvable addr errors. +func TestAttachUnknownSession(t *testing.T) { + r := New(Options{}) + if _, err := r.Attach(context.Background(), nameHandle("nope"), 0, 0); err == nil { + t.Fatal("expected error attaching to unknown session") + } +} + +func nameHandle(id string) ports.RuntimeHandle { + return ports.RuntimeHandle{ID: id} +} diff --git a/backend/internal/adapters/runtime/conpty/client.go b/backend/internal/adapters/runtime/conpty/client.go new file mode 100644 index 00000000..628551a9 --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/client.go @@ -0,0 +1,233 @@ +// client.go - loopback TCP client helpers that mirror pty-client.ts. +// Each function dials the host addr fresh (short-lived connection) and +// returns without maintaining state. Cross-platform: uses only stdlib net. +package conpty + +import ( + "encoding/json" + "errors" + "net" + "syscall" + "time" +) + +const ( + // ptyInputChunkRunes is the max runes per terminal-input frame. + // Mirrors PTY_INPUT_CHUNK_CHARS in pty-client.ts. + ptyInputChunkRunes = 512 + // ptyInputChunkDelay is the inter-chunk delay. Mirrors PTY_INPUT_CHUNK_DELAY_MS. + ptyInputChunkDelay = 15 * time.Millisecond + // ptyInputEnterDelay is the pause before sending Enter. Mirrors PTY_INPUT_ENTER_DELAY_MS. + ptyInputEnterDelay = 300 * time.Millisecond + + dialTimeout = 3 * time.Second + getOutputTimeout = 3 * time.Second + isAliveTimeout = 2 * time.Second +) + +// dialHost opens a TCP connection to addr with a deadline. Callers close it. +func dialHost(addr string, timeout time.Duration) (net.Conn, error) { + return net.DialTimeout("tcp", addr, timeout) +} + +// clientSendMessage chunks message by 512 runes and sends each as a +// MsgTerminalInput frame with 15ms gaps, then pauses 300ms and sends "\r". +// Mirrors ptyHostSendMessage from pty-client.ts. +func clientSendMessage(addr, message string) error { + conn, err := dialHost(addr, dialTimeout) + if err != nil { + return err + } + defer func() { _ = conn.Close() }() + + runes := []rune(message) + for i := 0; i < len(runes); i += ptyInputChunkRunes { + end := i + ptyInputChunkRunes + if end > len(runes) { + end = len(runes) + } + chunk := string(runes[i:end]) + frame, err := EncodeMessage(MsgTerminalInput, []byte(chunk)) + if err != nil { + return err + } + if _, err := conn.Write(frame); err != nil { + return err + } + // Inter-chunk delay only between chunks, not after the last one. + if end < len(runes) { + time.Sleep(ptyInputChunkDelay) + } + } + + // Brief pause before Enter (matches TS: Enter sent as a separate frame). + time.Sleep(ptyInputEnterDelay) + frame, err := EncodeMessage(MsgTerminalInput, []byte("\r")) + if err != nil { + return err + } + _, err = conn.Write(frame) + return err +} + +// clientGetOutput sends MsgGetOutputReq and reads frames until MsgGetOutputRes. +// Returns "" on timeout or connection failure (no error), matching the TS. +// lines <= 0 is handled by the caller (runtime.go rejects it before calling). +func clientGetOutput(addr string, lines int) (string, error) { + conn, err := dialHost(addr, getOutputTimeout) + if err != nil { + return "", nil // ponytail: connect failure -> "" like the TS + } + defer func() { _ = conn.Close() }() + + _ = conn.SetDeadline(time.Now().Add(getOutputTimeout)) + + req, _ := json.Marshal(GetOutputReq{Lines: lines}) + reqFrame, _ := EncodeMessage(MsgGetOutputReq, req) // req is small JSON, never overflows uint32 + if _, err := conn.Write(reqFrame); err != nil { + return "", nil + } + + resultC := make(chan string, 1) + parser := NewMessageParser(func(msgType byte, payload []byte) { + if msgType == MsgGetOutputRes { + select { + case resultC <- string(payload): + default: + } + } + }) + + buf := make([]byte, 4096) + for { + n, err := conn.Read(buf) + if n > 0 { + parser.Feed(buf[:n]) + } + select { + case text := <-resultC: + return text, nil + default: + } + if err != nil { + break + } + } + // Drain the channel one last time after the read loop ends. + select { + case text := <-resultC: + return text, nil + default: + return "", nil // timeout or EOF before response + } +} + +// clientIsAlive probes the host with MsgStatusReq and distinguishes three +// outcomes for the reaper (see IsAlive in runtime.go): +// +// - alive==true, transientErr==nil: a valid MsgStatusRes was received. +// - alive==false, transientErr==nil: the host is DEFINITIVELY gone (the dial +// was refused: nothing is listening on the loopback addr). +// - alive==false, transientErr!=nil: a TRANSIENT probe failure (network +// timeout, or any connected-then-failed I/O error). The reaper records this +// as ProbeFailed and retries instead of reaping a possibly-live session. +// +// When unsure, we prefer transient (return the error) rather than reporting +// death. Mirrors ptyHostIsAlive from pty-client.ts on the alive path: host +// reachable == alive, regardless of the inner agent's alive field. +func clientIsAlive(addr string) (alive bool, transientErr error) { + conn, err := dialHost(addr, isAliveTimeout) + if err != nil { + // A dial timeout is transient (the loopback hiccupped). A refused + // connection means nothing is listening -> definitively gone. Any + // other dial failure is treated as transient ("when unsure, retry"). + if isTimeout(err) { + return false, err + } + if isConnRefused(err) { + return false, nil + } + return false, err + } + defer func() { _ = conn.Close() }() + + _ = conn.SetDeadline(time.Now().Add(isAliveTimeout)) + + statusReqFrame, _ := EncodeMessage(MsgStatusReq, nil) // nil payload, never overflows + if _, err := conn.Write(statusReqFrame); err != nil { + // We connected, then the write failed: connected-then-failed I/O is + // transient (the host may still be up; the conn was disrupted). + return false, err + } + + aliveC := make(chan bool, 1) + parser := NewMessageParser(func(msgType byte, payload []byte) { + if msgType == MsgStatusRes { + var sp StatusPayload + ok := json.Unmarshal(payload, &sp) == nil + select { + case aliveC <- ok: + default: + } + } + }) + + buf := make([]byte, 4096) + var lastErr error + for { + n, err := conn.Read(buf) + if n > 0 { + parser.Feed(buf[:n]) + } + select { + case result := <-aliveC: + return result, nil + default: + } + if err != nil { + lastErr = err + break + } + } + select { + case result := <-aliveC: + return result, nil + default: + // Connected but never got a STATUS_RES: read timeout or mid-read EOF. + // lastErr is the error that broke the read loop (always non-nil here). + return false, lastErr + } +} + +// isTimeout reports whether err is a network timeout (dial timeout or +// read-deadline expiry). Cross-platform via the net.Error interface. +func isTimeout(err error) bool { + var ne net.Error + return errors.As(err, &ne) && ne.Timeout() +} + +// isConnRefused reports whether err is a fast "connection refused" dial +// failure (nothing listening). errors.Is(ECONNREFUSED) covers Unix and modern +// Windows; the explicit WSAECONNREFUSED (10061) guards older Windows runtimes +// where the errno is not mapped to syscall.ECONNREFUSED. +func isConnRefused(err error) bool { + if errors.Is(err, syscall.ECONNREFUSED) { + return true + } + const wsaeconnrefused = syscall.Errno(10061) + return errors.Is(err, wsaeconnrefused) +} + +// clientKill sends MsgKillReq best-effort. Connect failure is a no-op +// (host already dead). Mirrors ptyHostKill from pty-client.ts. +func clientKill(addr string) error { + conn, err := dialHost(addr, isAliveTimeout) + if err != nil { + return nil // already dead + } + defer func() { _ = conn.Close() }() + _ = conn.SetDeadline(time.Now().Add(isAliveTimeout)) + killFrame, _ := EncodeMessage(MsgKillReq, nil) // nil payload, never overflows + _, _ = conn.Write(killFrame) + return nil +} diff --git a/backend/internal/adapters/runtime/conpty/host.go b/backend/internal/adapters/runtime/conpty/host.go new file mode 100644 index 00000000..55c027e6 --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/host.go @@ -0,0 +1,280 @@ +// Package conpty - host.go implements the serve engine for the pty-host +// detached process. It owns the agent's PTY (via the ptyConn seam), exposes +// it over a loopback TCP socket using the B1 binary protocol, replays +// scrollback to new clients, fans output to all connected clients, and shuts +// down gracefully (ConPTY dispose first, then clients, then listener). +// +// This file is cross-platform; only the real conptyConn impl is Windows-tagged. +package conpty + +import ( + "context" + "encoding/json" + "io" + "net" + "sync" + "time" +) + +// ptyConn is the host's handle to the running agent's pseudo-terminal. +// The real impl (conptyConn) lives in host_conpty_windows.go; tests use a fake. +type ptyConn interface { + io.Reader // PTY output (raw bytes from the terminal) + io.Writer // PTY input (keystrokes to the terminal) + Resize(cols, rows int) error + Close() error // dispose the ConPTY + Done() <-chan struct{} // closed when the child process exits + ExitCode() (int, bool) // (code, true) once exited; (0, false) while running + PID() int +} + +// ServeConfig carries everything the host needs. +type ServeConfig struct { + SessionID string + Listener net.Listener // caller provides (loopback); engine owns Accept loop + PTY ptyConn + Ring *Ring +} + +// Serve runs the host event loop until the listener closes or Shutdown is +// invoked via the returned ShutdownFunc. It pumps PTY output into the ring +// and broadcasts to all clients, accepts new clients (replaying ring snapshot), +// and dispatches client messages. On PTY exit it broadcasts a status update +// but stays alive (keep-alive, mirroring tmux behavior). Returns when shut down. +func Serve(ctx context.Context, cfg ServeConfig) error { + h := &host{ + cfg: cfg, + clients: make(map[net.Conn]struct{}), + shutdownC: make(chan struct{}), + } + return h.run(ctx) +} + +// host holds the mutable state for a single pty-host session. +type host struct { + cfg ServeConfig + mu sync.Mutex + clients map[net.Conn]struct{} + + shutdownOnce sync.Once + shutdownC chan struct{} // closed when Shutdown is called +} + +// run is the main event loop. +func (h *host) run(ctx context.Context) error { + // Pump PTY output to ring + broadcast. + go h.pumpPTY() + + // Watch for ctx cancellation and trigger shutdown. + go func() { + select { + case <-ctx.Done(): + h.shutdown() + case <-h.shutdownC: + } + }() + + // runAcceptLoop accepts connections until the listener closes. A listener + // close is normal (shutdown or external) and is treated as success. + h.runAcceptLoop() + return nil +} + +// runAcceptLoop runs the Accept loop until the listener closes or returns an +// error. Listener-close errors are swallowed; they signal normal shutdown. +func (h *host) runAcceptLoop() { + for { + conn, err := h.cfg.Listener.Accept() + if err != nil { + return + } + go h.handleConn(conn) + } +} + +// shutdown is idempotent: disposes the ConPTY, closes clients, closes the +// listener. Mirrors the pty-host.ts shutdown() function. +// ponytail: 50ms sleep after pty.Close() gives the OS ConPTY helper +// (conpty_console_list_agent.exe) time to release cleanly; avoids the +// 0x800700e8 error dialog on Windows. +func (h *host) shutdown() { + h.shutdownOnce.Do(func() { + close(h.shutdownC) + + // 1. Dispose the ConPTY first (critical ordering). + _ = h.cfg.PTY.Close() + + // 2. Brief grace so the OS ConPTY helper can clean up. + time.Sleep(50 * time.Millisecond) + + // 3. Close all client connections. + h.mu.Lock() + for c := range h.clients { + _ = c.Close() + } + h.clients = make(map[net.Conn]struct{}) + h.mu.Unlock() + + // 4. Close the listener to unblock Accept. + _ = h.cfg.Listener.Close() + }) +} + +// pumpPTY reads PTY output continuously, appends to the ring, and broadcasts +// to clients. On PTY exit it flushes the partial line and sends a status +// update but does NOT close the listener (keep-alive). +func (h *host) pumpPTY() { + buf := make([]byte, 32*1024) + for { + n, err := h.cfg.PTY.Read(buf) + if n > 0 { + chunk := make([]byte, n) + copy(chunk, buf[:n]) + h.cfg.Ring.Append(chunk) + if frame, err := EncodeMessage(MsgTerminalData, chunk); err == nil { + h.broadcast(frame) + } + } + if err != nil { + break + } + } + + // PTY reader is done (process exited or PTY closed). Wait for the Done + // signal so ExitCode is populated before we send the status broadcast. + <-h.cfg.PTY.Done() + + h.cfg.Ring.FlushPartial() + + code, _ := h.cfg.PTY.ExitCode() + pid := h.cfg.PTY.PID() + h.broadcast(statusFrame(false, pid, &code)) + // Keep-alive: do NOT shutdown here. The host stays up so clients can + // still connect and read scrollback. +} + +// broadcast sends msg to all connected clients, removing any that error. +func (h *host) broadcast(msg []byte) { + h.mu.Lock() + defer h.mu.Unlock() + for c := range h.clients { + if _, err := c.Write(msg); err != nil { + _ = c.Close() + delete(h.clients, c) + } + } +} + +// sendTo sends msg to a single conn (best-effort; removes on error). +func (h *host) sendTo(conn net.Conn, msg []byte) { + if _, err := conn.Write(msg); err != nil { + h.mu.Lock() + _ = conn.Close() + delete(h.clients, conn) + h.mu.Unlock() + } +} + +// handleConn manages the lifecycle of a single client connection. +func (h *host) handleConn(conn net.Conn) { + // Scrollback replay: take the ring snapshot, write it to the conn, and add + // the conn to the broadcast set all under a SINGLE h.mu hold. broadcast() + // also takes h.mu, so it cannot interleave: any PTY chunk that arrives is + // either already in this snapshot, or is broadcast strictly after the conn + // joins the set. Doing this in two separate locks would let a chunk slip + // into the gap (in neither the snapshot nor this client's broadcast) and be + // silently dropped. + // ponytail: the snapshot write happens while holding h.mu. It is bounded by + // MaxOutputLines (the ring cap), so the lock hold is bounded; upgrade path + // is a per-client send queue if a slow client ever stalls broadcast. + h.mu.Lock() + snap := h.cfg.Ring.Snapshot() + if len(snap) > 0 { + snapFrame, err := EncodeMessage(MsgTerminalData, snap) + if err == nil { + _, err = conn.Write(snapFrame) + } + if err != nil { + h.mu.Unlock() + _ = conn.Close() + return + } + } + h.clients[conn] = struct{}{} + h.mu.Unlock() + + defer func() { + h.mu.Lock() + delete(h.clients, conn) + h.mu.Unlock() + _ = conn.Close() + }() + + parser := NewMessageParser(func(msgType byte, payload []byte) { + h.handleClientMsg(conn, msgType, payload) + }) + + buf := make([]byte, 4096) + for { + n, err := conn.Read(buf) + if n > 0 { + parser.Feed(buf[:n]) + } + if err != nil { + return + } + } +} + +// handleClientMsg dispatches a decoded client message. Mirrors handleClientMessage +// from pty-host.ts. +func (h *host) handleClientMsg(conn net.Conn, msgType byte, payload []byte) { + switch msgType { + case MsgTerminalInput: + if _, alive := h.cfg.PTY.ExitCode(); !alive { + _, _ = h.cfg.PTY.Write(payload) + } + + case MsgResize: + if _, alive := h.cfg.PTY.ExitCode(); !alive { + var rp ResizePayload + if err := json.Unmarshal(payload, &rp); err == nil { + _ = h.cfg.PTY.Resize(rp.Cols, rp.Rows) + } + // Malformed resize: ignore (matches TS behavior). + } + + case MsgGetOutputReq: + lines := 50 // default matches TS + var req GetOutputReq + if err := json.Unmarshal(payload, &req); err == nil && req.Lines > 0 { + lines = req.Lines + } + text := h.cfg.Ring.Tail(lines) + if frame, err := EncodeMessage(MsgGetOutputRes, []byte(text)); err == nil { + h.sendTo(conn, frame) + } + + case MsgStatusReq: + code, exited := h.cfg.PTY.ExitCode() + alive := !exited + pid := h.cfg.PTY.PID() + var codePtr *int + if exited { + codePtr = &code + } + h.sendTo(conn, statusFrame(alive, pid, codePtr)) + + case MsgKillReq: + // Trigger graceful shutdown; returns immediately (idempotent). + go h.shutdown() + } +} + +// statusFrame builds a MsgStatusRes frame. +func statusFrame(alive bool, pid int, exitCode *int) []byte { + sp := StatusPayload{Alive: alive, PID: pid, ExitCode: exitCode} + b, _ := json.Marshal(sp) + frame, _ := EncodeMessage(MsgStatusRes, b) // b is small JSON, never overflows uint32 + return frame +} diff --git a/backend/internal/adapters/runtime/conpty/host_conpty_other.go b/backend/internal/adapters/runtime/conpty/host_conpty_other.go new file mode 100644 index 00000000..aca39d13 --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/host_conpty_other.go @@ -0,0 +1,12 @@ +//go:build !windows + +package conpty + +import "errors" + +// newConPTY is a stub on non-Windows platforms. The serve engine (host.go) and +// tests use a fake ptyConn; this stub only exists to keep the package buildable +// on Darwin/Linux so the engine can be imported and tested without Windows. +func newConPTY(cwd, shellCmd string, shellArgs []string) (ptyConn, error) { + return nil, errors.New("conpty: unsupported on this OS") +} diff --git a/backend/internal/adapters/runtime/conpty/host_conpty_windows.go b/backend/internal/adapters/runtime/conpty/host_conpty_windows.go new file mode 100644 index 00000000..cc554726 --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/host_conpty_windows.go @@ -0,0 +1,102 @@ +//go:build windows + +package conpty + +import ( + "fmt" + "os" + "sync" + + gopty "github.com/aymanbagabas/go-pty" +) + +// conptyConn is the real ptyConn implementation backed by go-pty's ConPty +// (Windows ConPTY API). Only compiled on Windows. +type conptyConn struct { + pty gopty.ConPty + cmd *gopty.Cmd + + once sync.Once + doneC chan struct{} + exitCode int + exited bool + exitMu sync.Mutex +} + +// newConPTY creates a ConPTY session running shellCmd in cwd with shellArgs. +// It starts the process and returns a ptyConn ready for use. +func newConPTY(cwd, shellCmd string, shellArgs []string) (ptyConn, error) { + // go-pty's New() returns a ConPty on Windows. + p, err := gopty.New() + if err != nil { + return nil, fmt.Errorf("conpty: create pty: %w", err) + } + cp, ok := p.(gopty.ConPty) + if !ok { + _ = p.Close() + return nil, fmt.Errorf("conpty: expected ConPty on windows, got %T", p) + } + + // Set an initial size matching node-pty defaults from pty-host.ts. + if err := cp.Resize(220, 50); err != nil { + _ = cp.Close() + return nil, fmt.Errorf("conpty: initial resize: %w", err) + } + + cmd := cp.Command(shellCmd, shellArgs...) + cmd.Dir = cwd + // Inherit parent env so PATH, HOME, etc. are available. + cmd.Env = os.Environ() + + if err := cmd.Start(); err != nil { + _ = cp.Close() + return nil, fmt.Errorf("conpty: start command: %w", err) + } + + c := &conptyConn{ + pty: cp, + cmd: cmd, + doneC: make(chan struct{}), + } + + go c.wait() + return c, nil +} + +func (c *conptyConn) wait() { + _ = c.cmd.Wait() + code := 0 + if c.cmd.ProcessState != nil { + code = c.cmd.ProcessState.ExitCode() + } + c.exitMu.Lock() + c.exitCode = code + c.exited = true + c.exitMu.Unlock() + c.once.Do(func() { close(c.doneC) }) +} + +func (c *conptyConn) Read(b []byte) (int, error) { return c.pty.Read(b) } +func (c *conptyConn) Write(b []byte) (int, error) { return c.pty.Write(b) } +func (c *conptyConn) Close() error { + err := c.pty.Close() + // Best-effort kill: a child that ignores ConPTY EOF still gets terminated + // so Done() fires. Mirrors pty.kill() in pty-host.ts. + if c.cmd.Process != nil { + _ = c.cmd.Process.Kill() + } + return err +} +func (c *conptyConn) Resize(cols, rows int) error { return c.pty.Resize(cols, rows) } +func (c *conptyConn) Done() <-chan struct{} { return c.doneC } +func (c *conptyConn) PID() int { + if c.cmd.Process == nil { + return 0 + } + return c.cmd.Process.Pid +} +func (c *conptyConn) ExitCode() (int, bool) { + c.exitMu.Lock() + defer c.exitMu.Unlock() + return c.exitCode, c.exited +} diff --git a/backend/internal/adapters/runtime/conpty/host_main.go b/backend/internal/adapters/runtime/conpty/host_main.go new file mode 100644 index 00000000..3e53b8db --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/host_main.go @@ -0,0 +1,90 @@ +// host_main.go is the RunHost entrypoint for the "ao pty-host" subcommand. +// It is cross-platform: the loopback TCP bind and signal wiring work on all +// OSes; only the ConPTY creation (newConPTY) is OS-gated via build tags. +package conpty + +import ( + "context" + "fmt" + "io" + "net" + "os" + "os/signal" + "syscall" +) + +// RunHost is the "ao pty-host" entrypoint. argv is everything after the +// subcommand name: [shellArg...] +// +// It binds 127.0.0.1:0 (OS assigns the port), creates the ConPTY, prints +// "READY: \n" to stdout (the parent process reads this to learn the +// port), installs SIGTERM/SIGINT handlers, then runs Serve. Returns a process +// exit code. +// +// ponytail: loopback bind only; any local process on this host can connect to +// the assigned port. A per-session random token handshake is the upgrade path +// if multi-user isolation is needed. +func RunHost(args []string, stdout io.Writer) int { + if len(args) < 3 { + fmt.Fprintf(os.Stderr, "usage: ao pty-host [shellArg...]\n") + return 1 + } + + sessionID := args[0] + cwd := args[1] + shellCmd := args[2] + shellArgs := args[3:] + + // Bind before creating the PTY so we can report READY atomically. + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + fmt.Fprintf(os.Stderr, "pty-host [%s]: listen: %v\n", sessionID, err) + return 1 + } + tcpAddr, ok := ln.Addr().(*net.TCPAddr) + if !ok { + _ = ln.Close() + fmt.Fprintf(os.Stderr, "pty-host [%s]: listener is not TCP\n", sessionID) + return 1 + } + port := tcpAddr.Port + + pty, err := newConPTY(cwd, shellCmd, shellArgs) + if err != nil { + _ = ln.Close() + fmt.Fprintf(os.Stderr, "pty-host [%s]: newConPTY: %v\n", sessionID, err) + return 1 + } + + // Print READY after both the listener and the PTY are up. + _, _ = fmt.Fprintf(stdout, "READY:%d %d\n", pty.PID(), port) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Install signal handlers so SIGTERM/SIGINT trigger graceful shutdown. + sigC := make(chan os.Signal, 1) + signal.Notify(sigC, syscall.SIGTERM, syscall.SIGINT) + go func() { + select { + case sig := <-sigC: + fmt.Fprintf(os.Stderr, "pty-host [%s]: signal %v, shutting down\n", sessionID, sig) + cancel() + case <-ctx.Done(): + } + }() + + ring := NewRing() + cfg := ServeConfig{ + SessionID: sessionID, + Listener: ln, + PTY: pty, + Ring: ring, + } + + if err := Serve(ctx, cfg); err != nil { + fmt.Fprintf(os.Stderr, "pty-host [%s]: serve: %v\n", sessionID, err) + return 1 + } + return 0 +} diff --git a/backend/internal/adapters/runtime/conpty/host_test.go b/backend/internal/adapters/runtime/conpty/host_test.go new file mode 100644 index 00000000..95dcf551 --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/host_test.go @@ -0,0 +1,602 @@ +package conpty + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "strings" + "sync" + "testing" + "time" +) + +// --------------------------------------------------------------------------- +// fakePTY implements ptyConn using in-memory pipes. Used only in tests; +// the real ConPTY impl is Windows-only. +// --------------------------------------------------------------------------- + +type fakePTY struct { + // output is what the fake "terminal" writes to the host (PTY -> host reader) + outR *io.PipeReader + outW *io.PipeWriter + + // input is what the host writes to the fake terminal (keystrokes) + inR *io.PipeReader + inW *io.PipeWriter + + resizeMu sync.Mutex + resizes []ResizePayload + + doneOnce sync.Once + doneC chan struct{} + exitCode int + closed bool + closeMu sync.Mutex + + pid int +} + +func newFakePTY(pid int) *fakePTY { + outR, outW := io.Pipe() + inR, inW := io.Pipe() + return &fakePTY{ + outR: outR, + outW: outW, + inR: inR, + inW: inW, + doneC: make(chan struct{}), + pid: pid, + } +} + +// WriteOutput simulates the PTY producing output (e.g. shell printing text). +func (f *fakePTY) WriteOutput(data []byte) (int, error) { return f.outW.Write(data) } + +// CloseOutput simulates the PTY process exiting (closes the read side). +func (f *fakePTY) CloseOutput(code int) { + f.exitCode = code + f.outW.Close() +} + +// ReadInput lets tests inspect what the host forwarded to the PTY. +func (f *fakePTY) ReadInput(buf []byte) (int, error) { return f.inR.Read(buf) } + +// ptyConn interface implementation. +func (f *fakePTY) Read(b []byte) (int, error) { return f.outR.Read(b) } +func (f *fakePTY) Write(b []byte) (int, error) { return f.inW.Write(b) } + +func (f *fakePTY) Resize(cols, rows int) error { + f.resizeMu.Lock() + defer f.resizeMu.Unlock() + f.resizes = append(f.resizes, ResizePayload{Cols: cols, Rows: rows}) + return nil +} + +func (f *fakePTY) Close() error { + f.closeMu.Lock() + defer f.closeMu.Unlock() + f.closed = true + // Close both pipes so pumpPTY and any Read calls unblock. + _ = f.outW.Close() + _ = f.inW.Close() + f.doneOnce.Do(func() { close(f.doneC) }) + return nil +} + +func (f *fakePTY) Done() <-chan struct{} { return f.doneC } + +func (f *fakePTY) ExitCode() (int, bool) { + select { + case <-f.doneC: + return f.exitCode, true + default: + return 0, false + } +} + +func (f *fakePTY) PID() int { return f.pid } + +// signalExit simulates the child process exiting, triggering the Done channel +// and ExitCode returning true. +func (f *fakePTY) signalExit(code int) { + f.exitCode = code + f.doneOnce.Do(func() { close(f.doneC) }) + _ = f.outW.Close() // unblocks pumpPTY's Read +} + +// --------------------------------------------------------------------------- +// testClient wraps a net.Conn and a MessageParser for easy frame reading. +// --------------------------------------------------------------------------- + +type testClient struct { + conn net.Conn + frameC chan struct { + typ byte + payload []byte + } + parser *MessageParser +} + +func newTestClient(t *testing.T, addr string) *testClient { + t.Helper() + conn, err := net.Dial("tcp", addr) + if err != nil { + t.Fatalf("dial %s: %v", addr, err) + } + tc := &testClient{ + conn: conn, + frameC: make(chan struct { + typ byte + payload []byte + }, 64), + } + tc.parser = NewMessageParser(func(msgType byte, payload []byte) { + tc.frameC <- struct { + typ byte + payload []byte + }{msgType, payload} + }) + go func() { + buf := make([]byte, 4096) + for { + n, err := conn.Read(buf) + if n > 0 { + tc.parser.Feed(buf[:n]) + } + if err != nil { + close(tc.frameC) + return + } + } + }() + return tc +} + +// readFrame blocks until a frame arrives or 2s times out. +func (tc *testClient) readFrame(t *testing.T) (typ byte, payload []byte) { + t.Helper() + select { + case f, ok := <-tc.frameC: + if !ok { + t.Fatal("client frame channel closed (connection dropped)") + } + return f.typ, f.payload + case <-time.After(2 * time.Second): + t.Fatal("timeout waiting for frame") + return 0, nil + } +} + +// send writes a framed message to the server. +func (tc *testClient) send(msgType byte, payload []byte) error { + frame, err := EncodeMessage(msgType, payload) + if err != nil { + return err + } + _, err = tc.conn.Write(frame) + return err +} + +func (tc *testClient) close() { _ = tc.conn.Close() } + +// --------------------------------------------------------------------------- +// Helper: start a Serve with a freshly created listener + fakePTY. +// --------------------------------------------------------------------------- + +type serveFixture struct { + pty *fakePTY + ring *Ring + ln net.Listener + addr string + cancel context.CancelFunc + done chan error +} + +func startServe(t *testing.T, pid int) *serveFixture { + t.Helper() + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + pty := newFakePTY(pid) + ring := NewRing() + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan error, 1) + go func() { + done <- Serve(ctx, ServeConfig{ + SessionID: fmt.Sprintf("test-%d", pid), + Listener: ln, + PTY: pty, + Ring: ring, + }) + }() + return &serveFixture{ + pty: pty, + ring: ring, + ln: ln, + addr: ln.Addr().String(), + cancel: cancel, + done: done, + } +} + +// waitDone waits for Serve to return (up to 2s). +func (f *serveFixture) waitDone(t *testing.T) { + t.Helper() + select { + case <-f.done: + case <-time.After(2 * time.Second): + t.Fatal("Serve did not return in time") + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +// TestScrollbackReplay: seed the ring, connect a client; first frame must be +// MsgTerminalData containing the ring snapshot. +func TestScrollbackReplay(t *testing.T) { + f := startServe(t, 100) + defer f.cancel() + + // Seed ring directly before the client connects. + f.ring.Append([]byte("line1\nline2\n")) + snap := f.ring.Snapshot() + + c := newTestClient(t, f.addr) + defer c.close() + + typ, payload := c.readFrame(t) + if typ != MsgTerminalData { + t.Fatalf("got type 0x%02x, want MsgTerminalData", typ) + } + if string(payload) != string(snap) { + t.Fatalf("scrollback payload = %q, want %q", payload, snap) + } +} + +// TestScrollbackLiveOrdering_NoDrop is a regression test for the bug where the +// new-client handler took the ring Snapshot and registered the client in two +// separate h.mu acquisitions. A PTY chunk arriving in that gap landed in +// neither the snapshot nor that client's broadcast and was silently dropped, +// producing a hole in the middle of the client's stream. +// +// The PTY emits a long stream of numbered chunks continuously while a client +// connects. The fakePTY's WriteOutput blocks until pumpPTY consumes each chunk, +// so output is interleaved with the connect, exercising the race window. The +// guaranteed invariant is: the client's received byte stream must be a +// CONTIGUOUS suffix of the full PTY sequence, i.e. it may legitimately start +// late (the snapshot only captures whatever was written before the connect), +// but once it starts there must be NO internal gap. The old two-step code +// dropped a chunk between snapshot and registration, leaving an internal hole; +// this test detects that hole. Reliable under -race -count=20: the +// continuous-emit setup reliably lands a chunk in the race window, and it +// reliably fails against the old code. +func TestScrollbackLiveOrdering_NoDrop(t *testing.T) { + f := startServe(t, 200) + defer f.cancel() + + // Build the full byte sequence the PTY will ever have produced. Each chunk + // is a complete line ("[NNNN]\n") so it lands in the ring's snapshot (the + // ring only stores completed lines), exercising the snapshot-write path as + // well as the live-broadcast boundary where the drop bug lived. + const nChunks = 300 + chunk := func(i int) []byte { return []byte(fmt.Sprintf("[%04d]\n", i)) } + + // Emit continuously; each WriteOutput blocks until pumpPTY reads it. + emitDone := make(chan struct{}) + go func() { + defer close(emitDone) + for i := 0; i < nChunks; i++ { + if _, err := f.pty.WriteOutput(chunk(i)); err != nil { + return + } + } + }() + + // Connect mid-stream so the snapshot is taken while chunks are in flight. + c := newTestClient(t, f.addr) + defer c.close() + + // Collect frames until the client's stream contains the final chunk (the + // last line the PTY emits), or until the overall deadline. The sentinel is + // the last line's bytes; once we have seen it, the whole tail has arrived. + sentinel := string(chunk(nChunks - 1)) + var got []byte + deadline := time.After(5 * time.Second) +collect: + for !strings.Contains(string(got), sentinel) { + select { + case fr, ok := <-c.frameC: + if !ok { + break collect + } + if fr.typ != MsgTerminalData { + t.Fatalf("unexpected frame type 0x%02x", fr.typ) + } + got = append(got, fr.payload...) + case <-deadline: + break collect + } + } + // Emitter must have finished (it produces the sentinel last). + select { + case <-emitDone: + case <-time.After(time.Second): + t.Fatal("emitter did not finish") + } + if !strings.Contains(string(got), sentinel) { + t.Fatalf("client never received the final chunk %q; got %d bytes", sentinel, len(got)) + } + + // Parse the received bytes back into the ordered list of line indices. + // Each line is "[NNNN]\n". The client may legitimately start late (the + // snapshot only captures lines written before the connect), and a line may + // appear twice at the snapshot/live seam (a chunk landing in the ring just + // before this client registers can be both snapshotted and broadcast). The + // DROP bug instead produced a MISSING index in the middle. So the invariant + // is: the indices, in order, are non-decreasing, advance by 0 or 1 each + // step (no jump that skips an index), and reach the final index nChunks-1. + lines := strings.Split(string(got), "\n") + // Trailing "" after the final \n. + if lines[len(lines)-1] == "" { + lines = lines[:len(lines)-1] + } + prev := -1 + for li, line := range lines { + var idx int + if _, err := fmt.Sscanf(line, "[%04d]", &idx); err != nil { + t.Fatalf("unparseable line %d %q in client stream: %v", li, line, err) + } + if li == 0 { + prev = idx + continue + } + if idx != prev && idx != prev+1 { + t.Fatalf("non-contiguous line indices (dropped chunk): %d followed by %d", prev, idx) + } + prev = idx + } + if prev != nChunks-1 { + t.Fatalf("client stream did not reach the final chunk: last index %d, want %d", prev, nChunks-1) + } +} + +// TestFanOut: two clients receive the same PTY output. +func TestFanOut(t *testing.T) { + f := startServe(t, 101) + defer f.cancel() + + c1 := newTestClient(t, f.addr) + defer c1.close() + c2 := newTestClient(t, f.addr) + defer c2.close() + + // Write PTY output after both clients have connected. + // We need to give the server a moment to register both clients; use a + // brief sync by sending a status req from each and waiting for responses. + // ponytail: channel-based sync via status round-trip avoids sleeps. + _ = c1.send(MsgStatusReq, nil) + _ = c2.send(MsgStatusReq, nil) + // Drain status responses. + c1.readFrame(t) + c2.readFrame(t) + + msg := []byte("hello from pty\n") + if _, err := f.pty.WriteOutput(msg); err != nil { + t.Fatalf("WriteOutput: %v", err) + } + + // Both clients should receive a MsgTerminalData with msg. + for _, c := range []*testClient{c1, c2} { + typ, payload := c.readFrame(t) + if typ != MsgTerminalData { + t.Fatalf("got type 0x%02x, want MsgTerminalData", typ) + } + if string(payload) != string(msg) { + t.Fatalf("payload = %q, want %q", payload, msg) + } + } +} + +// TestTerminalInput: MsgTerminalInput from a client reaches the fakePTY's input. +func TestTerminalInput(t *testing.T) { + f := startServe(t, 102) + defer f.cancel() + + c := newTestClient(t, f.addr) + defer c.close() + + keystrokes := []byte("ls -la\r") + if err := c.send(MsgTerminalInput, keystrokes); err != nil { + t.Fatalf("send: %v", err) + } + + buf := make([]byte, len(keystrokes)) + if _, err := io.ReadFull(f.pty.inR, buf); err != nil { + t.Fatalf("read from pty input: %v", err) + } + if string(buf) != string(keystrokes) { + t.Fatalf("pty input = %q, want %q", buf, keystrokes) + } +} + +// TestResize: MsgResize calls fakePTY.Resize with the right cols/rows. +func TestResize(t *testing.T) { + f := startServe(t, 103) + defer f.cancel() + + c := newTestClient(t, f.addr) + defer c.close() + + payload, _ := json.Marshal(ResizePayload{Cols: 132, Rows: 40}) + if err := c.send(MsgResize, payload); err != nil { + t.Fatalf("send: %v", err) + } + + // Poll for the resize to arrive (it's async). Channel-based: send a + // status req and wait for its reply, which guarantees the resize was + // processed (single goroutine handles all messages per connection). + _ = c.send(MsgStatusReq, nil) + c.readFrame(t) // discard status response + + f.pty.resizeMu.Lock() + resizes := f.pty.resizes + f.pty.resizeMu.Unlock() + + if len(resizes) != 1 { + t.Fatalf("got %d resize calls, want 1", len(resizes)) + } + if resizes[0].Cols != 132 || resizes[0].Rows != 40 { + t.Fatalf("resize = %+v, want {132 40}", resizes[0]) + } +} + +// TestGetOutputReq: MsgGetOutputReq returns MsgGetOutputRes with ring.Tail(n). +func TestGetOutputReq(t *testing.T) { + f := startServe(t, 104) + defer f.cancel() + + f.ring.Append([]byte("alpha\nbeta\ngamma\n")) + + c := newTestClient(t, f.addr) + defer c.close() + + // Drain scrollback frame. + c.readFrame(t) + + reqPayload, _ := json.Marshal(GetOutputReq{Lines: 2}) + if err := c.send(MsgGetOutputReq, reqPayload); err != nil { + t.Fatalf("send: %v", err) + } + + typ, payload := c.readFrame(t) + if typ != MsgGetOutputRes { + t.Fatalf("got type 0x%02x, want MsgGetOutputRes", typ) + } + want := f.ring.Tail(2) + if string(payload) != want { + t.Fatalf("GetOutputRes = %q, want %q", payload, want) + } +} + +// TestStatusReq_AliveAndExited: MsgStatusReq returns alive:true while running; +// after the PTY exits, returns alive:false with exitCode. Listener stays open. +func TestStatusReq_AliveAndExited(t *testing.T) { + f := startServe(t, 105) + defer f.cancel() + + c := newTestClient(t, f.addr) + defer c.close() + + // While running: expect alive:true. + if err := c.send(MsgStatusReq, nil); err != nil { + t.Fatalf("send: %v", err) + } + typ, payload := c.readFrame(t) + if typ != MsgStatusRes { + t.Fatalf("got type 0x%02x, want MsgStatusRes", typ) + } + var sp StatusPayload + if err := json.Unmarshal(payload, &sp); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !sp.Alive { + t.Fatalf("expected alive=true, got false") + } + if sp.PID != 105 { + t.Fatalf("expected pid=105, got %d", sp.PID) + } + + // Simulate PTY exit. + f.pty.signalExit(42) + + // Drain the broadcast status-res that pumpPTY sends on exit. + exitBcast, _ := c.readFrame(t) + if exitBcast != MsgStatusRes { + t.Fatalf("exit broadcast type = 0x%02x, want MsgStatusRes", exitBcast) + } + + // Now a new status req should report alive:false. + if err := c.send(MsgStatusReq, nil); err != nil { + t.Fatalf("send: %v", err) + } + typ2, payload2 := c.readFrame(t) + if typ2 != MsgStatusRes { + t.Fatalf("got type 0x%02x, want MsgStatusRes", typ2) + } + var sp2 StatusPayload + if err := json.Unmarshal(payload2, &sp2); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if sp2.Alive { + t.Fatalf("expected alive=false after exit") + } + if sp2.ExitCode == nil || *sp2.ExitCode != 42 { + t.Fatalf("expected exitCode=42, got %v", sp2.ExitCode) + } + + // Keep-alive: the listener must still accept new connections. + c2 := newTestClient(t, f.addr) + defer c2.close() + if err := c2.send(MsgStatusReq, nil); err != nil { + t.Fatalf("keep-alive send: %v", err) + } + _, _ = c2.readFrame(t) // just verify it didn't crash +} + +// TestKillReq: MsgKillReq disposes the fakePTY, drops clients, closes +// listener, and Serve returns. +func TestKillReq(t *testing.T) { + f := startServe(t, 106) + + c := newTestClient(t, f.addr) + + if err := c.send(MsgKillReq, nil); err != nil { + t.Fatalf("send: %v", err) + } + + // Serve should return within 2s (includes the 50ms grace sleep). + f.waitDone(t) + + // PTY Close must have been called. + f.pty.closeMu.Lock() + closed := f.pty.closed + f.pty.closeMu.Unlock() + if !closed { + t.Fatal("expected pty.Close() to be called on kill") + } + + // Listener should be closed: new dial must fail. + conn, err := net.DialTimeout("tcp", f.addr, 200*time.Millisecond) + if err == nil { + _ = conn.Close() + t.Fatal("expected listener to be closed after kill, but Dial succeeded") + } + + c.close() +} + +// TestShutdownViaCtxCancel: cancelling the context triggers graceful shutdown. +func TestShutdownViaCtxCancel(t *testing.T) { + f := startServe(t, 107) + + c := newTestClient(t, f.addr) + defer c.close() + + // Cancel the context. + f.cancel() + + f.waitDone(t) + + // PTY Close must have been called. + f.pty.closeMu.Lock() + closed := f.pty.closed + f.pty.closeMu.Unlock() + if !closed { + t.Fatal("expected pty.Close() on ctx cancel") + } +} diff --git a/backend/internal/adapters/runtime/conpty/pidalive_unix.go b/backend/internal/adapters/runtime/conpty/pidalive_unix.go new file mode 100644 index 00000000..52f463ea --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/pidalive_unix.go @@ -0,0 +1,26 @@ +//go:build !windows + +package conpty + +import ( + "errors" + "os" + "syscall" +) + +// pidAlive probes PID liveness via signal 0. nil and EPERM both mean alive +// (process exists but may not be signallable). ESRCH means dead. +// Mirrors ptyregistry.defaultPidAlive (same signal-0 pattern). +func pidAlive(pid int) bool { + err := syscall.Kill(pid, 0) + if err == nil { + return true + } + return errors.Is(err, syscall.EPERM) +} + +// defaultOSProcessFinder wraps os.FindProcess for Unix (always succeeds on +// Unix; the returned handle is valid for Kill). +func defaultOSProcessFinder(pid int) (processKiller, error) { + return os.FindProcess(pid) +} diff --git a/backend/internal/adapters/runtime/conpty/pidalive_windows.go b/backend/internal/adapters/runtime/conpty/pidalive_windows.go new file mode 100644 index 00000000..a70a842a --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/pidalive_windows.go @@ -0,0 +1,30 @@ +//go:build windows + +package conpty + +import ( + "fmt" + "os" + + "golang.org/x/sys/windows" +) + +// pidAlive probes PID liveness on Windows by opening the process handle with +// SYNCHRONIZE (minimal permission). Failure means the process is gone. +func pidAlive(pid int) bool { + h, err := windows.OpenProcess(windows.SYNCHRONIZE, false, uint32(pid)) + if err != nil { + return false + } + _ = windows.CloseHandle(h) + return true +} + +// defaultOSProcessFinder wraps os.FindProcess for Windows. +func defaultOSProcessFinder(pid int) (processKiller, error) { + p, err := os.FindProcess(pid) + if err != nil { + return nil, fmt.Errorf("os.FindProcess(%d): %w", pid, err) + } + return p, nil +} diff --git a/backend/internal/adapters/runtime/conpty/proto.go b/backend/internal/adapters/runtime/conpty/proto.go new file mode 100644 index 00000000..662f78e8 --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/proto.go @@ -0,0 +1,99 @@ +// Package conpty implements the Windows ConPTY runtime adapter for agent sessions. +// This file contains the OS-agnostic binary framing protocol codec used by the +// named-pipe protocol between pty-host.js and this Go client. +// +// Frame layout: [1-byte type][4-byte big-endian length][payload] +package conpty + +import ( + "encoding/binary" + "fmt" + "math" +) + +// Message type constants. Values must match pty-host.ts MSG_* constants exactly. +const ( + MsgTerminalData byte = 0x01 // host -> client: raw PTY output + MsgTerminalInput byte = 0x02 // client -> host: raw keystrokes + MsgResize byte = 0x03 // client -> host: JSON {cols, rows} + MsgGetOutputReq byte = 0x04 // client -> host: JSON {lines} + MsgGetOutputRes byte = 0x05 // host -> client: UTF-8 text + MsgStatusReq byte = 0x06 // client -> host: empty + MsgStatusRes byte = 0x07 // host -> client: JSON {alive, pid, exitCode?} + MsgKillReq byte = 0x08 // client -> host: empty +) + +// JSON payload structs shared with later tasks (kept minimal). + +// ResizePayload is the JSON body for MsgResize. +type ResizePayload struct { + Cols int `json:"cols"` + Rows int `json:"rows"` +} + +// StatusPayload is the JSON body for MsgStatusRes. +type StatusPayload struct { + Alive bool `json:"alive"` + PID int `json:"pid"` + ExitCode *int `json:"exitCode,omitempty"` +} + +// GetOutputReq is the JSON body for MsgGetOutputReq. +type GetOutputReq struct { + Lines int `json:"lines"` +} + +// EncodeMessage encodes a single frame into the binary protocol format. +// It allocates a fresh slice of exactly 5+len(payload) bytes. +// Returns an error if the payload exceeds the 4-byte length field capacity. +func EncodeMessage(msgType byte, payload []byte) ([]byte, error) { + n := len(payload) + if n > math.MaxUint32 { + return nil, fmt.Errorf("conpty: payload too large (%d bytes, max %d)", n, math.MaxUint32) + } + payloadLen := uint32(n) // safe: n <= math.MaxUint32 checked above + frame := make([]byte, 5+n) + frame[0] = msgType + binary.BigEndian.PutUint32(frame[1:5], payloadLen) + copy(frame[5:], payload) + return frame, nil +} + +// MessageParser is a streaming parser for the binary framing protocol. +// It accumulates arbitrary-sized chunks from a pipe/socket stream and fires +// onMessage exactly once per complete frame, regardless of chunk boundaries. +// Safe to call Feed from a single goroutine; not concurrency-safe itself. +type MessageParser struct { + buf []byte + onMessage func(msgType byte, payload []byte) +} + +// NewMessageParser returns a parser that calls onMessage for each complete frame. +// onMessage receives a COPY of the payload so callers may retain it safely. +func NewMessageParser(onMessage func(msgType byte, payload []byte)) *MessageParser { + return &MessageParser{onMessage: onMessage} +} + +// Feed appends chunk to the internal buffer and dispatches all complete frames. +// It matches the semantics of MessageParser.feed in pty-host.ts exactly: +// arbitrary chunk boundaries and multiple frames per chunk are both handled. +func (p *MessageParser) Feed(chunk []byte) { + p.buf = append(p.buf, chunk...) + + for len(p.buf) >= 5 { + payloadLen := binary.BigEndian.Uint32(p.buf[1:5]) + frameLen := 5 + int(payloadLen) + if len(p.buf) < frameLen { + break + } + + msgType := p.buf[0] + // ponytail: explicit copy so callers that retain the slice are not + // corrupted when p.buf grows/reallocates on a later Feed call. + payload := make([]byte, payloadLen) + copy(payload, p.buf[5:frameLen]) + + p.buf = p.buf[frameLen:] + p.onMessage(msgType, payload) + } +} diff --git a/backend/internal/adapters/runtime/conpty/proto_test.go b/backend/internal/adapters/runtime/conpty/proto_test.go new file mode 100644 index 00000000..1014f83a --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/proto_test.go @@ -0,0 +1,200 @@ +package conpty + +import ( + "bytes" + "encoding/binary" + "testing" +) + +// TestEncodeMessage verifies the 5-byte header and payload are written correctly. +func TestEncodeMessage(t *testing.T) { + payload := []byte("hello") + frame, err := EncodeMessage(MsgTerminalData, payload) + if err != nil { + t.Fatalf("EncodeMessage: %v", err) + } + + if len(frame) != 5+len(payload) { + t.Fatalf("frame len = %d, want %d", len(frame), 5+len(payload)) + } + if frame[0] != MsgTerminalData { + t.Errorf("type byte = 0x%02x, want 0x%02x", frame[0], MsgTerminalData) + } + gotLen := binary.BigEndian.Uint32(frame[1:5]) + if int(gotLen) != len(payload) { + t.Errorf("length field = %d, want %d", gotLen, len(payload)) + } + if !bytes.Equal(frame[5:], payload) { + t.Errorf("payload = %q, want %q", frame[5:], payload) + } +} + +// TestEncodeMessageZeroPayload verifies a zero-length payload encodes correctly. +func TestEncodeMessageZeroPayload(t *testing.T) { + frame, err := EncodeMessage(MsgStatusReq, nil) + if err != nil { + t.Fatalf("EncodeMessage: %v", err) + } + if len(frame) != 5 { + t.Fatalf("frame len = %d, want 5", len(frame)) + } + if frame[0] != MsgStatusReq { + t.Errorf("type byte = 0x%02x, want 0x%02x", frame[0], MsgStatusReq) + } + if got := binary.BigEndian.Uint32(frame[1:5]); got != 0 { + t.Errorf("length field = %d, want 0", got) + } +} + +// collected accumulates (type, payload) pairs received by a MessageParser. +type collected struct { + typ byte + payload []byte +} + +func collect(frames *[]collected) func(byte, []byte) { + return func(typ byte, payload []byte) { + *frames = append(*frames, collected{typ, payload}) + } +} + +// TestParserSingleFrame feeds one complete frame and expects one callback. +func TestParserSingleFrame(t *testing.T) { + var got []collected + p := NewMessageParser(collect(&got)) + + f, _ := EncodeMessage(MsgTerminalData, []byte("hi")) + p.Feed(f) + + if len(got) != 1 { + t.Fatalf("got %d messages, want 1", len(got)) + } + if got[0].typ != MsgTerminalData { + t.Errorf("type = 0x%02x, want 0x%02x", got[0].typ, MsgTerminalData) + } + if !bytes.Equal(got[0].payload, []byte("hi")) { + t.Errorf("payload = %q, want %q", got[0].payload, "hi") + } +} + +// TestParserTwoFramesOneChunk feeds two frames concatenated and expects two callbacks. +func TestParserTwoFramesOneChunk(t *testing.T) { + var got []collected + p := NewMessageParser(collect(&got)) + + f1, _ := EncodeMessage(MsgTerminalData, []byte("frame1")) + f2, _ := EncodeMessage(MsgTerminalInput, []byte("frame2")) + chunk := append(f1, f2...) + p.Feed(chunk) + + if len(got) != 2 { + t.Fatalf("got %d messages, want 2", len(got)) + } + if got[0].typ != MsgTerminalData || string(got[0].payload) != "frame1" { + t.Errorf("message 0 = {%02x, %q}", got[0].typ, got[0].payload) + } + if got[1].typ != MsgTerminalInput || string(got[1].payload) != "frame2" { + t.Errorf("message 1 = {%02x, %q}", got[1].typ, got[1].payload) + } +} + +// TestParserByteAtATime feeds one frame one byte at a time and expects exactly +// one callback with the correct type and payload. +func TestParserByteAtATime(t *testing.T) { + var got []collected + p := NewMessageParser(collect(&got)) + + frame, _ := EncodeMessage(MsgResize, []byte(`{"cols":80,"rows":24}`)) + for _, b := range frame { + p.Feed([]byte{b}) + } + + if len(got) != 1 { + t.Fatalf("got %d messages, want 1", len(got)) + } + if got[0].typ != MsgResize { + t.Errorf("type = 0x%02x, want 0x%02x", got[0].typ, MsgResize) + } + want := []byte(`{"cols":80,"rows":24}`) + if !bytes.Equal(got[0].payload, want) { + t.Errorf("payload = %q, want %q", got[0].payload, want) + } +} + +// TestParserInterleavedTypes feeds frames of different types and verifies order. +func TestParserInterleavedTypes(t *testing.T) { + types := []byte{MsgStatusReq, MsgKillReq, MsgGetOutputReq, MsgStatusRes} + payloads := [][]byte{nil, nil, []byte(`{"lines":10}`), []byte(`{"alive":true,"pid":42}`)} + + var chunk []byte + for i, typ := range types { + f, _ := EncodeMessage(typ, payloads[i]) + chunk = append(chunk, f...) + } + + var got []collected + p := NewMessageParser(collect(&got)) + p.Feed(chunk) + + if len(got) != len(types) { + t.Fatalf("got %d messages, want %d", len(got), len(types)) + } + for i, g := range got { + if g.typ != types[i] { + t.Errorf("[%d] type = 0x%02x, want 0x%02x", i, g.typ, types[i]) + } + if !bytes.Equal(g.payload, payloads[i]) { + t.Errorf("[%d] payload = %q, want %q", i, g.payload, payloads[i]) + } + } +} + +// TestParserPayloadIsCopy verifies that the payload delivered to onMessage is a +// true copy, not a subslice of the parser's internal buffer. It exercises the +// aliasing path that matters in practice: feed frame1, capture its payload, then +// feed frame2 of the SAME length so the parser reuses the same buffer region; +// frame1's captured bytes must be unchanged. This catches a regression where +// payload was a raw subslice of p.buf instead of a make+copy. +func TestParserPayloadIsCopy(t *testing.T) { + var got []collected + p := NewMessageParser(collect(&got)) + + // Feed frame1 and capture the delivered payload pointer. + frame1, _ := EncodeMessage(MsgTerminalData, []byte("original")) + p.Feed(frame1) + if len(got) != 1 { + t.Fatalf("after frame1: got %d messages, want 1", len(got)) + } + captured := got[0].payload + + // Feed frame2 with the same payload length so the parser's internal buffer + // overwrites the exact byte range that frame1 occupied. + frame2, _ := EncodeMessage(MsgTerminalInput, []byte("XXXXXXXX")) // same len as "original" + p.Feed(frame2) + if len(got) != 2 { + t.Fatalf("after frame2: got %d messages, want 2", len(got)) + } + + // frame1's captured payload must be unaffected by the subsequent Feed. + if !bytes.Equal(captured, []byte("original")) { + t.Errorf("frame1 payload aliased internal buffer: got %q after frame2", captured) + } +} + +// TestParserZeroLengthFrame verifies a zero-payload frame (e.g. MsgStatusReq) parses. +func TestParserZeroLengthFrame(t *testing.T) { + var got []collected + p := NewMessageParser(collect(&got)) + f, _ := EncodeMessage(MsgStatusReq, nil) + p.Feed(f) + + if len(got) != 1 { + t.Fatalf("got %d messages, want 1", len(got)) + } + if got[0].typ != MsgStatusReq { + t.Errorf("type = 0x%02x, want 0x%02x", got[0].typ, MsgStatusReq) + } + if len(got[0].payload) != 0 { + t.Errorf("payload len = %d, want 0", len(got[0].payload)) + } +} diff --git a/backend/internal/adapters/runtime/conpty/ptyregistry/pidalive_unix.go b/backend/internal/adapters/runtime/conpty/ptyregistry/pidalive_unix.go new file mode 100644 index 00000000..e7e197b9 --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/ptyregistry/pidalive_unix.go @@ -0,0 +1,19 @@ +//go:build !windows + +package ptyregistry + +import ( + "errors" + "syscall" +) + +// defaultPidAlive probes PID liveness via signal 0. nil and EPERM both mean +// alive (process exists but may not be queryable). ESRCH means dead. +// Mirrors process.kill(pid, 0) with EPERM-means-alive from the TS source. +func defaultPidAlive(pid int) bool { + err := syscall.Kill(pid, 0) + if err == nil { + return true + } + return errors.Is(err, syscall.EPERM) +} diff --git a/backend/internal/adapters/runtime/conpty/ptyregistry/pidalive_windows.go b/backend/internal/adapters/runtime/conpty/ptyregistry/pidalive_windows.go new file mode 100644 index 00000000..1048ce5d --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/ptyregistry/pidalive_windows.go @@ -0,0 +1,19 @@ +//go:build windows + +package ptyregistry + +import ( + "golang.org/x/sys/windows" +) + +// defaultPidAlive probes PID liveness via OpenProcess. SUCCESS means alive +// (CloseHandle and return true). ERROR_ACCESS_DENIED mirrors EPERM: the +// process exists but cannot be queried, so treat as alive. +func defaultPidAlive(pid int) bool { + h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid)) + if err == nil { + _ = windows.CloseHandle(h) + return true + } + return err == windows.ERROR_ACCESS_DENIED +} diff --git a/backend/internal/adapters/runtime/conpty/ptyregistry/ptyregistry_test.go b/backend/internal/adapters/runtime/conpty/ptyregistry/ptyregistry_test.go new file mode 100644 index 00000000..12b04143 --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/ptyregistry/ptyregistry_test.go @@ -0,0 +1,229 @@ +package ptyregistry + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + "time" +) + +// withFakePidAlive replaces the pidAlive var for the duration of the test. +func withFakePidAlive(t *testing.T, fn func(pid int) bool) { + t.Helper() + orig := pidAlive + pidAlive = fn + t.Cleanup(func() { pidAlive = orig }) +} + +// setupHome points HOME at a temp dir and returns the expected registry path. +func setupHome(t *testing.T) string { + t.Helper() + dir := t.TempDir() + t.Setenv("HOME", dir) + return dir + "/.ao/windows-pty-hosts.json" +} + +func nowRFC3339() string { + return time.Now().UTC().Format(time.RFC3339) +} + +func TestRegisterThenList(t *testing.T) { + setupHome(t) + withFakePidAlive(t, func(int) bool { return true }) + + e := Entry{SessionID: "s1", PtyHostPID: 1234, PipePath: `\\.\pipe\ao-s1`, RegisteredAt: nowRFC3339()} + if err := Register(e); err != nil { + t.Fatal(err) + } + + got, err := List() + if err != nil { + t.Fatal(err) + } + if len(got) != 1 || got[0].SessionID != "s1" { + t.Fatalf("expected [s1], got %v", got) + } +} + +func TestRegisterReplaceSameID(t *testing.T) { + setupHome(t) + withFakePidAlive(t, func(int) bool { return true }) + + e1 := Entry{SessionID: "s1", PtyHostPID: 111, PipePath: `\\.\pipe\ao-s1-a`, RegisteredAt: nowRFC3339()} + e2 := Entry{SessionID: "s1", PtyHostPID: 222, PipePath: `\\.\pipe\ao-s1-b`, RegisteredAt: nowRFC3339()} + if err := Register(e1); err != nil { + t.Fatal(err) + } + if err := Register(e2); err != nil { + t.Fatal(err) + } + + got, err := List() + if err != nil { + t.Fatal(err) + } + if len(got) != 1 { + t.Fatalf("expected 1 entry, got %d", len(got)) + } + if got[0].PtyHostPID != 222 { + t.Fatalf("expected PID 222, got %d", got[0].PtyHostPID) + } +} + +func TestUnregisterRemoves(t *testing.T) { + setupHome(t) + withFakePidAlive(t, func(int) bool { return true }) + + e := Entry{SessionID: "s1", PtyHostPID: 1234, PipePath: `\\.\pipe\ao-s1`, RegisteredAt: nowRFC3339()} + if err := Register(e); err != nil { + t.Fatal(err) + } + if err := Unregister("s1"); err != nil { + t.Fatal(err) + } + got, err := List() + if err != nil { + t.Fatal(err) + } + if len(got) != 0 { + t.Fatalf("expected empty, got %v", got) + } +} + +func TestUnregisterNoOpWhenAbsent(t *testing.T) { + setupHome(t) + withFakePidAlive(t, func(int) bool { return true }) + + if err := Unregister("nonexistent"); err != nil { + t.Fatal(err) + } +} + +func TestListPrunesDeadPIDs(t *testing.T) { + regPath := setupHome(t) + + // PID 1 alive, PID 2 dead. + alive := map[int]bool{1: true, 2: false} + withFakePidAlive(t, func(pid int) bool { return alive[pid] }) + + e1 := Entry{SessionID: "s1", PtyHostPID: 1, PipePath: `\\.\pipe\ao-s1`, RegisteredAt: nowRFC3339()} + e2 := Entry{SessionID: "s2", PtyHostPID: 2, PipePath: `\\.\pipe\ao-s2`, RegisteredAt: nowRFC3339()} + if err := Register(e1); err != nil { + t.Fatal(err) + } + if err := Register(e2); err != nil { + t.Fatal(err) + } + + got, err := List() + if err != nil { + t.Fatal(err) + } + if len(got) != 1 || got[0].SessionID != "s1" { + t.Fatalf("expected [s1], got %v", got) + } + + // Verify the on-disk file was rewritten with only the live entry. + data, err := os.ReadFile(regPath) + if err != nil { + t.Fatal(err) + } + var disk []Entry + if err := json.Unmarshal(data, &disk); err != nil { + t.Fatal(err) + } + if len(disk) != 1 || disk[0].SessionID != "s1" { + t.Fatalf("disk should have only s1, got %v", disk) + } +} + +func TestEmptyResultDeletesFile(t *testing.T) { + regPath := setupHome(t) + withFakePidAlive(t, func(int) bool { return true }) + + e := Entry{SessionID: "s1", PtyHostPID: 1, PipePath: `\\.\pipe\ao-s1`, RegisteredAt: nowRFC3339()} + if err := Register(e); err != nil { + t.Fatal(err) + } + // Unregister last entry -> file should be deleted. + if err := Unregister("s1"); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(regPath); !os.IsNotExist(err) { + t.Fatal("expected registry file to be deleted") + } +} + +func TestClearDeletesFile(t *testing.T) { + regPath := setupHome(t) + withFakePidAlive(t, func(int) bool { return true }) + + e := Entry{SessionID: "s1", PtyHostPID: 1, PipePath: `\\.\pipe\ao-s1`, RegisteredAt: nowRFC3339()} + if err := Register(e); err != nil { + t.Fatal(err) + } + if err := Clear(); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(regPath); !os.IsNotExist(err) { + t.Fatal("expected registry file to be deleted after Clear") + } +} + +func TestMalformedJSONReturnsEmpty(t *testing.T) { + setupHome(t) + withFakePidAlive(t, func(int) bool { return true }) + + // Write malformed JSON directly. + path, _ := registryFile() + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte("not json {{{"), 0o600); err != nil { + t.Fatal(err) + } + + got, err := List() + if err != nil { + t.Fatal(err) + } + if len(got) != 0 { + t.Fatalf("expected empty on malformed JSON, got %v", got) + } +} + +func TestMissingFileReturnsEmpty(t *testing.T) { + setupHome(t) + withFakePidAlive(t, func(int) bool { return true }) + + got, err := List() + if err != nil { + t.Fatal(err) + } + if len(got) != 0 { + t.Fatalf("expected empty for missing file, got %v", got) + } +} + +func TestAtomicWriteProducesValidJSON(t *testing.T) { + regPath := setupHome(t) + withFakePidAlive(t, func(int) bool { return true }) + + e := Entry{SessionID: "s1", PtyHostPID: 99, PipePath: `\\.\pipe\ao-s1`, RegisteredAt: nowRFC3339()} + if err := Register(e); err != nil { + t.Fatal(err) + } + + data, err := os.ReadFile(regPath) + if err != nil { + t.Fatal(err) + } + var entries []Entry + if err := json.Unmarshal(data, &entries); err != nil { + t.Fatalf("registry file is not valid JSON: %v", err) + } + if len(entries) != 1 || entries[0].PtyHostPID != 99 { + t.Fatalf("unexpected entries: %v", entries) + } +} diff --git a/backend/internal/adapters/runtime/conpty/ptyregistry/registry.go b/backend/internal/adapters/runtime/conpty/ptyregistry/registry.go new file mode 100644 index 00000000..9f8fe614 --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/ptyregistry/registry.go @@ -0,0 +1,164 @@ +// Package ptyregistry is a sideband JSON list of live Windows pty-host +// processes so ao stop can find and graceful-kill them even when session +// metadata is lost. Ported from agent-orchestrator's windows-pty-registry.ts. +package ptyregistry + +import ( + "encoding/json" + "errors" + "os" + "path/filepath" +) + +// Entry is one registered pty-host process. +type Entry struct { + SessionID string `json:"sessionId"` + PtyHostPID int `json:"ptyHostPid"` + PipePath string `json:"pipePath"` + RegisteredAt string `json:"registeredAt"` // RFC3339; set by caller +} + +// pidAlive is the PID-liveness probe. Tests replace it with a fake. +// defaultPidAlive is provided in build-tagged files (pidalive_unix.go / +// pidalive_windows.go). +var pidAlive = defaultPidAlive + +// registryFile resolves ~/.ao/windows-pty-hosts.json. Uses os.UserHomeDir() +// so t.Setenv("HOME", dir) in tests redirects reads/writes to a temp dir. +// ponytail: HOME-based resolution; no AO_DATA_DIR override needed here. +func registryFile() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, ".ao", "windows-pty-hosts.json"), nil +} + +// readRaw reads and defensively parses the registry. Missing file or malformed +// JSON both return an empty slice (mirrors readRaw in the TS source). +func readRaw() []Entry { + path, err := registryFile() + if err != nil { + return nil + } + data, err := os.ReadFile(path) + if err != nil { + // Missing file is fine. + return nil + } + var parsed []json.RawMessage + if err := json.Unmarshal(data, &parsed); err != nil { + return nil + } + out := make([]Entry, 0, len(parsed)) + for _, raw := range parsed { + var e Entry + if err := json.Unmarshal(raw, &e); err != nil { + continue + } + // Drop entries missing required fields (mirrors TS filter). + if e.SessionID == "" || e.PtyHostPID == 0 || e.PipePath == "" { + continue + } + out = append(out, e) + } + return out +} + +// writeRaw atomically writes entries to the registry file. When entries is +// empty it deletes the file instead (mirrors writeRaw in the TS source). +func writeRaw(entries []Entry) error { + path, err := registryFile() + if err != nil { + return err + } + + if len(entries) == 0 { + err := os.Remove(path) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + return nil + } + + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0o700); err != nil { + return err + } + + data, err := json.MarshalIndent(entries, "", " ") + if err != nil { + return err + } + + // Atomic write: temp file in same dir then rename (same filesystem). + tmp, err := os.CreateTemp(dir, "pty-hosts-*.json.tmp") + if err != nil { + return err + } + tmpName := tmp.Name() + defer func() { + // Best-effort cleanup of temp file on failure. + _ = os.Remove(tmpName) + }() + + if _, err := tmp.Write(data); err != nil { + _ = tmp.Close() + return err + } + if err := tmp.Close(); err != nil { + return err + } + return os.Rename(tmpName, path) +} + +// Register adds or replaces the entry for entry.SessionID. registeredAt must +// be set by the caller (e.g. time.Now().UTC().Format(time.RFC3339)). +func Register(entry Entry) error { + next := make([]Entry, 0) + for _, e := range readRaw() { + if e.SessionID != entry.SessionID { + next = append(next, e) + } + } + next = append(next, entry) + return writeRaw(next) +} + +// Unregister removes the entry for sessionID. No-op if absent. +func Unregister(sessionID string) error { + all := readRaw() + next := make([]Entry, 0, len(all)) + for _, e := range all { + if e.SessionID != sessionID { + next = append(next, e) + } + } + if len(next) == len(all) { + return nil // absent, no-op + } + return writeRaw(next) +} + +// List returns all entries whose PtyHostPID is still alive, auto-pruning dead +// ones. The file is rewritten if any entries were pruned. +func List() ([]Entry, error) { + all := readRaw() + live := make([]Entry, 0, len(all)) + for _, e := range all { + if pidAlive(e.PtyHostPID) { + live = append(live, e) + } + } + if len(live) != len(all) { + if err := writeRaw(live); err != nil { + return live, err + } + } + return live, nil +} + +// Clear deletes the registry file. Best-effort; used by tests and recovery. +func Clear() error { + return writeRaw(nil) +} diff --git a/backend/internal/adapters/runtime/conpty/ring.go b/backend/internal/adapters/runtime/conpty/ring.go new file mode 100644 index 00000000..06d28c18 --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/ring.go @@ -0,0 +1,83 @@ +package conpty + +import ( + "strings" + "sync" +) + +// MaxOutputLines is the rolling line-buffer cap, matching MAX_OUTPUT_LINES in pty-host.ts. +const MaxOutputLines = 1000 + +// Ring is a bounded rolling buffer of terminal output lines, ANSI codes preserved. +// It mirrors the appendOutput state machine from pty-host.ts. +// Concurrent Append and Snapshot/Tail calls are safe. +type Ring struct { + mu sync.Mutex + lines []string // each entry is "line\n" (or bare text on FlushPartial) + partialLine string +} + +// NewRing returns an empty Ring. +func NewRing() *Ring { + return &Ring{} +} + +// Append mirrors appendOutput from pty-host.ts: prepend the current partialLine, +// split on newlines, store completed lines with "\n" re-appended, keep the last +// element as the new partialLine, then trim to MaxOutputLines. +func (r *Ring) Append(raw []byte) { + r.mu.Lock() + defer r.mu.Unlock() + + text := r.partialLine + string(raw) + parts := strings.Split(text, "\n") + // The last element is either "" (text ended with \n) or an incomplete line. + r.partialLine = parts[len(parts)-1] + for _, line := range parts[:len(parts)-1] { + r.lines = append(r.lines, line+"\n") + } + if len(r.lines) > MaxOutputLines { + // ponytail: slice off the head; ceiling: O(n) copy on every trim cycle. + // Upgrade path: circular buffer if trim rate is very high. + r.lines = r.lines[len(r.lines)-MaxOutputLines:] + } +} + +// FlushPartial pushes any in-progress partial line as a final entry. +// Called on PTY exit to mirror the pty-host.ts onExit handler. +func (r *Ring) FlushPartial() { + r.mu.Lock() + defer r.mu.Unlock() + + if r.partialLine == "" { + return + } + r.lines = append(r.lines, r.partialLine) + r.partialLine = "" +} + +// Snapshot returns all stored lines concatenated as raw bytes for scrollback replay. +// The in-progress partialLine is NOT included (matches TS outputBuffer.join("")). +func (r *Ring) Snapshot() []byte { + r.mu.Lock() + defer r.mu.Unlock() + + return []byte(strings.Join(r.lines, "")) +} + +// Tail returns the last n stored lines joined as a string. +// Mirrors the MSG_GET_OUTPUT_REQ handler: start = max(0, len-lines). +// n <= 0 returns "". +func (r *Ring) Tail(n int) string { + r.mu.Lock() + defer r.mu.Unlock() + + if n <= 0 { + return "" + } + start := len(r.lines) - n + if start < 0 { + start = 0 + } + return strings.Join(r.lines[start:], "") +} diff --git a/backend/internal/adapters/runtime/conpty/ring_test.go b/backend/internal/adapters/runtime/conpty/ring_test.go new file mode 100644 index 00000000..140131d3 --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/ring_test.go @@ -0,0 +1,161 @@ +package conpty + +import ( + "strings" + "sync" + "testing" +) + +// TestRingAppendPartialThenComplete verifies partial-line accumulation and +// that Snapshot/Tail reflect only completed lines. +func TestRingAppendPartialThenComplete(t *testing.T) { + r := NewRing() + r.Append([]byte("hel")) + r.Append([]byte("lo\nwor")) + + snap := string(r.Snapshot()) + if snap != "hello\n" { + t.Errorf("Snapshot = %q, want %q", snap, "hello\n") + } + + tail := r.Tail(10) + if tail != "hello\n" { + t.Errorf("Tail(10) = %q, want %q", tail, "hello\n") + } + + // Flush the partial "wor" + r.FlushPartial() + snap = string(r.Snapshot()) + if snap != "hello\nwor" { + t.Errorf("after FlushPartial Snapshot = %q, want %q", snap, "hello\nwor") + } +} + +// TestRingExceedsMaxOutputLines verifies the buffer trims to MaxOutputLines. +func TestRingExceedsMaxOutputLines(t *testing.T) { + r := NewRing() + // Push 1005 lines. + for i := 0; i < 1005; i++ { + r.Append([]byte("x\n")) + } + + snap := r.Snapshot() + got := strings.Count(string(snap), "\n") + if got != MaxOutputLines { + t.Errorf("stored %d lines, want %d", got, MaxOutputLines) + } +} + +// TestRingFlushPartialNoNewline verifies FlushPartial pushes a trailing line. +func TestRingFlushPartialNoNewline(t *testing.T) { + r := NewRing() + r.Append([]byte("line1\npartial")) + r.FlushPartial() + + snap := string(r.Snapshot()) + if !strings.Contains(snap, "partial") { + t.Errorf("Snapshot missing 'partial': %q", snap) + } + + // Calling FlushPartial again is a no-op. + r.FlushPartial() + snap2 := string(r.Snapshot()) + if snap2 != snap { + t.Errorf("second FlushPartial changed snapshot: %q -> %q", snap, snap2) + } +} + +// TestRingTailEdgeCases covers n > stored count and n <= 0. +func TestRingTailEdgeCases(t *testing.T) { + r := NewRing() + r.Append([]byte("a\nb\n")) + + if got := r.Tail(100); got != "a\nb\n" { + t.Errorf("Tail(100) = %q, want %q", got, "a\nb\n") + } + if got := r.Tail(0); got != "" { + t.Errorf("Tail(0) = %q, want empty", got) + } + if got := r.Tail(-1); got != "" { + t.Errorf("Tail(-1) = %q, want empty", got) + } +} + +// TestRingANSIRoundTrip verifies raw ANSI escape sequences survive storage intact. +func TestRingANSIRoundTrip(t *testing.T) { + ansi := "\x1b[31mhi\x1b[0m\n" + r := NewRing() + r.Append([]byte(ansi)) + + snap := string(r.Snapshot()) + if snap != ansi { + t.Errorf("Snapshot = %q, want %q", snap, ansi) + } + tail := r.Tail(1) + if tail != ansi { + t.Errorf("Tail(1) = %q, want %q", tail, ansi) + } +} + +// TestRingTailSubset verifies Tail returns exactly the last n lines. +func TestRingTailSubset(t *testing.T) { + r := NewRing() + for i := 0; i < 10; i++ { + r.Append([]byte("line\n")) + } + + tail3 := r.Tail(3) + if got := strings.Count(tail3, "\n"); got != 3 { + t.Errorf("Tail(3) contains %d newlines, want 3", got) + } +} + +// TestRingSnapshotExcludesPartial verifies the in-progress partial line is NOT +// included in Snapshot (matches TS semantics: only outputBuffer, not partialLine). +func TestRingSnapshotExcludesPartial(t *testing.T) { + r := NewRing() + r.Append([]byte("complete\npartial")) + + snap := string(r.Snapshot()) + if strings.Contains(snap, "partial") { + t.Errorf("Snapshot includes partial line: %q", snap) + } + if !strings.Contains(snap, "complete\n") { + t.Errorf("Snapshot missing complete line: %q", snap) + } +} + +// TestRingConcurrent validates the advertised goroutine-safety of Ring under the +// race detector. It spawns 10 writer goroutines (Append) and 10 reader goroutines +// (Snapshot + Tail) that all run concurrently; any data race will be caught by +// "go test -race". The test itself only asserts no panic and no race. +func TestRingConcurrent(t *testing.T) { + const goroutines = 10 + const iters = 100 + + r := NewRing() + var wg sync.WaitGroup + + for i := 0; i < goroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < iters; j++ { + r.Append([]byte("line\n")) + } + }() + } + + for i := 0; i < goroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < iters; j++ { + _ = r.Snapshot() + _ = r.Tail(10) + } + }() + } + + wg.Wait() +} diff --git a/backend/internal/adapters/runtime/conpty/runtime.go b/backend/internal/adapters/runtime/conpty/runtime.go new file mode 100644 index 00000000..09c6d95d --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/runtime.go @@ -0,0 +1,235 @@ +// runtime.go - conpty Runtime adapter. Implements ports.Runtime and +// ports.Attacher (see attach.go). Drives sessions via the B3 pty-host over +// loopback TCP, using the B1 protocol and the B2 registry for restart recovery. +package conpty + +import ( + "context" + "fmt" + "regexp" + "sync" + "time" + + "github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/conpty/ptyregistry" + "github.com/aoagents/agent-orchestrator/backend/internal/ports" +) + +// Ensure Runtime satisfies the port at compile time (Attach in attach.go). +var _ ports.Runtime = (*Runtime)(nil) + +// validSessionID matches agent-orchestrator's assertValidSessionId. +var validSessionID = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) + +// hostSession is the in-memory state for a live pty-host connection. +type hostSession struct { + addr string + pid int +} + +// Options configures the Runtime. All fields are optional; zero values use +// sensible defaults. The Spawner field is injectable for tests. +type Options struct { + // Spawner overrides the default OS-level process spawner. If nil, + // defaultSpawnHost is used (Windows-only; returns an error on other OSes). + Spawner hostSpawner +} + +// Runtime is the conpty runtime adapter. +type Runtime struct { + spawner hostSpawner + + mu sync.Mutex + sessions map[string]*hostSession // sessionID -> live session +} + +// New creates a Runtime with the given options. +func New(opts Options) *Runtime { + sp := opts.Spawner + if sp == nil { + sp = defaultSpawnHost + } + return &Runtime{ + spawner: sp, + sessions: make(map[string]*hostSession), + } +} + +// Create spawns a detached pty-host for the session, waits for READY, stores +// the addr+pid in-memory and in the B2 registry, and returns the handle. +// Returns an error if sessionID is invalid, already exists, or spawn fails. +func (r *Runtime) Create(ctx context.Context, cfg ports.RuntimeConfig) (ports.RuntimeHandle, error) { + id := string(cfg.SessionID) + if !validSessionID.MatchString(id) { + return ports.RuntimeHandle{}, fmt.Errorf("conpty: invalid session id %q: must match ^[a-zA-Z0-9_-]+$", id) + } + if cfg.WorkspacePath == "" { + return ports.RuntimeHandle{}, fmt.Errorf("conpty: workspace path required") + } + if len(cfg.Argv) == 0 { + return ports.RuntimeHandle{}, fmt.Errorf("conpty: argv required") + } + + r.mu.Lock() + if _, dup := r.sessions[id]; dup { + r.mu.Unlock() + return ports.RuntimeHandle{}, fmt.Errorf("conpty: session %q already exists; destroy before re-creating", id) + } + // Reserve the slot before the async spawn so a concurrent Create for the + // same id fails immediately (no gap between check and set). + r.sessions[id] = nil + r.mu.Unlock() + + addr, pid, err := r.spawner(ctx, id, cfg.WorkspacePath, cfg.Argv, cfg.Env) + if err != nil { + r.mu.Lock() + delete(r.sessions, id) + r.mu.Unlock() + return ports.RuntimeHandle{}, fmt.Errorf("conpty: spawn pty-host for %q: %w", id, err) + } + + sess := &hostSession{addr: addr, pid: pid} + + r.mu.Lock() + r.sessions[id] = sess + r.mu.Unlock() + + // Register in B2 registry for daemon-restart recovery (best-effort). + _ = ptyregistry.Register(ptyregistry.Entry{ + SessionID: id, + PtyHostPID: pid, + PipePath: addr, // ponytail: reuse PipePath field for loopback addr + RegisteredAt: time.Now().UTC().Format(time.RFC3339), + }) + + return ports.RuntimeHandle{ID: id}, nil +} + +// Destroy gracefully kills the pty-host, waits up to ~500ms for the pid to +// exit, then force-kills it. Removes the session from the map and the registry. +// Idempotent: unknown/already-gone session returns nil. +func (r *Runtime) Destroy(ctx context.Context, handle ports.RuntimeHandle) error { + sess := r.resolve(handle.ID) + if sess == nil { + return nil // unknown or already gone + } + + // Ask host to shut down gracefully (triggers shutdown() in Serve). + _ = clientKill(sess.addr) + + // Poll up to ~500ms (20 x 25ms) for the pty-host pid to exit. + // ponytail: signal-0 probe; upgrade to process-tree kill if orphan ConPTY + // helpers appear. + deadline := time.Now().Add(500 * time.Millisecond) + for time.Now().Before(deadline) { + if !pidAlive(sess.pid) { + break + } + time.Sleep(25 * time.Millisecond) + } + + // Best-effort force-kill (the host's graceful shutdown already disposed + // the ConPTY child; killing the host process is sufficient). + if p, err := findProcess(sess.pid); err == nil { + _ = p.Kill() + } + + r.mu.Lock() + delete(r.sessions, handle.ID) + r.mu.Unlock() + + _ = ptyregistry.Unregister(handle.ID) + return nil +} + +// IsAlive distinguishes three outcomes so the reaper never spuriously reaps a +// live session on a transient probe failure: +// +// - (true, nil): the pty-host answered a status probe -> alive. +// - (false, nil): DEFINITIVELY gone. Either the session resolves to nothing +// (no in-memory entry and no registry entry), or the dial was refused +// (nothing listening on the loopback addr). +// - (false, err): a TRANSIENT probe failure (loopback timeout, connected- +// then-failed I/O). The reaper records ProbeFailed and retries rather than +// treating it as a death conclusion. +// +// tmux/zellij return a non-nil error for transient failures for the same +// reason; conpty matches that contract here. +func (r *Runtime) IsAlive(ctx context.Context, handle ports.RuntimeHandle) (bool, error) { + sess := r.resolve(handle.ID) + if sess == nil { + return false, nil // no in-memory entry, no registry entry -> definitively gone + } + return clientIsAlive(sess.addr) +} + +// SendMessage chunks message and writes it to the pty-host followed by Enter. +func (r *Runtime) SendMessage(ctx context.Context, handle ports.RuntimeHandle, message string) error { + sess := r.resolve(handle.ID) + if sess == nil { + return fmt.Errorf("conpty: session %q not found", handle.ID) + } + return clientSendMessage(sess.addr, message) +} + +// GetOutput returns the last lines lines from the pty-host ring buffer. +func (r *Runtime) GetOutput(ctx context.Context, handle ports.RuntimeHandle, lines int) (string, error) { + if lines <= 0 { + return "", fmt.Errorf("conpty: lines must be > 0") + } + sess := r.resolve(handle.ID) + if sess == nil { + return "", fmt.Errorf("conpty: session %q not found", handle.ID) + } + return clientGetOutput(sess.addr, lines) +} + +// resolve looks up a session by id: first the in-memory map, then the B2 +// registry (for daemon-restart recovery). Returns nil if not found either way. +func (r *Runtime) resolve(id string) *hostSession { + r.mu.Lock() + sess := r.sessions[id] + r.mu.Unlock() + if sess != nil { + return sess + } + + // Registry fallback: scan for the entry by session id. + entries, err := ptyregistry.List() + if err != nil { + return nil + } + for _, e := range entries { + if e.SessionID != id { + continue + } + // Re-populate the map so subsequent calls skip the file scan. + recovered := &hostSession{addr: e.PipePath, pid: e.PtyHostPID} + r.mu.Lock() + // Only store if another goroutine hasn't beaten us. + if r.sessions[id] == nil { + r.sessions[id] = recovered + } else { + recovered = r.sessions[id] + } + r.mu.Unlock() + return recovered + } + return nil +} + +// findProcess wraps os.FindProcess to make it swappable in tests. +// ponytail: direct call; no interface needed at this scale. +func findProcess(pid int) (processKiller, error) { + p, err := osProcessFinder(pid) + return p, err +} + +// processKiller is the subset of *os.Process used by Destroy. +type processKiller interface { + Kill() error +} + +// osProcessFinder is the production implementation; tests may replace it. +// The real defaultOSProcessFinder is in pidalive_unix.go / pidalive_windows.go +// (same files that provide pidAlive). +var osProcessFinder = defaultOSProcessFinder diff --git a/backend/internal/adapters/runtime/conpty/runtime_test.go b/backend/internal/adapters/runtime/conpty/runtime_test.go new file mode 100644 index 00000000..efd647ae --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/runtime_test.go @@ -0,0 +1,668 @@ +package conpty + +import ( + "bytes" + "context" + "fmt" + "io" + "net" + "os" + "strings" + "testing" + "time" + + "github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/conpty/ptyregistry" + "github.com/aoagents/agent-orchestrator/backend/internal/domain" + "github.com/aoagents/agent-orchestrator/backend/internal/ports" +) + +// livePID returns a PID that is guaranteed to be alive (the current process). +// Using this as the fake pty-host PID means ptyregistry.List() will not prune +// the entry during tests. Do NOT use this for the Destroy test: Destroy calls +// Kill on the pid, so use deadPID() there instead. +func livePID() int { return os.Getpid() } + +// deadPID returns a PID that is guaranteed to be dead (no process). This is +// used in Destroy tests so the force-kill step is a safe no-op. +// ponytail: PID 2147483647 (MaxInt32) is never a real process; signal-0 returns ESRCH. +func deadPID() int { return 2147483647 } + +// --------------------------------------------------------------------------- +// Test harness: in-process pty-host backed by a fakePTY. +// --------------------------------------------------------------------------- + +// inProcHost starts a Serve engine with a fakePTY on a real 127.0.0.1:0 +// listener and returns a fake spawner that returns that addr and a fake pid. +// The caller must call cleanup() to shut down the host. +type inProcHost struct { + addr string + pid int + pty *fakePTY + ring *Ring + cancel context.CancelFunc + done chan error + ln net.Listener +} + +func startInProcHost(t *testing.T, sessionID string, fakePID int) *inProcHost { + t.Helper() + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + pty := newFakePTY(fakePID) + ring := NewRing() + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan error, 1) + go func() { + done <- Serve(ctx, ServeConfig{ + SessionID: sessionID, + Listener: ln, + PTY: pty, + Ring: ring, + }) + }() + return &inProcHost{ + addr: ln.Addr().String(), + pid: fakePID, + pty: pty, + ring: ring, + cancel: cancel, + done: done, + ln: ln, + } +} + +func (h *inProcHost) cleanup(t *testing.T) { + t.Helper() + h.cancel() + select { + case <-h.done: + case <-time.After(2 * time.Second): + t.Log("warning: inProcHost did not stop within 2s") + } +} + +// fakeSpawnerFor returns a hostSpawner that starts an in-process host for a +// single session ID and records which sessions have been spawned. +// The returned map maps sessionID -> *inProcHost for test inspection. +func fakeSpawnerFor(t *testing.T, hosts map[string]*inProcHost, fakePID int) hostSpawner { + t.Helper() + return func(ctx context.Context, sessionID, cwd string, argv []string, env map[string]string) (string, int, error) { + h := startInProcHost(t, sessionID, fakePID) + if hosts != nil { + hosts[sessionID] = h + } + return h.addr, h.pid, nil + } +} + +// --------------------------------------------------------------------------- +// Redirect ptyregistry to a temp HOME so tests don't pollute ~/.ao +// --------------------------------------------------------------------------- + +func isolateRegistry(t *testing.T) { + t.Helper() + dir := t.TempDir() + t.Setenv("HOME", dir) +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +// TestCreate_RegistersSession verifies Create returns {ID: sessionID}, writes +// to the in-memory map, and registers in the ptyregistry. +func TestCreate_RegistersSession(t *testing.T) { + isolateRegistry(t) + hosts := map[string]*inProcHost{} + rt := New(Options{Spawner: fakeSpawnerFor(t, hosts, livePID())}) + + ctx := context.Background() + handle, err := rt.Create(ctx, ports.RuntimeConfig{ + SessionID: domain.SessionID("sess-abc"), + WorkspacePath: "/tmp/workspace", + Argv: []string{"claude-code"}, + }) + if err != nil { + t.Fatalf("Create: %v", err) + } + + if handle.ID != "sess-abc" { + t.Fatalf("handle.ID = %q, want %q", handle.ID, "sess-abc") + } + + // In-memory map must have the entry. + rt.mu.Lock() + sess := rt.sessions["sess-abc"] + rt.mu.Unlock() + if sess == nil { + t.Fatal("session not in in-memory map after Create") + } + + // Registry must have the entry. + entries, err := ptyregistry.List() + if err != nil { + t.Fatalf("List: %v", err) + } + var found bool + for _, e := range entries { + if e.SessionID == "sess-abc" { + found = true + } + } + if !found { + t.Fatal("session not in registry after Create") + } + + hosts["sess-abc"].cleanup(t) +} + +// TestCreate_DuplicateErrors verifies a second Create for the same session id fails. +func TestCreate_DuplicateErrors(t *testing.T) { + isolateRegistry(t) + hosts := map[string]*inProcHost{} + rt := New(Options{Spawner: fakeSpawnerFor(t, hosts, livePID())}) + ctx := context.Background() + + if _, err := rt.Create(ctx, ports.RuntimeConfig{ + SessionID: "sess-dup", + WorkspacePath: "/tmp/w", + Argv: []string{"sh"}, + }); err != nil { + t.Fatalf("first Create: %v", err) + } + + _, err := rt.Create(ctx, ports.RuntimeConfig{ + SessionID: "sess-dup", + WorkspacePath: "/tmp/w", + Argv: []string{"sh"}, + }) + if err == nil { + t.Fatal("expected error on duplicate Create, got nil") + } + if !strings.Contains(err.Error(), "already exists") { + t.Fatalf("error %q should contain 'already exists'", err.Error()) + } + + hosts["sess-dup"].cleanup(t) +} + +// TestCreate_InvalidIDErrors verifies Create rejects invalid session ids. +func TestCreate_InvalidIDErrors(t *testing.T) { + isolateRegistry(t) + rt := New(Options{Spawner: fakeSpawnerFor(t, nil, livePID())}) + ctx := context.Background() + + for _, bad := range []string{"", "has space", "has/slash", "has.dot"} { + _, err := rt.Create(ctx, ports.RuntimeConfig{ + SessionID: domain.SessionID(bad), + WorkspacePath: "/tmp/w", + Argv: []string{"sh"}, + }) + if err == nil { + t.Fatalf("Create(%q): expected error for invalid id, got nil", bad) + } + } +} + +// TestSendMessage_DeliversChunkedTextAndEnter verifies clientSendMessage sends +// the text + "\r" to the fakePTY input. +func TestSendMessage_DeliversChunkedTextAndEnter(t *testing.T) { + isolateRegistry(t) + hosts := map[string]*inProcHost{} + rt := New(Options{Spawner: fakeSpawnerFor(t, hosts, livePID())}) + ctx := context.Background() + + handle, err := rt.Create(ctx, ports.RuntimeConfig{ + SessionID: "sess-sm", + WorkspacePath: "/tmp/w", + Argv: []string{"sh"}, + }) + if err != nil { + t.Fatalf("Create: %v", err) + } + h := hosts["sess-sm"] + defer h.cleanup(t) + + msg := "hello world" + // Collect PTY input in background. + inputC := make(chan []byte, 4) + go func() { + buf := make([]byte, 1024) + for { + n, err := h.pty.inR.Read(buf) + if n > 0 { + cp := make([]byte, n) + copy(cp, buf[:n]) + inputC <- cp + } + if err != nil { + return + } + } + }() + + if err := rt.SendMessage(ctx, handle, msg); err != nil { + t.Fatalf("SendMessage: %v", err) + } + + // Collect all received bytes within 2s. + var received []byte + deadline := time.After(2 * time.Second) + // Expect at least msg + "\r". + for !bytes.Contains(received, []byte("\r")) { + select { + case chunk := <-inputC: + received = append(received, chunk...) + case <-deadline: + t.Fatalf("timeout waiting for PTY input; got %q so far", received) + } + } + + if !bytes.HasPrefix(received, []byte(msg)) { + t.Fatalf("PTY input = %q, want prefix %q then \\r", received, msg) + } + if !bytes.Contains(received, []byte("\r")) { + t.Fatalf("PTY input = %q, missing trailing \\r", received) + } +} + +// TestSendMessage_LargeMessageChunked verifies a message > 512 runes is +// delivered correctly (host receives full text + "\r"). +func TestSendMessage_LargeMessageChunked(t *testing.T) { + isolateRegistry(t) + hosts := map[string]*inProcHost{} + rt := New(Options{Spawner: fakeSpawnerFor(t, hosts, livePID())}) + ctx := context.Background() + + handle, _ := rt.Create(ctx, ports.RuntimeConfig{ + SessionID: "sess-lg", + WorkspacePath: "/tmp/w", + Argv: []string{"sh"}, + }) + h := hosts["sess-lg"] + defer h.cleanup(t) + + // Build a message longer than 512 runes (use multi-byte runes to test + // rune-boundary splitting). + var sb strings.Builder + for i := 0; i < 600; i++ { + sb.WriteRune('A' + rune(i%26)) + } + msg := sb.String() + + inputDone := make(chan []byte, 1) + go func() { + // Read until we see "\r". + var acc []byte + buf := make([]byte, 4096) + for { + n, err := h.pty.inR.Read(buf) + if n > 0 { + acc = append(acc, buf[:n]...) + } + if bytes.Contains(acc, []byte("\r")) { + inputDone <- acc + return + } + if err != nil { + inputDone <- acc + return + } + } + }() + + if err := rt.SendMessage(ctx, handle, msg); err != nil { + t.Fatalf("SendMessage: %v", err) + } + + select { + case got := <-inputDone: + // Strip trailing \r for comparison. + trimmed := strings.TrimSuffix(string(got), "\r") + if trimmed != msg { + t.Fatalf("PTY received %d chars, want %d\ngot: %q\nwant: %q", len(trimmed), len(msg), trimmed[:min(50, len(trimmed))], msg[:min(50, len(msg))]) + } + case <-time.After(5 * time.Second): + t.Fatal("timeout waiting for large message delivery") + } +} + +// TestGetOutput_ReturnsRingTail verifies GetOutput returns the ring's tail. +func TestGetOutput_ReturnsRingTail(t *testing.T) { + isolateRegistry(t) + hosts := map[string]*inProcHost{} + rt := New(Options{Spawner: fakeSpawnerFor(t, hosts, livePID())}) + ctx := context.Background() + + handle, _ := rt.Create(ctx, ports.RuntimeConfig{ + SessionID: "sess-go", + WorkspacePath: "/tmp/w", + Argv: []string{"sh"}, + }) + h := hosts["sess-go"] + defer h.cleanup(t) + + // Seed the ring. + h.ring.Append([]byte("line1\nline2\nline3\n")) + + text, err := rt.GetOutput(ctx, handle, 2) + if err != nil { + t.Fatalf("GetOutput: %v", err) + } + want := h.ring.Tail(2) + if text != want { + t.Fatalf("GetOutput = %q, want %q", text, want) + } +} + +// TestIsAlive_TrueWhileServing_FalseAfterClose verifies IsAlive returns true +// while the host listens and false after its listener is closed. +func TestIsAlive_TrueWhileServing_FalseAfterClose(t *testing.T) { + isolateRegistry(t) + hosts := map[string]*inProcHost{} + rt := New(Options{Spawner: fakeSpawnerFor(t, hosts, livePID())}) + ctx := context.Background() + + handle, _ := rt.Create(ctx, ports.RuntimeConfig{ + SessionID: "sess-ia", + WorkspacePath: "/tmp/w", + Argv: []string{"sh"}, + }) + h := hosts["sess-ia"] + + alive, err := rt.IsAlive(ctx, handle) + if err != nil { + t.Fatalf("IsAlive: %v", err) + } + if !alive { + t.Fatal("expected IsAlive=true while serving") + } + + // Shut down the host. + h.cancel() + <-h.done + + // Give the listener a moment to close. + time.Sleep(100 * time.Millisecond) + + alive2, err2 := rt.IsAlive(ctx, handle) + if err2 != nil { + t.Fatalf("IsAlive after close: %v", err2) + } + if alive2 { + t.Fatal("expected IsAlive=false after host closed") + } +} + +// TestIsAlive_FalseForUnknownSession verifies IsAlive returns (false, nil) for +// a session not in the map or registry. +func TestIsAlive_FalseForUnknownSession(t *testing.T) { + isolateRegistry(t) + rt := New(Options{Spawner: fakeSpawnerFor(t, nil, livePID())}) + ctx := context.Background() + + alive, err := rt.IsAlive(ctx, ports.RuntimeHandle{ID: "ghost-session"}) + if err != nil { + t.Fatalf("IsAlive: unexpected error: %v", err) + } + if alive { + t.Fatal("expected IsAlive=false for unknown session") + } +} + +// TestDestroy_KillsHostAndCleansUp verifies Destroy triggers clientKill, +// removes the map + registry entry, and is idempotent on second call. +// Uses deadPID() so the force-kill step is a safe no-op (the fake pty-host +// has no real OS process; clientKill already shut it down via the loopback). +func TestDestroy_KillsHostAndCleansUp(t *testing.T) { + isolateRegistry(t) + hosts := map[string]*inProcHost{} + rt := New(Options{Spawner: fakeSpawnerFor(t, hosts, deadPID())}) + ctx := context.Background() + + handle, err := rt.Create(ctx, ports.RuntimeConfig{ + SessionID: "sess-destroy", + WorkspacePath: "/tmp/w", + Argv: []string{"sh"}, + }) + if err != nil { + t.Fatalf("Create: %v", err) + } + h := hosts["sess-destroy"] + + // Destroy should succeed. + if err := rt.Destroy(ctx, handle); err != nil { + t.Fatalf("Destroy: %v", err) + } + + // Wait for Serve to stop (clientKill triggers shutdown). + select { + case <-h.done: + case <-time.After(3 * time.Second): + t.Fatal("host did not stop after Destroy") + } + + // fakePTY.Close must have been called. + h.pty.closeMu.Lock() + closed := h.pty.closed + h.pty.closeMu.Unlock() + if !closed { + t.Fatal("expected fakePTY.Close() after Destroy") + } + + // Map entry must be gone. + rt.mu.Lock() + _, exists := rt.sessions["sess-destroy"] + rt.mu.Unlock() + if exists { + t.Fatal("expected map entry removed after Destroy") + } + + // Registry entry must be gone. + entries, _ := ptyregistry.List() + for _, e := range entries { + if e.SessionID == "sess-destroy" { + t.Fatal("expected registry entry removed after Destroy") + } + } + + // Second Destroy must be idempotent (returns nil). + if err := rt.Destroy(ctx, handle); err != nil { + t.Fatalf("second Destroy: expected nil, got %v", err) + } +} + +// TestResolveViaRegistry verifies that with an empty in-memory map but a +// registry entry pointing at a live in-process host, IsAlive and SendMessage +// still work (simulates a daemon restart). +func TestResolveViaRegistry(t *testing.T) { + isolateRegistry(t) + + // Start a host directly (not through Create) to simulate a pre-existing + // pty-host from a previous daemon run. Use the current process PID so + // ptyregistry.List() does not prune the entry as dead. + h := startInProcHost(t, "sess-reg", livePID()) + defer h.cleanup(t) + + // Manually register the host in the registry. + err := ptyregistry.Register(ptyregistry.Entry{ + SessionID: "sess-reg", + PtyHostPID: h.pid, + PipePath: h.addr, // addr stored in PipePath field + RegisteredAt: fmt.Sprintf("%d", time.Now().Unix()), + }) + if err != nil { + t.Fatalf("Register: %v", err) + } + + // Create a Runtime with an empty in-memory map (simulates daemon restart). + rt := New(Options{Spawner: fakeSpawnerFor(t, nil, livePID())}) + ctx := context.Background() + + // IsAlive must work via registry resolution. + alive, err := rt.IsAlive(ctx, ports.RuntimeHandle{ID: "sess-reg"}) + if err != nil { + t.Fatalf("IsAlive via registry: %v", err) + } + if !alive { + t.Fatal("expected IsAlive=true via registry resolution") + } + + // SendMessage must work via registry resolution. + inputC := make(chan []byte, 4) + go func() { + buf := make([]byte, 512) + for { + n, err := h.pty.inR.Read(buf) + if n > 0 { + cp := make([]byte, n) + copy(cp, buf[:n]) + inputC <- cp + } + if err != nil { + return + } + } + }() + + if err := rt.SendMessage(ctx, ports.RuntimeHandle{ID: "sess-reg"}, "ping"); err != nil { + t.Fatalf("SendMessage via registry: %v", err) + } + + // Collect PTY input. + var received []byte + deadline := time.After(3 * time.Second) + for !bytes.Contains(received, []byte("\r")) { + select { + case chunk := <-inputC: + received = append(received, chunk...) + case <-deadline: + t.Fatalf("timeout waiting for PTY input via registry; got %q", received) + } + } + if !bytes.Contains(received, []byte("ping")) { + t.Fatalf("PTY did not receive 'ping'; got %q", received) + } +} + +// --------------------------------------------------------------------------- +// Unit tests for client helpers (dial a fresh in-proc host directly). +// --------------------------------------------------------------------------- + +// TestClientGetOutput_TimesOutReturnsEmpty verifies clientGetOutput returns "" +// (no error) if no response arrives within the timeout. We test the happy path +// instead (timeout path would require a non-responding server). +func TestClientGetOutput_HappyPath(t *testing.T) { + f := startServe(t, 3001) + defer f.cancel() + + f.ring.Append([]byte("alpha\nbeta\ngamma\n")) + + text, err := clientGetOutput(f.addr, 2) + if err != nil { + t.Fatalf("clientGetOutput: %v", err) + } + want := f.ring.Tail(2) + if text != want { + t.Fatalf("clientGetOutput = %q, want %q", text, want) + } +} + +// TestClientIsAlive_TrueAndFalse verifies clientIsAlive returns (true, nil) for +// a live host and (false, nil) for a refused address (definitively gone). +func TestClientIsAlive_TrueAndFalse(t *testing.T) { + f := startServe(t, 3002) + defer f.cancel() + + if alive, err := clientIsAlive(f.addr); err != nil || !alive { + t.Fatalf("clientIsAlive(live) = (%v, %v), want (true, nil)", alive, err) + } + + f.cancel() + // Wait for listener to close. + select { + case <-f.done: + case <-time.After(2 * time.Second): + } + time.Sleep(50 * time.Millisecond) + + // After close the OS refuses the connection on the freed port -> gone. + if alive, err := clientIsAlive(f.addr); alive || err != nil { + t.Fatalf("clientIsAlive(closed) = (%v, %v), want (false, nil)", alive, err) + } +} + +// TestIsAlive_RefusedIsGone_TimeoutIsTransient is the reaper-safety regression +// test. It asserts the dead-vs-transient split that keeps a single transient +// loopback hiccup from spuriously reaping a live idle session: +// +// (a) a resolved-but-REFUSED host -> IsAlive == (false, nil) [ProbeDead] +// (b) a resolved host whose probe TIMES OUT -> (false, non-nil) [ProbeFailed] +func TestIsAlive_RefusedIsGone_TimeoutIsTransient(t *testing.T) { + isolateRegistry(t) + + // (a) Refused: bind+close a listener to obtain a port nothing listens on. + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + refusedAddr := ln.Addr().String() + _ = ln.Close() + + rtRefused := New(Options{Spawner: fakeSpawnerFor(t, nil, livePID())}) + rtRefused.mu.Lock() + rtRefused.sessions["gone"] = &hostSession{addr: refusedAddr, pid: livePID()} + rtRefused.mu.Unlock() + + alive, err := rtRefused.IsAlive(context.Background(), ports.RuntimeHandle{ID: "gone"}) + if alive || err != nil { + t.Fatalf("IsAlive(refused) = (%v, %v), want (false, nil) definitively gone", alive, err) + } + + // (b) Transient timeout: a listener that Accepts but never replies. The + // short isAliveTimeout read deadline fires before any STATUS_RES arrives, + // which must surface as a non-nil (transient) error, not a death. + silent, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen silent: %v", err) + } + defer silent.Close() + go func() { + for { + c, err := silent.Accept() + if err != nil { + return + } + // Hold the connection open without ever sending a STATUS_RES. + go func(c net.Conn) { + time.Sleep(isAliveTimeout + time.Second) + _ = c.Close() + }(c) + } + }() + + rtSilent := New(Options{Spawner: fakeSpawnerFor(t, nil, livePID())}) + rtSilent.mu.Lock() + rtSilent.sessions["stuck"] = &hostSession{addr: silent.Addr().String(), pid: livePID()} + rtSilent.mu.Unlock() + + alive, err = rtSilent.IsAlive(context.Background(), ports.RuntimeHandle{ID: "stuck"}) + if alive { + t.Fatalf("IsAlive(silent) alive=true, want false") + } + if err == nil { + t.Fatal("IsAlive(silent) err=nil, want non-nil transient error so the reaper records ProbeFailed") + } +} + +// TestClientKill_Idempotent verifies clientKill on a dead address returns nil. +func TestClientKill_Idempotent(t *testing.T) { + if err := clientKill("127.0.0.1:1"); err != nil { + t.Fatalf("clientKill on unreachable addr: %v", err) + } +} + +// Ensure the packages compile (import check). +var _ = io.Discard diff --git a/backend/internal/adapters/runtime/conpty/spawn.go b/backend/internal/adapters/runtime/conpty/spawn.go new file mode 100644 index 00000000..a32a996f --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/spawn.go @@ -0,0 +1,11 @@ +// spawn.go - injectable hostSpawner seam. The real detached-process spawn is +// Windows-only (spawn_windows.go). This file defines the type and the +// defaultSpawnHost variable; the non-windows stub is in spawn_other.go. +package conpty + +import "context" + +// hostSpawner starts a detached pty-host for the session and returns its +// loopback address ("127.0.0.1:PORT") and OS pid once it prints READY. +// Injectable for tests: replace this field on Options before calling New. +type hostSpawner func(ctx context.Context, sessionID, cwd string, argv []string, env map[string]string) (addr string, pid int, err error) diff --git a/backend/internal/adapters/runtime/conpty/spawn_other.go b/backend/internal/adapters/runtime/conpty/spawn_other.go new file mode 100644 index 00000000..342836aa --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/spawn_other.go @@ -0,0 +1,16 @@ +//go:build !windows + +// spawn_other.go - stub for non-Windows platforms. The real detached-process +// spawn lives in spawn_windows.go and uses Windows process-creation flags. +package conpty + +import ( + "context" + "errors" +) + +// defaultSpawnHost is a stub on non-Windows platforms. Tests inject their own +// spawner; this only needs to keep the package buildable on Darwin/Linux. +func defaultSpawnHost(_ context.Context, _, _ string, _ []string, _ map[string]string) (string, int, error) { + return "", 0, errors.New("conpty spawn: unsupported on this OS") +} diff --git a/backend/internal/adapters/runtime/conpty/spawn_windows.go b/backend/internal/adapters/runtime/conpty/spawn_windows.go new file mode 100644 index 00000000..e8de3d7e --- /dev/null +++ b/backend/internal/adapters/runtime/conpty/spawn_windows.go @@ -0,0 +1,121 @@ +//go:build windows + +// spawn_windows.go - real detached pty-host spawner for Windows using +// CREATE_NEW_PROCESS_GROUP + DETACHED_PROCESS so the host survives daemon exit. +package conpty + +import ( + "bufio" + "context" + "fmt" + "io" + "os" + "os/exec" + "regexp" + "strconv" + "strings" + "time" + + "golang.org/x/sys/windows" +) + +// readyRE matches the "READY: " line printed by RunHost. +var readyRE = regexp.MustCompile(`READY:(\d+) (\d+)`) + +const spawnReadyTimeout = 10 * time.Second + +// defaultSpawnHost resolves the current executable, builds the pty-host argv, +// and spawns it detached on Windows. It reads stdout for "READY: " +// with a 10s timeout, then unrefs (detaches) the child. Returns the loopback +// address and the pty-host OS PID. +func defaultSpawnHost(ctx context.Context, sessionID, cwd string, argv []string, env map[string]string) (string, int, error) { + exe, err := os.Executable() + if err != nil { + return "", 0, fmt.Errorf("conpty spawn: resolve executable: %w", err) + } + + // Build: pty-host + args := append([]string{"pty-host", sessionID, cwd}, argv...) + + // Merge env: inherit parent, then overlay caller-provided vars. + merged := os.Environ() + for k, v := range env { + merged = append(merged, k+"="+v) + } + + cmd := exec.CommandContext(ctx, exe, args...) + cmd.Dir = cwd + cmd.Env = merged + + // Windows process-creation flags: detached + hidden console. + // ponytail: DETACHED_PROCESS puts the child in its own console; without it + // the child is killed when the parent's console closes. CREATE_NEW_PROCESS_GROUP + // insulates it from Ctrl+C sent to the parent. windowsHide suppresses the flash. + cmd.SysProcAttr = &windows.SysProcAttr{ + CreationFlags: windows.DETACHED_PROCESS | windows.CREATE_NEW_PROCESS_GROUP, + HideWindow: true, + } + + stdout, err := cmd.StdoutPipe() + if err != nil { + return "", 0, fmt.Errorf("conpty spawn: stdout pipe: %w", err) + } + // Stderr is discarded; pty-host writes diagnostics there but we don't need them. + cmd.Stderr = io.Discard + + if err := cmd.Start(); err != nil { + return "", 0, fmt.Errorf("conpty spawn: start: %w", err) + } + + // Read READY line with a timeout. + readyC := make(chan struct { + addr string + pid int + err error + }, 1) + + go func() { + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + m := readyRE.FindStringSubmatch(line) + if m != nil { + pid, _ := strconv.Atoi(m[1]) + port, _ := strconv.Atoi(m[2]) + readyC <- struct { + addr string + pid int + err error + }{"127.0.0.1:" + strconv.Itoa(port), pid, nil} + return + } + } + readyC <- struct { + addr string + pid int + err error + }{"", 0, fmt.Errorf("conpty spawn: pty-host exited without printing READY")} + }() + + timer := time.NewTimer(spawnReadyTimeout) + defer timer.Stop() + + select { + case r := <-readyC: + if r.err != nil { + _ = cmd.Process.Kill() + return "", 0, r.err + } + // Unref: detach stdout so the child is not blocked, then release reference + // so our process can exit while the child keeps running. + stdout.Close() + cmd.Process.Release() // nolint: errcheck - best-effort detach + return r.addr, cmd.Process.Pid, nil + case <-timer.C: + _ = cmd.Process.Kill() + return "", 0, fmt.Errorf("conpty spawn: pty-host startup timeout (%s)", spawnReadyTimeout) + case <-ctx.Done(): + _ = cmd.Process.Kill() + return "", 0, ctx.Err() + } +} diff --git a/backend/internal/terminal/pty_unix.go b/backend/internal/adapters/runtime/ptyexec/spawn_unix.go similarity index 64% rename from backend/internal/terminal/pty_unix.go rename to backend/internal/adapters/runtime/ptyexec/spawn_unix.go index c613bbd8..52372630 100644 --- a/backend/internal/terminal/pty_unix.go +++ b/backend/internal/adapters/runtime/ptyexec/spawn_unix.go @@ -1,6 +1,10 @@ //go:build !windows -package terminal +// Package ptyexec spawns a local PTY around an attach CLI (tmux/zellij) and +// exposes it as a ports.Stream. It is the shared spawn the terminal layer used +// to own directly; extracting it lets each runtime adapter back its Attach with +// the same creack/pty (unix) or go-pty ConPTY (windows) plumbing. +package ptyexec import ( "context" @@ -12,19 +16,21 @@ import ( "time" "github.com/creack/pty" + + "github.com/aoagents/agent-orchestrator/backend/internal/ports" ) -// defaultSpawn starts argv on a real PTY via creack/pty, sized rows×cols from -// birth when a size is known: `zellij attach` reads the tty size once at -// startup, and a post-spawn TIOCSWINSZ depends on SIGWINCH delivery that can -// race the client installing its handler — StartWithSize makes the first read -// correct by construction. env, when non-nil, replaces the inherited -// environment (mirrors exec.Cmd.Env semantics). ctx cancellation closes the PTY -// through the same graceful detach path as an explicit client close. Windows uses -// a stub (see pty_windows.go) until a ConPTY path is added. -func defaultSpawn(ctx context.Context, argv, env []string, rows, cols uint16) (ptyProcess, error) { +// Spawn starts argv on a real PTY via creack/pty, sized rows×cols from birth +// when a size is known: an attach client reads the tty size once at startup, and +// a post-spawn TIOCSWINSZ depends on SIGWINCH delivery that can race the client +// installing its handler; StartWithSize makes the first read correct by +// construction. env, when non-nil, replaces the inherited environment (mirrors +// exec.Cmd.Env semantics). ctx cancellation closes the PTY through the same +// graceful detach path as an explicit client close. Windows uses a ConPTY path +// (see spawn_windows.go). +func Spawn(ctx context.Context, argv, env []string, rows, cols uint16) (ports.Stream, error) { if len(argv) == 0 { - return nil, errors.New("terminal: empty attach command") + return nil, errors.New("ptyexec: empty attach command") } if err := ctx.Err(); err != nil { return nil, err @@ -65,7 +71,7 @@ func (p *creackPTY) Resize(rows, cols uint16) error { err := pty.Setsize(p.f, &pty.Winsize{Rows: rows, Cols: cols}) // Always follow with an explicit SIGWINCH: the kernel only raises one when // the size actually changed, so a re-asserted (identical) grid would never - // reach a zellij client that missed or lost the original signal — the + // reach an attach client that missed or lost the original signal; the // session would stay laid out for a stale size, with no repaint until the // next real change (the frontend re-sends its grid after each resize burst // for exactly this self-heal; see useTerminalSession). The client re-reads @@ -77,17 +83,17 @@ func (p *creackPTY) Resize(rows, cols uint16) error { } // detachGrace is how long Close waits for a SIGTERM'd attach process to exit -// on its own before falling back to SIGKILL. A zellij client that is being +// on its own before falling back to SIGKILL. An attach client that is being // drained detaches in ~50ms; the grace only runs out for a wedged process. const detachGrace = 250 * time.Millisecond // Close stops the attach process and releases the PTY. // -// SIGTERM first, SIGKILL as fallback: a SIGTERM'd `zellij attach` deregisters -// itself from the zellij server before exiting, while a SIGKILL'd one leaves +// SIGTERM first, SIGKILL as fallback: a SIGTERM'd attach client deregisters +// itself from its mux server before exiting, while a SIGKILL'd one leaves // deregistration to the server noticing the dead socket. A dead-but-registered -// client pins the session's size (zellij sizes a session to its smallest -// client), so the next attach renders for the ghost's grid — the "terminal +// client pins the session's size (a mux sizes a session to its smallest +// client), so the next attach renders for the ghost's grid; the "terminal // doesn't repaint to the new size" desync. The master stays open through the // grace so the run loop's copyOut keeps draining the client's shutdown output // (a blocked tty write would stall the graceful exit past the grace). diff --git a/backend/internal/terminal/pty_unix_test.go b/backend/internal/adapters/runtime/ptyexec/spawn_unix_test.go similarity index 85% rename from backend/internal/terminal/pty_unix_test.go rename to backend/internal/adapters/runtime/ptyexec/spawn_unix_test.go index 9a8bd9a0..5435312d 100644 --- a/backend/internal/terminal/pty_unix_test.go +++ b/backend/internal/adapters/runtime/ptyexec/spawn_unix_test.go @@ -1,6 +1,6 @@ //go:build !windows -package terminal +package ptyexec import ( "context" @@ -14,7 +14,7 @@ import ( // exactly once. Without the sync.Once a second Wait blocks forever, so this test // would hang (caught by the watchdog) rather than fail. func TestCreackPTYCloseIsIdempotent(t *testing.T) { - p, err := defaultSpawn(context.Background(), []string{"/bin/sh", "-c", "sleep 30"}, nil, 0, 0) + p, err := Spawn(context.Background(), []string{"/bin/sh", "-c", "sleep 30"}, nil, 0, 0) if err != nil { t.Fatalf("spawn: %v", err) } @@ -35,12 +35,12 @@ func TestCreackPTYCloseIsIdempotent(t *testing.T) { // TestCreackPTYResizeSignalsOnIdenticalSize guards the resize self-heal: the // kernel only raises SIGWINCH when TIOCSWINSZ actually changes the size, so a -// re-asserted (identical) grid relies on Resize's explicit signal. A zellij +// re-asserted (identical) grid relies on Resize's explicit signal. An attach // client that lost the original update would otherwise keep its server laid -// out for a stale size forever — the "terminal doesn't repaint after resizing +// out for a stale size forever; the "terminal doesn't repaint after resizing // the pane" desync. func TestCreackPTYResizeSignalsOnIdenticalSize(t *testing.T) { - p, err := defaultSpawn(context.Background(), + p, err := Spawn(context.Background(), []string{"/bin/sh", "-c", `trap 'echo WINCHED' WINCH; while :; do sleep 0.05; done`}, nil, 0, 0) if err != nil { t.Fatalf("spawn: %v", err) @@ -49,7 +49,7 @@ func TestCreackPTYResizeSignalsOnIdenticalSize(t *testing.T) { // Give the shell a beat to install the trap, then resize twice to the SAME // size. The first call changes the size (fresh PTYs start at 0x0) and the - // second is identical — only the explicit signal can deliver it. + // second is identical; only the explicit signal can deliver it. time.Sleep(200 * time.Millisecond) if err := p.Resize(24, 80); err != nil { t.Fatalf("resize 1: %v", err) @@ -78,11 +78,11 @@ func TestCreackPTYResizeSignalsOnIdenticalSize(t *testing.T) { } // TestCreackPTYSpawnsAtRequestedSize: the child must see the requested grid on -// its very first TIOCGWINSZ, with no SIGWINCH involved — sizing after exec +// its very first TIOCGWINSZ, with no SIGWINCH involved; sizing after exec // races the client installing its WINCH handler (a missed signal strands the -// zellij session at the previous client's size). +// session at the previous client's size). func TestCreackPTYSpawnsAtRequestedSize(t *testing.T) { - p, err := defaultSpawn(context.Background(), []string{"/bin/sh", "-c", "stty size"}, nil, 40, 140) + p, err := Spawn(context.Background(), []string{"/bin/sh", "-c", "stty size"}, nil, 40, 140) if err != nil { t.Fatalf("spawn: %v", err) } @@ -107,12 +107,12 @@ func TestCreackPTYSpawnsAtRequestedSize(t *testing.T) { } // TestCreackPTYCloseTermsBeforeKill: Close must give the attach process a -// chance to exit on SIGTERM (a zellij client deregisters from its server on +// chance to exit on SIGTERM (an attach client deregisters from its server on // SIGTERM; a straight SIGKILL leaves a ghost client that pins the session's // size), and must still return promptly for a process that ignores SIGTERM. func TestCreackPTYCloseTermsBeforeKill(t *testing.T) { t.Run("cooperative process exits within the grace", func(t *testing.T) { - p, err := defaultSpawn(context.Background(), + p, err := Spawn(context.Background(), []string{"/bin/sh", "-c", `trap 'exit 0' TERM; while :; do sleep 0.05; done`}, nil, 0, 0) if err != nil { t.Fatalf("spawn: %v", err) @@ -126,7 +126,7 @@ func TestCreackPTYCloseTermsBeforeKill(t *testing.T) { }) t.Run("TERM-ignoring process is killed after the grace", func(t *testing.T) { - p, err := defaultSpawn(context.Background(), + p, err := Spawn(context.Background(), []string{"/bin/sh", "-c", `trap '' TERM; while :; do sleep 0.05; done`}, nil, 0, 0) if err != nil { t.Fatalf("spawn: %v", err) diff --git a/backend/internal/terminal/pty_windows.go b/backend/internal/adapters/runtime/ptyexec/spawn_windows.go similarity index 70% rename from backend/internal/terminal/pty_windows.go rename to backend/internal/adapters/runtime/ptyexec/spawn_windows.go index 5a83400f..8c7136a0 100644 --- a/backend/internal/terminal/pty_windows.go +++ b/backend/internal/adapters/runtime/ptyexec/spawn_windows.go @@ -1,6 +1,6 @@ //go:build windows -package terminal +package ptyexec import ( "context" @@ -8,6 +8,7 @@ import ( "sync" "time" + "github.com/aoagents/agent-orchestrator/backend/internal/ports" winpty "github.com/aymanbagabas/go-pty" ) @@ -16,16 +17,16 @@ import ( // child's stdin) before falling back to Kill. const detachGrace = 250 * time.Millisecond -// defaultSpawn starts argv on a Windows ConPTY and exposes the console pipes -// through the same ptyProcess interface used by the Unix creack/pty path. -// go-pty creates the pseudo-console at 80x25 internally, so we only Resize -// when the caller actually has a grid (mirroring StartWithSize on Unix). -// env, when non-nil, replaces the inherited environment via Win32's native -// CreateProcess env block (mirrors exec.Cmd.Env semantics) — this is how a -// per-session ZELLIJ_SOCKET_DIR reaches the zellij attach client. -func defaultSpawn(ctx context.Context, argv []string, env []string, rows, cols uint16) (ptyProcess, error) { +// Spawn starts argv on a Windows ConPTY and exposes the console pipes through +// the same ports.Stream interface used by the Unix creack/pty path. go-pty +// creates the pseudo-console at 80x25 internally, so we only Resize when the +// caller actually has a grid (mirroring StartWithSize on Unix). env, when +// non-nil, replaces the inherited environment via Win32's native CreateProcess +// env block (mirrors exec.Cmd.Env semantics); this is how a per-session env var +// reaches the spawned attach client. +func Spawn(ctx context.Context, argv, env []string, rows, cols uint16) (ports.Stream, error) { if len(argv) == 0 { - return nil, errors.New("terminal: empty attach command") + return nil, errors.New("ptyexec: empty attach command") } pty, err := winpty.New() if err != nil { diff --git a/backend/internal/terminal/pty_windows_test.go b/backend/internal/adapters/runtime/ptyexec/spawn_windows_test.go similarity index 73% rename from backend/internal/terminal/pty_windows_test.go rename to backend/internal/adapters/runtime/ptyexec/spawn_windows_test.go index ee8c7d61..7dab0ad3 100644 --- a/backend/internal/terminal/pty_windows_test.go +++ b/backend/internal/adapters/runtime/ptyexec/spawn_windows_test.go @@ -1,6 +1,6 @@ //go:build windows -package terminal +package ptyexec import ( "bytes" @@ -10,12 +10,14 @@ import ( "strings" "testing" "time" + + "github.com/aoagents/agent-orchestrator/backend/internal/ports" ) -func TestDefaultSpawnWindowsStreamsOutput(t *testing.T) { - p, err := defaultSpawn(context.Background(), []string{"cmd.exe", "/D", "/Q", "/K"}, nil, 24, 80) +func TestSpawnWindowsStreamsOutput(t *testing.T) { + p, err := Spawn(context.Background(), []string{"cmd.exe", "/D", "/Q", "/K"}, nil, 24, 80) if err != nil { - t.Fatalf("defaultSpawn: %v", err) + t.Fatalf("Spawn: %v", err) } defer p.Close() if _, err := p.Write([]byte("echo AO_CONPTY_OK\r\n")); err != nil { @@ -28,17 +30,17 @@ func TestDefaultSpawnWindowsStreamsOutput(t *testing.T) { } } -func TestDefaultSpawnWindowsRejectsEmptyCommand(t *testing.T) { - _, err := defaultSpawn(context.Background(), nil, nil, 0, 0) +func TestSpawnWindowsRejectsEmptyCommand(t *testing.T) { + _, err := Spawn(context.Background(), nil, nil, 0, 0) if err == nil || !strings.Contains(err.Error(), "empty attach command") { t.Fatalf("expected empty attach command error, got %v", err) } } func TestConPTYCloseIsIdempotent(t *testing.T) { - p, err := defaultSpawn(context.Background(), []string{"cmd.exe", "/D", "/Q", "/K"}, nil, 24, 80) + p, err := Spawn(context.Background(), []string{"cmd.exe", "/D", "/Q", "/K"}, nil, 24, 80) if err != nil { - t.Fatalf("defaultSpawn: %v", err) + t.Fatalf("Spawn: %v", err) } done := make(chan struct{}) @@ -55,7 +57,7 @@ func TestConPTYCloseIsIdempotent(t *testing.T) { } } -func readPTYUntil(t *testing.T, p ptyProcess, marker string, timeout time.Duration) string { +func readPTYUntil(t *testing.T, p ports.Stream, marker string, timeout time.Duration) string { t.Helper() type result struct { out string diff --git a/backend/internal/adapters/runtime/runtimeselect/runtimeselect.go b/backend/internal/adapters/runtime/runtimeselect/runtimeselect.go new file mode 100644 index 00000000..3092590f --- /dev/null +++ b/backend/internal/adapters/runtime/runtimeselect/runtimeselect.go @@ -0,0 +1,37 @@ +// Package runtimeselect picks the correct runtime backend by platform: +// tmux on Darwin/Linux, conpty (ConPTY) on Windows. +package runtimeselect + +import ( + "context" + "log/slog" + "runtime" + + "github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/conpty" + "github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/tmux" + "github.com/aoagents/agent-orchestrator/backend/internal/ports" +) + +// Runtime is the union interface that both tmux and conpty satisfy. +// It extends ports.Runtime (Create/Destroy/IsAlive) with the additional methods +// the daemon wires directly, including ports.Attacher (Attach) so the terminal +// layer can open a Stream against the selected runtime. +type Runtime interface { + ports.Runtime // Create, Destroy, IsAlive + ports.Attacher + SendMessage(ctx context.Context, handle ports.RuntimeHandle, message string) error + GetOutput(ctx context.Context, handle ports.RuntimeHandle, lines int) (string, error) +} + +// Compile-time assertions: both adapters must implement the union interface. +var _ Runtime = (*tmux.Runtime)(nil) +var _ Runtime = (*conpty.Runtime)(nil) + +// New returns the per-platform runtime: tmux on Darwin/Linux, conpty on Windows. +// log is accepted for signature stability with callers but is currently unused. +func New(_ *slog.Logger) Runtime { + if runtime.GOOS != "windows" { + return tmux.New(tmux.Options{}) + } + return conpty.New(conpty.Options{}) +} diff --git a/backend/internal/adapters/runtime/tmux/commands.go b/backend/internal/adapters/runtime/tmux/commands.go new file mode 100644 index 00000000..1a55d363 --- /dev/null +++ b/backend/internal/adapters/runtime/tmux/commands.go @@ -0,0 +1,71 @@ +package tmux + +import "fmt" + +// newSessionArgs builds args for `tmux new-session -d -s -x 220 -y 50 +// -c -c `. The shell -c form runs the launch command +// inside the configured shell so exported env vars and quoting work correctly. +func newSessionArgs(id, cwd, shellPath, launchCmd string) []string { + return []string{ + "new-session", "-d", + "-s", id, + "-x", "220", + "-y", "50", + "-c", cwd, + shellPath, "-c", launchCmd, + } +} + +// setStatusOffArgs hides the tmux status bar for the given session. +// set-option uses pane-targeting syntax which does not accept the `=` prefix, +// so we pass the session name directly. +func setStatusOffArgs(id string) []string { + return []string{"set-option", "-t", id, "status", "off"} +} + +// setMouseOnArgs enables tmux mouse mode so the terminal's SGR mouse-wheel +// reports scroll the pane via copy-mode; without it, wheel scrolling no-ops. +// Pane-targeting, so no `=` prefix (see setStatusOffArgs). +func setMouseOnArgs(id string) []string { + return []string{"set-option", "-t", id, "mouse", "on"} +} + +// killSessionArgs builds args for `tmux kill-session -t =`. The `=` prefix +// requests exact-name matching so a session "foo" does not accidentally match +// "foobar" (tmux otherwise does unique-prefix matching). +func killSessionArgs(id string) []string { + return []string{"kill-session", "-t", exactSessionTarget(id)} +} + +// hasSessionArgs builds args for `tmux has-session -t =`. The `=` prefix +// requests exact-name matching (see killSessionArgs). +func hasSessionArgs(id string) []string { + return []string{"has-session", "-t", exactSessionTarget(id)} +} + +// exactSessionTarget wraps id in tmux's exact-match prefix `=` so session- +// selection commands (-t) target only the session with that precise name. +// Only kill-session and has-session support this prefix; pane-targeting +// commands (send-keys, capture-pane, set-option) use a plain session name. +func exactSessionTarget(id string) string { + return "=" + id +} + +// sendKeysLiteralArgs builds args for `tmux send-keys -t -l `. +// The -l flag stops tmux interpreting words like "Enter" as key names so the +// text is sent verbatim. +func sendKeysLiteralArgs(id, chunk string) []string { + return []string{"send-keys", "-t", id, "-l", chunk} +} + +// sendEnterArgs builds args for `tmux send-keys -t Enter` to submit the +// queued input. +func sendEnterArgs(id string) []string { + return []string{"send-keys", "-t", id, "Enter"} +} + +// capturePaneArgs builds args for `tmux capture-pane -t -p -S -`. +// -p prints to stdout; -S - starts n lines back in history. +func capturePaneArgs(id string, lines int) []string { + return []string{"capture-pane", "-t", id, "-p", "-S", fmt.Sprintf("-%d", lines)} +} diff --git a/backend/internal/adapters/runtime/tmux/tmux.go b/backend/internal/adapters/runtime/tmux/tmux.go new file mode 100644 index 00000000..8388a571 --- /dev/null +++ b/backend/internal/adapters/runtime/tmux/tmux.go @@ -0,0 +1,494 @@ +// Package tmux implements ports.Runtime using tmux sessions on Darwin/Linux. +package tmux + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "os" + "os/exec" + "regexp" + "sort" + "strings" + "time" + "unicode/utf8" + + "github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/ptyexec" + "github.com/aoagents/agent-orchestrator/backend/internal/domain" + "github.com/aoagents/agent-orchestrator/backend/internal/ports" +) + +const ( + defaultTimeout = 5 * time.Second + defaultChunkBytes = 16 * 1024 +) + +var sessionIDPattern = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) + +var getenv = os.Getenv + +// Options configures a tmux Runtime. Every field has a sensible default (see +// New), so the zero value is usable. +type Options struct { + Binary string // default "tmux" (resolved via exec.LookPath) + Shell string // default $SHELL else /bin/sh + Timeout time.Duration // default 5s + ChunkSize int // default 16*1024 +} + +// Runtime runs agent sessions inside tmux sessions, driving them via the tmux +// CLI. It implements ports.Runtime. +type Runtime struct { + binary string + shell string + timeout time.Duration + chunkSize int + runner runner +} + +var _ ports.Runtime = (*Runtime)(nil) +var _ ports.Attacher = (*Runtime)(nil) + +type runner interface { + Run(ctx context.Context, env []string, name string, args ...string) ([]byte, error) +} + +type execRunner struct{} + +func (execRunner) Run(ctx context.Context, env []string, name string, args ...string) ([]byte, error) { + cmd := exec.CommandContext(ctx, name, args...) + cmd.Env = append(append([]string(nil), os.Environ()...), env...) + return cmd.CombinedOutput() +} + +// New builds a tmux Runtime, filling unset Options with defaults: binary "tmux" +// (resolved via exec.LookPath), shell from $SHELL (else /bin/sh), and the +// default timeout and output chunk size. +func New(opts Options) *Runtime { + binary := opts.Binary + if binary == "" { + if path, err := exec.LookPath("tmux"); err == nil { + binary = path + } else { + binary = "tmux" + } + } + timeout := opts.Timeout + if timeout == 0 { + timeout = defaultTimeout + } + shellPath := opts.Shell + if shellPath == "" { + shellPath = getenv("SHELL") + } + if shellPath == "" { + shellPath = "/bin/sh" + } + chunkSize := opts.ChunkSize + if chunkSize <= 0 { + chunkSize = defaultChunkBytes + } + return &Runtime{ + binary: binary, + shell: shellPath, + timeout: timeout, + chunkSize: chunkSize, + runner: execRunner{}, + } +} + +// Create starts a new tmux session in the workspace, running the agent's +// launch command with a keep-alive shell, and returns a handle to it. +func (r *Runtime) Create(ctx context.Context, cfg ports.RuntimeConfig) (ports.RuntimeHandle, error) { + id, err := tmuxSessionName(cfg.SessionID) + if err != nil { + return ports.RuntimeHandle{}, err + } + if cfg.WorkspacePath == "" { + return ports.RuntimeHandle{}, errors.New("tmux runtime: workspace path is required") + } + if len(cfg.Argv) == 0 { + return ports.RuntimeHandle{}, errors.New("tmux runtime: launch command is required") + } + if err := validateEnvKeys(cfg.Env); err != nil { + return ports.RuntimeHandle{}, err + } + + launchCmd := buildLaunchCommand(cfg) + args := newSessionArgs(id, cfg.WorkspacePath, r.shell, launchCmd) + if _, err := r.run(ctx, args...); err != nil { + return ports.RuntimeHandle{}, fmt.Errorf("tmux runtime: create session %s: %w", id, err) + } + + // Hide the status bar in the embedded terminal: it clutters the view and + // was not designed for the in-browser display context. + if _, err := r.run(ctx, setStatusOffArgs(id)...); err != nil { + _ = r.Destroy(context.Background(), ports.RuntimeHandle{ID: id}) + return ports.RuntimeHandle{}, fmt.Errorf("tmux runtime: set status %s: %w", id, err) + } + + // Enable mouse mode so the embedded terminal's SGR wheel reports scroll the + // pane (see setMouseOnArgs). Without it, wheel scrolling silently no-ops. + if _, err := r.run(ctx, setMouseOnArgs(id)...); err != nil { + _ = r.Destroy(context.Background(), ports.RuntimeHandle{ID: id}) + return ports.RuntimeHandle{}, fmt.Errorf("tmux runtime: set mouse %s: %w", id, err) + } + + handle := ports.RuntimeHandle{ID: id} + alive, err := r.IsAlive(ctx, handle) + if err != nil { + _ = r.Destroy(context.Background(), handle) + return ports.RuntimeHandle{}, fmt.Errorf("tmux runtime: verify session %s: %w", id, err) + } + if !alive { + _ = r.Destroy(context.Background(), handle) + return ports.RuntimeHandle{}, fmt.Errorf("tmux runtime: session %s exited before ready", id) + } + return handle, nil +} + +// Destroy kills the handle's tmux session. An already-gone session is treated +// as success (idempotent). +func (r *Runtime) Destroy(ctx context.Context, handle ports.RuntimeHandle) error { + id, err := handleID(handle) + if err != nil { + return err + } + out, err := r.run(ctx, killSessionArgs(id)...) + if err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) && killSessionMissingOutput(string(out)) { + return nil + } + return fmt.Errorf("tmux runtime: destroy session %s: %w", id, err) + } + return nil +} + +// IsAlive reports whether the handle's session still exists via `tmux +// has-session`. Exit 0 means alive. A non-zero exit with output indicating the +// session or server is missing is a definitive false, nil. Any other non-zero +// exit is a probe error (not proof of death) so callers (the reaper feeding +// the LCM) treat it as a failed probe and never kill a session on a transient +// error. +func (r *Runtime) IsAlive(ctx context.Context, handle ports.RuntimeHandle) (bool, error) { + id, err := handleID(handle) + if err != nil { + return false, err + } + out, err := r.run(ctx, hasSessionArgs(id)...) + if err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) && sessionMissingOutput(string(out)) { + return false, nil + } + return false, fmt.Errorf("tmux runtime: probe session %s: %w", id, err) + } + return true, nil +} + +// SendMessage sends literal text to the session (chunked via send-keys -l) then +// presses Enter to submit. +// +// ponytail: send-keys -l chunked is simpler than load-buffer/paste-buffer; the +// ceiling is very large messages may be slower, but chunk size defaults to 16 KB +// which is ample for agent prompts. +func (r *Runtime) SendMessage(ctx context.Context, handle ports.RuntimeHandle, message string) error { + id, err := handleID(handle) + if err != nil { + return err + } + for _, chunk := range chunks(message, r.chunkSize) { + if _, err := r.run(ctx, sendKeysLiteralArgs(id, chunk)...); err != nil { + return fmt.Errorf("tmux runtime: send message %s: %w", id, err) + } + } + if _, err := r.run(ctx, sendEnterArgs(id)...); err != nil { + return fmt.Errorf("tmux runtime: send enter %s: %w", id, err) + } + return nil +} + +// GetOutput returns the last `lines` lines of the session pane's captured +// output. +func (r *Runtime) GetOutput(ctx context.Context, handle ports.RuntimeHandle, lines int) (string, error) { + id, err := handleID(handle) + if err != nil { + return "", err + } + if lines <= 0 { + return "", errors.New("tmux runtime: lines must be positive") + } + out, err := r.run(ctx, capturePaneArgs(id, lines)...) + if err != nil { + return "", fmt.Errorf("tmux runtime: capture output %s: %w", id, err) + } + return tailLines(trimTrailingBlankLines(string(out)), lines), nil +} + +// Attach opens a fresh attach Stream by spawning `tmux attach-session` on a +// local PTY, sized rows x cols from birth when known. ctx cancellation closes +// the PTY. +func (r *Runtime) Attach(ctx context.Context, handle ports.RuntimeHandle, rows, cols uint16) (ports.Stream, error) { + argv, err := r.attachCommand(handle) + if err != nil { + return nil, err + } + return ptyexec.Spawn(ctx, argv, nil, rows, cols) +} + +// attachCommand returns the argv to attach a terminal to the session. +// tmux needs no per-session env block. +func (r *Runtime) attachCommand(handle ports.RuntimeHandle) ([]string, error) { + id, err := handleID(handle) + if err != nil { + return nil, err + } + return []string{r.binary, "attach-session", "-t", id}, nil +} + +// run wraps runner.Run with a per-call timeout context. +func (r *Runtime) run(ctx context.Context, args ...string) ([]byte, error) { + cmdCtx, cancel := context.WithTimeout(ctx, r.timeout) + defer cancel() + out, err := r.runner.Run(cmdCtx, nil, r.binary, args...) + if cmdCtx.Err() != nil { + return out, cmdCtx.Err() + } + if err != nil { + return out, commandError{err: err, output: strings.TrimSpace(string(out))} + } + return out, nil +} + +// -- session name helpers -- + +func tmuxSessionName(id domain.SessionID) (string, error) { + raw := string(id) + if raw == "" { + return "", errors.New("tmux runtime: session id is required") + } + return SessionName(raw), nil +} + +// SessionName returns the tmux session name the runtime registers for a given +// session id, applying the same sanitisation Create does. Callers that print an +// attach hint must use this rather than the raw id. +func SessionName(id string) string { + if sessionIDPattern.MatchString(id) && len(id) <= 48 { + return id + } + return sanitizedSessionName(id) +} + +func sanitizedSessionName(raw string) string { + var b strings.Builder + lastDash := false + for _, r := range raw { + valid := (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' + if valid { + b.WriteRune(r) + lastDash = false + continue + } + if !lastDash { + b.WriteByte('-') + lastDash = true + } + } + base := strings.Trim(b.String(), "-") + if base == "" { + base = "session" + } + if len(base) > 32 { + base = strings.TrimRight(base[:32], "-") + } + sum := sha256.Sum256([]byte(raw)) + return base + "-" + hex.EncodeToString(sum[:4]) +} + +func handleID(handle ports.RuntimeHandle) (string, error) { + id := handle.ID + if id == "" { + return "", errors.New("tmux runtime: session id is required") + } + if !sessionIDPattern.MatchString(id) { + return "", fmt.Errorf("tmux runtime: invalid handle id %q", id) + } + return id, nil +} + +// -- output detection helpers -- + +// sessionMissingOutput reports whether a non-zero `tmux has-session` or +// `tmux kill-session` exit is definitively "session does not exist" rather +// than a transient probe failure. +func sessionMissingOutput(out string) bool { + s := strings.ToLower(out) + return strings.Contains(s, "can't find session") || + strings.Contains(s, "no server running") || + strings.Contains(s, "error connecting") || + strings.Contains(s, "session not found") +} + +// killSessionMissingOutput reports whether a non-zero `tmux kill-session` +// failed because the session was already gone. +func killSessionMissingOutput(out string) bool { + return sessionMissingOutput(out) +} + +// -- text helpers -- + +func chunks(s string, maxBytes int) []string { + if s == "" { + return []string{""} + } + if maxBytes <= 0 || len(s) <= maxBytes { + return []string{s} + } + parts := []string{} + for s != "" { + if len(s) <= maxBytes { + parts = append(parts, s) + break + } + end := maxBytes + for end > 0 && !utf8.ValidString(s[:end]) { + end-- + } + if end == 0 { + _, size := utf8.DecodeRuneInString(s) + end = size + } + parts = append(parts, s[:end]) + s = s[end:] + } + return parts +} + +func tailLines(s string, n int) string { + if n <= 0 || s == "" { + return "" + } + lines := strings.SplitAfter(s, "\n") + if lines[len(lines)-1] == "" { + lines = lines[:len(lines)-1] + } + if len(lines) <= n { + return s + } + return strings.Join(lines[len(lines)-n:], "") +} + +func trimTrailingBlankLines(s string) string { + if s == "" { + return "" + } + lines := strings.SplitAfter(s, "\n") + if lines[len(lines)-1] == "" { + lines = lines[:len(lines)-1] + } + for len(lines) > 0 && strings.TrimRight(lines[len(lines)-1], "\r\n") == "" { + lines = lines[:len(lines)-1] + } + return strings.Join(lines, "") +} + +// -- env / quoting helpers -- + +func validateEnvKeys(env map[string]string) error { + for key := range env { + if !validEnvKey(key) { + return fmt.Errorf("tmux runtime: invalid env key %q", key) + } + } + return nil +} + +func validEnvKey(key string) bool { + if key == "" { + return false + } + for i, r := range key { + if r == '_' || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') { + continue + } + if i > 0 && r >= '0' && r <= '9' { + continue + } + return false + } + return true +} + +func sortedKeys(m map[string]string) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +func shellQuote(s string) string { + return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'" +} + +// buildLaunchCommand builds the shell command string passed to `sh -c`. It +// exports env vars, then runs argv, then execs a keep-alive interactive shell +// so the tmux session survives the agent exiting. +// +// PATH from cfg.Env is exported last, after all other keys, so an explicit +// override takes effect. +func buildLaunchCommand(cfg ports.RuntimeConfig) string { + path := cfg.Env["PATH"] + if path == "" { + path = getenv("PATH") + } + + var b strings.Builder + for _, key := range sortedKeys(cfg.Env) { + if key == "PATH" { + continue + } + b.WriteString("export ") + b.WriteString(key) + b.WriteString("=") + b.WriteString(shellQuote(cfg.Env[key])) + b.WriteString("; ") + } + if path != "" { + b.WriteString("export PATH=") + b.WriteString(shellQuote(path)) + b.WriteString("; ") + } + // Quote each argv word so spaces inside a word are preserved. + parts := make([]string, len(cfg.Argv)) + for i, a := range cfg.Argv { + parts[i] = shellQuote(a) + } + b.WriteString(strings.Join(parts, " ")) + // Keep the tmux session alive after the agent exits so the operator can + // inspect the terminal. The shell variable expansion picks up $SHELL from + // the process env if set, otherwise falls back to /bin/sh. + b.WriteString(`; exec "${SHELL:-/bin/sh}" -i`) + return b.String() +} + +// -- error type -- + +type commandError struct { + err error + output string +} + +func (e commandError) Error() string { + if e.output == "" { + return e.err.Error() + } + return e.err.Error() + ": " + e.output +} + +func (e commandError) Unwrap() error { return e.err } diff --git a/backend/internal/adapters/runtime/tmux/tmux_integration_test.go b/backend/internal/adapters/runtime/tmux/tmux_integration_test.go new file mode 100644 index 00000000..1f491f8d --- /dev/null +++ b/backend/internal/adapters/runtime/tmux/tmux_integration_test.go @@ -0,0 +1,143 @@ +package tmux + +import ( + "context" + "os/exec" + "strings" + "testing" + "time" + + "github.com/aoagents/agent-orchestrator/backend/internal/domain" + "github.com/aoagents/agent-orchestrator/backend/internal/ports" +) + +func TestRuntimeIntegration(t *testing.T) { + if _, err := exec.LookPath("tmux"); err != nil { + t.Skip("tmux unavailable") + } + + ctx := context.Background() + id := strings.ReplaceAll(t.Name(), "/", "_") + r := New(Options{Timeout: 5 * time.Second}) + + // Ensure clean slate: ignore errors (session may not exist). + _ = r.Destroy(ctx, ports.RuntimeHandle{ID: id}) + + t.Cleanup(func() { + // Always destroy so a test failure never leaks a tmux session. + _ = r.Destroy(context.Background(), ports.RuntimeHandle{ID: id}) + }) + + h, err := r.Create(ctx, ports.RuntimeConfig{ + SessionID: domain.SessionID(id), + WorkspacePath: t.TempDir(), + // Run a trivial command then drop into an interactive shell (the keep-alive + // exec is added by buildLaunchCommand, but we also verify here that output + // appears). + Argv: []string{"sh", "-c", "echo hello-from-tmux"}, + Env: map[string]string{"AO_SESSION_ID": id}, + }) + if err != nil { + t.Fatalf("Create: %v", err) + } + + alive, err := r.IsAlive(ctx, h) + if err != nil { + t.Fatalf("IsAlive: %v", err) + } + if !alive { + t.Fatal("alive = false, want true after create") + } + + // Wait for the echo output to appear (the session may take a moment to + // write it to the pane history). + out := waitForOutput(t, r, h, "hello-from-tmux", 5*time.Second) + if !strings.Contains(out, "hello-from-tmux") { + t.Fatalf("output = %q, want hello-from-tmux", out) + } + + // Send a command and verify it echoes back. + if err := r.SendMessage(ctx, h, "echo hello-send"); err != nil { + t.Fatalf("SendMessage: %v", err) + } + out = waitForOutput(t, r, h, "hello-send", 5*time.Second) + if !strings.Contains(out, "hello-send") { + t.Fatalf("output after SendMessage = %q, want hello-send", out) + } + + // Destroy and verify liveness goes false. + if err := r.Destroy(ctx, h); err != nil { + t.Fatalf("Destroy: %v", err) + } + alive, err = r.IsAlive(ctx, h) + if err != nil { + t.Fatalf("IsAlive after destroy: %v", err) + } + if alive { + t.Fatal("alive after destroy = true, want false") + } +} + +// TestRuntimeIntegrationExactSessionParsing verifies that IsAlive uses exact +// session matching and does not treat a prefix as a live session. +func TestRuntimeIntegrationExactSessionParsing(t *testing.T) { + if _, err := exec.LookPath("tmux"); err != nil { + t.Skip("tmux unavailable") + } + + ctx := context.Background() + base := strings.ReplaceAll(t.Name(), "/", "_") + longID := base + "_long" + prefixID := base + + r := New(Options{Timeout: 5 * time.Second}) + _ = r.Destroy(ctx, ports.RuntimeHandle{ID: longID}) + _ = r.Destroy(ctx, ports.RuntimeHandle{ID: prefixID}) + + t.Cleanup(func() { + _ = r.Destroy(context.Background(), ports.RuntimeHandle{ID: longID}) + _ = r.Destroy(context.Background(), ports.RuntimeHandle{ID: prefixID}) + }) + + h, err := r.Create(ctx, ports.RuntimeConfig{ + SessionID: domain.SessionID(longID), + WorkspacePath: t.TempDir(), + Argv: []string{"sh", "-c", "echo ready"}, + }) + if err != nil { + t.Fatalf("Create: %v", err) + } + + // tmux has-session -t should NOT match because tmux + // requires the exact session name when using -t with a plain string (not a + // glob). Verify by probing the prefix handle directly. + prefixAlive, err := r.IsAlive(ctx, ports.RuntimeHandle{ID: prefixID}) + if err != nil { + // tmux may return an error (session not found) rather than exit 0. + // That is acceptable here: the point is the prefix must not be alive. + t.Logf("IsAlive prefix returned error (acceptable): %v", err) + } + if prefixAlive { + _ = r.Destroy(ctx, h) + t.Fatal("prefix handle reported alive; tmux session matching is not exact") + } +} + +// waitForOutput polls GetOutput until out contains want or the deadline passes. +func waitForOutput(t *testing.T, r *Runtime, h ports.RuntimeHandle, want string, deadline time.Duration) string { + t.Helper() + end := time.Now().Add(deadline) + var out string + for time.Now().Before(end) { + var err error + out, err = r.GetOutput(context.Background(), h, 50) + if err != nil { + t.Fatalf("GetOutput: %v", err) + } + if strings.Contains(out, want) { + return out + } + time.Sleep(100 * time.Millisecond) + } + return out +} diff --git a/backend/internal/adapters/runtime/tmux/tmux_test.go b/backend/internal/adapters/runtime/tmux/tmux_test.go new file mode 100644 index 00000000..7a2851ee --- /dev/null +++ b/backend/internal/adapters/runtime/tmux/tmux_test.go @@ -0,0 +1,605 @@ +package tmux + +import ( + "context" + "errors" + "os/exec" + "reflect" + "strings" + "testing" + "time" + + "github.com/aoagents/agent-orchestrator/backend/internal/domain" + "github.com/aoagents/agent-orchestrator/backend/internal/ports" +) + +// -- fakeRunner test seam (mirrors zellij_test.go exactly) -- + +type fakeRunner struct { + calls []runnerCall + outputs [][]byte + err error +} + +type runnerCall struct { + env []string + name string + args []string +} + +func (f *fakeRunner) Run(_ context.Context, env []string, name string, args ...string) ([]byte, error) { + f.calls = append(f.calls, runnerCall{env: append([]string(nil), env...), name: name, args: append([]string(nil), args...)}) + var out []byte + if len(f.outputs) > 0 { + out = f.outputs[0] + f.outputs = f.outputs[1:] + } + if f.err != nil { + return out, f.err + } + return out, nil +} + +// -- helpers -- + +func newTestRuntime(chunkSize int) (*Runtime, *fakeRunner) { + fr := &fakeRunner{} + r := New(Options{Binary: "tmux-test", Timeout: time.Second, Shell: "/bin/sh", ChunkSize: chunkSize}) + r.runner = fr + return r, fr +} + +// -- Options / New tests -- + +func TestNewDefaultsToPortableShell(t *testing.T) { + t.Setenv("SHELL", "") + r := New(Options{}) + if got := r.shell; got != "/bin/sh" { + t.Fatalf("default shell = %q, want /bin/sh", got) + } +} + +func TestNewPicksUpShellFromEnv(t *testing.T) { + t.Setenv("SHELL", "/bin/zsh") + r := New(Options{}) + if got := r.shell; got != "/bin/zsh" { + t.Fatalf("shell = %q, want /bin/zsh", got) + } +} + +// -- command builder tests -- + +func TestCommandBuilders(t *testing.T) { + if got, want := newSessionArgs("sess-1", "/tmp/ws", "/bin/sh", `echo hi; exec "${SHELL:-/bin/sh}" -i`), + []string{"new-session", "-d", "-s", "sess-1", "-x", "220", "-y", "50", "-c", "/tmp/ws", "/bin/sh", "-c", `echo hi; exec "${SHELL:-/bin/sh}" -i`}; !reflect.DeepEqual(got, want) { + t.Fatalf("newSessionArgs = %#v, want %#v", got, want) + } + // set-option uses pane-targeting (no = prefix). + if got, want := setStatusOffArgs("sess-1"), []string{"set-option", "-t", "sess-1", "status", "off"}; !reflect.DeepEqual(got, want) { + t.Fatalf("setStatusOffArgs = %#v, want %#v", got, want) + } + if got, want := setMouseOnArgs("sess-1"), []string{"set-option", "-t", "sess-1", "mouse", "on"}; !reflect.DeepEqual(got, want) { + t.Fatalf("setMouseOnArgs = %#v, want %#v", got, want) + } + // kill-session and has-session use exact-match prefix =. + if got, want := killSessionArgs("sess-1"), []string{"kill-session", "-t", "=sess-1"}; !reflect.DeepEqual(got, want) { + t.Fatalf("killSessionArgs = %#v, want %#v", got, want) + } + if got, want := hasSessionArgs("sess-1"), []string{"has-session", "-t", "=sess-1"}; !reflect.DeepEqual(got, want) { + t.Fatalf("hasSessionArgs = %#v, want %#v", got, want) + } + if got, want := sendKeysLiteralArgs("sess-1", "hello"), []string{"send-keys", "-t", "sess-1", "-l", "hello"}; !reflect.DeepEqual(got, want) { + t.Fatalf("sendKeysLiteralArgs = %#v, want %#v", got, want) + } + if got, want := sendEnterArgs("sess-1"), []string{"send-keys", "-t", "sess-1", "Enter"}; !reflect.DeepEqual(got, want) { + t.Fatalf("sendEnterArgs = %#v, want %#v", got, want) + } + if got, want := capturePaneArgs("sess-1", 10), []string{"capture-pane", "-t", "sess-1", "-p", "-S", "-10"}; !reflect.DeepEqual(got, want) { + t.Fatalf("capturePaneArgs = %#v, want %#v", got, want) + } +} + +// -- session name sanitization -- + +func TestSessionNameSanitizesSpecialChars(t *testing.T) { + got, err := tmuxSessionName("repo/issue#42.1") + if err != nil { + t.Fatalf("tmuxSessionName: %v", err) + } + if !sessionIDPattern.MatchString(got) { + t.Fatalf("sanitized id %q fails pattern", got) + } + if !strings.HasPrefix(got, "repo-issue-42-1-") { + t.Fatalf("sanitized id = %q, want readable prefix", got) + } + if got == "repo/issue#42.1" { + t.Fatal("sanitized id still contains raw unsafe characters") + } +} + +func TestSessionNamePassesThroughShortConforming(t *testing.T) { + if got := SessionName("myproj-1"); got != "myproj-1" { + t.Fatalf("SessionName = %q, want unchanged", got) + } +} + +func TestSessionNameMatchesCreateNaming(t *testing.T) { + long := domain.SessionID(strings.Repeat("x", 60) + "-1") + viaCreate, err := tmuxSessionName(long) + if err != nil { + t.Fatalf("tmuxSessionName: %v", err) + } + if got := SessionName(string(long)); got != viaCreate { + t.Fatalf("SessionName = %q, but Create uses %q", got, viaCreate) + } + if SessionName(string(long)) == string(long) { + t.Fatal("expected long id to be sanitised to a different name") + } +} + +// -- env key validation -- + +func TestCreateRejectsInvalidEnvKeys(t *testing.T) { + r, fr := newTestRuntime(0) + _ = fr + _, err := r.Create(context.Background(), ports.RuntimeConfig{ + SessionID: "sess-1", + WorkspacePath: "/tmp/ws", + Argv: []string{"echo", "hi"}, + Env: map[string]string{"BAD KEY": "x"}, + }) + if err == nil || !strings.Contains(err.Error(), "invalid env key") { + t.Fatalf("Create err = %v, want invalid env key", err) + } +} + +// -- Create tests -- + +func TestCreateIssuesNewSessionAndStatusOff(t *testing.T) { + // new-session, set-option status, set-option mouse, has-session (exit 0 = alive) + r, fr := newTestRuntime(0) + fr.outputs = [][]byte{nil, nil, nil, nil} + + h, err := r.Create(context.Background(), ports.RuntimeConfig{ + SessionID: "sess-1", + WorkspacePath: "/tmp/ws", + Argv: []string{"echo", "hi"}, + Env: map[string]string{"AO_SESSION_ID": "sess-1"}, + }) + if err != nil { + t.Fatalf("Create: %v", err) + } + if h.ID != "sess-1" { + t.Fatalf("handle ID = %q, want sess-1", h.ID) + } + // Expect 4 calls: new-session, set-option status, set-option mouse, has-session. + if len(fr.calls) != 4 { + t.Fatalf("calls = %d, want 4", len(fr.calls)) + } + + // Call 0: new-session + if got := fr.calls[0].args[0]; got != "new-session" { + t.Fatalf("call[0] = %q, want new-session", got) + } + // Check -s , -c are present. + joined := strings.Join(fr.calls[0].args, " ") + if !strings.Contains(joined, "-s sess-1") { + t.Fatalf("new-session args missing -s sess-1: %v", fr.calls[0].args) + } + if !strings.Contains(joined, "-c /tmp/ws") { + t.Fatalf("new-session args missing -c /tmp/ws: %v", fr.calls[0].args) + } + // Ensure -x and -y are set. + if !strings.Contains(joined, "-x 220") || !strings.Contains(joined, "-y 50") { + t.Fatalf("new-session args missing -x/-y: %v", fr.calls[0].args) + } + + // Call 1: set-option status off (plain target, pane-targeting does not use =). + if got, want := fr.calls[1].args, setStatusOffArgs("sess-1"); !reflect.DeepEqual(got, want) { + t.Fatalf("call[1] = %#v, want %#v", got, want) + } + + // Call 2: set-option mouse on (enables wheel-scroll of the pane). + if got, want := fr.calls[2].args, setMouseOnArgs("sess-1"); !reflect.DeepEqual(got, want) { + t.Fatalf("call[2] = %#v, want %#v", got, want) + } + + // Call 3: has-session (IsAlive, uses exact-match target =sess-1). + if got, want := fr.calls[3].args, hasSessionArgs("sess-1"); !reflect.DeepEqual(got, want) { + t.Fatalf("call[3] = %#v, want %#v", got, want) + } +} + +func TestCreateLaunchCommandContainsKeepAliveShell(t *testing.T) { + r, fr := newTestRuntime(0) + fr.outputs = [][]byte{nil, nil, nil} + + _, err := r.Create(context.Background(), ports.RuntimeConfig{ + SessionID: "sess-1", + WorkspacePath: "/tmp/ws", + Argv: []string{"myagent", "--flag"}, + }) + if err != nil { + t.Fatalf("Create: %v", err) + } + // The launch command is the last argument to new-session (after shellPath -c). + args := fr.calls[0].args + launchCmd := args[len(args)-1] + if !strings.Contains(launchCmd, `exec "${SHELL:-/bin/sh}" -i`) { + t.Fatalf("launch command missing keep-alive shell: %q", launchCmd) + } + if !strings.Contains(launchCmd, "'myagent'") { + t.Fatalf("launch command missing quoted argv: %q", launchCmd) + } +} + +func TestCreateLaunchCommandExportsEnvVars(t *testing.T) { + oldGetenv := getenv + getenv = func(key string) string { + if key == "PATH" { + return "/usr/bin:/bin" + } + return "" + } + defer func() { getenv = oldGetenv }() + + r, fr := newTestRuntime(0) + fr.outputs = [][]byte{nil, nil, nil} + + _, err := r.Create(context.Background(), ports.RuntimeConfig{ + SessionID: "sess-1", + WorkspacePath: "/tmp/ws", + Argv: []string{"myagent"}, + Env: map[string]string{ + "AO_SESSION_ID": "sess-1", + "ODD": "can't", + "PATH": "/custom/bin:/usr/bin", + }, + }) + if err != nil { + t.Fatalf("Create: %v", err) + } + args := fr.calls[0].args + launchCmd := args[len(args)-1] + for _, want := range []string{ + "export AO_SESSION_ID='sess-1';", + "export ODD='can'\\''t';", + "export PATH='/custom/bin:/usr/bin';", + } { + if !strings.Contains(launchCmd, want) { + t.Fatalf("launch command missing %q in: %q", want, launchCmd) + } + } +} + +func TestCreateDestroysAndReturnsErrorWhenNotAlive(t *testing.T) { + // Use a specialized fakeRunner that returns an exit error only for the 3rd call. + r2, _ := newTestRuntime(0) + fr3 := &fakeRunnerSelectiveErr{exitErrAt: 3} + fr3.outputs = [][]byte{nil, nil, nil, []byte("can't find session: sess-1")} + r2.runner = fr3 + + _, err := r2.Create(context.Background(), ports.RuntimeConfig{ + SessionID: "sess-1", + WorkspacePath: "/tmp/ws", + Argv: []string{"myagent"}, + }) + if err == nil { + t.Fatal("Create: got nil, want error when session not alive after create") + } + // Verify Destroy was called (kill-session). + hasKill := false + for _, c := range fr3.calls { + if len(c.args) > 0 && c.args[0] == "kill-session" { + hasKill = true + } + } + if !hasKill { + t.Fatal("expected kill-session cleanup call when session not alive") + } +} + +// fakeRunnerSelectiveErr returns an exec.ExitError for the call at index exitErrAt. +type fakeRunnerSelectiveErr struct { + calls []runnerCall + outputs [][]byte + exitErrAt int +} + +func (f *fakeRunnerSelectiveErr) Run(_ context.Context, env []string, name string, args ...string) ([]byte, error) { + idx := len(f.calls) + f.calls = append(f.calls, runnerCall{env: append([]string(nil), env...), name: name, args: append([]string(nil), args...)}) + var out []byte + if len(f.outputs) > 0 { + out = f.outputs[0] + f.outputs = f.outputs[1:] + } + if idx == f.exitErrAt { + return out, &exec.ExitError{} + } + return out, nil +} + +// -- Destroy tests -- + +func TestDestroyIsIdempotentWhenSessionMissing(t *testing.T) { + r, fr := newTestRuntime(0) + fr.outputs = [][]byte{[]byte("can't find session: sess-1")} + fr.err = &exec.ExitError{} + + if err := r.Destroy(context.Background(), ports.RuntimeHandle{ID: "sess-1"}); err != nil { + t.Fatalf("Destroy: %v", err) + } + if len(fr.calls) != 1 || fr.calls[0].args[0] != "kill-session" { + t.Fatalf("calls = %#v, want only kill-session", fr.calls) + } +} + +func TestDestroyIsIdempotentWhenNoServer(t *testing.T) { + r, fr := newTestRuntime(0) + fr.outputs = [][]byte{[]byte("no server running on /tmp/tmux-1000/default")} + fr.err = &exec.ExitError{} + + if err := r.Destroy(context.Background(), ports.RuntimeHandle{ID: "sess-1"}); err != nil { + t.Fatalf("Destroy no-server: %v", err) + } +} + +func TestDestroyReportsUnexpectedFailures(t *testing.T) { + r, fr := newTestRuntime(0) + fr.outputs = [][]byte{[]byte("permission denied")} + fr.err = &exec.ExitError{} + + if err := r.Destroy(context.Background(), ports.RuntimeHandle{ID: "sess-1"}); err == nil { + t.Fatal("Destroy: got nil, want unexpected failure error") + } +} + +func TestDestroyArgs(t *testing.T) { + r, fr := newTestRuntime(0) + fr.outputs = [][]byte{nil} + + if err := r.Destroy(context.Background(), ports.RuntimeHandle{ID: "sess-1"}); err != nil { + t.Fatalf("Destroy: %v", err) + } + // killSessionArgs uses exact-match target =. + if got, want := fr.calls[0].args, killSessionArgs("sess-1"); !reflect.DeepEqual(got, want) { + t.Fatalf("destroy args = %#v, want %#v", got, want) + } +} + +// -- IsAlive tests -- + +func TestIsAliveReturnsTrueOnExitZero(t *testing.T) { + r, fr := newTestRuntime(0) + fr.outputs = [][]byte{nil} + + alive, err := r.IsAlive(context.Background(), ports.RuntimeHandle{ID: "sess-1"}) + if err != nil { + t.Fatalf("IsAlive: %v", err) + } + if !alive { + t.Fatal("alive = false, want true") + } + if got, want := fr.calls[0].args, hasSessionArgs("sess-1"); !reflect.DeepEqual(got, want) { + t.Fatalf("has-session args = %#v, want %#v", got, want) + } +} + +func TestIsAliveReturnsFalseNilOnCantFindSession(t *testing.T) { + r, fr := newTestRuntime(0) + fr.outputs = [][]byte{[]byte("can't find session: sess-1")} + fr.err = &exec.ExitError{} + + alive, err := r.IsAlive(context.Background(), ports.RuntimeHandle{ID: "sess-1"}) + if err != nil { + t.Fatalf("IsAlive: %v", err) + } + if alive { + t.Fatal("alive = true, want false") + } +} + +func TestIsAliveReturnsFalseNilOnNoServer(t *testing.T) { + r, fr := newTestRuntime(0) + fr.outputs = [][]byte{[]byte("no server running on /tmp/tmux-1000/default")} + fr.err = &exec.ExitError{} + + alive, err := r.IsAlive(context.Background(), ports.RuntimeHandle{ID: "sess-1"}) + if err != nil { + t.Fatalf("IsAlive: %v", err) + } + if alive { + t.Fatal("alive = true, want false") + } +} + +func TestIsAliveReturnsFalseNilOnErrorConnecting(t *testing.T) { + r, fr := newTestRuntime(0) + fr.outputs = [][]byte{[]byte("error connecting to /tmp/tmux-1000/default (No such file or directory)")} + fr.err = &exec.ExitError{} + + alive, err := r.IsAlive(context.Background(), ports.RuntimeHandle{ID: "sess-1"}) + if err != nil { + t.Fatalf("IsAlive error connecting: %v", err) + } + if alive { + t.Fatal("alive = true, want false") + } +} + +// IsAlive must treat any non-"missing" non-zero exit as a probe error so the +// reaper never reads a transient failure as proof of death. +func TestIsAliveReportsOtherExitFailuresAsProbeErrors(t *testing.T) { + r, fr := newTestRuntime(0) + fr.outputs = [][]byte{[]byte("unexpected internal error")} + fr.err = &exec.ExitError{} + + alive, err := r.IsAlive(context.Background(), ports.RuntimeHandle{ID: "sess-1"}) + if err == nil { + t.Fatal("IsAlive: got nil, want probe error; failed probe must not read as dead") + } + if alive { + t.Fatal("alive = true on probe failure") + } +} + +// -- SendMessage tests -- + +func TestSendMessageChunksAndSendsEnter(t *testing.T) { + r, fr := newTestRuntime(5) // chunkSize=5 + // "hello世界": hello=5 bytes, 世=3 bytes, 界=3 bytes => 3 sends + 1 Enter + if err := r.SendMessage(context.Background(), ports.RuntimeHandle{ID: "sess-1"}, "hello世界"); err != nil { + t.Fatalf("SendMessage: %v", err) + } + if len(fr.calls) != 4 { + t.Fatalf("calls = %d, want 4 (3 chunks + Enter)", len(fr.calls)) + } + if got, want := fr.calls[0].args, sendKeysLiteralArgs("sess-1", "hello"); !reflect.DeepEqual(got, want) { + t.Fatalf("chunk 1 args = %#v, want %#v", got, want) + } + if got, want := fr.calls[1].args, sendKeysLiteralArgs("sess-1", "世"); !reflect.DeepEqual(got, want) { + t.Fatalf("chunk 2 args = %#v, want %#v", got, want) + } + if got, want := fr.calls[2].args, sendKeysLiteralArgs("sess-1", "界"); !reflect.DeepEqual(got, want) { + t.Fatalf("chunk 3 args = %#v, want %#v", got, want) + } + if got, want := fr.calls[3].args, sendEnterArgs("sess-1"); !reflect.DeepEqual(got, want) { + t.Fatalf("Enter args = %#v, want %#v", got, want) + } +} + +func TestSendMessageUsesLiteralFlag(t *testing.T) { + r, fr := newTestRuntime(0) + if err := r.SendMessage(context.Background(), ports.RuntimeHandle{ID: "sess-1"}, "Enter"); err != nil { + t.Fatalf("SendMessage: %v", err) + } + // First call must use -l so "Enter" is sent literally, not as a key binding. + if fr.calls[0].args[3] != "-l" { + t.Fatalf("send-keys args[3] = %q, want -l", fr.calls[0].args[3]) + } +} + +// -- GetOutput tests -- + +func TestGetOutputValidatesLines(t *testing.T) { + r, _ := newTestRuntime(0) + _, err := r.GetOutput(context.Background(), ports.RuntimeHandle{ID: "sess-1"}, 0) + if err == nil { + t.Fatal("GetOutput lines=0: got nil, want error") + } +} + +func TestGetOutputTrimsLines(t *testing.T) { + r, fr := newTestRuntime(0) + fr.outputs = [][]byte{[]byte("one\ntwo\nthree\n")} + + out, err := r.GetOutput(context.Background(), ports.RuntimeHandle{ID: "sess-1"}, 2) + if err != nil { + t.Fatalf("GetOutput: %v", err) + } + if out != "two\nthree\n" { + t.Fatalf("output = %q, want last two lines", out) + } +} + +func TestGetOutputTrimsTrailingScreenPaddingBeforeTailing(t *testing.T) { + r, fr := newTestRuntime(0) + fr.outputs = [][]byte{[]byte("ready\nprompt> echo hi\nhi\n\n\n\n")} + + out, err := r.GetOutput(context.Background(), ports.RuntimeHandle{ID: "sess-1"}, 2) + if err != nil { + t.Fatalf("GetOutput: %v", err) + } + if out != "prompt> echo hi\nhi\n" { + t.Fatalf("output = %q, want last non-padding lines", out) + } +} + +func TestGetOutputArgs(t *testing.T) { + r, fr := newTestRuntime(0) + fr.outputs = [][]byte{[]byte("output\n")} + + _, err := r.GetOutput(context.Background(), ports.RuntimeHandle{ID: "sess-1"}, 10) + if err != nil { + t.Fatalf("GetOutput: %v", err) + } + if got, want := fr.calls[0].args, capturePaneArgs("sess-1", 10); !reflect.DeepEqual(got, want) { + t.Fatalf("capture-pane args = %#v, want %#v", got, want) + } +} + +// -- AttachCommand tests -- + +func TestAttachCommandReturnsExpectedArgv(t *testing.T) { + r := New(Options{Binary: "/usr/bin/tmux", Timeout: time.Second}) + argv, err := r.attachCommand(ports.RuntimeHandle{ID: "sess-1"}) + if err != nil { + t.Fatalf("AttachCommand: %v", err) + } + want := []string{"/usr/bin/tmux", "attach-session", "-t", "sess-1"} + if !reflect.DeepEqual(argv, want) { + t.Fatalf("argv = %#v, want %#v", argv, want) + } +} + +func TestAttachCommandRejectsInvalidHandle(t *testing.T) { + r := New(Options{}) + _, err := r.attachCommand(ports.RuntimeHandle{ID: ""}) + if err == nil { + t.Fatal("AttachCommand empty handle: got nil, want error") + } +} + +// -- commandError tests -- + +func TestCommandErrorUnwraps(t *testing.T) { + base := errors.New("base") + err := commandError{err: base, output: "details"} + if !errors.Is(err, base) { + t.Fatal("commandError should unwrap base error") + } + if !strings.Contains(err.Error(), "details") { + t.Fatalf("error = %q, want output details", err.Error()) + } +} + +// -- text helper tests -- + +func TestChunks(t *testing.T) { + if got := chunks("", 5); !reflect.DeepEqual(got, []string{""}) { + t.Fatalf("chunks empty = %#v", got) + } + if got := chunks("hello", 10); !reflect.DeepEqual(got, []string{"hello"}) { + t.Fatalf("chunks fits = %#v", got) + } + // UTF-8 boundary: 世 is 3 bytes; with chunkSize=5 "hello世界" splits at 5,6,6 + got := chunks("hello世界", 5) + if len(got) != 3 { + t.Fatalf("chunks count = %d, want 3: %#v", len(got), got) + } + if got[0] != "hello" || got[1] != "世" || got[2] != "界" { + t.Fatalf("chunks = %#v, want [hello 世 界]", got) + } +} + +func TestTailLines(t *testing.T) { + if got := tailLines("a\nb\nc\n", 2); got != "b\nc\n" { + t.Fatalf("tailLines = %q, want b/c", got) + } + if got := tailLines("a\nb\n", 5); got != "a\nb\n" { + t.Fatalf("tailLines fewer = %q", got) + } + if got := tailLines("", 5); got != "" { + t.Fatalf("tailLines empty = %q", got) + } +} + +func TestTrimTrailingBlankLines(t *testing.T) { + if got := trimTrailingBlankLines("a\nb\n\n\n"); got != "a\nb\n" { + t.Fatalf("trimTrailingBlankLines = %q, want a/b", got) + } + if got := trimTrailingBlankLines(""); got != "" { + t.Fatalf("trimTrailingBlankLines empty = %q", got) + } +} diff --git a/backend/internal/adapters/runtime/zellij/commands.go b/backend/internal/adapters/runtime/zellij/commands.go deleted file mode 100644 index b410bd50..00000000 --- a/backend/internal/adapters/runtime/zellij/commands.go +++ /dev/null @@ -1,405 +0,0 @@ -package zellij - -import ( - "encoding/base64" - "encoding/binary" - "fmt" - "runtime" - "sort" - "strconv" - "strings" - "unicode/utf16" - - "github.com/aoagents/agent-orchestrator/backend/internal/ports" -) - -const ( - agentPaneName = "agent" - defaultChunkBytes = 16 * 1024 -) - -func versionArgs() []string { - return []string{"--version"} -} - -func createSessionArgs(id, layoutPath string) []string { - clientOptions := embeddedClientOptions() - args := make([]string, 0, 6+len(clientOptions)+6) - args = append(args, - "attach", "--create-background", id, - "options", - "--default-layout", layoutPath, - ) - args = append(args, clientOptions...) - args = append(args, - "--session-serialization", "false", - "--show-startup-tips", "false", - "--show-release-notes", "false", - ) - return args -} - -func listPanesArgs(id string) []string { - return []string{"--session", id, "action", "list-panes", "--all", "--json"} -} - -func pasteArgs(id, paneID, chunk string) []string { - if runtime.GOOS == "windows" { - return []string{"--session", id, "action", "write-chars", "--pane-id", paneID, chunk} - } - return []string{"--session", id, "action", "paste", "--pane-id", paneID, chunk} -} - -func sendEnterArgs(id, paneID string) []string { - return []string{"--session", id, "action", "send-keys", "--pane-id", paneID, "Enter"} -} - -func dumpScreenArgs(id, paneID string) []string { - return []string{"--session", id, "action", "dump-screen", "--pane-id", paneID, "--full"} -} - -func listSessionsArgs() []string { - return []string{"list-sessions", "--no-formatting"} -} - -// deleteSessionArgs builds the teardown command. `delete-session --force` -// kills a running session AND removes its serialized resurrection state in one -// step. Plain `kill-session` is not enough: zellij can keep the session in its -// global resurrection cache as "(EXITED - attach to resurrect)", and any later -// `zellij attach ` (e.g. the terminal mux re-opening a pane) would resurrect -// it — re-running the agent command for a session the daemon already destroyed. -func deleteSessionArgs(id string) []string { - return []string{"delete-session", "--force", id} -} - -func attachArgs(id string) []string { - clientOptions := embeddedClientOptions() - args := make([]string, 0, 3+len(clientOptions)) - args = append(args, - "attach", id, - "options", - ) - args = append(args, clientOptions...) - return args -} - -func embeddedClientOptions() []string { - return []string{ - "--pane-frames", "false", - // The dashboard terminal disables xterm local scrollback and lets the - // embedded zellij client own scrollback. Keep mouse mode on so wheel - // reports reach zellij, while leaving richer pointer behaviors off. - "--mouse-mode", "true", - "--advanced-mouse-actions", "false", - "--mouse-hover-effects", "false", - "--focus-follows-mouse", "false", - "--mouse-click-through", "false", - "--support-kitty-keyboard-protocol", "false", - } -} - -func handleIDValue(sessionID, paneID string) string { - return sessionID + "/" + paneID -} - -func terminalPaneID(id int) string { - return fmt.Sprintf("terminal_%d", id) -} - -func buildLayout(cfg ports.RuntimeConfig, shellPath string) string { - if runtime.GOOS == "windows" { - return directLayoutString(cfg.WorkspacePath, cfg.Argv) - } - spec := shellLaunchSpecFor(shellPath) - shellCommand := shellLaunchCommand(cfg, spec) - return layoutString(cfg.WorkspacePath, shellPath, spec.args, shellCommand) -} - -// windowsLaunchArgv returns the argv zellij executes on Windows to start the -// agent. The trampoline reads the launch spec from AO_LAUNCH_SPEC, so KDL -// args quoting cannot mangle codex's `--config key=value` flags. -func windowsLaunchArgv(launcherBinary string) []string { - command := launcherBinary - if command == "" { - command = "ao" - } - return []string{command, "launch"} -} - -func windowsFallbackShellArgv(shellPath string) []string { - if strings.TrimSpace(shellPath) == "" { - shellPath = "powershell.exe" - } - base := strings.ToLower(filepathBase(shellPath)) - if strings.Contains(base, "cmd") { - return []string{shellPath, "/D", "/Q", "/K"} - } - if strings.Contains(base, "powershell") || strings.Contains(base, "pwsh") { - return []string{shellPath, "-NoLogo", "-NoProfile", "-NoExit"} - } - if strings.Contains(base, "sh") { - return []string{shellPath, "-i"} - } - return []string{shellPath} -} - -// directLayoutString builds a layout that runs argv[0] with argv[1:] as zellij -// `args`, with no intermediate shell. Used on Windows where wrapping the agent -// in powershell/cmd quoting is unsound for arbitrary argv (e.g. codex's -// `--config key="value with spaces"`). -func directLayoutString(workspacePath string, argv []string) string { - command := "" - args := []string{} - if len(argv) > 0 { - command = argv[0] - args = argv[1:] - } - - var b strings.Builder - b.WriteString("layout {\n") - b.WriteString(" cwd ") - b.WriteString(kdlQuote(workspacePath)) - b.WriteString("\n") - b.WriteString(" pane command=") - b.WriteString(kdlQuote(command)) - b.WriteString(" name=") - b.WriteString(kdlQuote(agentPaneName)) - b.WriteString(" borderless=true {\n") - if len(args) > 0 { - b.WriteString(" args ") - b.WriteString(kdlJoin(args)) - b.WriteString("\n") - } - b.WriteString(" }\n") - b.WriteString("}\n") - return b.String() -} - -type shellLaunchSpec struct { - args []string -} - -func shellLaunchSpecFor(shellPath string) shellLaunchSpec { - base := strings.ToLower(filepathBase(shellPath)) - if strings.Contains(base, "cmd") { - return shellLaunchSpec{args: []string{"/D", "/S", "/K"}} - } - if strings.Contains(base, "powershell") || strings.Contains(base, "pwsh") { - return shellLaunchSpec{args: []string{"-NoLogo", "-NoProfile", "-NoExit", "-EncodedCommand"}} - } - return shellLaunchSpec{args: []string{"-lc"}} -} - -func layoutString(workspacePath, shellPath string, shellArgs []string, shellCommand string) string { - return "layout {\n" + - " cwd " + kdlQuote(workspacePath) + "\n" + - " pane command=" + kdlQuote(shellPath) + " name=" + kdlQuote(agentPaneName) + " borderless=true {\n" + - " args " + kdlJoin(shellArgs) + " " + kdlQuote(shellCommand) + "\n" + - " }\n" + - "}\n" -} - -func shellLaunchCommand(cfg ports.RuntimeConfig, spec shellLaunchSpec) string { - if len(spec.args) > 0 && spec.args[0] == "-NoLogo" { - return wrapLaunchCommandPowerShell(cfg) - } - if len(spec.args) > 0 && spec.args[0] == "/D" { - return wrapLaunchCommandCmd(cfg) - } - return wrapLaunchCommandUnix(cfg) -} - -func wrapLaunchCommandUnix(cfg ports.RuntimeConfig) string { - path := cfg.Env["PATH"] - if path == "" { - path = getenv("PATH") - } - - var b strings.Builder - for _, key := range sortedKeys(cfg.Env) { - if key == "PATH" { - continue - } - b.WriteString("export ") - b.WriteString(key) - b.WriteString("=") - b.WriteString(shellQuote(cfg.Env[key])) - b.WriteString("; ") - } - if path != "" { - b.WriteString("export PATH=") - b.WriteString(shellQuote(path)) - b.WriteString("; ") - } - b.WriteString(quoteArgvUnix(cfg.Argv)) - return b.String() -} - -func wrapLaunchCommandPowerShell(cfg ports.RuntimeConfig) string { - path := cfg.Env["PATH"] - if path == "" { - path = getenv("PATH") - } - - var b strings.Builder - for _, key := range sortedKeys(cfg.Env) { - if key == "PATH" { - continue - } - b.WriteString("$env:") - b.WriteString(key) - b.WriteString(" = ") - b.WriteString(psQuote(cfg.Env[key])) - b.WriteString("; ") - } - if path != "" { - b.WriteString("$env:PATH = ") - b.WriteString(psQuote(path)) - b.WriteString("; ") - } - b.WriteString(quoteArgvPowerShell(cfg.Argv)) - return powerShellEncodedCommand(b.String()) -} - -// powerShellEncodedCommand returns the base64'd UTF-16-LE form of script, -// suitable for `powershell.exe -EncodedCommand`. zellij's KDL `args` quoting -// is not robust enough to round-trip arbitrary PowerShell script text through -// a plain `-Command` argv slot, so we hand PowerShell a single opaque base64 -// blob instead. -func powerShellEncodedCommand(script string) string { - words := utf16.Encode([]rune(script)) - buf := make([]byte, len(words)*2) - for i, word := range words { - binary.LittleEndian.PutUint16(buf[i*2:], word) - } - return base64.StdEncoding.EncodeToString(buf) -} - -func wrapLaunchCommandCmd(cfg ports.RuntimeConfig) string { - path := cfg.Env["PATH"] - if path == "" { - path = getenv("PATH") - } - - var b strings.Builder - for _, key := range sortedKeys(cfg.Env) { - if key == "PATH" { - continue - } - b.WriteString("set \"") - b.WriteString(key) - b.WriteString("=") - b.WriteString(cmdQuote(cfg.Env[key])) - b.WriteString("\" && ") - } - if path != "" { - b.WriteString("set \"PATH=") - b.WriteString(cmdQuote(path)) - b.WriteString("\" && ") - } - b.WriteString(quoteArgvCmd(cfg.Argv)) - return b.String() -} - -func validateEnvKeys(env map[string]string) error { - for key := range env { - if !validEnvKey(key) { - return fmt.Errorf("zellij runtime: invalid env key %q", key) - } - } - return nil -} - -func validEnvKey(key string) bool { - if key == "" { - return false - } - for i, r := range key { - if r == '_' || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') { - continue - } - if i > 0 && r >= '0' && r <= '9' { - continue - } - return false - } - return true -} - -func sortedKeys(m map[string]string) []string { - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - return keys -} - -func shellQuote(s string) string { - return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'" -} - -func psQuote(s string) string { - return "'" + strings.ReplaceAll(s, "'", "''") + "'" -} - -func cmdQuote(s string) string { - return strings.ReplaceAll(s, "\"", "\"\"") -} - -// quoteArgvUnix renders argv as a POSIX-shell command, single-quoting each -// argument so a value with spaces stays one word under `sh -lc`. -func quoteArgvUnix(argv []string) string { - parts := make([]string, len(argv)) - for i, a := range argv { - parts[i] = shellQuote(a) - } - return strings.Join(parts, " ") -} - -// quoteArgvPowerShell renders argv for `powershell -Command`. The call operator -// `&` is required so a quoted first token is invoked as a command rather than -// echoed as a string literal. -func quoteArgvPowerShell(argv []string) string { - if len(argv) == 0 { - return "" - } - parts := make([]string, len(argv)) - for i, a := range argv { - parts[i] = psQuote(a) - } - return "& " + strings.Join(parts, " ") -} - -// quoteArgvCmd renders argv for cmd.exe, wrapping each argument in double quotes -// (doubling any embedded quote) so spaces don't split a single argument. -func quoteArgvCmd(argv []string) string { - parts := make([]string, len(argv)) - for i, a := range argv { - parts[i] = "\"" + strings.ReplaceAll(a, "\"", "\"\"") + "\"" - } - return strings.Join(parts, " ") -} - -func kdlQuote(s string) string { - return strconv.Quote(s) -} - -func kdlJoin(args []string) string { - parts := make([]string, 0, len(args)) - for _, arg := range args { - parts = append(parts, kdlQuote(arg)) - } - return strings.Join(parts, " ") -} - -func filepathBase(path string) string { - if path == "" { - return "" - } - i := strings.LastIndexAny(path, `/\`) - if i < 0 { - return path - } - return path[i+1:] -} diff --git a/backend/internal/adapters/runtime/zellij/process_other.go b/backend/internal/adapters/runtime/zellij/process_other.go deleted file mode 100644 index 69e955f3..00000000 --- a/backend/internal/adapters/runtime/zellij/process_other.go +++ /dev/null @@ -1,18 +0,0 @@ -//go:build !windows - -package zellij - -import ( - "errors" - "os/exec" -) - -// hideWindow is a no-op off Windows: only Windows pops a console window. -func hideWindow(*exec.Cmd) {} - -// startBackgroundProcess is a stub: the fire-and-forget path is only used by -// the Windows zellij codepath. Non-Windows builds create sessions -// synchronously via runner.Run. -func startBackgroundProcess(env []string, name string, args ...string) error { - return errors.New("zellij runtime: background spawn is windows-only") -} diff --git a/backend/internal/adapters/runtime/zellij/process_windows.go b/backend/internal/adapters/runtime/zellij/process_windows.go deleted file mode 100644 index 51301963..00000000 --- a/backend/internal/adapters/runtime/zellij/process_windows.go +++ /dev/null @@ -1,79 +0,0 @@ -//go:build windows - -package zellij - -import ( - "os/exec" - "strings" - "syscall" - - "golang.org/x/sys/windows" -) - -// hideWindow suppresses the console window for a console-subsystem child so -// frequent zellij calls (e.g. the reaper's list-sessions) don't flash on Windows. -func hideWindow(cmd *exec.Cmd) { - cmd.SysProcAttr = &syscall.SysProcAttr{ - HideWindow: true, - CreationFlags: windows.CREATE_NO_WINDOW, - } -} - -func startBackgroundProcess(env []string, name string, args ...string) error { - script := "Start-Process -FilePath " + psQuote(name) + " -ArgumentList " + psQuote(windowsCommandLine(args)) + " -WindowStyle Hidden" - cmd := exec.Command("powershell.exe", "-NoLogo", "-NoProfile", "-EncodedCommand", powerShellEncodedCommand(script)) - cmd.Env = env - cmd.SysProcAttr = &syscall.SysProcAttr{ - // CREATE_NO_WINDOW, not CREATE_NEW_CONSOLE: the latter creates a console - // then hides it, which flashed a window. - CreationFlags: windows.CREATE_NO_WINDOW, - HideWindow: true, - } - if err := cmd.Start(); err != nil { - return err - } - go func() { _ = cmd.Wait() }() - return nil -} - -func windowsCommandLine(args []string) string { - quoted := make([]string, len(args)) - for i, arg := range args { - quoted[i] = windowsQuoteArg(arg) - } - return strings.Join(quoted, " ") -} - -func windowsQuoteArg(arg string) string { - if arg == "" { - return `""` - } - if !strings.ContainsAny(arg, " \t\"") { - return arg - } - - var b strings.Builder - b.WriteByte('"') - backslashes := 0 - for _, r := range arg { - switch r { - case '\\': - backslashes++ - case '"': - b.WriteString(strings.Repeat(`\`, backslashes*2+1)) - b.WriteRune(r) - backslashes = 0 - default: - if backslashes > 0 { - b.WriteString(strings.Repeat(`\`, backslashes)) - backslashes = 0 - } - b.WriteRune(r) - } - } - if backslashes > 0 { - b.WriteString(strings.Repeat(`\`, backslashes*2)) - } - b.WriteByte('"') - return b.String() -} diff --git a/backend/internal/adapters/runtime/zellij/zellij.go b/backend/internal/adapters/runtime/zellij/zellij.go deleted file mode 100644 index 03a1f939..00000000 --- a/backend/internal/adapters/runtime/zellij/zellij.go +++ /dev/null @@ -1,950 +0,0 @@ -// Package zellij implements ports.Runtime using Zellij sessions. -package zellij - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "os" - "os/exec" - "path/filepath" - "regexp" - "runtime" - "strconv" - "strings" - "time" - "unicode/utf8" - - "github.com/aoagents/agent-orchestrator/backend/internal/agentlaunch" - "github.com/aoagents/agent-orchestrator/backend/internal/domain" - "github.com/aoagents/agent-orchestrator/backend/internal/ports" -) - -const ( - defaultTimeout = 5 * time.Second - defaultWindowsTimeout = 30 * time.Second - defaultZellijTerm = "xterm-256color" - defaultZellijColor = "truecolor" - minMajor = 0 - minMinor = 44 - minPatch = 3 -) - -var sessionIDPattern = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) -var paneIDPattern = regexp.MustCompile(`^terminal_\d+$`) - -var getenv = os.Getenv -var lookPath = exec.LookPath -var fileExists = func(path string) bool { - info, err := os.Stat(path) - return err == nil && !info.IsDir() -} - -// Options configures a zellij Runtime; every field has a sensible default -// (see New), so the zero value is usable. -type Options struct { - Binary string - Timeout time.Duration - Shell string - SocketDir string - ConfigDir string - ChunkSize int - LauncherBinary string -} - -// Runtime runs agent sessions inside zellij sessions, driving them via the -// zellij CLI. It implements ports.Runtime. -type Runtime struct { - binary string - timeout time.Duration - shell string - socketDir string - configDir string - chunkSize int - launcher string - runner runner -} - -var _ ports.Runtime = (*Runtime)(nil) - -// DefaultSocketDir returns a short, stable ZELLIJ_SOCKET_DIR for AO's daemon. -// zellij's own default lives under $TMPDIR (long on macOS), which leaves almost -// none of the ~103-byte unix-socket-path budget for the session name — a long -// session id then fails with "session name must be less than 0 characters". A -// short dir restores ample budget. Empty on Windows, where zellij is not used. -// Pure: callers that run zellij should MkdirAll the result. -func DefaultSocketDir() string { - if runtime.GOOS == "windows" { - return "" - } - return "/tmp/ao-zellij-" + strconv.Itoa(os.Getuid()) -} - -type runner interface { - Run(ctx context.Context, env []string, name string, args ...string) ([]byte, error) - Start(env []string, name string, args ...string) error -} - -type execRunner struct{} - -func (execRunner) Run(ctx context.Context, env []string, name string, args ...string) ([]byte, error) { - cmd := exec.CommandContext(ctx, name, args...) - cmd.Env = zellijCommandEnv(os.Environ(), env) - hideWindow(cmd) - return cmd.CombinedOutput() -} - -func (execRunner) Start(env []string, name string, args ...string) error { - return startBackgroundProcess(zellijCommandEnv(os.Environ(), env), name, args...) -} - -// New builds a zellij Runtime, filling unset Options with defaults: binary -// "zellij", shell from $SHELL (else /bin/sh, or powershell.exe on Windows), and -// the default timeout and output chunk size. -func New(opts Options) *Runtime { - binary := opts.Binary - if binary == "" { - binary = defaultBinary() - } - timeout := opts.Timeout - if timeout == 0 { - timeout = defaultCommandTimeout() - } - shellPath := opts.Shell - if shellPath == "" { - shellPath = os.Getenv("SHELL") - } - if shellPath == "" { - if runtime.GOOS == "windows" { - shellPath = "powershell.exe" - } else { - shellPath = "/bin/sh" - } - } - chunkSize := opts.ChunkSize - if chunkSize <= 0 { - chunkSize = defaultChunkBytes - } - launcher := opts.LauncherBinary - if launcher == "" { - launcher = defaultLauncherBinary() - } - return &Runtime{binary: binary, timeout: timeout, shell: shellPath, socketDir: opts.SocketDir, configDir: opts.ConfigDir, chunkSize: chunkSize, launcher: launcher, runner: execRunner{}} -} - -// defaultLauncherBinary returns the path used by the zellij Windows codepath -// to invoke the `ao launch` trampoline. On Windows the agent's argv is -// persisted to a temp spec file (see agentlaunch); zellij then runs this -// binary with `launch` and it execs the real agent. Falls back to plain "ao" -// if the daemon binary path cannot be resolved (PATH lookup at runtime). -func defaultLauncherBinary() string { - path, err := os.Executable() - if err == nil && isLauncherBinary(path) { - return path - } - return "ao" -} - -func isLauncherBinary(path string) bool { - name := strings.ToLower(filepath.Base(path)) - if runtime.GOOS == "windows" { - name = strings.TrimSuffix(name, ".exe") - } - return name == "ao" -} - -func defaultCommandTimeout() time.Duration { - if runtime.GOOS == "windows" { - return defaultWindowsTimeout - } - return defaultTimeout -} - -func defaultBinary() string { - names := []string{"zellij"} - if runtime.GOOS == "windows" { - names = []string{"zellij.exe", "zellij"} - } - for _, name := range names { - if path, err := lookPath(name); err == nil && path != "" { - return path - } - } - if runtime.GOOS == "windows" { - for _, candidate := range windowsZellijCandidates() { - if fileExists(candidate) { - return candidate - } - } - } - return "zellij" -} - -func windowsZellijCandidates() []string { - candidates := []string{} - if localAppData := getenv("LOCALAPPDATA"); localAppData != "" { - candidates = append(candidates, filepath.Join(localAppData, "Programs", "zellij", "zellij.exe")) - } - for _, key := range []string{"ProgramFiles", "ProgramFiles(x86)"} { - if dir := getenv(key); dir != "" { - candidates = append(candidates, - filepath.Join(dir, "zellij", "zellij.exe"), - filepath.Join(dir, "Zellij", "zellij.exe"), - ) - } - } - return candidates -} - -// Create starts a new zellij session in the workspace, running the agent's -// launch command, and returns a handle to it. -func (r *Runtime) Create(ctx context.Context, cfg ports.RuntimeConfig) (ports.RuntimeHandle, error) { - id, err := zellijSessionName(cfg.SessionID) - if err != nil { - return ports.RuntimeHandle{}, err - } - if cfg.WorkspacePath == "" { - return ports.RuntimeHandle{}, errors.New("zellij runtime: workspace path is required") - } - if len(cfg.Argv) == 0 { - return ports.RuntimeHandle{}, errors.New("zellij runtime: launch command is required") - } - if err := validateEnvKeys(cfg.Env); err != nil { - return ports.RuntimeHandle{}, err - } - if err := r.ensureSupportedVersion(ctx); err != nil { - return ports.RuntimeHandle{}, err - } - // Zellij keeps exited sessions in a resurrection cache. A previous partial - // spawn can therefore make `attach --create-background` fail with "Session - // already exists" even though AO has no usable runtime handle. Clear any - // same-name runtime state before creating the new AO-owned session. - if err := r.Destroy(ctx, ports.RuntimeHandle{ID: id}); err != nil { - return ports.RuntimeHandle{}, err - } - - layoutPath, launchEnv, cleanupLaunchSpec, err := r.writeLayout(cfg) - if err != nil { - return ports.RuntimeHandle{}, err - } - defer func() { _ = os.Remove(layoutPath) }() - cleanupOnFailure := true - defer func() { - if cleanupOnFailure && cleanupLaunchSpec != nil { - cleanupLaunchSpec() - } - }() - - if err := r.createSession(ctx, id, layoutPath, launchEnv); err != nil { - return ports.RuntimeHandle{}, fmt.Errorf("zellij runtime: create session %s: %w", id, err) - } - paneID, err := r.findAgentPane(ctx, id) - if err != nil { - _ = r.Destroy(context.Background(), ports.RuntimeHandle{ID: id}) - return ports.RuntimeHandle{}, err - } - if err := r.waitForPaneReady(ctx, id, paneID); err != nil { - _ = r.Destroy(context.Background(), ports.RuntimeHandle{ID: id}) - return ports.RuntimeHandle{}, err - } - handle := ports.RuntimeHandle{ID: handleIDValue(id, paneID)} - alive, err := r.IsAlive(ctx, handle) - if err != nil { - _ = r.Destroy(context.Background(), handle) - return ports.RuntimeHandle{}, fmt.Errorf("zellij runtime: verify session %s: %w", id, err) - } - if !alive { - _ = r.Destroy(context.Background(), handle) - return ports.RuntimeHandle{}, fmt.Errorf("zellij runtime: session %s exited before ready", id) - } - cleanupOnFailure = false - return handle, nil -} - -// createSession runs `zellij attach --create-background`. On Windows we spawn -// it via runner.Start (fire-and-forget) because the inherited daemon stdio -// confuses zellij's own readiness probe; on Unix we keep the synchronous run. -func (r *Runtime) createSession(ctx context.Context, id, layoutPath string, env map[string]string) error { - args := createSessionArgs(id, layoutPath) - if runtime.GOOS != "windows" { - _, err := r.run(ctx, args...) - return err - } - return r.startWithEnv(env, args...) -} - -// Destroy kills the handle's zellij session and deletes its serialized state, -// so the session can never be resurrected by a later `zellij attach`. An -// already-gone session is treated as success. -func (r *Runtime) Destroy(ctx context.Context, handle ports.RuntimeHandle) error { - id, _, err := handleID(handle) - if err != nil { - return err - } - out, err := r.run(ctx, deleteSessionArgs(id)...) - if err != nil { - var exitErr *exec.ExitError - if errors.As(err, &exitErr) && deleteSessionMissingOutput(string(out)) { - return nil - } - return fmt.Errorf("zellij runtime: destroy session %s: %w", id, err) - } - return nil -} - -// SendMessage pastes a message into the session's pane (chunked) and presses -// Enter to submit it. -func (r *Runtime) SendMessage(ctx context.Context, handle ports.RuntimeHandle, message string) error { - id, paneID, err := handleID(handle) - if err != nil { - return err - } - for _, chunk := range chunks(message, r.chunkSize) { - if _, err := r.run(ctx, pasteArgs(id, paneID, chunk)...); err != nil { - return fmt.Errorf("zellij runtime: paste message %s/%s: %w", id, paneID, err) - } - } - if _, err := r.run(ctx, sendEnterArgs(id, paneID)...); err != nil { - return fmt.Errorf("zellij runtime: send enter %s/%s: %w", id, paneID, err) - } - return nil -} - -// GetOutput returns the last `lines` lines of the session pane's screen dump. -func (r *Runtime) GetOutput(ctx context.Context, handle ports.RuntimeHandle, lines int) (string, error) { - id, paneID, err := handleID(handle) - if err != nil { - return "", err - } - if lines <= 0 { - return "", errors.New("zellij runtime: lines must be positive") - } - out, err := r.run(ctx, dumpScreenArgs(id, paneID)...) - if err != nil { - return "", fmt.Errorf("zellij runtime: capture output %s/%s: %w", id, paneID, err) - } - return tailLines(trimTrailingBlankLines(string(out)), lines), nil -} - -// IsAlive reports whether the handle's session still appears in `zellij -// list-sessions`. Only the documented "no sessions exist" failure counts as a -// definitive "not alive"; any other list-sessions failure is reported as a -// probe error so callers (the reaper feeding the LCM) treat it as a failed -// probe, never as proof of death. -func (r *Runtime) IsAlive(ctx context.Context, handle ports.RuntimeHandle) (bool, error) { - id, _, err := handleID(handle) - if err != nil { - return false, err - } - out, err := r.run(ctx, listSessionsArgs()...) - if err != nil { - var exitErr *exec.ExitError - if errors.As(err, &exitErr) && noActiveSessionsOutput(string(out)) { - return false, nil - } - return false, fmt.Errorf("zellij runtime: probe session %s: %w", id, err) - } - return sessionListedAlive(string(out), id), nil -} - -// noActiveSessionsOutput reports whether a non-zero `zellij list-sessions` -// failed because no sessions exist at all — the one exit-error case that is a -// definitive "dead" rather than a probe failure. zellij 0.44 emits either -// "No active zellij sessions found." or "There is no active session!". -func noActiveSessionsOutput(out string) bool { - s := strings.ToLower(out) - return strings.Contains(s, "no active") && strings.Contains(s, "session") -} - -func deleteSessionMissingOutput(out string) bool { - s := strings.ToLower(out) - if noActiveSessionsOutput(s) { - return true - } - return strings.Contains(s, "session") && - (strings.Contains(s, "not found") || - strings.Contains(s, "does not exist") || - strings.Contains(s, "not exist") || - strings.Contains(s, "not a session")) -} - -// AttachCommand returns the argv a human runs to attach their terminal to the -// session, plus an optional env block that the spawn should apply (used on -// Windows where wrapping the attach in an `env` shim is unsafe under ConPTY). -func (r *Runtime) AttachCommand(handle ports.RuntimeHandle) ([]string, []string, error) { - id, _, err := handleID(handle) - if err != nil { - return nil, nil, err - } - args := append([]string{}, r.baseArgs()...) - args = append(args, attachArgs(id)...) - argv, env := attachCommandWithEnv(r.binary, r.socketDir, args...) - return argv, env, nil -} - -func (r *Runtime) ensureSupportedVersion(ctx context.Context) error { - out, err := r.run(ctx, versionArgs()...) - if err != nil { - return fmt.Errorf("zellij runtime: check version: %w", err) - } - if _, err := CheckVersionOutput(string(out)); err != nil { - return fmt.Errorf("zellij runtime: check version: %w", err) - } - return nil -} - -func (r *Runtime) writeLayout(cfg ports.RuntimeConfig) (string, map[string]string, func(), error) { - launchEnv := cfg.Env - var cleanupLaunchSpec func() - if runtime.GOOS == "windows" { - specPath, err := agentlaunch.WriteTemp(agentlaunch.Spec{ - WorkspacePath: cfg.WorkspacePath, - Argv: cfg.Argv, - FallbackArgv: windowsFallbackShellArgv(r.shell), - }) - if err != nil { - return "", nil, nil, fmt.Errorf("zellij runtime: %w", err) - } - cleanupLaunchSpec = func() { _ = os.Remove(specPath) } - cfg.Argv = windowsLaunchArgv(r.launcher) - launchEnv = windowsLaunchEnv(cfg.Env, r.launcher, specPath) - } - - file, err := os.CreateTemp(os.TempDir(), "ao-zellij-layout-*.kdl") - if err != nil { - if cleanupLaunchSpec != nil { - cleanupLaunchSpec() - } - return "", nil, nil, fmt.Errorf("zellij runtime: create layout temp file: %w", err) - } - path := file.Name() - if _, err := file.WriteString(buildLayout(cfg, r.shell)); err != nil { - _ = file.Close() - _ = os.Remove(path) - if cleanupLaunchSpec != nil { - cleanupLaunchSpec() - } - return "", nil, nil, fmt.Errorf("zellij runtime: write layout temp file: %w", err) - } - if err := file.Close(); err != nil { - _ = os.Remove(path) - if cleanupLaunchSpec != nil { - cleanupLaunchSpec() - } - return "", nil, nil, fmt.Errorf("zellij runtime: close layout temp file: %w", err) - } - return path, launchEnv, cleanupLaunchSpec, nil -} - -// windowsLaunchEnv augments cfg.Env with the AO_LAUNCH_SPEC pointer the `ao -// launch` trampoline reads, and prepends the launcher's directory to PATH so -// the trampoline (i.e. `ao` itself) is resolvable in the spawned environment. -func windowsLaunchEnv(env map[string]string, launcherBinary, specPath string) map[string]string { - launchEnv := make(map[string]string, len(env)+2) - for k, v := range env { - launchEnv[k] = v - } - launchEnv[agentlaunch.EnvSpecPath] = specPath - if dir := launcherDir(launcherBinary); dir != "" { - base := launchEnv["PATH"] - if base == "" { - base = getenv("PATH") - } - if base == "" { - launchEnv["PATH"] = dir - } else { - launchEnv["PATH"] = dir + string(os.PathListSeparator) + base - } - } - return launchEnv -} - -func launcherDir(launcherBinary string) string { - if launcherBinary == "" || !filepath.IsAbs(launcherBinary) { - return "" - } - return filepath.Dir(launcherBinary) -} - -func (r *Runtime) findAgentPane(ctx context.Context, id string) (string, error) { - deadline := time.Now().Add(r.timeout) - var lastErr error - for { - out, err := r.run(ctx, listPanesArgs(id)...) - if err == nil { - paneID, parseErr := agentPaneID(out) - if parseErr == nil { - return paneID, nil - } - lastErr = parseErr - } else { - lastErr = err - } - if time.Now().After(deadline) { - return "", fmt.Errorf("zellij runtime: list panes %s: %w", id, lastErr) - } - select { - case <-ctx.Done(): - return "", ctx.Err() - case <-time.After(50 * time.Millisecond): - } - } -} - -func (r *Runtime) waitForPaneReady(ctx context.Context, id, paneID string) error { - if runtime.GOOS != "windows" { - return nil - } - - deadline := time.Now().Add(r.timeout) - var lastErr error - for { - out, err := r.run(ctx, listPanesArgs(id)...) - if err == nil { - pane, parseErr := paneByID(out, paneID) - if parseErr == nil { - if pane.Exited { - return fmt.Errorf("zellij runtime: pane %s/%s exited before ready", id, paneID) - } - if paneReady(pane) { - return nil - } - lastErr = fmt.Errorf("pane %s/%s is not ready", id, paneID) - } else { - lastErr = parseErr - } - } else { - lastErr = err - } - if time.Now().After(deadline) { - return fmt.Errorf("zellij runtime: wait for pane %s/%s: %w", id, paneID, lastErr) - } - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(50 * time.Millisecond): - } - } -} - -func (r *Runtime) run(ctx context.Context, args ...string) ([]byte, error) { - cmdCtx, cancel := context.WithTimeout(ctx, r.timeout) - defer cancel() - fullArgs := append(r.baseArgs(), args...) - out, err := r.runner.Run(cmdCtx, r.env(), r.binary, fullArgs...) - if cmdCtx.Err() != nil { - return out, cmdCtx.Err() - } - if err != nil { - return out, commandError{err: err, output: strings.TrimSpace(string(out))} - } - return out, nil -} - -// startWithEnv fires zellij in the background with extra env vars merged onto -// the runtime's base env. Used by the Windows createSession path so the daemon -// is not blocked waiting on zellij's `--create-background` to settle. -func (r *Runtime) startWithEnv(extra map[string]string, args ...string) error { - fullArgs := append(r.baseArgs(), args...) - if err := r.runner.Start(r.envWith(extra), r.binary, fullArgs...); err != nil { - return commandError{err: err} - } - return nil -} - -func (r *Runtime) baseArgs() []string { - args := []string{} - if r.configDir != "" { - args = append(args, "--config-dir", r.configDir) - } - return args -} - -func (r *Runtime) env() []string { - return r.envWith(nil) -} - -func (r *Runtime) envWith(extra map[string]string) []string { - env := zellijColorEnv(nil) - if r.socketDir == "" { - return appendRuntimeEnv(env, extra) - } - env = append(env, "ZELLIJ_SOCKET_DIR="+r.socketDir) - return appendRuntimeEnv(env, extra) -} - -func appendRuntimeEnv(env []string, extra map[string]string) []string { - for _, key := range sortedKeys(extra) { - env = append(env, key+"="+extra[key]) - } - return env -} - -func attachCommandWithEnv(binary, socketDir string, args ...string) ([]string, []string) { - if runtime.GOOS == "windows" { - // Windows ConPTY attaches the child directly. Avoid shell wrappers here: - // malformed ConPTY startup around powershell.exe/cmd.exe surfaces as modal - // application-error dialogs. Per-session ZELLIJ_SOCKET_DIR is delivered - // via the spawn's env block (CreateProcess) instead of an `env` shim. - var envBlock []string - if socketDir != "" { - envBlock = upsertEnv(append([]string(nil), os.Environ()...), "ZELLIJ_SOCKET_DIR="+socketDir) - } - return append([]string{binary}, args...), envBlock - } - env := zellijColorEnv(nil) - if socketDir != "" { - env = append(env, "ZELLIJ_SOCKET_DIR="+socketDir) - } - argv := []string{"env", "-u", "NO_COLOR"} - argv = append(argv, env...) - argv = append(argv, binary) - return append(argv, args...), nil -} - -func zellijCommandEnv(base, overrides []string) []string { - env := zellijColorEnv(append([]string(nil), base...)) - for _, pair := range overrides { - env = upsertEnv(env, pair) - } - return env -} - -func zellijColorEnv(env []string) []string { - if runtime.GOOS == "windows" { - return env - } - env = removeEnv(env, "NO_COLOR") - env = upsertEnv(env, "TERM="+defaultZellijTerm) - env = upsertEnv(env, "COLORTERM="+defaultZellijColor) - return env -} - -func upsertEnv(env []string, pair string) []string { - key, _, ok := strings.Cut(pair, "=") - if !ok { - return env - } - prefix := key + "=" - for i, current := range env { - if strings.HasPrefix(current, prefix) { - env[i] = pair - return env - } - } - return append(env, pair) -} - -func removeEnv(env []string, key string) []string { - prefix := key + "=" - out := env[:0] - for _, current := range env { - if strings.HasPrefix(current, prefix) { - continue - } - out = append(out, current) - } - return out -} - -func zellijSessionName(id domain.SessionID) (string, error) { - raw := string(id) - if raw == "" { - return "", errors.New("zellij runtime: session id is required") - } - return SessionName(raw), nil -} - -// SessionName returns the zellij session name the runtime registers for a given -// session id — applying the same sanitisation Create does. Callers that print an -// attach hint (e.g. `ao spawn`) must use this rather than the raw id, since a -// long or non-conforming id maps to a different, sanitised session name. -func SessionName(id string) string { - if sessionIDPattern.MatchString(id) && len(id) <= 48 { - return id - } - return sanitizedSessionName(id) -} - -func sanitizedSessionName(raw string) string { - var b strings.Builder - lastDash := false - for _, r := range raw { - valid := (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' - if valid { - b.WriteRune(r) - lastDash = false - continue - } - if !lastDash { - b.WriteByte('-') - lastDash = true - } - } - base := strings.Trim(b.String(), "-") - if base == "" { - base = "session" - } - if len(base) > 32 { - base = strings.TrimRight(base[:32], "-") - } - sum := sha256.Sum256([]byte(raw)) - return base + "-" + hex.EncodeToString(sum[:4]) -} - -func validateSessionID(id string) error { - if id == "" { - return errors.New("zellij runtime: session id is required") - } - if !sessionIDPattern.MatchString(id) { - return fmt.Errorf("zellij runtime: invalid session id %q", id) - } - return nil -} - -func validatePaneID(id string) error { - if id == "" { - return errors.New("zellij runtime: pane id is required") - } - if !paneIDPattern.MatchString(id) { - return fmt.Errorf("zellij runtime: invalid pane id %q", id) - } - return nil -} - -func handleID(handle ports.RuntimeHandle) (string, string, error) { - parts := strings.Split(handle.ID, "/") - if len(parts) == 1 { - if err := validateSessionID(parts[0]); err != nil { - return "", "", err - } - return parts[0], terminalPaneID(0), nil - } - if len(parts) != 2 { - return "", "", fmt.Errorf("zellij runtime: invalid handle id %q", handle.ID) - } - if err := validateSessionID(parts[0]); err != nil { - return "", "", err - } - if err := validatePaneID(parts[1]); err != nil { - return "", "", err - } - return parts[0], parts[1], nil -} - -type paneInfo struct { - ID int `json:"id"` - IsPlugin bool `json:"is_plugin"` - Title string `json:"title"` - Exited bool `json:"exited"` - TerminalCommand string `json:"terminal_command"` - PaneCommand string `json:"pane_command"` -} - -func agentPaneID(out []byte) (string, error) { - panes, err := parsePanes(out) - if err != nil { - return "", err - } - for _, pane := range panes { - if !pane.IsPlugin && pane.Title == agentPaneName { - return terminalPaneID(pane.ID), nil - } - } - for _, pane := range panes { - if !pane.IsPlugin { - return terminalPaneID(pane.ID), nil - } - } - return "", errors.New("agent pane not found") -} - -func paneByID(out []byte, paneID string) (paneInfo, error) { - panes, err := parsePanes(out) - if err != nil { - return paneInfo{}, err - } - for _, pane := range panes { - if !pane.IsPlugin && terminalPaneID(pane.ID) == paneID { - return pane, nil - } - } - return paneInfo{}, fmt.Errorf("pane %s not found", paneID) -} - -func parsePanes(out []byte) ([]paneInfo, error) { - var panes []paneInfo - if err := json.Unmarshal(out, &panes); err != nil { - return nil, fmt.Errorf("parse panes: %w", err) - } - return panes, nil -} - -func paneReady(pane paneInfo) bool { - if pane.PaneCommand != "" { - return true - } - return pane.TerminalCommand == "" -} - -func chunks(s string, maxBytes int) []string { - if s == "" { - return []string{""} - } - if maxBytes <= 0 || len(s) <= maxBytes { - return []string{s} - } - parts := []string{} - for s != "" { - if len(s) <= maxBytes { - parts = append(parts, s) - break - } - end := maxBytes - for end > 0 && !utf8.ValidString(s[:end]) { - end-- - } - if end == 0 { - _, size := utf8.DecodeRuneInString(s) - end = size - } - parts = append(parts, s[:end]) - s = s[end:] - } - return parts -} - -func tailLines(s string, n int) string { - if n <= 0 || s == "" { - return "" - } - lines := strings.SplitAfter(s, "\n") - if lines[len(lines)-1] == "" { - lines = lines[:len(lines)-1] - } - if len(lines) <= n { - return s - } - return strings.Join(lines[len(lines)-n:], "") -} - -func trimTrailingBlankLines(s string) string { - if s == "" { - return "" - } - lines := strings.SplitAfter(s, "\n") - if lines[len(lines)-1] == "" { - lines = lines[:len(lines)-1] - } - for len(lines) > 0 && strings.TrimRight(lines[len(lines)-1], "\r\n") == "" { - lines = lines[:len(lines)-1] - } - return strings.Join(lines, "") -} - -// RequiredVersion returns the minimum Zellij version AO's runtime adapter -// supports. -func RequiredVersion() string { return minSupportedVersion().String() } - -// CheckVersionOutput parses `zellij --version` output, returning the parsed -// version when it satisfies AO's minimum runtime requirement. -func CheckVersionOutput(out string) (string, error) { - version, err := parseVersion(out) - if err != nil { - return "", err - } - if compareVersion(version, minSupportedVersion()) < 0 { - return version.String(), fmt.Errorf("unsupported zellij version %s; require >= %s", version, RequiredVersion()) - } - return version.String(), nil -} - -func minSupportedVersion() semver { return semver{minMajor, minMinor, minPatch} } - -type semver struct { - major int - minor int - patch int -} - -func (v semver) String() string { - return fmt.Sprintf("%d.%d.%d", v.major, v.minor, v.patch) -} - -func parseVersion(out string) (semver, error) { - fields := strings.Fields(strings.TrimSpace(out)) - if len(fields) == 0 { - return semver{}, errors.New("empty version output") - } - raw := strings.TrimPrefix(fields[len(fields)-1], "v") - parts := strings.Split(raw, ".") - if len(parts) < 3 { - return semver{}, fmt.Errorf("invalid version output %q", strings.TrimSpace(out)) - } - major, err := parseVersionPart(parts[0]) - if err != nil { - return semver{}, fmt.Errorf("invalid version output %q", strings.TrimSpace(out)) - } - minor, err := parseVersionPart(parts[1]) - if err != nil { - return semver{}, fmt.Errorf("invalid version output %q", strings.TrimSpace(out)) - } - patch, err := parseVersionPart(parts[2]) - if err != nil { - return semver{}, fmt.Errorf("invalid version output %q", strings.TrimSpace(out)) - } - return semver{major: major, minor: minor, patch: patch}, nil -} - -func parseVersionPart(s string) (int, error) { - end := 0 - for end < len(s) && s[end] >= '0' && s[end] <= '9' { - end++ - } - if end == 0 { - return 0, errors.New("missing version number") - } - return strconv.Atoi(s[:end]) -} - -func compareVersion(a, b semver) int { - if a.major != b.major { - return a.major - b.major - } - if a.minor != b.minor { - return a.minor - b.minor - } - return a.patch - b.patch -} - -func sessionListedAlive(out, id string) bool { - for _, line := range strings.Split(out, "\n") { - line = strings.TrimSpace(line) - if line == "" { - continue - } - fields := strings.Fields(line) - if len(fields) == 0 || fields[0] != id { - continue - } - return !strings.Contains(line, "(EXITED") - } - return false -} - -type commandError struct { - err error - output string -} - -func (e commandError) Error() string { - if e.output == "" { - return e.err.Error() - } - return e.err.Error() + ": " + e.output -} - -func (e commandError) Unwrap() error { return e.err } diff --git a/backend/internal/adapters/runtime/zellij/zellij_integration_test.go b/backend/internal/adapters/runtime/zellij/zellij_integration_test.go deleted file mode 100644 index ac6a33e4..00000000 --- a/backend/internal/adapters/runtime/zellij/zellij_integration_test.go +++ /dev/null @@ -1,165 +0,0 @@ -package zellij - -import ( - "context" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - "testing" - "time" - - "github.com/aoagents/agent-orchestrator/backend/internal/ports" -) - -func TestRuntimeIntegration(t *testing.T) { - if _, err := exec.LookPath("zellij"); err != nil { - t.Skip("zellij unavailable") - } - - ctx := context.Background() - id := "ao_itest_zj" - socketDir := tempSocketDir(t, "ao-zj-itest-") - configDir := t.TempDir() - opts := Options{Timeout: 5 * time.Second, SocketDir: socketDir, ConfigDir: configDir} - if runtime.GOOS == "windows" { - opts.Timeout = 30 * time.Second - opts.LauncherBinary = buildAOForIntegration(t) - opts.Shell = "cmd.exe" - } - r := New(opts) - _ = r.Destroy(ctx, ports.RuntimeHandle{ID: id}) - argv := []string{"sh", "-lc", "printf ready-$AO_SESSION_ID\\n; exec sh -i"} - sendCommand := "echo hello-from-zellij" - if runtime.GOOS == "windows" { - argv = []string{"cmd.exe", "/D", "/Q", "/K", "echo ready-%AO_SESSION_ID%"} - } - - h, err := r.Create(ctx, ports.RuntimeConfig{ - SessionID: "ao_itest_zj", - WorkspacePath: t.TempDir(), - Argv: argv, - Env: map[string]string{"AO_SESSION_ID": id}, - }) - if err != nil { - t.Fatalf("Create: %v", err) - } - defer r.Destroy(ctx, h) - - alive, err := r.IsAlive(ctx, h) - if err != nil { - t.Fatalf("IsAlive: %v", err) - } - if !alive { - t.Fatal("alive = false, want true") - } - prefixAlive, err := r.IsAlive(ctx, ports.RuntimeHandle{ID: "ao_itest"}) - if err != nil { - t.Fatalf("IsAlive prefix: %v", err) - } - if prefixAlive { - t.Fatal("prefix handle reported alive; zellij session matching is not exact") - } - - out := waitForRuntimeOutput(t, r, h, "ready-") - if !strings.Contains(out, "ready-") { - t.Fatalf("output = %q, want ready output", out) - } - if err := r.SendMessage(ctx, h, sendCommand); err != nil { - t.Fatalf("SendMessage: %v", err) - } - out = waitForRuntimeOutput(t, r, h, "hello-from-zellij") - if !strings.Contains(out, "hello-from-zellij") { - t.Fatalf("output = %q, want sent command output", out) - } - - if err := r.Destroy(ctx, h); err != nil { - t.Fatalf("Destroy: %v", err) - } - alive, err = r.IsAlive(ctx, h) - if err != nil { - t.Fatalf("IsAlive after destroy: %v", err) - } - if alive { - t.Fatal("alive after destroy = true, want false") - } -} - -func waitForRuntimeOutput(t *testing.T, r *Runtime, h ports.RuntimeHandle, want string) string { - t.Helper() - deadline := time.Now().Add(3 * time.Second) - var out string - for time.Now().Before(deadline) { - var err error - out, err = r.GetOutput(context.Background(), h, 30) - if err != nil { - t.Fatalf("GetOutput: %v", err) - } - if strings.Contains(out, want) { - return out - } - time.Sleep(100 * time.Millisecond) - } - return out -} - -func buildAOForIntegration(t *testing.T) string { - t.Helper() - out := filepath.Join(t.TempDir(), "ao.exe") - cmd := exec.Command("go", "build", "-o", out, "./cmd/ao") - cmd.Dir = filepath.Clean(filepath.Join("..", "..", "..", "..")) - if raw, err := cmd.CombinedOutput(); err != nil { - t.Fatalf("build ao test launcher: %v: %s", err, strings.TrimSpace(string(raw))) - } - return out -} - -func tempSocketDir(t *testing.T, pattern string) string { - t.Helper() - parent := os.TempDir() - if runtime.GOOS != "windows" { - parent = "/tmp" - } - socketDir, err := os.MkdirTemp(parent, pattern) - if err != nil { - t.Fatalf("mkdir socket dir: %v", err) - } - t.Cleanup(func() { _ = os.RemoveAll(socketDir) }) - return socketDir -} - -func TestRuntimeIntegrationUsesExactSessionParsing(t *testing.T) { - if _, err := exec.LookPath("zellij"); err != nil { - t.Skip("zellij unavailable") - } - if runtime.GOOS == "windows" { - t.Skip("exact session parsing is covered by TestRuntimeIntegration on Windows") - } - - ctx := context.Background() - socketDir := tempSocketDir(t, "ao-zj-exact-itest-") - r := New(Options{Timeout: 5 * time.Second, SocketDir: socketDir, ConfigDir: t.TempDir()}) - longID := "ao_zj_exact_long" - prefixID := "ao_zj_exact" - _ = r.Destroy(ctx, ports.RuntimeHandle{ID: longID}) - _ = r.Destroy(ctx, ports.RuntimeHandle{ID: prefixID}) - - h, err := r.Create(ctx, ports.RuntimeConfig{ - SessionID: "ao_zj_exact_long", - WorkspacePath: t.TempDir(), - Argv: []string{"sh", "-lc", "printf ready\\n; exec sh -i"}, - }) - if err != nil { - t.Fatalf("Create: %v", err) - } - defer r.Destroy(ctx, h) - - alive, err := r.IsAlive(ctx, ports.RuntimeHandle{ID: prefixID}) - if err != nil { - t.Fatalf("IsAlive prefix: %v", err) - } - if alive { - t.Fatal("prefix handle reported alive; zellij session matching is not exact") - } -} diff --git a/backend/internal/adapters/runtime/zellij/zellij_test.go b/backend/internal/adapters/runtime/zellij/zellij_test.go deleted file mode 100644 index 657b575f..00000000 --- a/backend/internal/adapters/runtime/zellij/zellij_test.go +++ /dev/null @@ -1,734 +0,0 @@ -package zellij - -import ( - "context" - "errors" - "os/exec" - "reflect" - "runtime" - "strings" - "testing" - "time" - - "github.com/aoagents/agent-orchestrator/backend/internal/domain" - "github.com/aoagents/agent-orchestrator/backend/internal/ports" -) - -func TestNewDefaultsToPortableShell(t *testing.T) { - t.Setenv("SHELL", "") - r := New(Options{}) - want := "/bin/sh" - if runtime.GOOS == "windows" { - want = "powershell.exe" - } - if got := r.shell; got != want { - t.Fatalf("default shell = %q, want %q", got, want) - } -} - -func TestZellijCommandEnvNormalizesBrowserTerminalColors(t *testing.T) { - got := zellijCommandEnv( - []string{"NO_COLOR=1", "TERM=dumb", "COLORTERM=", "KEEP=yes"}, - []string{"ZELLIJ_SOCKET_DIR=/tmp/zj"}, - ) - - if runtime.GOOS == "windows" { - if !contains(got, "NO_COLOR=1") { - t.Fatalf("windows env = %#v, want NO_COLOR preserved", got) - } - return - } - - if containsKey(got, "NO_COLOR") { - t.Fatalf("NO_COLOR survived env normalization: %#v", got) - } - for _, want := range []string{"KEEP=yes", "TERM=xterm-256color", "COLORTERM=truecolor", "ZELLIJ_SOCKET_DIR=/tmp/zj"} { - if !contains(got, want) { - t.Fatalf("env missing %q in %#v", want, got) - } - } -} - -func expectedZellijEnv(socketDir string) []string { - env := []string{} - if runtime.GOOS != "windows" { - env = append(env, "TERM=xterm-256color", "COLORTERM=truecolor") - } - if socketDir != "" { - env = append(env, "ZELLIJ_SOCKET_DIR="+socketDir) - } - return env -} - -func expectedAttachEnvPrefix() []string { - if runtime.GOOS == "windows" { - return []string{} - } - return []string{"env", "-u", "NO_COLOR", "TERM=xterm-256color", "COLORTERM=truecolor"} -} - -func contains(values []string, want string) bool { - for _, value := range values { - if value == want { - return true - } - } - return false -} - -func containsKey(values []string, key string) bool { - prefix := key + "=" - for _, value := range values { - if strings.HasPrefix(value, prefix) { - return true - } - } - return false -} - -func TestCommandBuilders(t *testing.T) { - embeddedOptions := []string{ - "--pane-frames", "false", - "--mouse-mode", "true", - "--advanced-mouse-actions", "false", - "--mouse-hover-effects", "false", - "--focus-follows-mouse", "false", - "--mouse-click-through", "false", - "--support-kitty-keyboard-protocol", "false", - } - if got, want := versionArgs(), []string{"--version"}; !reflect.DeepEqual(got, want) { - t.Fatalf("versionArgs = %#v, want %#v", got, want) - } - wantCreate := append([]string{"attach", "--create-background", "sess-1", "options", "--default-layout", "/tmp/layout.kdl"}, embeddedOptions...) - wantCreate = append(wantCreate, "--session-serialization", "false", "--show-startup-tips", "false", "--show-release-notes", "false") - if got, want := createSessionArgs("sess-1", "/tmp/layout.kdl"), wantCreate; !reflect.DeepEqual(got, want) { - t.Fatalf("createSessionArgs = %#v, want %#v", got, want) - } - if got, want := listPanesArgs("sess-1"), []string{"--session", "sess-1", "action", "list-panes", "--all", "--json"}; !reflect.DeepEqual(got, want) { - t.Fatalf("listPanesArgs = %#v, want %#v", got, want) - } - pasteAction := "paste" - if runtime.GOOS == "windows" { - pasteAction = "write-chars" - } - if got, want := pasteArgs("sess-1", "terminal_0", "hello"), []string{"--session", "sess-1", "action", pasteAction, "--pane-id", "terminal_0", "hello"}; !reflect.DeepEqual(got, want) { - t.Fatalf("pasteArgs = %#v, want %#v", got, want) - } - if got, want := dumpScreenArgs("sess-1", "terminal_0"), []string{"--session", "sess-1", "action", "dump-screen", "--pane-id", "terminal_0", "--full"}; !reflect.DeepEqual(got, want) { - t.Fatalf("dumpScreenArgs = %#v, want %#v", got, want) - } - // delete-session --force (not kill-session): teardown must also purge the - // serialized resurrection state, or a later `zellij attach` re-creates the - // session — and re-runs its agent — after the daemon destroyed it. - if got, want := deleteSessionArgs("sess-1"), []string{"delete-session", "--force", "sess-1"}; !reflect.DeepEqual(got, want) { - t.Fatalf("deleteSessionArgs = %#v, want %#v", got, want) - } - wantAttach := append([]string{"attach", "sess-1", "options"}, embeddedOptions...) - if got, want := attachArgs("sess-1"), wantAttach; !reflect.DeepEqual(got, want) { - t.Fatalf("attachArgs = %#v, want %#v", got, want) - } -} - -func TestZellijSessionNameSanitizesIssueRefs(t *testing.T) { - got, err := zellijSessionName("repo/issue#42.1") - if err != nil { - t.Fatalf("zellijSessionName: %v", err) - } - if err := validateSessionID(got); err != nil { - t.Fatalf("sanitized id %q is invalid: %v", got, err) - } - if !strings.HasPrefix(got, "repo-issue-42-1-") { - t.Fatalf("sanitized id = %q, want readable prefix", got) - } - if got == "repo/issue#42.1" { - t.Fatal("sanitized id still contains raw unsafe characters") - } -} - -// SessionName must return the exact name Create registers a session under, so -// callers that print an attach hint (e.g. `ao spawn`) reference the real -// session. A short, conforming id passes through; a long one is sanitised to a -// different name — printing the raw id there would send users to a missing -// session. -func TestSessionNameMatchesCreateNaming(t *testing.T) { - short := "myproj-1" - if got := SessionName(short); got != short { - t.Fatalf("SessionName(%q) = %q, want it unchanged", short, got) - } - - long := domain.SessionID(strings.Repeat("x", 60) + "-1") - viaCreate, err := zellijSessionName(long) - if err != nil { - t.Fatalf("zellijSessionName: %v", err) - } - if got := SessionName(string(long)); got != viaCreate { - t.Fatalf("SessionName = %q, but Create uses %q", got, viaCreate) - } - if SessionName(string(long)) == string(long) { - t.Fatal("expected a long id to be sanitised to a different name") - } -} - -func TestValidateSessionAndPaneID(t *testing.T) { - for _, id := range []string{"sess-1", "S_2", "abc123"} { - if err := validateSessionID(id); err != nil { - t.Fatalf("validateSessionID(%q): %v", id, err) - } - } - for _, id := range []string{"", "sess.1", "sess/1", "$(boom)", "with space"} { - if err := validateSessionID(id); err == nil { - t.Fatalf("validateSessionID(%q): got nil, want error", id) - } - } - for _, id := range []string{"terminal_0", "terminal_42"} { - if err := validatePaneID(id); err != nil { - t.Fatalf("validatePaneID(%q): %v", id, err) - } - } - for _, id := range []string{"", "0", "plugin_0", "terminal_x", "terminal_1/2"} { - if err := validatePaneID(id); err == nil { - t.Fatalf("validatePaneID(%q): got nil, want error", id) - } - } -} - -func TestHandleID(t *testing.T) { - session, pane, err := handleID(ports.RuntimeHandle{ID: "sess-1/terminal_7"}) - if err != nil { - t.Fatalf("handleID: %v", err) - } - if session != "sess-1" || pane != "terminal_7" { - t.Fatalf("handleID = %q/%q", session, pane) - } -} - -func TestBuildLayoutExportsEnvAndRunsAgentCommand(t *testing.T) { - oldGetenv := getenv - getenv = func(key string) string { - if key == "PATH" { - return "/usr/bin:/bin" - } - return "" - } - defer func() { getenv = oldGetenv }() - - got := buildLayout(ports.RuntimeConfig{WorkspacePath: "/tmp/ws", Argv: []string{"ao", "run"}, Env: map[string]string{ - "AO_SESSION_ID": "sess-1", - "ODD": "can't", - "PATH": "/custom/bin:/usr/bin", - }}, "/bin/zsh") - if runtime.GOOS == "windows" { - for _, want := range []string{ - `cwd "/tmp/ws"`, - `pane command="ao" name="agent" borderless=true`, - `args "run"`, - } { - if !strings.Contains(got, want) { - t.Fatalf("direct windows layout missing %q in %q", want, got) - } - } - return - } - - for _, want := range []string{ - `cwd "/tmp/ws"`, - `pane command="/bin/zsh" name="agent" borderless=true`, - "export AO_SESSION_ID='sess-1';", - "export ODD='can'\\\\''t';", - "export PATH='/custom/bin:/usr/bin';", - "'ao' 'run'", - } { - if !strings.Contains(got, want) { - t.Fatalf("layout missing %q in %q", want, got) - } - } - if strings.Contains(got, "exec '/bin/zsh' -i") { - t.Fatalf("layout kept pane alive after agent exit: %q", got) - } -} - -func TestBuildLayoutUsesPowerShellLaunchOnWindowsShells(t *testing.T) { - oldGetenv := getenv - getenv = func(key string) string { - if key == "PATH" { - return `C:\custom\bin` - } - return "" - } - defer func() { getenv = oldGetenv }() - - got := buildLayout(ports.RuntimeConfig{WorkspacePath: `C:\ws`, Argv: []string{"Write-Host", "ready"}, Env: map[string]string{ - "AO_SESSION_ID": "sess-1", - }}, `C:\Program Files\PowerShell\7\pwsh.exe`) - if runtime.GOOS == "windows" { - for _, want := range []string{ - `cwd "C:\\ws"`, - `pane command="Write-Host" name="agent" borderless=true`, - `args "ready"`, - } { - if !strings.Contains(got, want) { - t.Fatalf("direct windows layout missing %q in %q", want, got) - } - } - return - } - - for _, want := range []string{ - `pane command="C:\\Program Files\\PowerShell\\7\\pwsh.exe" name="agent" borderless=true`, - `args "-NoLogo" "-NoProfile" "-NoExit" "-EncodedCommand"`, - } { - if !strings.Contains(got, want) { - t.Fatalf("powershell layout missing %q in %q", want, got) - } - } -} - -func TestBuildLayoutUsesCmdLaunchOnCmdShells(t *testing.T) { - oldGetenv := getenv - getenv = func(key string) string { - return "" - } - defer func() { getenv = oldGetenv }() - - got := buildLayout(ports.RuntimeConfig{WorkspacePath: `C:\ws`, Argv: []string{"echo", "ready"}, Env: map[string]string{ - "AO_SESSION_ID": "sess-1", - }}, `C:\Windows\System32\cmd.exe`) - if runtime.GOOS == "windows" { - for _, want := range []string{ - `cwd "C:\\ws"`, - `pane command="echo" name="agent" borderless=true`, - `args "ready"`, - } { - if !strings.Contains(got, want) { - t.Fatalf("direct windows layout missing %q in %q", want, got) - } - } - return - } - - for _, want := range []string{ - `pane command="C:\\Windows\\System32\\cmd.exe" name="agent" borderless=true`, - `args "/D" "/S" "/K"`, - `AO_SESSION_ID=sess-1`, - `\"echo\" \"ready\"`, - } { - if !strings.Contains(got, want) { - t.Fatalf("cmd layout missing %q in %q", want, got) - } - } -} - -func TestCreateRejectsInvalidEnvKeys(t *testing.T) { - r := New(Options{Binary: "zellij-test", Timeout: time.Second, Shell: "/bin/zsh"}) - r.runner = &fakeRunner{} - _, err := r.Create(context.Background(), ports.RuntimeConfig{ - SessionID: "sess-1", - WorkspacePath: "/tmp/ws", - Argv: []string{"echo", "ready"}, - Env: map[string]string{"BAD KEY": "x"}, - }) - if err == nil || !strings.Contains(err.Error(), "invalid env key") { - t.Fatalf("Create err = %v, want invalid env key", err) - } -} - -func TestCreateStartsSessionAndDiscoversPane(t *testing.T) { - panesOut := []byte(`[{"id":0,"is_plugin":true,"title":"zellij:tab-bar"},{"id":3,"is_plugin":false,"title":"agent"}]`) - outputs := [][]byte{ - []byte("zellij 0.44.3"), - nil, - nil, - panesOut, - } - if runtime.GOOS == "windows" { - outputs = append(outputs, panesOut) - } - outputs = append(outputs, []byte("sess-1 [Created 1s ago] \n")) - fr := &fakeRunner{outputs: outputs} - r := New(Options{Binary: "zellij-test", Timeout: time.Second, Shell: "/bin/zsh", SocketDir: "/tmp/zj", ConfigDir: "/tmp/cfg"}) - r.runner = fr - - handle, err := r.Create(context.Background(), ports.RuntimeConfig{ - SessionID: "sess-1", - WorkspacePath: "/tmp/ws", - Argv: []string{"echo", "ready"}, - Env: map[string]string{"AO_SESSION_ID": "sess-1"}, - }) - if err != nil { - t.Fatalf("Create: %v", err) - } - if handle != (ports.RuntimeHandle{ID: "sess-1/terminal_3"}) { - t.Fatalf("handle = %+v, want zellij handle", handle) - } - wantCalls := 5 - if runtime.GOOS == "windows" { - wantCalls = 6 - } - if len(fr.calls) != wantCalls { - t.Fatalf("calls = %d, want %d", len(fr.calls), wantCalls) - } - if got, want := fr.calls[0].args, []string{"--config-dir", "/tmp/cfg", "--version"}; !reflect.DeepEqual(got, want) { - t.Fatalf("version args = %#v, want %#v", got, want) - } - if got, want := fr.calls[1].args, []string{"--config-dir", "/tmp/cfg", "delete-session", "--force", "sess-1"}; !reflect.DeepEqual(got, want) { - t.Fatalf("delete args = %#v, want %#v", got, want) - } - if got := fr.calls[2].args[:5]; !reflect.DeepEqual(got, []string{"--config-dir", "/tmp/cfg", "attach", "--create-background", "sess-1"}) { - t.Fatalf("create args prefix = %#v", got) - } - if got := fr.calls[3].args; !reflect.DeepEqual(got, append([]string{"--config-dir", "/tmp/cfg"}, listPanesArgs("sess-1")...)) { - t.Fatalf("list panes args = %#v", got) - } - listSessionsCall := 4 - if runtime.GOOS == "windows" { - if got := fr.calls[4].args; !reflect.DeepEqual(got, append([]string{"--config-dir", "/tmp/cfg"}, listPanesArgs("sess-1")...)) { - t.Fatalf("ready list panes args = %#v", got) - } - listSessionsCall = 5 - } - if got := fr.calls[listSessionsCall].args; !reflect.DeepEqual(got, append([]string{"--config-dir", "/tmp/cfg"}, listSessionsArgs()...)) { - t.Fatalf("list sessions args = %#v", got) - } - if got, want := fr.calls[0].env, expectedZellijEnv("/tmp/zj"); !reflect.DeepEqual(got, want) { - t.Fatalf("env = %#v, want %#v", got, want) - } -} - -func TestCreateClearsStaleSessionBeforeCreating(t *testing.T) { - panesOut := []byte(`[{"id":1,"is_plugin":false,"title":"agent"}]`) - outputs := [][]byte{ - []byte("zellij 0.44.3"), - nil, - nil, - panesOut, - } - if runtime.GOOS == "windows" { - outputs = append(outputs, panesOut) - } - outputs = append(outputs, []byte("sess-1 [Created 1s ago] \n")) - fr := &fakeRunner{outputs: outputs} - r := New(Options{Binary: "zellij-test", Timeout: time.Second, Shell: "/bin/zsh"}) - r.runner = fr - - if _, err := r.Create(context.Background(), ports.RuntimeConfig{ - SessionID: "sess-1", - WorkspacePath: "/tmp/ws", - Argv: []string{"echo", "ready"}, - }); err != nil { - t.Fatalf("Create: %v", err) - } - - wantCalls := 5 - if runtime.GOOS == "windows" { - wantCalls = 6 - } - if len(fr.calls) != wantCalls { - t.Fatalf("calls = %d, want %d", len(fr.calls), wantCalls) - } - if got, want := fr.calls[1].args, []string{"delete-session", "--force", "sess-1"}; !reflect.DeepEqual(got, want) { - t.Fatalf("delete args = %#v, want %#v", got, want) - } - if got := fr.calls[2].args[:3]; !reflect.DeepEqual(got, []string{"attach", "--create-background", "sess-1"}) { - t.Fatalf("create args prefix = %#v", got) - } -} - -func TestAttachCommandUsesEmbeddedClientOptions(t *testing.T) { - r := New(Options{}) - args, _, err := r.AttachCommand(ports.RuntimeHandle{ID: "sess-1/terminal_0"}) - if err != nil { - t.Fatalf("AttachCommand: %v", err) - } - embeddedOptions := []string{ - "--pane-frames", "false", - "--mouse-mode", "true", - "--advanced-mouse-actions", "false", - "--mouse-hover-effects", "false", - "--focus-follows-mouse", "false", - "--mouse-click-through", "false", - "--support-kitty-keyboard-protocol", "false", - } - if runtime.GOOS == "windows" { - joined := strings.Join(args, " ") - for _, want := range embeddedOptions { - if !strings.Contains(joined, want) { - t.Fatalf("windows attach command missing %q: %#v", want, args) - } - } - return - } - want := append(expectedAttachEnvPrefix(), r.binary, "attach", "sess-1", "options") - want = append(want, embeddedOptions...) - if !reflect.DeepEqual(args, want) { - t.Fatalf("AttachCommand = %#v, want %#v", args, want) - } -} - -func TestAttachCommandUsesSocketDir(t *testing.T) { - r := New(Options{SocketDir: "/tmp/zj"}) - args, _, err := r.AttachCommand(ports.RuntimeHandle{ID: "sess-1/terminal_0"}) - if err != nil { - t.Fatalf("AttachCommand: %v", err) - } - if runtime.GOOS == "windows" { - if got, want := args[0], r.binary; got != want { - t.Fatalf("attach binary = %q, want %q", got, want) - } - return - } - if got, want := args[:6], []string{"env", "-u", "NO_COLOR", "TERM=xterm-256color", "COLORTERM=truecolor", "ZELLIJ_SOCKET_DIR=/tmp/zj"}; !reflect.DeepEqual(got, want) { - t.Fatalf("attach prefix = %#v, want %#v", got, want) - } - if got, want := args[6], r.binary; got != want { - t.Fatalf("attach binary = %q, want %q", got, want) - } -} - -func TestFindAgentPaneRetriesTransientErrors(t *testing.T) { - fr := &fakeRunner{outputs: [][]byte{[]byte("boom"), []byte(`[{"id":0,"is_plugin":false,"title":"agent"}]`)}} - r := New(Options{Timeout: time.Second}) - r.runner = fr - - got, err := r.findAgentPane(context.Background(), "sess-1") - if err != nil { - t.Fatalf("findAgentPane: %v", err) - } - if got != "terminal_0" { - t.Fatalf("findAgentPane = %q, want terminal_0", got) - } - if len(fr.calls) != 2 { - t.Fatalf("calls = %d, want 2", len(fr.calls)) - } -} - -func TestParseVersion(t *testing.T) { - for _, tc := range []struct { - in string - want semver - }{ - {in: "zellij 0.44.3", want: semver{0, 44, 3}}, - {in: "zellij v1.2.3\n", want: semver{1, 2, 3}}, - {in: "zellij 0.44.3-dev", want: semver{0, 44, 3}}, - } { - got, err := parseVersion(tc.in) - if err != nil { - t.Fatalf("parseVersion(%q): %v", tc.in, err) - } - if got != tc.want { - t.Fatalf("parseVersion(%q) = %v, want %v", tc.in, got, tc.want) - } - } - if _, err := parseVersion("zellij nope"); err == nil { - t.Fatal("parseVersion invalid: got nil, want error") - } - if compareVersion(semver{0, 44, 2}, semver{0, 44, 3}) >= 0 { - t.Fatal("compareVersion should order 0.44.2 before 0.44.3") - } - if got := RequiredVersion(); got != "0.44.3" { - t.Fatalf("RequiredVersion = %q, want 0.44.3", got) - } - if got, err := CheckVersionOutput("zellij 0.44.3"); err != nil || got != "0.44.3" { - t.Fatalf("CheckVersionOutput supported = %q, %v", got, err) - } - if _, err := CheckVersionOutput("zellij 0.44.2"); err == nil { - t.Fatal("CheckVersionOutput unsupported: got nil error") - } -} - -func TestSendMessageChunksAndSendsEnter(t *testing.T) { - fr := &fakeRunner{} - r := New(Options{Timeout: time.Second, ChunkSize: 5}) - r.runner = fr - - if err := r.SendMessage(context.Background(), ports.RuntimeHandle{ID: "sess-1/terminal_0"}, "hello世界"); err != nil { - t.Fatalf("SendMessage: %v", err) - } - if len(fr.calls) != 4 { - t.Fatalf("calls = %d, want 4", len(fr.calls)) - } - if got, want := fr.calls[0].args, pasteArgs("sess-1", "terminal_0", "hello"); !reflect.DeepEqual(got, want) { - t.Fatalf("paste 1 args = %#v, want %#v", got, want) - } - if got, want := fr.calls[1].args, pasteArgs("sess-1", "terminal_0", "世"); !reflect.DeepEqual(got, want) { - t.Fatalf("paste 2 args = %#v, want %#v", got, want) - } - if got, want := fr.calls[2].args, pasteArgs("sess-1", "terminal_0", "界"); !reflect.DeepEqual(got, want) { - t.Fatalf("paste 3 args = %#v, want %#v", got, want) - } - if got, want := fr.calls[3].args, sendEnterArgs("sess-1", "terminal_0"); !reflect.DeepEqual(got, want) { - t.Fatalf("enter args = %#v, want %#v", got, want) - } -} - -func TestGetOutputTrimsLines(t *testing.T) { - fr := &fakeRunner{outputs: [][]byte{[]byte("one\ntwo\nthree\n")}} - r := New(Options{Timeout: time.Second}) - r.runner = fr - - out, err := r.GetOutput(context.Background(), ports.RuntimeHandle{ID: "sess-1/terminal_0"}, 2) - if err != nil { - t.Fatalf("GetOutput: %v", err) - } - if out != "two\nthree\n" { - t.Fatalf("output = %q, want last two lines", out) - } -} - -func TestGetOutputTrimsTrailingScreenPaddingBeforeTailing(t *testing.T) { - fr := &fakeRunner{outputs: [][]byte{[]byte("ready\nprompt> echo hi\nhi\n\n\n\n")}} - r := New(Options{Timeout: time.Second}) - r.runner = fr - - out, err := r.GetOutput(context.Background(), ports.RuntimeHandle{ID: "sess-1/terminal_0"}, 2) - if err != nil { - t.Fatalf("GetOutput: %v", err) - } - if out != "prompt> echo hi\nhi\n" { - t.Fatalf("output = %q, want last non-padding lines", out) - } -} - -func TestIsAliveParsesNoFormattingOutput(t *testing.T) { - fr := &fakeRunner{outputs: [][]byte{[]byte("sess-1 [Created 1s ago] \nold [Created 2s ago] (EXITED - attach to resurrect)\n")}} - r := New(Options{Timeout: time.Second}) - r.runner = fr - - alive, err := r.IsAlive(context.Background(), ports.RuntimeHandle{ID: "sess-1/terminal_0"}) - if err != nil { - t.Fatalf("IsAlive: %v", err) - } - if !alive { - t.Fatal("alive = false, want true") - } - if sessionListedAlive("sess-1-long [Created 1s ago]", "sess-1") { - t.Fatal("prefix matched as alive") - } - if sessionListedAlive("sess-1 [Created 1s ago] (EXITED - attach to resurrect)", "sess-1") { - t.Fatal("exited session matched as alive") - } -} - -// IsAlive may treat a non-zero list-sessions ONLY as "not alive" when zellij -// says no sessions exist at all. Any other exit failure is a probe error: the -// reaper reports it as a failed probe and the LCM must never read it as death. -func TestIsAliveTreatsNoSessionsExitAsNotAlive(t *testing.T) { - fr := &fakeRunner{outputs: [][]byte{[]byte("No active zellij sessions found.")}, err: &exec.ExitError{}} - r := New(Options{Timeout: time.Second}) - r.runner = fr - - alive, err := r.IsAlive(context.Background(), ports.RuntimeHandle{ID: "sess-1/terminal_0"}) - if err != nil { - t.Fatalf("IsAlive: %v", err) - } - if alive { - t.Fatal("alive = true, want false") - } -} - -func TestIsAliveReportsOtherExitFailuresAsProbeErrors(t *testing.T) { - fr := &fakeRunner{outputs: [][]byte{[]byte("thread 'main' panicked")}, err: &exec.ExitError{}} - r := New(Options{Timeout: time.Second}) - r.runner = fr - - alive, err := r.IsAlive(context.Background(), ports.RuntimeHandle{ID: "sess-1/terminal_0"}) - if err == nil { - t.Fatal("IsAlive: got nil error, want probe failure — a failed probe must not read as dead") - } - if alive { - t.Fatal("alive = true on probe failure") - } -} - -func TestDestroyIsIdempotentWhenSessionMissing(t *testing.T) { - fr := &fakeRunner{outputs: [][]byte{[]byte("No active zellij sessions found.")}, err: &exec.ExitError{}} - r := New(Options{Timeout: time.Second}) - r.runner = fr - - if err := r.Destroy(context.Background(), ports.RuntimeHandle{ID: "sess-1/terminal_0"}); err != nil { - t.Fatalf("Destroy: %v", err) - } - if len(fr.calls) != 1 || fr.calls[0].args[0] != "delete-session" { - t.Fatalf("calls = %#v, want only delete-session", fr.calls) - } -} - -func TestDestroyReportsUnexpectedExitFailures(t *testing.T) { - fr := &fakeRunner{outputs: [][]byte{[]byte("permission denied")}, err: &exec.ExitError{}} - r := New(Options{Timeout: time.Second}) - r.runner = fr - - if err := r.Destroy(context.Background(), ports.RuntimeHandle{ID: "sess-1/terminal_0"}); err == nil { - t.Fatal("Destroy: got nil, want unexpected delete-session failure") - } -} - -// Destroy must delete the session's serialized state, not merely kill it: a -// killed-but-cached session is resurrected (agent re-run included) by any later -// `zellij attach`, bringing a terminated session's runtime back to life. -func TestDestroyForceDeletesSerializedSession(t *testing.T) { - fr := &fakeRunner{} - r := New(Options{Timeout: time.Second}) - r.runner = fr - - if err := r.Destroy(context.Background(), ports.RuntimeHandle{ID: "sess-1/terminal_0"}); err != nil { - t.Fatalf("Destroy: %v", err) - } - if len(fr.calls) != 1 { - t.Fatalf("calls = %d, want 1", len(fr.calls)) - } - if got, want := fr.calls[0].args, []string{"delete-session", "--force", "sess-1"}; !reflect.DeepEqual(got, want) { - t.Fatalf("destroy args = %#v, want %#v", got, want) - } -} - -func TestGetOutputValidatesLines(t *testing.T) { - r := New(Options{Timeout: time.Second}) - _, err := r.GetOutput(context.Background(), ports.RuntimeHandle{ID: "sess-1/terminal_0"}, 0) - if err == nil { - t.Fatal("GetOutput lines=0: got nil, want error") - } -} - -type fakeRunner struct { - calls []runnerCall - outputs [][]byte - err error -} - -type runnerCall struct { - env []string - name string - args []string -} - -func (f *fakeRunner) Run(_ context.Context, env []string, name string, args ...string) ([]byte, error) { - f.calls = append(f.calls, runnerCall{env: append([]string(nil), env...), name: name, args: append([]string(nil), args...)}) - var out []byte - if len(f.outputs) > 0 { - out = f.outputs[0] - f.outputs = f.outputs[1:] - } - if f.err != nil { - return out, f.err - } - return out, nil -} - -func (f *fakeRunner) Start(env []string, name string, args ...string) error { - f.calls = append(f.calls, runnerCall{env: append([]string(nil), env...), name: name, args: append([]string(nil), args...)}) - if len(f.outputs) > 0 { - f.outputs = f.outputs[1:] - } - return f.err -} - -func TestCommandErrorUnwraps(t *testing.T) { - base := errors.New("base") - err := commandError{err: base, output: "details"} - if !errors.Is(err, base) { - t.Fatal("commandError should unwrap base error") - } - if !strings.Contains(err.Error(), "details") { - t.Fatalf("error = %q, want output details", err.Error()) - } -} diff --git a/backend/internal/adapters/workspace/gitworktree/commands.go b/backend/internal/adapters/workspace/gitworktree/commands.go index 287bc661..cc0339bf 100644 --- a/backend/internal/adapters/workspace/gitworktree/commands.go +++ b/backend/internal/adapters/workspace/gitworktree/commands.go @@ -27,6 +27,14 @@ func worktreeRemoveArgs(repo, path string) []string { return []string{"-C", repo, "worktree", "remove", path} } +// worktreeForceRemoveArgs passes --force to bypass git's dirty-worktree check. +// Only ForceDestroy may call this. It is safe only AFTER the session's +// uncommitted work has been captured (Task 2's StashUncommitted). Callers that +// have not yet captured work must use worktreeRemoveArgs / Destroy instead. +func worktreeForceRemoveArgs(repo, path string) []string { + return []string{"-C", repo, "worktree", "remove", "--force", path} +} + func worktreePruneArgs(repo string) []string { return []string{"-C", repo, "worktree", "prune"} } @@ -42,6 +50,63 @@ func worktreeListPorcelainArgs(repo string) []string { return []string{"-C", repo, "worktree", "list", "--porcelain"} } +// addAllTempIndexArgs stages all tracked and non-ignored untracked files into a +// temp index file without touching the real index or the working tree. +// GIT_INDEX_FILE must be set in the command's environment before calling. +func addAllTempIndexArgs(worktree string) []string { + return []string{"-C", worktree, "add", "-A"} +} + +// writeTreeArgs flushes the temp index into a tree object and prints the SHA. +// GIT_INDEX_FILE must be set in the command's environment. +func writeTreeArgs(worktree string) []string { + return []string{"-C", worktree, "write-tree"} +} + +// commitTreeArgs creates a commit object from a tree SHA. parent is the HEAD +// SHA to set as parent; message is the commit message. When parent is empty +// (unborn HEAD), the -p flag is omitted. +func commitTreeArgs(worktree, treeSHA, parent, message string) []string { + args := []string{"-C", worktree, "commit-tree", treeSHA} + if parent != "" { + args = append(args, "-p", parent) + } + args = append(args, "-m", message) + return args +} + +// updateRefArgs creates or moves a ref to point at a commit SHA. +func updateRefArgs(worktree, ref, commitSHA string) []string { + return []string{"-C", worktree, "update-ref", ref, commitSHA} +} + +// deleteRefArgs deletes a ref unconditionally. +func deleteRefArgs(worktree, ref string) []string { + return []string{"-C", worktree, "update-ref", "-d", ref} +} + +// revParseHeadArgs returns the HEAD commit SHA in the worktree. +// Exit code 128 means the repo has no commits (unborn HEAD). +func revParseHeadArgs(worktree string) []string { + return []string{"-C", worktree, "rev-parse", "--verify", "HEAD"} +} + +// cherryPickNoCommitArgs applies a single commit's diff onto the current +// working tree via a true three-way merge without committing or moving HEAD. +// git cherry-pick --no-commit computes the diff between and its parent +// and 3-way-merges it onto the current working tree. On conflict it leaves +// textual conflict markers in the affected files and exits non-zero. New files +// added in the preserve commit come through as additions. Because -n is used, +// no sequencer state is left that would require a cherry-pick --quit afterward. +func cherryPickNoCommitArgs(worktree, commitSHA string) []string { + return []string{"-C", worktree, "cherry-pick", "--no-commit", commitSHA} +} + +// ignoredCountArgs lists files skipped because of .gitignore (dry-run, no mutation). +func ignoredCountArgs(worktree string) []string { + return []string{"-C", worktree, "status", "--ignored", "--porcelain"} +} + func baseRefCandidates(branch, defaultBranch string) []string { candidates := []string{"origin/" + branch} if strings.Contains(defaultBranch, "/") { diff --git a/backend/internal/adapters/workspace/gitworktree/workspace.go b/backend/internal/adapters/workspace/gitworktree/workspace.go index d42b3363..058bdcf3 100644 --- a/backend/internal/adapters/workspace/gitworktree/workspace.go +++ b/backend/internal/adapters/workspace/gitworktree/workspace.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "log/slog" "os" "os/exec" "path/filepath" @@ -26,6 +27,11 @@ var ( ErrUnsafePath = errors.New("gitworktree: unsafe workspace path") ) +// ErrPreservedConflict is an adapter-local alias of ports.ErrPreservedConflict. +// Tests inside this package use this name; callers outside use ports.ErrPreservedConflict +// and errors.Is works because the adapter wraps the ports sentinel. +var ErrPreservedConflict = ports.ErrPreservedConflict + // ErrBranchCheckedOutElsewhere and ErrBranchNotFetched are adapter-local aliases // of the port-level sentinels: they preserve the gitworktree-prefixed message // while letting the service layer match on ports.ErrWorkspaceBranchCheckedOutElsewhere @@ -185,6 +191,225 @@ func (w *Workspace) Destroy(ctx context.Context, info ports.WorkspaceInfo) error return nil } +// ForceDestroy removes the session's worktree unconditionally (--force), prunes +// it from git's worktree list, and falls back to os.RemoveAll if any filesystem +// residue remains. +// +// ponytail: only safe to call AFTER the session's uncommitted work has been +// captured via StashUncommitted. Calling it before capture silently +// discards agent work. For interactive teardown (ao session kill, ao cleanup) +// use Destroy, which refuses dirty worktrees via ErrWorkspaceDirty. +func (w *Workspace) ForceDestroy(ctx context.Context, info ports.WorkspaceInfo) error { + if info.ProjectID == "" { + return errors.New("gitworktree: project id is required") + } + if info.Path == "" { + return fmt.Errorf("%w: empty path", ErrUnsafePath) + } + repo, err := w.repoPath(info.ProjectID) + if err != nil { + return err + } + path, err := w.validateManagedPath(info.Path) + if err != nil { + return err + } + // --force bypasses git's dirty check; errors here are advisory (the path may + // already be gone). We proceed to prune regardless. + _, _ = w.run(ctx, w.binary, worktreeForceRemoveArgs(repo, path)...) + if _, err := w.run(ctx, w.binary, worktreePruneArgs(repo)...); err != nil { + return fmt.Errorf("gitworktree: worktree prune: %w", err) + } + // os.RemoveAll as a backstop: cleans up filesystem residue left behind if + // git worktree remove --force still left the directory (e.g. files outside + // git tracking). + if err := os.RemoveAll(path); err != nil { + return fmt.Errorf("gitworktree: force remove path %q: %w", path, err) + } + return nil +} + +// StashUncommitted captures all uncommitted work in the session's worktree +// into a git commit object WITHOUT mutating the working tree or the global +// stash stack. The commit is stored at refs/ao/preserved/. +// +// It builds the preserve commit through a temporary index file so tracked +// edits AND new non-ignored files are captured while .gitignore-d files are +// silently skipped (honoured because we never pass -f/--force to git-add). +// +// Returns the full ref name (e.g. "refs/ao/preserved/sess-1"). Returns an +// empty string (and no error) if the worktree is clean. +func (w *Workspace) StashUncommitted(ctx context.Context, info ports.WorkspaceInfo) (string, error) { + if info.Path == "" { + return "", fmt.Errorf("%w: empty path", ErrUnsafePath) + } + if info.SessionID == "" { + return "", errors.New("gitworktree: session id is required for StashUncommitted") + } + + // Early exit for clean worktrees: nothing to preserve. + dirty, err := w.isDirty(ctx, info.Path) + if err != nil { + return "", fmt.Errorf("gitworktree: StashUncommitted dirty check: %w", err) + } + if !dirty { + return "", nil + } + + // Log the count of ignored paths that will be skipped. + if skipCount, err := w.countIgnoredPaths(ctx, info.Path); err == nil { + slog.InfoContext(ctx, "gitworktree: StashUncommitted skipping ignored paths", + "session", string(info.SessionID), + "skipped_count", skipCount, + ) + } + + // Reserve a unique path for the temp index in the system temp dir (not ~/.ao). + // We must NOT pre-create the file: git requires GIT_INDEX_FILE to either not + // exist (it creates it) or be a valid git index. os.CreateTemp gives us a + // unique name; we close and remove it immediately so git gets an absent path. + tmpIdx, err := os.CreateTemp("", "ao-preserve-idx-*") + if err != nil { + return "", fmt.Errorf("gitworktree: reserve temp index path: %w", err) + } + tmpIdxPath := tmpIdx.Name() + _ = tmpIdx.Close() + // Remove now so git sees an absent path (not a 0-byte corrupt index). + _ = os.Remove(tmpIdxPath) + // Deferred remove is a best-effort cleanup in case git leaves the file. + defer func() { _ = os.Remove(tmpIdxPath) }() + + // Stage all tracked and non-ignored untracked files into the temp index. + // GIT_INDEX_FILE overrides the index so the real index is never touched. + addCmd := exec.CommandContext(ctx, w.binary, addAllTempIndexArgs(info.Path)...) + addCmd.Env = append(os.Environ(), "GIT_INDEX_FILE="+tmpIdxPath) + if out, err := addCmd.CombinedOutput(); err != nil { + return "", commandError{args: append([]string{w.binary}, addAllTempIndexArgs(info.Path)...), output: string(out), err: err} + } + + // Write the staged tree to get a tree SHA. + writeTreeCmd := exec.CommandContext(ctx, w.binary, writeTreeArgs(info.Path)...) + writeTreeCmd.Env = append(os.Environ(), "GIT_INDEX_FILE="+tmpIdxPath) + treeOut, err := writeTreeCmd.CombinedOutput() + if err != nil { + return "", commandError{args: append([]string{w.binary}, writeTreeArgs(info.Path)...), output: string(treeOut), err: err} + } + treeSHA := strings.TrimSpace(string(treeOut)) + + // Resolve HEAD. An unborn HEAD (no commits yet) means we omit the -p flag + // from commit-tree so the preserve commit has no parent. + headOut, headErr := w.run(ctx, w.binary, revParseHeadArgs(info.Path)...) + headSHA := "" + if headErr == nil { + headSHA = strings.TrimSpace(string(headOut)) + } + // headErr != nil means unborn HEAD: headSHA stays empty, commit-tree gets no -p. + + // If the preserve tree SHA equals HEAD's tree SHA the working tree is + // effectively clean from git's perspective (only ignored files differ). + if headSHA != "" { + headTreeOut, err := w.run(ctx, w.binary, "-C", info.Path, "rev-parse", headSHA+"^{tree}") + if err == nil { + headTreeSHA := strings.TrimSpace(string(headTreeOut)) + if headTreeSHA == treeSHA { + // Nothing to preserve beyond ignored files. + return "", nil + } + } + } + + // Create a commit object that wraps the preserve tree. + msg := "ao preserved " + string(info.SessionID) + commitOut, err := w.run(ctx, w.binary, commitTreeArgs(info.Path, treeSHA, headSHA, msg)...) + if err != nil { + return "", fmt.Errorf("gitworktree: commit-tree: %w", err) + } + commitSHA := strings.TrimSpace(string(commitOut)) + + // Point the preserve ref at the commit. + ref := "refs/ao/preserved/" + string(info.SessionID) + if _, err := w.run(ctx, w.binary, updateRefArgs(info.Path, ref, commitSHA)...); err != nil { + return "", fmt.Errorf("gitworktree: update-ref %q: %w", ref, err) + } + return ref, nil +} + +// countIgnoredPaths returns the number of entries listed by +// "git status --ignored --porcelain" that start with "!!" (ignored). +func (w *Workspace) countIgnoredPaths(ctx context.Context, worktree string) (int, error) { + out, err := w.run(ctx, w.binary, ignoredCountArgs(worktree)...) + if err != nil { + return 0, fmt.Errorf("gitworktree: count ignored: %w", err) + } + count := 0 + for _, line := range strings.Split(string(out), "\n") { + if strings.HasPrefix(line, "!! ") { + count++ + } + } + return count, nil +} + +// ApplyPreserved replays the capture created by StashUncommitted onto the +// (freshly re-added) worktree using a true three-way merge (cherry-pick --no-commit). +// On clean success, the preserve ref is deleted. +// On conflict, the ref is kept, conflict markers are left in the affected files, +// and ErrPreservedConflict (wrapped) is returned so the caller can surface it. +// +// NEVER deletes the preserve ref on a failed or conflicted apply. +func (w *Workspace) ApplyPreserved(ctx context.Context, info ports.WorkspaceInfo, ref string) error { + if info.Path == "" { + return fmt.Errorf("%w: empty path", ErrUnsafePath) + } + if ref == "" { + return errors.New("gitworktree: ApplyPreserved: ref must not be empty") + } + + // Resolve the ref to its commit SHA. + resolveOut, err := w.run(ctx, w.binary, revParseVerifyArgs(info.Path, ref)...) + if err != nil { + return fmt.Errorf("gitworktree: ApplyPreserved resolve ref %q: %w", ref, err) + } + commitSHA := strings.TrimSpace(string(resolveOut)) + + // Apply the preserve commit via "git cherry-pick --no-commit ". + // cherry-pick computes the diff between the preserve commit and its parent + // (the HEAD at save time) and 3-way-merges it onto the current working tree. + // On conflict it leaves textual conflict markers in the affected files and + // exits non-zero WITHOUT committing or moving HEAD. Conflict detection uses + // the exit code only (not output text) to stay locale-independent. + applyErr := w.runCherryPickNoCommit(ctx, info.Path, commitSHA) + if applyErr != nil { + // Any non-zero exit from the merge step is a conflict: keep the ref, + // leave conflict markers in place, and surface the sentinel. + return fmt.Errorf("%w: %w", ErrPreservedConflict, applyErr) + } + + // Clean apply: remove the preserve ref so it is never replayed twice. + if _, err := w.run(ctx, w.binary, deleteRefArgs(info.Path, ref)...); err != nil { + // Log but do not fail: the work is already applied. A dangling preserve + // ref is harmless; the next StashUncommitted will overwrite it. + slog.WarnContext(ctx, "gitworktree: ApplyPreserved could not delete preserve ref", + "ref", ref, + "err", err, + ) + } + return nil +} + +// runCherryPickNoCommit runs "git -C cherry-pick --no-commit " +// and captures combined output so any conflict details are available in the +// returned commandError. Exit code detection happens in the caller. +func (w *Workspace) runCherryPickNoCommit(ctx context.Context, worktree, commitSHA string) error { + args := cherryPickNoCommitArgs(worktree, commitSHA) + cmd := exec.CommandContext(ctx, w.binary, args...) + out, err := cmd.CombinedOutput() + if err != nil { + return commandError{args: append([]string{w.binary}, args...), output: string(out), err: err} + } + return nil +} + // Restore re-attaches to an existing worktree for the session if one is still // present, recreating the handle without disturbing its contents. func (w *Workspace) Restore(ctx context.Context, cfg ports.WorkspaceConfig) (ports.WorkspaceInfo, error) { diff --git a/backend/internal/adapters/workspace/gitworktree/workspace_forcedestroy_test.go b/backend/internal/adapters/workspace/gitworktree/workspace_forcedestroy_test.go new file mode 100644 index 00000000..d2b20a4d --- /dev/null +++ b/backend/internal/adapters/workspace/gitworktree/workspace_forcedestroy_test.go @@ -0,0 +1,96 @@ +package gitworktree + +import ( + "context" + "errors" + "os" + "path/filepath" + "testing" + + "github.com/aoagents/agent-orchestrator/backend/internal/ports" +) + +// TestWorkspaceIntegrationForceDestroyDirtyWorktree is the RED/GREEN test for +// ForceDestroy. It creates a real git worktree, dirties it with an uncommitted +// file (which normal Destroy refuses via ErrWorkspaceDirty), then calls +// ForceDestroy and asserts: +// +// (a) the worktree path no longer exists on disk. +// (b) the worktree is deregistered from `git worktree list`. +func TestWorkspaceIntegrationForceDestroyDirtyWorktree(t *testing.T) { + git := requireGit(t) + tmp := t.TempDir() + repo := setupOriginClone(t, git, tmp) + root := filepath.Join(tmp, "managed") + ws, err := New(Options{Binary: git, ManagedRoot: root, RepoResolver: StaticRepoResolver{"proj": repo}}) + if err != nil { + t.Fatalf("new: %v", err) + } + ctx := context.Background() + cfg := ports.WorkspaceConfig{ProjectID: "proj", SessionID: "sess-fd", Branch: "feature/force-destroy"} + + info, err := ws.Create(ctx, cfg) + if err != nil { + t.Fatalf("create: %v", err) + } + + // Dirty the worktree with uncommitted work: normal Destroy must refuse this. + wip := filepath.Join(info.Path, "wip.txt") + if err := os.WriteFile(wip, []byte("uncommitted work\n"), 0o600); err != nil { + t.Fatalf("write wip: %v", err) + } + + // Confirm that safe Destroy refuses the dirty worktree (guard: this is the + // contract we must NOT break). + if destroyErr := ws.Destroy(ctx, info); !errors.Is(destroyErr, ports.ErrWorkspaceDirty) { + t.Fatalf("Destroy dirty error = %v, want ports.ErrWorkspaceDirty", destroyErr) + } + // Path must still be intact after refused Destroy. + if _, err := os.Stat(wip); err != nil { + t.Fatalf("dirty worktree was removed by Destroy: %v", err) + } + + // ForceDestroy must succeed even though the worktree is dirty. + if err := ws.ForceDestroy(ctx, info); err != nil { + t.Fatalf("ForceDestroy: %v", err) + } + + // (a) Path no longer exists on disk. + if _, err := os.Stat(info.Path); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("path after ForceDestroy stat err = %v, want not exist", err) + } + + // (b) Worktree is deregistered from git worktree list. + records, err := ws.listRecords(ctx, repo) + if err != nil { + t.Fatalf("listRecords after ForceDestroy: %v", err) + } + if _, ok := findWorktree(records, info.Path); ok { + t.Fatalf("worktree %q still registered after ForceDestroy", info.Path) + } +} + +// TestForceDestroyArgs verifies the new force arg builder emits --force +// and leaves worktreeRemoveArgs byte-for-byte unchanged (review item RA guard). +func TestForceDestroyArgs(t *testing.T) { + repo := "/repo" + path := "/managed/proj/sess" + + safe := worktreeRemoveArgs(repo, path) + for _, a := range safe { + if a == "--force" || a == "-f" { + t.Fatalf("worktreeRemoveArgs contains --force: %v", safe) + } + } + + forced := worktreeForceRemoveArgs(repo, path) + hasForce := false + for _, a := range forced { + if a == "--force" { + hasForce = true + } + } + if !hasForce { + t.Fatalf("worktreeForceRemoveArgs missing --force: %v", forced) + } +} diff --git a/backend/internal/adapters/workspace/gitworktree/workspace_integration_test.go b/backend/internal/adapters/workspace/gitworktree/workspace_integration_test.go index 6c5f47d2..cc1f47c4 100644 --- a/backend/internal/adapters/workspace/gitworktree/workspace_integration_test.go +++ b/backend/internal/adapters/workspace/gitworktree/workspace_integration_test.go @@ -217,6 +217,12 @@ func setupOriginClone(t *testing.T, git, tmp string) string { runGit(t, git, seed, "remote", "add", "origin", origin) runGit(t, git, seed, "push", "-u", "origin", "main") run(t, git, "clone", origin, repo) + // A clone does not copy the seed's local identity, and CI runners have no + // global git identity to fall back on, so commit/commit-tree in this repo's + // worktrees would fail with "empty ident name". Set it on the clone; worktrees + // inherit the common dir config. + runGit(t, git, repo, "config", "user.email", "ao@example.com") + runGit(t, git, repo, "config", "user.name", "Ao Agents") runGit(t, git, repo, "checkout", "main") return repo } diff --git a/backend/internal/adapters/workspace/gitworktree/workspace_preserve_test.go b/backend/internal/adapters/workspace/gitworktree/workspace_preserve_test.go new file mode 100644 index 00000000..faf2dc46 --- /dev/null +++ b/backend/internal/adapters/workspace/gitworktree/workspace_preserve_test.go @@ -0,0 +1,252 @@ +package gitworktree + +import ( + "context" + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/aoagents/agent-orchestrator/backend/internal/ports" +) + +// TestWorkspaceIntegrationStashApplyRoundTrip is the primary correctness test +// for the save-on-close / restore-on-open lifecycle: +// +// 1. Create a worktree with a tracked-file edit, a new non-ignored file, +// and a file covered by .gitignore. +// 2. StashUncommitted: assert the returned ref is non-empty. +// 3. ForceDestroy: remove the worktree unconditionally. +// 4. Re-add the worktree via Restore (simulating the re-open path). +// 5. ApplyPreserved: replay the captured state. +// 6. Assert that the tracked edit and the new non-ignored file reappear, +// and the .gitignore-matched file does NOT reappear. +func TestWorkspaceIntegrationStashApplyRoundTrip(t *testing.T) { + git := requireGit(t) + tmp := t.TempDir() + repo := setupOriginClone(t, git, tmp) + root := filepath.Join(tmp, "managed") + ws, err := New(Options{Binary: git, ManagedRoot: root, RepoResolver: StaticRepoResolver{"proj": repo}}) + if err != nil { + t.Fatalf("new: %v", err) + } + ctx := context.Background() + cfg := ports.WorkspaceConfig{ProjectID: "proj", SessionID: "sess-preserve", Branch: "feature/preserve"} + + info, err := ws.Create(ctx, cfg) + if err != nil { + t.Fatalf("create: %v", err) + } + + // Stage 1: create a .gitignore that covers a secret file. + if err := os.WriteFile(filepath.Join(info.Path, ".gitignore"), []byte("secret.txt\n"), 0o644); err != nil { + t.Fatalf("write .gitignore: %v", err) + } + runGit(t, git, info.Path, "add", ".gitignore") + runGit(t, git, info.Path, "commit", "-m", "add gitignore") + + // Stage 2: create uncommitted work: + // - tracked-file edit: modify README.md (already committed from seed) + if err := os.WriteFile(filepath.Join(info.Path, "README.md"), []byte("edited by agent\n"), 0o644); err != nil { + t.Fatalf("write README: %v", err) + } + // - new non-ignored file: should be captured + if err := os.WriteFile(filepath.Join(info.Path, "agent-work.go"), []byte("package main\n"), 0o644); err != nil { + t.Fatalf("write agent-work.go: %v", err) + } + // - ignored file: must NOT be captured + if err := os.WriteFile(filepath.Join(info.Path, "secret.txt"), []byte("super-secret\n"), 0o644); err != nil { + t.Fatalf("write secret.txt: %v", err) + } + + // StashUncommitted: must return a non-empty ref. + ref, err := ws.StashUncommitted(ctx, info) + if err != nil { + t.Fatalf("StashUncommitted: %v", err) + } + if ref == "" { + t.Fatal("StashUncommitted returned empty ref for dirty worktree") + } + if !strings.HasPrefix(ref, "refs/ao/preserved/") { + t.Fatalf("ref = %q, want refs/ao/preserved/... prefix", ref) + } + + // ForceDestroy: simulate session close. + if err := ws.ForceDestroy(ctx, info); err != nil { + t.Fatalf("ForceDestroy: %v", err) + } + if _, err := os.Stat(info.Path); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("worktree path still exists after ForceDestroy") + } + + // Restore: simulate re-open / re-attach. + restored, err := ws.Restore(ctx, cfg) + if err != nil { + t.Fatalf("Restore: %v", err) + } + if restored.Path != info.Path { + t.Fatalf("restored path = %q, want %q", restored.Path, info.Path) + } + + // ApplyPreserved: replay the captured state. + if err := ws.ApplyPreserved(ctx, restored, ref); err != nil { + t.Fatalf("ApplyPreserved: %v", err) + } + + // Tracked edit must reappear. + readmeBytes, err := os.ReadFile(filepath.Join(restored.Path, "README.md")) + if err != nil { + t.Fatalf("read README after apply: %v", err) + } + if string(readmeBytes) != "edited by agent\n" { + t.Fatalf("README content = %q, want %q", string(readmeBytes), "edited by agent\n") + } + + // New non-ignored file must reappear. + if _, err := os.Stat(filepath.Join(restored.Path, "agent-work.go")); err != nil { + t.Fatalf("agent-work.go missing after apply: %v", err) + } + + // Ignored file must NOT reappear. + if _, err := os.Stat(filepath.Join(restored.Path, "secret.txt")); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("secret.txt exists after apply but must not (it was .gitignore-d)") + } + + // After a successful apply the ref must be deleted. + checkRefArgs := revParseVerifyArgs(repo, ref) + if out, err := ws.run(ctx, ws.binary, checkRefArgs...); err == nil { + t.Fatalf("preserve ref %q still exists after successful ApplyPreserved (points to %s)", ref, strings.TrimSpace(string(out))) + } +} + +// TestWorkspaceIntegrationApplyPreservedConflict verifies the spec for a +// conflicting apply (plan edge case 5): +// +// 1. Set up a repo where the base HEAD has a tracked file with content "A". +// 2. StashUncommitted after editing the file to "B" (preserve commit: B over A). +// 3. After ForceDestroy and Restore, diverge the same file to content "C" +// (simulating a base-moved or independently-edited state). +// 4. ApplyPreserved must: +// (a) return an error that satisfies errors.Is(err, ErrPreservedConflict), +// (b) leave the preserve ref intact (NOT delete it), +// (c) leave textual conflict markers in the conflicting file. +func TestWorkspaceIntegrationApplyPreservedConflict(t *testing.T) { + git := requireGit(t) + tmp := t.TempDir() + repo := setupOriginClone(t, git, tmp) + root := filepath.Join(tmp, "managed") + ws, err := New(Options{Binary: git, ManagedRoot: root, RepoResolver: StaticRepoResolver{"proj": repo}}) + if err != nil { + t.Fatalf("new: %v", err) + } + ctx := context.Background() + cfg := ports.WorkspaceConfig{ProjectID: "proj", SessionID: "sess-conflict", Branch: "feature/conflict-test"} + + info, err := ws.Create(ctx, cfg) + if err != nil { + t.Fatalf("create: %v", err) + } + + // Write base content "A" into a tracked file and commit it so it is the + // HEAD tree that StashUncommitted will use as the parent. + conflictFile := filepath.Join(info.Path, "shared.txt") + if err := os.WriteFile(conflictFile, []byte("A\n"), 0o644); err != nil { + t.Fatalf("write base A: %v", err) + } + runGit(t, git, info.Path, "add", "shared.txt") + runGit(t, git, info.Path, "commit", "-m", "base: A") + + // Edit to "B" without committing: this is what the agent had in flight. + if err := os.WriteFile(conflictFile, []byte("B\n"), 0o644); err != nil { + t.Fatalf("write B: %v", err) + } + + // Preserve: StashUncommitted captures B-over-A into a ref. + ref, err := ws.StashUncommitted(ctx, info) + if err != nil { + t.Fatalf("StashUncommitted: %v", err) + } + if ref == "" { + t.Fatal("StashUncommitted returned empty ref for dirty worktree") + } + + // Simulate session close. + if err := ws.ForceDestroy(ctx, info); err != nil { + t.Fatalf("ForceDestroy: %v", err) + } + + // Restore: re-add the worktree (re-open path). + restored, err := ws.Restore(ctx, cfg) + if err != nil { + t.Fatalf("Restore: %v", err) + } + + // Diverge the same file to content "C" in the restored worktree so that + // cherry-pick --no-commit will produce a real three-way conflict (A -> B + // from preserve vs A -> C in the current tree). + conflictFileRestored := filepath.Join(restored.Path, "shared.txt") + if err := os.WriteFile(conflictFileRestored, []byte("C\n"), 0o644); err != nil { + t.Fatalf("write C: %v", err) + } + // Stage the diverging edit so it is in the index; cherry-pick merges against + // the index, not just the working tree. + runGit(t, git, restored.Path, "add", "shared.txt") + + // ApplyPreserved must detect the conflict and return ErrPreservedConflict. + applyErr := ws.ApplyPreserved(ctx, restored, ref) + if applyErr == nil { + t.Fatal("ApplyPreserved returned nil, want ErrPreservedConflict") + } + if !errors.Is(applyErr, ErrPreservedConflict) { + t.Fatalf("ApplyPreserved error = %v, want errors.Is(..., ErrPreservedConflict)", applyErr) + } + + // (b) The preserve ref must still exist. + checkRefArgs := revParseVerifyArgs(repo, ref) + if _, err := ws.run(ctx, ws.binary, checkRefArgs...); err != nil { + t.Fatalf("preserve ref %q was deleted after a conflicting apply, must be kept: %v", ref, err) + } + + // (c) The conflicting file must contain textual conflict markers. + contents, err := os.ReadFile(conflictFileRestored) + if err != nil { + t.Fatalf("read conflicting file: %v", err) + } + if !strings.Contains(string(contents), "<<<<<<<") { + t.Fatalf("conflicting file has no conflict markers after ApplyPreserved conflict; content:\n%s", string(contents)) + } +} + +// TestWorkspaceIntegrationStashCleanWorktree proves that StashUncommitted on a +// clean worktree returns an empty ref and no error (nothing to preserve). +func TestWorkspaceIntegrationStashCleanWorktree(t *testing.T) { + git := requireGit(t) + tmp := t.TempDir() + repo := setupOriginClone(t, git, tmp) + root := filepath.Join(tmp, "managed") + ws, err := New(Options{Binary: git, ManagedRoot: root, RepoResolver: StaticRepoResolver{"proj": repo}}) + if err != nil { + t.Fatalf("new: %v", err) + } + ctx := context.Background() + cfg := ports.WorkspaceConfig{ProjectID: "proj", SessionID: "sess-clean", Branch: "feature/clean-stash"} + + info, err := ws.Create(ctx, cfg) + if err != nil { + t.Fatalf("create: %v", err) + } + + ref, err := ws.StashUncommitted(ctx, info) + if err != nil { + t.Fatalf("StashUncommitted on clean worktree: %v", err) + } + if ref != "" { + t.Fatalf("StashUncommitted on clean worktree returned non-empty ref %q, want empty", ref) + } + + // Cleanup. + if err := ws.Destroy(ctx, info); err != nil { + t.Fatalf("destroy clean worktree: %v", err) + } +} diff --git a/backend/internal/cli/doctor.go b/backend/internal/cli/doctor.go index 60b0e296..96492e32 100644 --- a/backend/internal/cli/doctor.go +++ b/backend/internal/cli/doctor.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "regexp" + "runtime" "strconv" "strings" "time" @@ -18,7 +19,6 @@ import ( "github.com/spf13/cobra" "github.com/aoagents/agent-orchestrator/backend/internal/adapters/agent/codex" - "github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/zellij" "github.com/aoagents/agent-orchestrator/backend/internal/config" ) @@ -168,7 +168,7 @@ func (c *commandContext) runDoctor(ctx context.Context) []doctorCheck { checks = append(checks, c.checkGit(ctx), - c.checkZellij(ctx), + c.checkTerminalRuntime(ctx), c.checkAOBinary(), ) for _, harness := range doctorHarnesses { @@ -290,22 +290,36 @@ func (c *commandContext) checkGit(ctx context.Context) doctorCheck { return doctorCheck{Level: doctorPass, Section: doctorSectionTools, Name: "git", Message: fmt.Sprintf("%s (version %s; supports worktrees)", path, version)} } -func (c *commandContext) checkZellij(ctx context.Context) doctorCheck { - path, err := c.deps.LookPath("zellij") +// checkTerminalRuntime checks the runtime multiplexer used on this platform: +// tmux on Darwin/Linux, ConPTY (built-in) on Windows. +func (c *commandContext) checkTerminalRuntime(ctx context.Context) doctorCheck { + if runtime.GOOS == "windows" { + return doctorCheck{ + Level: doctorPass, + Section: doctorSectionTools, + Name: "conpty", + Message: "ConPTY (built-in): no external terminal multiplexer required on Windows", + } + } + return c.checkTmux(ctx) +} + +func (c *commandContext) checkTmux(ctx context.Context) doctorCheck { + path, err := c.deps.LookPath("tmux") if err != nil || path == "" { - return doctorCheck{Level: doctorWarn, Section: doctorSectionTools, Name: "zellij", Message: "not found in PATH"} + return doctorCheck{Level: doctorWarn, Section: doctorSectionTools, Name: "tmux", Message: "not found in PATH"} } reqCtx, cancel := context.WithTimeout(ctx, probeTimeout) defer cancel() - out, err := c.deps.CommandOutput(reqCtx, path, "--version") + out, err := c.deps.CommandOutput(reqCtx, path, "-V") if err != nil { - return doctorCheck{Level: doctorFail, Section: doctorSectionTools, Name: "zellij", Message: fmt.Sprintf("%s: %v", path, err)} + return doctorCheck{Level: doctorFail, Section: doctorSectionTools, Name: "tmux", Message: fmt.Sprintf("%s: %v", path, err)} } - version, err := zellij.CheckVersionOutput(string(out)) - if err != nil { - return doctorCheck{Level: doctorFail, Section: doctorSectionTools, Name: "zellij", Message: fmt.Sprintf("%s: %v", path, err)} + version := firstOutputLine(out) + if version == "" { + version = "version unknown" } - return doctorCheck{Level: doctorPass, Section: doctorSectionTools, Name: "zellij", Message: fmt.Sprintf("%s (version %s; require >= %s)", path, version, zellij.RequiredVersion())} + return doctorCheck{Level: doctorPass, Section: doctorSectionTools, Name: "tmux", Message: fmt.Sprintf("%s (%s)", path, version)} } // checkHooksLog surfaces recent agent hook delivery failures. `ao hooks` diff --git a/backend/internal/cli/doctor_test.go b/backend/internal/cli/doctor_test.go index c1bf1694..daf7c949 100644 --- a/backend/internal/cli/doctor_test.go +++ b/backend/internal/cli/doctor_test.go @@ -10,6 +10,7 @@ import ( "net/http/httptest" "os" "path/filepath" + "runtime" "strings" "testing" "time" @@ -52,53 +53,64 @@ func TestDoctorFailsWhenGitMissing(t *testing.T) { } } -func TestDoctorChecksZellijVersion(t *testing.T) { +func TestDoctorChecksTmuxVersion(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("ao doctor emits a conpty check on Windows, not tmux") + } setConfigEnv(t) - c := doctorContext(t, map[string]string{"git": "/bin/git", "zellij": "/bin/zellij"}, func(_ context.Context, name string, args ...string) ([]byte, error) { + c := doctorContext(t, map[string]string{"git": "/bin/git", "tmux": "/bin/tmux"}, func(_ context.Context, name string, args ...string) ([]byte, error) { switch name { case "/bin/git": return []byte("git version 2.43.0\n"), nil - case "/bin/zellij": - if len(args) != 1 || args[0] != "--version" { - t.Fatalf("unexpected zellij command: %s %v", name, args) + case "/bin/tmux": + if len(args) != 1 || args[0] != "-V" { + t.Fatalf("unexpected tmux command: %s %v", name, args) } - return []byte("zellij 0.44.3\n"), nil + return []byte("tmux 3.3a\n"), nil default: t.Fatalf("unexpected command: %s %v", name, args) return nil, nil } }) - check := findDoctorCheck(t, c.runDoctor(context.Background()), "zellij") - if check.Level != doctorPass || !strings.Contains(check.Message, "0.44.3") { - t.Fatalf("zellij check = %+v, want PASS with version", check) + check := findDoctorCheck(t, c.runDoctor(context.Background()), "tmux") + if check.Level != doctorPass || !strings.Contains(check.Message, "3.3a") { + t.Fatalf("tmux check = %+v, want PASS with version", check) } } -func TestDoctorFailsUnsupportedZellijVersion(t *testing.T) { +// TestDoctorChecksTmuxVersionFailsOnError covers the case where tmux is found +// but the version command fails. +func TestDoctorChecksTmuxVersionFailsOnError(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("ao doctor emits a conpty check on Windows, not tmux") + } setConfigEnv(t) - c := doctorContext(t, map[string]string{"git": "/bin/git", "zellij": "/bin/zellij"}, func(_ context.Context, name string, _ ...string) ([]byte, error) { + c := doctorContext(t, map[string]string{"git": "/bin/git", "tmux": "/bin/tmux"}, func(_ context.Context, name string, _ ...string) ([]byte, error) { if name == "/bin/git" { return []byte("git version 2.43.0\n"), nil } - return []byte("zellij 0.44.2\n"), nil + return nil, errors.New("exec: tmux: not found") }) - check := findDoctorCheck(t, c.runDoctor(context.Background()), "zellij") - if check.Level != doctorFail || !strings.Contains(check.Message, "require >= 0.44.3") { - t.Fatalf("zellij check = %+v, want FAIL with minimum version", check) + check := findDoctorCheck(t, c.runDoctor(context.Background()), "tmux") + if check.Level != doctorFail { + t.Fatalf("tmux check = %+v, want FAIL on version error", check) } } -func TestDoctorWarnsWhenZellijMissing(t *testing.T) { +func TestDoctorWarnsWhenTmuxMissing(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("ao doctor emits a conpty check on Windows, not tmux") + } setConfigEnv(t) c := doctorContext(t, map[string]string{"git": "/bin/git"}, func(context.Context, string, ...string) ([]byte, error) { return []byte("git version 2.43.0\n"), nil }) - check := findDoctorCheck(t, c.runDoctor(context.Background()), "zellij") + check := findDoctorCheck(t, c.runDoctor(context.Background()), "tmux") if check.Level != doctorWarn { - t.Fatalf("zellij check = %+v, want WARN", check) + t.Fatalf("tmux check = %+v, want WARN", check) } } diff --git a/backend/internal/cli/ptyhost.go b/backend/internal/cli/ptyhost.go new file mode 100644 index 00000000..e7ca8481 --- /dev/null +++ b/backend/internal/cli/ptyhost.go @@ -0,0 +1,29 @@ +package cli + +import ( + "os" + + "github.com/spf13/cobra" + + "github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/conpty" +) + +// newPtyHostCommand registers the "ao pty-host" hidden subcommand that the +// conpty runtime spawns on Windows to host a ConPTY session over loopback TCP. +// DisableFlagParsing ensures agent shell args with leading dashes are not +// consumed by cobra before being passed to RunHost. +func newPtyHostCommand() *cobra.Command { + return &cobra.Command{ + Use: "pty-host", + Short: "Run a ConPTY pty-host process (internal)", + Hidden: true, + DisableFlagParsing: true, + RunE: func(_ *cobra.Command, args []string) error { + code := conpty.RunHost(args, os.Stdout) + if code != 0 { + os.Exit(code) + } + return nil + }, + } +} diff --git a/backend/internal/cli/root.go b/backend/internal/cli/root.go index 3bb1fced..da2db9c0 100644 --- a/backend/internal/cli/root.go +++ b/backend/internal/cli/root.go @@ -188,6 +188,7 @@ func NewRootCommand(deps Deps) *cobra.Command { root.AddCommand(newPreviewCommand(ctx)) root.AddCommand(newHooksCommand(ctx)) root.AddCommand(newLaunchCommand(ctx)) + root.AddCommand(newPtyHostCommand()) root.AddCommand(newImportCommand(ctx)) root.AddCommand(newProjectCommand(ctx)) root.AddCommand(newSessionCommand(ctx)) diff --git a/backend/internal/cli/spawn.go b/backend/internal/cli/spawn.go index b5cc7174..d19d540d 100644 --- a/backend/internal/cli/spawn.go +++ b/backend/internal/cli/spawn.go @@ -4,11 +4,12 @@ import ( "context" "fmt" "net/url" + "runtime" "github.com/spf13/cobra" "github.com/spf13/pflag" - "github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/zellij" + "github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/tmux" ) type spawnOptions struct { @@ -97,14 +98,14 @@ func newSpawnCommand(ctx *commandContext) *cobra.Command { if _, err := fmt.Fprintf(out, "spawned session %s (%s)%s\n", res.Session.ID, res.Session.Status, claimLabel); err != nil { return err } - // The daemon runs zellij under a short, non-default socket dir (see - // zellij.DefaultSocketDir), so a plain `zellij attach` wouldn't find - // the session — prefix the env so the hint is copy-pasteable. Use the - // sanitised name zellij actually registers (zellij.SessionName): a long - // session id maps to a different name than the raw id. - attach := fmt.Sprintf("zellij attach %s", zellij.SessionName(res.Session.ID)) - if dir := zellij.DefaultSocketDir(); dir != "" { - attach = fmt.Sprintf("ZELLIJ_SOCKET_DIR=%s %s", dir, attach) + // Print a copy-pasteable attach hint for the selected runtime. + // On Darwin/Linux: tmux attach-session using the sanitised session name. + // On Windows: ConPTY has no user-facing attach CLI; use the AO dashboard. + var attach string + if runtime.GOOS != "windows" { + attach = fmt.Sprintf("tmux attach -t %s", tmux.SessionName(res.Session.ID)) + } else { + attach = "Attach from the AO dashboard (ConPTY sessions have no CLI attach command)" } _, err := fmt.Fprintf(out, "attach with: %s\n", attach) return err diff --git a/backend/internal/daemon/daemon.go b/backend/internal/daemon/daemon.go index 59791c86..3603fbe9 100644 --- a/backend/internal/daemon/daemon.go +++ b/backend/internal/daemon/daemon.go @@ -13,7 +13,7 @@ import ( "syscall" "time" - "github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/zellij" + "github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/runtimeselect" "github.com/aoagents/agent-orchestrator/backend/internal/config" "github.com/aoagents/agent-orchestrator/backend/internal/httpd" "github.com/aoagents/agent-orchestrator/backend/internal/notify" @@ -82,26 +82,16 @@ func Run() error { return err } - // Terminal streaming: the Zellij runtime supplies the PTY-attach command and - // liveness; the CDC broadcaster feeds the session-state channel. The manager + // Terminal streaming: the selected runtime (tmux on macOS/Linux, conpty on Windows) supplies the + // attach Stream and liveness; the CDC broadcaster feeds the session-state channel. The manager // is handed to httpd, which mounts it at /mux. Raw PTY bytes never flow - // through the CDC change_log — only session-state events do. - // zellij's default socket dir is too long on macOS for long session ids - // (see zellij.DefaultSocketDir); use a short, stable one and ensure it exists. - zellijSocketDir := zellij.DefaultSocketDir() - if zellijSocketDir != "" { - if err := os.MkdirAll(zellijSocketDir, 0o700); err != nil { - // Don't abort startup, but surface it: every spawn's zellij session - // would otherwise fail later with an opaque socket-bind error. - log.Warn("could not create zellij socket dir; spawns may fail", "dir", zellijSocketDir, "error", err) - } - } - runtimeAdapter := zellij.New(zellij.Options{SocketDir: zellijSocketDir}) + // through the CDC change_log -- only session-state events do. + runtimeAdapter := runtimeselect.New(log) termMgr := terminal.NewManager(runtimeAdapter, cdcPipe.Broadcaster, log) defer termMgr.Close() // The agent messenger sends validated user input to the session's live - // zellij pane. Keep this path small until durable inbox semantics are needed. + // runtime pane. Keep this path small until durable inbox semantics are needed. // Built before the Lifecycle Manager so the LCM can use it for SCM-driven // agent nudges (CI failure, review feedback, merge conflict). messenger := newSessionMessenger(store, runtimeAdapter, log) @@ -116,10 +106,10 @@ func Run() error { lcStack.scmDone = startSCMObserver(ctx, store, lcStack.LCM, log) // Wire the controller-facing session service over the same store + LCM, the - // zellij runtime, a gitworktree workspace, the per-session agent resolver + // selected runtime, a gitworktree workspace, the per-session agent resolver // (AO_AGENT validated here for compatibility), and the agent messenger, then mount it // on the API. - sessionSvc, reviewSvc, err := startSession(cfg, runtimeAdapter, store, lcStack.LCM, messenger, telemetrySink, log) + sessionSvc, reviewSvc, sessMgr, err := startSession(cfg, runtimeAdapter, store, lcStack.LCM, messenger, telemetrySink, log) if err != nil { stop() lcStack.Stop() @@ -151,12 +141,34 @@ func Run() error { return err } + // Reconcile sessions on boot: adopt crash-surviving runtimes, capture and + // terminate dead ones, reap leaked tmux, then restore shutdown-saved + // sessions. Best-effort: a failure is logged but never blocks boot. Placed + // before srv.Run so sessions are consistent before the server serves. + if reconcileErr := sessMgr.Reconcile(ctx); reconcileErr != nil { + log.Error("reconcile sessions on boot failed", "err", reconcileErr) + } + runErr := srv.Run(ctx) + // Save and tear down all live sessions before the store closes. Both SIGTERM + // and POST /shutdown funnel through srv.Run returning (SIGTERM cancels ctx, + // which srv.Run selects on; POST /shutdown closes the shutdownRequested channel, + // which srv.Run also selects on), so this single call site covers both paths. + // + // Use a fresh context with a bounded deadline: the ctx that caused srv.Run + // to return is already cancelled, so passing it would abort the save + // immediately and leave every session unsaved. + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), shutdownSaveTimeout) + defer shutdownCancel() + if saveErr := sessMgr.SaveAndTeardownAll(shutdownCtx); saveErr != nil { + log.Error("save sessions on shutdown failed", "err", saveErr) + } + // Shut the background goroutines down in order: cancel the context FIRST so // their loops exit, then wait for them to drain. Doing this explicitly (not // via defer) avoids the LIFO trap where a Stop() that blocks on ctx-cancel - // runs before the cancel — which would hang any non-signal exit path. + // runs before the cancel: a non-signal exit path would hang otherwise. stop() <-previewDone lcStack.Stop() @@ -166,6 +178,10 @@ func Run() error { return runErr } +// shutdownSaveTimeout bounds the SaveAndTeardownAll call on shutdown so a +// pathological session cannot stall the process exit indefinitely. +const shutdownSaveTimeout = 30 * time.Second + // newLogger returns the daemon's slog logger. It writes to stderr so supervisors // can capture it separately from any structured stdout protocol added later. func newLogger() *slog.Logger { diff --git a/backend/internal/daemon/lifecycle_wiring.go b/backend/internal/daemon/lifecycle_wiring.go index 3bf5ff7d..676dcb8e 100644 --- a/backend/internal/daemon/lifecycle_wiring.go +++ b/backend/internal/daemon/lifecycle_wiring.go @@ -10,7 +10,7 @@ import ( "github.com/aoagents/agent-orchestrator/backend/internal/adapters/agent/activitydispatch" agentregistry "github.com/aoagents/agent-orchestrator/backend/internal/adapters/agent/registry" "github.com/aoagents/agent-orchestrator/backend/internal/adapters/reviewer" - "github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/zellij" + "github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/runtimeselect" "github.com/aoagents/agent-orchestrator/backend/internal/adapters/workspace/gitworktree" "github.com/aoagents/agent-orchestrator/backend/internal/config" "github.com/aoagents/agent-orchestrator/backend/internal/domain" @@ -58,18 +58,29 @@ func (l *lifecycleStack) Stop() { } } +// sessionLifecycle is the narrow surface of sessionmanager.Manager used for +// boot/shutdown wiring. A minimal interface keeps the daemon testable without +// depending on the concrete manager type. +type sessionLifecycle interface { + Reconcile(ctx context.Context) error + RestoreAll(ctx context.Context) error + SaveAndTeardownAll(ctx context.Context) error +} + // startSession builds the controller-facing session service: a session manager -// over the real zellij runtime, a per-session gitworktree workspace, the shared +// over the selected runtime, a per-session gitworktree workspace, the shared // store + LCM, the per-session agent resolver, and the agent messenger. The -// returned service is mounted at httpd APIDeps.Sessions. -func startSession(cfg config.Config, runtime *zellij.Runtime, store *sqlite.Store, lcm *lifecycle.Manager, messenger ports.AgentMessenger, telemetry ports.EventSink, log *slog.Logger) (*sessionsvc.Service, reviewsvc.Manager, error) { +// returned service is mounted at httpd APIDeps.Sessions. It also returns the +// manager so the caller can wire Reconcile/SaveAndTeardownAll into the +// boot/shutdown sequence. +func startSession(cfg config.Config, runtime runtimeselect.Runtime, store *sqlite.Store, lcm *lifecycle.Manager, messenger ports.AgentMessenger, telemetry ports.EventSink, log *slog.Logger) (*sessionsvc.Service, reviewsvc.Manager, sessionLifecycle, error) { defaultAgent := cfg.Agent if defaultAgent == "" { defaultAgent = config.DefaultAgent } agents, err := buildAgentResolver(defaultAgent, log) if err != nil { - return nil, nil, err + return nil, nil, nil, err } ws, err := gitworktree.New(gitworktree.Options{ // Per-session worktrees live under the data dir, so a single AO_DATA_DIR @@ -81,7 +92,7 @@ func startSession(cfg config.Config, runtime *zellij.Runtime, store *sqlite.Stor RepoResolver: projectRepoResolver{store: store}, }) if err != nil { - return nil, nil, fmt.Errorf("session workspace: %w", err) + return nil, nil, nil, fmt.Errorf("session workspace: %w", err) } mgr := sessionmanager.New(sessionmanager.Deps{ Runtime: runtime, @@ -113,7 +124,7 @@ func startSession(cfg config.Config, runtime *zellij.Runtime, store *sqlite.Stor // writer. reviewers, err := reviewer.NewResolver() if err != nil { - return nil, nil, fmt.Errorf("reviewer resolver: %w", err) + return nil, nil, nil, fmt.Errorf("reviewer resolver: %w", err) } reviewEngine := reviewcore.New(reviewcore.Deps{ Store: store, @@ -123,11 +134,11 @@ func startSession(cfg config.Config, runtime *zellij.Runtime, store *sqlite.Stor Launcher: reviewcore.NewLauncher(reviewers, runtime), }) reviewSvc := reviewsvc.New(reviewEngine, store, reviewsvc.WithLifecycleReducer(lcm)) - return sessionSvc, reviewSvc, nil + return sessionSvc, reviewSvc, mgr, nil } // runtimeMessageSender is the narrow part of the concrete runtime needed by -// ao send. zellij.Runtime already implements this via SendMessage. +// ao send. Both tmux.Runtime and conpty.Runtime implement this via SendMessage. type runtimeMessageSender interface { SendMessage(ctx context.Context, handle ports.RuntimeHandle, message string) error } diff --git a/backend/internal/daemon/wiring_test.go b/backend/internal/daemon/wiring_test.go index a3acd553..21dbcbd5 100644 --- a/backend/internal/daemon/wiring_test.go +++ b/backend/internal/daemon/wiring_test.go @@ -10,7 +10,8 @@ import ( "time" "github.com/aoagents/agent-orchestrator/backend/internal/adapters" - "github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/zellij" + "github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/runtimeselect" + "github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/tmux" telemetryadapter "github.com/aoagents/agent-orchestrator/backend/internal/adapters/telemetry" "github.com/aoagents/agent-orchestrator/backend/internal/cdc" "github.com/aoagents/agent-orchestrator/backend/internal/config" @@ -149,9 +150,9 @@ func TestWiring_StartSessionBuildsSessionService(t *testing.T) { lcm := lifecycle.New(store, nil) cfg := config.Config{DataDir: t.TempDir()} - runtime := zellij.New(zellij.Options{}) - messenger := newSessionMessenger(store, runtime, log) - svc, reviewSvc, err := startSession(cfg, runtime, store, lcm, messenger, telemetryadapter.NoopSink{}, log) + rt := runtimeselect.New(nil) + messenger := newSessionMessenger(store, rt, log) + svc, reviewSvc, lc, err := startSession(cfg, rt, store, lcm, messenger, telemetryadapter.NoopSink{}, log) if err != nil { t.Fatalf("startSession: %v", err) } @@ -161,6 +162,9 @@ func TestWiring_StartSessionBuildsSessionService(t *testing.T) { if reviewSvc == nil { t.Fatal("startSession returned nil review service") } + if lc == nil { + t.Fatal("startSession returned nil session lifecycle") + } } type captureRuntimeSender struct { @@ -320,7 +324,7 @@ func TestWiring_StartLifecycleThreadsMessengerIntoLCM(t *testing.T) { log := slog.New(slog.NewTextHandler(io.Discard, nil)) messenger := &captureMessenger{} - stack := startLifecycle(ctx, store, zellij.New(zellij.Options{}), messenger, nil, nil, log) + stack := startLifecycle(ctx, store, tmux.New(tmux.Options{}), messenger, nil, nil, log) t.Cleanup(stack.Stop) t.Cleanup(cancel) @@ -377,22 +381,67 @@ func TestProjectRepoResolver_ResolvesRegisteredProject(t *testing.T) { } } -// TestDaemonZellijSocketDir_LeavesBudgetForSessionNames guards the fix for the -// zellij "session name must be less than 0 characters" spawn failure: the -// daemon's socket dir must be short enough that a max-length (48-char) session -// name still fits the ~103-byte unix-domain-socket-path budget. zellij's long -// $TMPDIR default (the bug) would fail this. -func TestDaemonZellijSocketDir_LeavesBudgetForSessionNames(t *testing.T) { - dir := zellij.DefaultSocketDir() - if dir == "" { - t.Skip("zellij not used on this platform") - } - const ( - unixSocketPathMax = 103 // sun_path budget zellij enforces on macOS - zellijOverhead = 24 // zellij's version subdir + separators (generous) - maxSessionName = 48 // zellijSessionName's cap - ) - if budget := unixSocketPathMax - len(dir) - zellijOverhead; budget < maxSessionName { - t.Fatalf("zellij socket dir %q too long: %d bytes left for the session name, need >= %d", dir, budget, maxSessionName) +// fakeSessionLifecycle records calls to Reconcile, RestoreAll, and +// SaveAndTeardownAll so tests can assert the daemon wiring invokes the correct +// methods without needing a real runtime or worktree. +type fakeSessionLifecycle struct { + reconcileCalled bool + restoreAllCalled bool + saveAndTeardownCalled bool + reconcileErr error + restoreErr error + saveErr error +} + +func (f *fakeSessionLifecycle) Reconcile(_ context.Context) error { + f.reconcileCalled = true + return f.reconcileErr +} + +func (f *fakeSessionLifecycle) RestoreAll(_ context.Context) error { + f.restoreAllCalled = true + return f.restoreErr +} + +func (f *fakeSessionLifecycle) SaveAndTeardownAll(_ context.Context) error { + f.saveAndTeardownCalled = true + return f.saveErr +} + +// TestWiring_SessionLifecycleInterfaceInvokedByDaemon asserts the +// sessionLifecycle interface is satisfied by *sessionmanager.Manager (compile +// check) and that Reconcile, RestoreAll, and SaveAndTeardownAll dispatch +// correctly through the interface, matching what daemon.go wires at +// boot/shutdown. +func TestWiring_SessionLifecycleInterfaceInvokedByDaemon(t *testing.T) { + // Verify *sessionmanager.Manager satisfies the interface at compile time. + var _ sessionLifecycle = (*sessionmanager.Manager)(nil) + + fake := &fakeSessionLifecycle{} + ctx := context.Background() + + // Dispatch through the interface variable to exercise the real dispatch + // path, not just direct struct method calls. + var sl sessionLifecycle = fake + + if err := sl.Reconcile(ctx); err != nil { + t.Fatalf("Reconcile: %v", err) + } + if !fake.reconcileCalled { + t.Fatal("Reconcile was not called through the interface") + } + + if err := sl.RestoreAll(ctx); err != nil { + t.Fatalf("RestoreAll: %v", err) + } + if !fake.restoreAllCalled { + t.Fatal("RestoreAll was not called through the interface") + } + + if err := sl.SaveAndTeardownAll(ctx); err != nil { + t.Fatalf("SaveAndTeardownAll: %v", err) + } + if !fake.saveAndTeardownCalled { + t.Fatal("SaveAndTeardownAll was not called through the interface") } } diff --git a/backend/internal/domain/project.go b/backend/internal/domain/project.go index 3f872f09..fd0fc25e 100644 --- a/backend/internal/domain/project.go +++ b/backend/internal/domain/project.go @@ -56,5 +56,10 @@ type SessionWorktreeRecord struct { BaseSHA string WorktreePath string PreservedRef string - State string + // ponytail: State mirrors session_worktrees.state, an enum that is unused + // multi-repo scaffolding. The save/restore lifecycle reads and writes only + // PreservedRef and row presence; State is never set by any live code path + // and always resolves to the column default ('active' on insert). Wire it + // when multi-repo worktree lifecycle states actually ship. + State string } diff --git a/backend/internal/httpd/terminal_mux_test.go b/backend/internal/httpd/terminal_mux_test.go index e0c9929a..7114a06e 100644 --- a/backend/internal/httpd/terminal_mux_test.go +++ b/backend/internal/httpd/terminal_mux_test.go @@ -13,24 +13,25 @@ import ( "github.com/coder/websocket" "github.com/coder/websocket/wsjson" + "github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/ptyexec" "github.com/aoagents/agent-orchestrator/backend/internal/config" "github.com/aoagents/agent-orchestrator/backend/internal/ports" "github.com/aoagents/agent-orchestrator/backend/internal/terminal" ) -// stubSource attaches a throwaway shell command instead of a real Zellij pane, so +// stubSource attaches a throwaway shell command instead of a real mux pane, so // the /mux path exercises the genuine upgrade + wsjson + Serve + creack/pty flow -// without needing Zellij. The pane reports alive until the first attach happens -// (the mux refuses to attach to a dead pane), then dead, so the command's exit is -// treated as the pane being gone (no re-attach). +// without needing a runtime. The pane reports alive until the first attach +// happens (the mux refuses to attach to a dead pane), then dead, so the +// command's exit is treated as the pane being gone (no re-attach). type stubSource struct { argv []string attached atomic.Bool } -func (s *stubSource) AttachCommand(ports.RuntimeHandle) ([]string, []string, error) { +func (s *stubSource) Attach(ctx context.Context, _ ports.RuntimeHandle, rows, cols uint16) (ports.Stream, error) { s.attached.Store(true) - return s.argv, nil, nil + return ptyexec.Spawn(ctx, s.argv, nil, rows, cols) } func (s *stubSource) IsAlive(context.Context, ports.RuntimeHandle) (bool, error) { diff --git a/backend/internal/integration/lifecycle_sqlite_test.go b/backend/internal/integration/lifecycle_sqlite_test.go index b985046b..4b70fb25 100644 --- a/backend/internal/integration/lifecycle_sqlite_test.go +++ b/backend/internal/integration/lifecycle_sqlite_test.go @@ -15,14 +15,43 @@ import ( "github.com/aoagents/agent-orchestrator/backend/internal/storage/sqlite" ) -type stubRuntime struct{ created, destroyed int } +type stubRuntime struct { + created int + destroyed int + // aliveByHandle scripts IsAlive per handle ID. If a handle ID is absent, + // IsAlive returns true (default: alive), matching the pre-existing behavior + // that all other tests relied on. + aliveByHandle map[string]bool + destroyedHandles []string +} func (s *stubRuntime) Create(context.Context, ports.RuntimeConfig) (ports.RuntimeHandle, error) { s.created++ return ports.RuntimeHandle{ID: "h1"}, nil } -func (s *stubRuntime) Destroy(context.Context, ports.RuntimeHandle) error { s.destroyed++; return nil } -func (s *stubRuntime) IsAlive(context.Context, ports.RuntimeHandle) (bool, error) { return true, nil } +func (s *stubRuntime) Destroy(_ context.Context, h ports.RuntimeHandle) error { + s.destroyed++ + s.destroyedHandles = append(s.destroyedHandles, h.ID) + return nil +} +func (s *stubRuntime) IsAlive(_ context.Context, h ports.RuntimeHandle) (bool, error) { + if s.aliveByHandle != nil { + if alive, ok := s.aliveByHandle[h.ID]; ok { + return alive, nil + } + } + return true, nil +} + +// wasDestroyed reports whether Destroy was called with the given handle ID. +func (s *stubRuntime) wasDestroyed(handleID string) bool { + for _, id := range s.destroyedHandles { + if id == handleID { + return true + } + } + return false +} type stubAgent struct{} @@ -63,6 +92,13 @@ func (s *stubWorkspace) Destroy(context.Context, ports.WorkspaceInfo) error { func (s *stubWorkspace) Restore(ctx context.Context, cfg ports.WorkspaceConfig) (ports.WorkspaceInfo, error) { return s.Create(ctx, cfg) } +func (s *stubWorkspace) ForceDestroy(context.Context, ports.WorkspaceInfo) error { return nil } +func (s *stubWorkspace) StashUncommitted(_ context.Context, _ ports.WorkspaceInfo) (string, error) { + return "", nil +} +func (s *stubWorkspace) ApplyPreserved(_ context.Context, _ ports.WorkspaceInfo, _ string) error { + return nil +} type captureMessenger struct{ msgs []string } @@ -74,6 +110,7 @@ func (c *captureMessenger) Send(_ context.Context, _ domain.SessionID, msg strin type stack struct { store *sqlite.Store sm *sessionsvc.Service + mgr *sessionmanager.Manager lcm *lifecycle.Manager prm *prsvc.Manager rt *stubRuntime @@ -107,7 +144,7 @@ func newStack(t *testing.T) *stack { ws := &stubWorkspace{} mgr := sessionmanager.New(sessionmanager.Deps{Runtime: rt, Agents: stubAgents{}, Workspace: ws, Store: store, Messenger: msg, Lifecycle: lcm, LookPath: func(string) (string, error) { return "/usr/bin/true", nil }}) sm := sessionsvc.New(mgr, store) - return &stack{store: store, sm: sm, lcm: lcm, prm: prm, rt: rt, ws: ws, msg: msg} + return &stack{store: store, sm: sm, mgr: mgr, lcm: lcm, prm: prm, rt: rt, ws: ws, msg: msg} } func TestSpawnPRKillRoundTrip(t *testing.T) { @@ -168,6 +205,93 @@ func TestRestoreRoundTripPreservesMetadata(t *testing.T) { } } +// TestReconcile_TerminatesDeadLiveSessionAndReapsLeakedTmux exercises +// Manager.Reconcile against a real sqlite.Store: +// +// - Session A: is_terminated=0 but its runtime is GONE => Reconcile must +// mark it terminated in the DB. +// - Session B: is_terminated=1 but its runtime is still ALIVE (leaked teardown) +// => Reconcile must call Destroy on its handle. +func TestReconcile_TerminatesDeadLiveSessionAndReapsLeakedTmux(t *testing.T) { + ctx := context.Background() + st := newStack(t) + + // Script liveness: handle "hdl-A" is dead; handle "hdl-B" is alive. + st.rt.aliveByHandle = map[string]bool{ + "hdl-A": false, + "hdl-B": true, + } + + now := time.Now().UTC() + + // Seed session A: live in the DB (is_terminated=0) but runtime is gone. + // WorkspacePath and Branch must be non-empty so reconcileLive actually probes + // IsAlive (it short-circuits on missing path/branch). + recA := domain.SessionRecord{ + ProjectID: "mer", + Kind: domain.KindWorker, + Harness: domain.HarnessClaudeCode, + IsTerminated: false, + Metadata: domain.SessionMetadata{ + Branch: "ao/mer-a/root", + WorkspacePath: "/ws/mer-a", + RuntimeHandleID: "hdl-A", + }, + Activity: domain.Activity{State: domain.ActivityIdle, LastActivityAt: now}, + CreatedAt: now, + UpdatedAt: now, + } + recA, err := st.store.CreateSession(ctx, recA) + if err != nil { + t.Fatalf("seed session A: %v", err) + } + + // Seed session B: terminated in the DB (is_terminated=1) but runtime leaked. + recB := domain.SessionRecord{ + ProjectID: "mer", + Kind: domain.KindWorker, + Harness: domain.HarnessClaudeCode, + IsTerminated: true, + Metadata: domain.SessionMetadata{ + Branch: "ao/mer-b/root", + WorkspacePath: "/ws/mer-b", + RuntimeHandleID: "hdl-B", + }, + Activity: domain.Activity{State: domain.ActivityIdle, LastActivityAt: now}, + CreatedAt: now, + UpdatedAt: now, + } + recB, err = st.store.CreateSession(ctx, recB) + if err != nil { + t.Fatalf("seed session B: %v", err) + } + // recB is already built with IsTerminated=true, so CreateSession stores it terminated; the UpdateSession below is redundant but kept for clarity. + if err := st.store.UpdateSession(ctx, recB); err != nil { + t.Fatalf("patch session B terminated: %v", err) + } + + if err := st.mgr.Reconcile(ctx); err != nil { + t.Fatalf("Reconcile: %v", err) + } + + // Session A must now be terminated in the store. + gotA, ok, err := st.store.GetSession(ctx, recA.ID) + if err != nil { + t.Fatalf("get session A: %v", err) + } + if !ok { + t.Fatalf("session A: not found after Reconcile") + } + if !gotA.IsTerminated { + t.Fatalf("session A: want is_terminated=true after Reconcile, got false") + } + + // Session B's leaked runtime must have been destroyed. + if !st.rt.wasDestroyed("hdl-B") { + t.Fatalf("session B: want Destroy called for handle hdl-B; destroyed handles: %v", st.rt.destroyedHandles) + } +} + func TestCDCPollerReceivesSessionAndPREvents(t *testing.T) { ctx := context.Background() st := newStack(t) diff --git a/backend/internal/ports/outbound.go b/backend/internal/ports/outbound.go index 668da516..60f2a980 100644 --- a/backend/internal/ports/outbound.go +++ b/backend/internal/ports/outbound.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "io" "github.com/aoagents/agent-orchestrator/backend/internal/domain" ) @@ -99,6 +100,20 @@ type RuntimeHandle struct { ID string } +// Stream is one live terminal attach: PTY-like bytes plus resize. Returned +// already-open by a Runtime's Attach. tmux/zellij back it with a local PTY around +// their attach CLI; conpty backs it with a loopback connection to the pty-host. +type Stream interface { + io.ReadWriteCloser + Resize(rows, cols uint16) error +} + +// Attacher opens a fresh attach Stream for a session handle, sized rows x cols from +// birth (0 means size not yet known). ctx cancellation must terminate the stream. +type Attacher interface { + Attach(ctx context.Context, handle RuntimeHandle, rows, cols uint16) (Stream, error) +} + // The Agent port and its supporting types live in agent.go. // Workspace is the isolated checkout an agent works in (a git worktree or clone). @@ -106,6 +121,24 @@ type Workspace interface { Create(ctx context.Context, cfg WorkspaceConfig) (WorkspaceInfo, error) Destroy(ctx context.Context, info WorkspaceInfo) error Restore(ctx context.Context, cfg WorkspaceConfig) (WorkspaceInfo, error) + // ForceDestroy removes the worktree unconditionally, bypassing the + // dirty-worktree refusal that Destroy enforces. It is only safe to call + // AFTER the session's uncommitted work has been captured via StashUncommitted. + // Never call it from interactive teardown paths. + ForceDestroy(ctx context.Context, info WorkspaceInfo) error + // StashUncommitted captures all uncommitted work in the worktree as a git + // commit object stored at refs/ao/preserved/, WITHOUT mutating + // the working tree or the global stash stack. Tracked edits and new + // non-ignored files are captured; .gitignore-d files are skipped (the count + // of skipped ignored paths is logged). Returns the ref name on success, or + // an empty string if the worktree is clean (nothing to preserve). + StashUncommitted(ctx context.Context, info WorkspaceInfo) (ref string, err error) + // ApplyPreserved replays a capture created by StashUncommitted onto the + // worktree identified by info. On clean success the preserve ref is deleted. + // On conflict, the ref is kept, conflict markers are left in the working + // tree, and ErrPreservedConflict (wrapped) is returned. The ref must never + // be deleted on a failed or conflicted apply. + ApplyPreserved(ctx context.Context, info WorkspaceInfo, ref string) error } // Workspace-level sentinels surfaced through Create/Restore/Destroy so callers @@ -125,6 +158,12 @@ var ( // it holds uncommitted changes or untracked files. Teardown is never // forced; callers treat the workspace as intentionally preserved. ErrWorkspaceDirty = errors.New("workspace: uncommitted changes present") + // ErrPreservedConflict is returned by ApplyPreserved when replaying a + // preserved ref onto the worktree produces merge conflicts. The ref is + // kept intact (never deleted on conflict); the working tree is left with + // conflict markers for manual resolution. Adapters wrap this sentinel via + // fmt.Errorf so callers can match it with errors.Is. + ErrPreservedConflict = errors.New("workspace: preserved apply produced conflicts") ) // WorkspaceConfig is the spec for creating or restoring a session's workspace. diff --git a/backend/internal/service/session/service.go b/backend/internal/service/session/service.go index 090c11b9..facfc7bd 100644 --- a/backend/internal/service/session/service.go +++ b/backend/internal/service/session/service.go @@ -453,6 +453,9 @@ func toAPIError(err error) error { return apierr.Conflict("SESSION_TERMINATED", "Session is terminated", nil) case errors.Is(err, sessionmanager.ErrIncompleteHandle): return apierr.Conflict("SESSION_INCOMPLETE_HANDLE", "Session is missing runtime or workspace handles", nil) + case errors.Is(err, sessionmanager.ErrNotResumable): + return apierr.Conflict("SESSION_NOT_RESUMABLE", + "This session has no saved agent session or prompt to resume from", nil) case errors.Is(err, sessionmanager.ErrProjectNotResolvable): return apierr.Invalid("PROJECT_NOT_RESOLVABLE", "Project is not registered or has no repo — register it with `ao project add`", nil) case errors.Is(err, sessionmanager.ErrUnknownHarness): diff --git a/backend/internal/service/session/service_test.go b/backend/internal/service/session/service_test.go index 81f02d29..3b0476f2 100644 --- a/backend/internal/service/session/service_test.go +++ b/backend/internal/service/session/service_test.go @@ -858,3 +858,17 @@ func containsString(values []string, want string) bool { } return false } + +func TestToAPIError_NotResumable(t *testing.T) { + err := toAPIError(fmt.Errorf("restore foo: %w", sessionmanager.ErrNotResumable)) + var ae *apierr.Error + if !errors.As(err, &ae) { + t.Fatalf("want *apierr.Error, got %T: %v", err, err) + } + if ae.Kind != apierr.KindConflict { + t.Errorf("kind = %v, want %v", ae.Kind, apierr.KindConflict) + } + if ae.Code != "SESSION_NOT_RESUMABLE" { + t.Errorf("code = %q, want SESSION_NOT_RESUMABLE", ae.Code) + } +} diff --git a/backend/internal/session_manager/manager.go b/backend/internal/session_manager/manager.go index 104982bd..2fab927f 100644 --- a/backend/internal/session_manager/manager.go +++ b/backend/internal/session_manager/manager.go @@ -25,6 +25,13 @@ var ( ErrNotRestorable = errors.New("session: not restorable (not terminal)") ErrTerminated = errors.New("session: terminated") ErrIncompleteHandle = errors.New("session: incomplete teardown handle") + // ErrNotResumable means there is nothing for Restore to relaunch from: the + // harness adapter cannot resume the session (no native or derivable session + // id) AND no prompt was saved to fresh-launch from. Resumability is decided + // by the adapter (e.g. Claude Code pins a deterministic --session-id, so it + // resumes with no captured token), not by inspecting metadata fields here. + // Distinct from ErrNotRestorable (which is "not terminal yet"). + ErrNotResumable = errors.New("session: nothing to resume from") // ErrProjectNotResolvable means the spawn's project has no usable repo // (unregistered, archived, or missing a path). The API maps it to a 400. ErrProjectNotResolvable = errors.New("session: project repo not resolvable") @@ -60,6 +67,9 @@ type lifecycleRecorder interface { type runtimeController interface { Create(ctx context.Context, cfg ports.RuntimeConfig) (ports.RuntimeHandle, error) Destroy(ctx context.Context, handle ports.RuntimeHandle) error + // IsAlive reports whether the handle's runtime session still exists. Used by + // Reconcile on boot to adopt crash-surviving sessions and reap leaked ones. + IsAlive(ctx context.Context, handle ports.RuntimeHandle) (bool, error) } // Store is the persistence surface needed by the internal session Manager. @@ -77,6 +87,15 @@ type Store interface { // when the row had already progressed past seed state — preserving the // no-resurrection guarantee for live sessions. DeleteSession(ctx context.Context, id domain.SessionID) (bool, error) + // UpsertSessionWorktree records or updates the worktree row for a session. + // SaveAndTeardownAll writes the preserved_ref here (even when empty) as the + // "shutdown-saved" marker before ForceDestroying the worktree. + UpsertSessionWorktree(ctx context.Context, row domain.SessionWorktreeRecord) error + // ListSessionWorktrees returns every worktree row for a session. RestoreAll + // uses this to identify sessions saved by the last SaveAndTeardownAll: the + // presence of any row is the marker; preserved_ref may be empty for clean + // worktrees. + ListSessionWorktrees(ctx context.Context, id domain.SessionID) ([]domain.SessionWorktreeRecord, error) } // Manager coordinates internal session spawn, restore, kill, and cleanup over @@ -467,9 +486,10 @@ func (m *Manager) Restore(ctx context.Context, id domain.SessionID) (domain.Sess if meta.WorkspacePath == "" || meta.Branch == "" { return domain.SessionRecord{}, fmt.Errorf("restore %s: %w", id, ErrIncompleteHandle) } - if meta.AgentSessionID == "" && meta.Prompt == "" { - return domain.SessionRecord{}, fmt.Errorf("restore %s: nothing to resume from", id) - } + // Resumability is NOT decided here: a promptless session can still be fully + // resumable when the harness pins a deterministic session id (Claude Code). + // restoreArgv asks the adapter and returns ErrNotResumable only when the + // adapter cannot resume AND there is no prompt to fresh-launch from. project, err := m.loadProject(ctx, rec.ProjectID) if err != nil { @@ -532,6 +552,287 @@ func (m *Manager) getRecord(ctx context.Context, id domain.SessionID) (domain.Se return rec, nil } +// SaveAndTeardownAll captures uncommitted work and tears down every live +// session that has a workspace path. It is the shutdown path for the daemon: +// each session's uncommitted work is stashed into a preserve ref, the ref is +// written to session_worktrees (the "shutdown-saved" marker) BEFORE the +// worktree is force-removed. The DB write is committed before the worktree is +// destroyed so a crash between the two leaves the ref in place and the row +// present; RestoreAll will replay both. +// +// Failures on individual sessions are logged and do not abort the loop. +// ForceDestroy is never called if capture or the DB write did not succeed. +func (m *Manager) SaveAndTeardownAll(ctx context.Context) error { + recs, err := m.store.ListAllSessions(ctx) + if err != nil { + return fmt.Errorf("save-teardown-all: list sessions: %w", err) + } + for _, rec := range recs { + if rec.IsTerminated { + continue + } + if rec.Metadata.WorkspacePath == "" || rec.Metadata.Branch == "" { + continue + } + if err := m.saveAndTeardownOne(ctx, rec); err != nil { + m.logger.Error("save-teardown-all: session failed, skipping", "sessionID", rec.ID, "error", err) + } + } + return nil +} + +// saveAndTeardownOne runs the capture-then-destroy sequence for a single +// session. The DB write (UpsertSessionWorktree) is committed before +// ForceDestroy; if either capture or the DB write fails, ForceDestroy is +// not called. +func (m *Manager) saveAndTeardownOne(ctx context.Context, rec domain.SessionRecord) error { + ws := workspaceInfo(rec) + + // 1. Capture uncommitted work (ref may be "" for clean worktrees). + ref, err := m.workspace.StashUncommitted(ctx, ws) + if err != nil { + return fmt.Errorf("save %s: stash: %w", rec.ID, err) + } + + // 2. Write the shutdown-saved marker to the DB. The row's presence (even + // with an empty preserved_ref) is what RestoreAll uses to identify sessions + // saved by this run. This MUST be committed before ForceDestroy. + row := domain.SessionWorktreeRecord{ + SessionID: rec.ID, + RepoName: domain.RootWorkspaceRepoName, + Branch: rec.Metadata.Branch, + WorktreePath: rec.Metadata.WorkspacePath, + PreservedRef: ref, + } + if err := m.store.UpsertSessionWorktree(ctx, row); err != nil { + return fmt.Errorf("save %s: upsert worktree row: %w", rec.ID, err) + } + + // 3. Mark terminal via the LCM (same path Kill uses). + if err := m.lcm.MarkTerminated(ctx, rec.ID); err != nil { + return fmt.Errorf("save %s: mark terminated: %w", rec.ID, err) + } + + // 4. Runtime teardown (best-effort; same pattern as Kill). + handle := runtimeHandle(rec.Metadata) + if handle.ID != "" { + if err := m.runtime.Destroy(ctx, handle); err != nil { + m.logger.Warn("save-teardown-all: runtime destroy failed", "sessionID", rec.ID, "error", err) + } + } + + // 5. Force-remove the worktree (safe: work is captured in step 1 and the + // DB write in step 2 is already committed). + if err := m.workspace.ForceDestroy(ctx, ws); err != nil { + m.logger.Warn("save-teardown-all: force destroy failed", "sessionID", rec.ID, "error", err) + } + return nil +} + +// reconcileLive handles a single non-terminated session on boot. If its runtime +// session is still alive (tmux is the persistence layer, so it survives a daemon +// crash) we adopt it: a no-op, the agent keeps running. If the runtime is gone, +// the agent died with the daemon, so we save-and-tear-down to the SAME end state +// a graceful shutdown produces: capture uncommitted work into a preserve ref, +// record the session_worktrees restore marker, mark terminated, and remove the +// worktree. RestoreAll (which Reconcile runs immediately after) then relaunches +// it on this same boot, resuming history. Crash recovery thus matches graceful +// restart instead of silently abandoning the session. +// +// If the work capture fails we mark terminated WITHOUT a marker and leave the +// worktree intact: better to skip the relaunch than to tear down un-preserved +// work or relaunch onto an inconsistent worktree. +func (m *Manager) reconcileLive(ctx context.Context, rec domain.SessionRecord) error { + if rec.Metadata.WorkspacePath == "" || rec.Metadata.Branch == "" { + return nil + } + handle := runtimeHandle(rec.Metadata) + if handle.ID != "" { + alive, err := m.runtime.IsAlive(ctx, handle) + if err != nil { + // A failed probe is not proof of death: leave the session as-is. + return fmt.Errorf("reconcile %s: probe: %w", rec.ID, err) + } + if alive { + return nil // adopt: the session survived the crash. + } + } + // Runtime is gone: capture uncommitted work first. + ws := workspaceInfo(rec) + ref, err := m.workspace.StashUncommitted(ctx, ws) + if err != nil { + // Could not capture work: do NOT write a restore marker or tear down the + // worktree (that would risk losing un-preserved work). Mark terminated so + // a dead session is not left looking live; the worktree stays put. + m.logger.Warn("reconcile: stash uncommitted failed; terminating without restore marker", "sessionID", rec.ID, "error", err) + if mErr := m.lcm.MarkTerminated(ctx, rec.ID); mErr != nil { + return fmt.Errorf("reconcile %s: mark terminated: %w", rec.ID, mErr) + } + return nil + } + // Work captured. Record the shutdown-saved marker BEFORE tearing down the + // worktree, mirroring saveAndTeardownOne, so RestoreAll relaunches it. + row := domain.SessionWorktreeRecord{ + SessionID: rec.ID, + RepoName: domain.RootWorkspaceRepoName, + Branch: rec.Metadata.Branch, + WorktreePath: rec.Metadata.WorkspacePath, + PreservedRef: ref, + } + if err := m.store.UpsertSessionWorktree(ctx, row); err != nil { + return fmt.Errorf("reconcile %s: upsert worktree marker: %w", rec.ID, err) + } + if err := m.lcm.MarkTerminated(ctx, rec.ID); err != nil { + return fmt.Errorf("reconcile %s: mark terminated: %w", rec.ID, err) + } + // Remove the worktree (work is captured in the ref): RestoreAll re-creates it + // clean and replays the ref. The dead runtime needs no Destroy. + if err := m.workspace.ForceDestroy(ctx, ws); err != nil { + m.logger.Warn("reconcile: force destroy failed after marker", "sessionID", rec.ID, "error", err) + } + return nil +} + +// reconcileReap kills the leaked tmux session of a session the DB already marks +// terminated. This covers the teardown that marked the row terminated but failed +// to kill the runtime (e.g. ForceDestroy/Destroy errored after MarkTerminated). +// Destroy is idempotent, so an already-gone session is a no-op. +func (m *Manager) reconcileReap(ctx context.Context, rec domain.SessionRecord) error { + handle := runtimeHandle(rec.Metadata) + if handle.ID == "" { + return nil + } + alive, err := m.runtime.IsAlive(ctx, handle) + if err != nil { + return fmt.Errorf("reconcile reap %s: probe: %w", rec.ID, err) + } + if !alive { + return nil + } + if err := m.runtime.Destroy(ctx, handle); err != nil { + return fmt.Errorf("reconcile reap %s: destroy: %w", rec.ID, err) + } + return nil +} + +// Reconcile is the boot-time consistency pass. It replaces the bare RestoreAll +// call so that however the previous daemon died (clean shutdown, SIGKILL, or +// crash), live reality matches the DB: +// +// 1. Live pass: for each non-terminated session, adopt it if its runtime +// survived, else capture work and mark terminated (reconcileLive). +// 2. Reap pass: for each terminated session whose runtime leaked, kill it +// (reconcileReap). Runs before restore so a restored session does not +// collide with a leaked tmux of the same name. +// 3. Restore pass: relaunch shutdown-saved sessions (existing RestoreAll). +// +// Best-effort throughout: a per-session failure is logged and never aborts the +// pass or blocks boot. +func (m *Manager) Reconcile(ctx context.Context) error { + recs, err := m.store.ListAllSessions(ctx) + if err != nil { + return fmt.Errorf("reconcile: list sessions: %w", err) + } + for _, rec := range recs { + if rec.IsTerminated { + continue + } + if err := m.reconcileLive(ctx, rec); err != nil { + m.logger.Error("reconcile: live pass failed, skipping", "sessionID", rec.ID, "error", err) + } + } + for _, rec := range recs { + if !rec.IsTerminated { + continue + } + if err := m.reconcileReap(ctx, rec); err != nil { + m.logger.Error("reconcile: reap pass failed, skipping", "sessionID", rec.ID, "error", err) + } + } + return m.RestoreAll(ctx) +} + +// RestoreAll relaunches every terminated session that was saved by the last +// SaveAndTeardownAll. The "shutdown-saved" marker is the presence of a +// session_worktrees row for the session; sessions the user killed before +// shutdown have no such row and are left terminated. +// +// For each saved session: +// 1. Ensure the worktree exists via workspace.Restore. +// 2. If a preserve ref is recorded, replay it via ApplyPreserved; on conflict +// log and continue (still relaunch the agent, never delete the ref). +// 3. Relaunch via the existing Restore method. +// +// Failures on individual sessions are logged and do not abort the loop. +func (m *Manager) RestoreAll(ctx context.Context) error { + recs, err := m.store.ListAllSessions(ctx) + if err != nil { + return fmt.Errorf("restore-all: list sessions: %w", err) + } + for _, rec := range recs { + if !rec.IsTerminated { + continue + } + // Check the shutdown-saved marker: is there a session_worktrees row? + rows, err := m.store.ListSessionWorktrees(ctx, rec.ID) + if err != nil { + m.logger.Error("restore-all: list worktrees failed", "sessionID", rec.ID, "error", err) + continue + } + if len(rows) == 0 { + // No marker: this session was killed by the user before shutdown. + continue + } + + // Collect the preserve ref (may be "" for clean worktrees). + var preserveRef string + for _, r := range rows { + if r.PreservedRef != "" { + preserveRef = r.PreservedRef + break + } + } + + // Step 1: ensure the worktree exists. workspace.Restore re-creates it + // if it was removed by SaveAndTeardownAll. + project, err := m.loadProject(ctx, rec.ProjectID) + if err != nil { + m.logger.Error("restore-all: load project failed", "sessionID", rec.ID, "error", err) + continue + } + ws, err := m.workspace.Restore(ctx, ports.WorkspaceConfig{ + ProjectID: rec.ProjectID, + SessionID: rec.ID, + Kind: rec.Kind, + SessionPrefix: sessionPrefix(project), + Branch: rec.Metadata.Branch, + }) + if err != nil { + m.logger.Error("restore-all: workspace restore failed", "sessionID", rec.ID, "error", err) + continue + } + + // Step 2: replay preserve ref when one was recorded. + if preserveRef != "" { + if applyErr := m.workspace.ApplyPreserved(ctx, ws, preserveRef); applyErr != nil { + if errors.Is(applyErr, ports.ErrPreservedConflict) { + m.logger.Warn("restore-all: apply preserved produced conflicts; agent relaunched with conflict markers in place", + "sessionID", rec.ID, "ref", preserveRef, "error", applyErr) + } else { + m.logger.Error("restore-all: apply preserved failed", "sessionID", rec.ID, "error", applyErr) + } + // Continue: always relaunch even on conflict (never delete the ref here). + } + } + + // Step 3: relaunch via the existing single-session Restore method. + if _, err := m.Restore(ctx, rec.ID); err != nil { + m.logger.Error("restore-all: relaunch failed", "sessionID", rec.ID, "error", err) + } + } + return nil +} + // Send delivers a message to a running session's agent via the messenger. func (m *Manager) Send(ctx context.Context, id domain.SessionID, message string) error { if err := m.messenger.Send(ctx, id, message); err != nil { @@ -931,6 +1232,12 @@ func restoreArgv(ctx context.Context, agent ports.Agent, id domain.SessionID, wo if ok { return cmd, nil } + // The adapter reports no session to resume (no native or derivable session + // id). A saved prompt lets us relaunch fresh; with neither, there is + // genuinely nothing to restore from. + if meta.Prompt == "" { + return nil, ErrNotResumable + } argv, err := agent.GetLaunchCommand(ctx, ports.LaunchConfig{ SessionID: string(id), WorkspacePath: workspacePath, diff --git a/backend/internal/session_manager/manager_test.go b/backend/internal/session_manager/manager_test.go index b7f3dcca..6f23a271 100644 --- a/backend/internal/session_manager/manager_test.go +++ b/backend/internal/session_manager/manager_test.go @@ -24,10 +24,20 @@ type fakeStore struct { projects map[string]domain.ProjectRecord num int deleteErr error + // worktrees maps session ID to its saved worktree rows (shutdown-saved marker). + worktrees map[domain.SessionID][]domain.SessionWorktreeRecord + // sharedLog, when non-nil, receives an ordered call entry for each + // UpsertSessionWorktree invocation so ordering tests can compare across fakes. + sharedLog *[]string } func newFakeStore() *fakeStore { - return &fakeStore{sessions: map[domain.SessionID]domain.SessionRecord{}, pr: map[domain.SessionID]domain.PRFacts{}, projects: map[string]domain.ProjectRecord{}} + return &fakeStore{ + sessions: map[domain.SessionID]domain.SessionRecord{}, + pr: map[domain.SessionID]domain.PRFacts{}, + projects: map[string]domain.ProjectRecord{}, + worktrees: map[domain.SessionID][]domain.SessionWorktreeRecord{}, + } } func (f *fakeStore) GetProject(_ context.Context, id string) (domain.ProjectRecord, bool, error) { r, ok := f.projects[id] @@ -84,10 +94,30 @@ func (f *fakeStore) GetDisplayPRFactsForSession(_ context.Context, id domain.Ses } return domain.PRFacts{}, false, nil } +func (f *fakeStore) UpsertSessionWorktree(_ context.Context, row domain.SessionWorktreeRecord) error { + if f.sharedLog != nil { + *f.sharedLog = append(*f.sharedLog, "UpsertSessionWorktree:"+string(row.SessionID)) + } + rows := f.worktrees[row.SessionID] + for i, r := range rows { + if r.RepoName == row.RepoName { + rows[i] = row + f.worktrees[row.SessionID] = rows + return nil + } + } + f.worktrees[row.SessionID] = append(rows, row) + return nil +} +func (f *fakeStore) ListSessionWorktrees(_ context.Context, id domain.SessionID) ([]domain.SessionWorktreeRecord, error) { + return f.worktrees[id], nil +} type fakeLCM struct { store *fakeStore completed int + // terminated counts MarkTerminated calls per session id. + terminated map[domain.SessionID]int } func (l *fakeLCM) MarkSpawned(_ context.Context, id domain.SessionID, metadata domain.SessionMetadata) error { @@ -100,6 +130,10 @@ func (l *fakeLCM) MarkSpawned(_ context.Context, id domain.SessionID, metadata d return nil } func (l *fakeLCM) MarkTerminated(_ context.Context, id domain.SessionID) error { + if l.terminated == nil { + l.terminated = map[domain.SessionID]int{} + } + l.terminated[id]++ rec := l.store.sessions[id] rec.IsTerminated = true rec.Activity = domain.Activity{State: domain.ActivityExited, LastActivityAt: time.Now()} @@ -111,6 +145,10 @@ type fakeRuntime struct { createErr error created, destroyed int lastCfg ports.RuntimeConfig + // aliveByHandle maps a RuntimeHandle.ID to its liveness; missing = false. + aliveByHandle map[string]bool + aliveErr error + destroyedIDs []string } func (r *fakeRuntime) Create(_ context.Context, cfg ports.RuntimeConfig) (ports.RuntimeHandle, error) { @@ -121,7 +159,17 @@ func (r *fakeRuntime) Create(_ context.Context, cfg ports.RuntimeConfig) (ports. r.created++ return ports.RuntimeHandle{ID: "h1"}, nil } -func (r *fakeRuntime) Destroy(context.Context, ports.RuntimeHandle) error { r.destroyed++; return nil } +func (r *fakeRuntime) Destroy(_ context.Context, handle ports.RuntimeHandle) error { + r.destroyed++ + r.destroyedIDs = append(r.destroyedIDs, handle.ID) + return nil +} +func (r *fakeRuntime) IsAlive(_ context.Context, handle ports.RuntimeHandle) (bool, error) { + if r.aliveErr != nil { + return false, r.aliveErr + } + return r.aliveByHandle[handle.ID], nil +} type fakeAgent struct{} @@ -180,6 +228,15 @@ type singleAgent struct{ agent ports.Agent } func (s singleAgent) Agent(domain.AgentHarness) (ports.Agent, bool) { return s.agent, true } +// alwaysResumeAgent mimics Claude Code: it pins a deterministic session id, so +// GetRestoreCommand can resume any session even with no captured agentSessionId +// and no prompt. +type alwaysResumeAgent struct{ fakeAgent } + +func (alwaysResumeAgent) GetRestoreCommand(_ context.Context, cfg ports.RestoreConfig) ([]string, bool, error) { + return []string{"resume", cfg.Session.ID}, true, nil +} + // missingAgents resolves no harness, simulating a typo'd or unregistered agent. type missingAgents struct{} @@ -193,6 +250,18 @@ type fakeWorkspace struct { // path, when set, is returned as the workspace path so provisioning tests // can point at a real temp directory. path string + // stashRef is returned by StashUncommitted (empty means clean worktree). + stashRef string + stashErr error + applyErr error + forceDestroyErr error + // stashCalls counts StashUncommitted invocations. + stashCalls int + // calls records the sequence of workspace method calls for ordering assertions. + calls []string + // sharedLog, when non-nil, receives entries alongside calls so ordering + // tests can compare workspace calls against store calls in one sequence. + sharedLog *[]string } func (w *fakeWorkspace) Create(_ context.Context, cfg ports.WorkspaceConfig) (ports.WorkspaceInfo, error) { @@ -213,6 +282,27 @@ func (w *fakeWorkspace) Destroy(context.Context, ports.WorkspaceInfo) error { func (w *fakeWorkspace) Restore(ctx context.Context, cfg ports.WorkspaceConfig) (ports.WorkspaceInfo, error) { return w.Create(ctx, cfg) } +func (w *fakeWorkspace) ForceDestroy(_ context.Context, info ports.WorkspaceInfo) error { + entry := "ForceDestroy:" + string(info.SessionID) + w.calls = append(w.calls, entry) + if w.sharedLog != nil { + *w.sharedLog = append(*w.sharedLog, entry) + } + return w.forceDestroyErr +} +func (w *fakeWorkspace) StashUncommitted(_ context.Context, info ports.WorkspaceInfo) (string, error) { + w.stashCalls++ + entry := "StashUncommitted:" + string(info.SessionID) + w.calls = append(w.calls, entry) + if w.sharedLog != nil { + *w.sharedLog = append(*w.sharedLog, entry) + } + return w.stashRef, w.stashErr +} +func (w *fakeWorkspace) ApplyPreserved(_ context.Context, info ports.WorkspaceInfo, ref string) error { + w.calls = append(w.calls, "ApplyPreserved:"+string(info.SessionID)) + return w.applyErr +} type fakeMessenger struct{ msgs []string } @@ -819,6 +909,54 @@ func TestRestore_FallbackLaunchCarriesSystemPrompt(t *testing.T) { } } +// TestRestore_PromptlessOrchestratorResumesViaAdapter locks the orchestrator +// fix: a promptless session with no captured agentSessionId is still restorable +// when the adapter can resume it (Claude pins a deterministic --session-id). +// Before the fix the metadata-only guard rejected it with ErrNotResumable, so +// every boot abandoned the orchestrator and spawned a fresh one. +func TestRestore_PromptlessOrchestratorResumesViaAdapter(t *testing.T) { + st := newFakeStore() + st.sessions["mer-1"] = domain.SessionRecord{ + ID: "mer-1", ProjectID: "mer", Kind: domain.KindOrchestrator, IsTerminated: true, + // No AgentSessionID, no Prompt: exactly how orchestrators are persisted. + Metadata: domain.SessionMetadata{WorkspacePath: "/ws/mer-1", Branch: "ao/mer-orchestrator"}, + Activity: domain.Activity{State: domain.ActivityExited}, + } + rt := &fakeRuntime{} + lookPath := func(string) (string, error) { return "/bin/true", nil } + m := New(Deps{Runtime: rt, Agents: singleAgent{agent: alwaysResumeAgent{}}, Workspace: &fakeWorkspace{}, Store: st, Messenger: &fakeMessenger{}, Lifecycle: &fakeLCM{store: st}, LookPath: lookPath}) + + if _, err := m.Restore(ctx, "mer-1"); err != nil { + t.Fatalf("promptless orchestrator must restore via adapter resume, got err = %v", err) + } + if rt.created != 1 { + t.Fatalf("runtime.Create = %d, want 1 (resumed)", rt.created) + } + if st.sessions["mer-1"].IsTerminated { + t.Error("orchestrator must be live after restore") + } +} + +// TestRestore_RefusesPromptlessWhenAdapterCannotResume preserves the typed +// error: a promptless session whose adapter cannot resume (no native session id) +// has genuinely nothing to relaunch from and must still return ErrNotResumable. +func TestRestore_RefusesPromptlessWhenAdapterCannotResume(t *testing.T) { + st := newFakeStore() + st.sessions["mer-1"] = domain.SessionRecord{ + ID: "mer-1", ProjectID: "mer", Kind: domain.KindWorker, IsTerminated: true, + Metadata: domain.SessionMetadata{WorkspacePath: "/ws/mer-1", Branch: "ao/mer-1/root"}, + Activity: domain.Activity{State: domain.ActivityExited}, + } + lookPath := func(string) (string, error) { return "/bin/true", nil } + // fakeAgents resolves to fakeAgent, whose GetRestoreCommand returns ok=false + // without an agentSessionId. + m := New(Deps{Runtime: &fakeRuntime{}, Agents: fakeAgents{}, Workspace: &fakeWorkspace{}, Store: st, Messenger: &fakeMessenger{}, Lifecycle: &fakeLCM{store: st}, LookPath: lookPath}) + + if _, err := m.Restore(ctx, "mer-1"); !errors.Is(err, ErrNotResumable) { + t.Fatalf("Restore err = %v, want ErrNotResumable", err) + } +} + // TestRestore_WorkerPointsAtCurrentOrchestrator: a restored worker's // coordination hint must reference the orchestrator active at restore time, // not the one from its original spawn. @@ -1076,3 +1214,509 @@ func TestSpawn_KeepsExplicitBranch(t *testing.T) { t.Fatalf("explicit branch = %q, want feature/x", got) } } + +// ---- SaveAndTeardownAll / RestoreAll tests ---- + +// newLifecycleManager builds a manager wired with a recording workspace fake +// for the shutdown lifecycle tests. +func newLifecycleManager() (*Manager, *fakeStore, *fakeRuntime, *fakeWorkspace) { + st := newFakeStore() + st.projects["mer"] = domain.ProjectRecord{ID: "mer", Config: testRoleAgents()} + rt := &fakeRuntime{} + ws := &fakeWorkspace{} + lookPath := func(string) (string, error) { return "/bin/true", nil } + m := New(Deps{ + Runtime: rt, + Agents: fakeAgents{}, + Workspace: ws, + Store: st, + Messenger: &fakeMessenger{}, + Lifecycle: &fakeLCM{store: st}, + LookPath: lookPath, + }) + return m, st, rt, ws +} + +// TestSaveAndTeardownAll_CaptureOrderAndMarker verifies (a): for a live session +// with a workspace, SaveAndTeardownAll must call StashUncommitted BEFORE +// UpsertSessionWorktree (writing preserved_ref) BEFORE ForceDestroy. +func TestSaveAndTeardownAll_CaptureOrderAndMarker(t *testing.T) { + m, st, _, ws := newLifecycleManager() + + // Wire a shared ordered call log so we can assert cross-fake ordering: + // both fakeStore and fakeWorkspace append to the same slice. + var sharedLog []string + st.sharedLog = &sharedLog + ws.sharedLog = &sharedLog + + // A live session with a workspace path and runtime handle. + ws.stashRef = "refs/ao/preserved/mer-1" + st.sessions["mer-1"] = domain.SessionRecord{ + ID: "mer-1", + ProjectID: "mer", + Kind: domain.KindWorker, + Metadata: domain.SessionMetadata{WorkspacePath: "/ws/mer-1", Branch: "ao/mer-1/root", RuntimeHandleID: "h1"}, + Activity: domain.Activity{State: domain.ActivityActive}, + } + + if err := m.SaveAndTeardownAll(ctx); err != nil { + t.Fatalf("SaveAndTeardownAll err = %v", err) + } + + // Stash must come before ForceDestroy in the call log. + stashIdx, forceIdx := -1, -1 + for i, c := range ws.calls { + if c == "StashUncommitted:mer-1" { + stashIdx = i + } + if c == "ForceDestroy:mer-1" { + forceIdx = i + } + } + if stashIdx == -1 { + t.Fatal("StashUncommitted was not called") + } + if forceIdx == -1 { + t.Fatal("ForceDestroy was not called") + } + if stashIdx >= forceIdx { + t.Fatalf("StashUncommitted (call %d) must come before ForceDestroy (call %d)", stashIdx, forceIdx) + } + + // UpsertSessionWorktree (DB write) must be committed BEFORE ForceDestroy. + // Use the shared ordered log to compare positions across the store and workspace. + upsertIdx, sharedForceIdx := -1, -1 + for i, c := range sharedLog { + if c == "UpsertSessionWorktree:mer-1" { + upsertIdx = i + } + if c == "ForceDestroy:mer-1" { + sharedForceIdx = i + } + } + if upsertIdx == -1 { + t.Fatal("UpsertSessionWorktree was not called") + } + if sharedForceIdx == -1 { + t.Fatal("ForceDestroy was not recorded in shared log") + } + if upsertIdx >= sharedForceIdx { + t.Fatalf("UpsertSessionWorktree (pos %d) must come before ForceDestroy (pos %d) in shared call log %v", upsertIdx, sharedForceIdx, sharedLog) + } + + // DB write (UpsertSessionWorktree) must have recorded the correct row. + rows := st.worktrees["mer-1"] + if len(rows) == 0 { + t.Fatal("UpsertSessionWorktree was not called: no worktree row for mer-1") + } + if rows[0].PreservedRef != "refs/ao/preserved/mer-1" { + t.Fatalf("preserved_ref = %q, want refs/ao/preserved/mer-1", rows[0].PreservedRef) + } + + // The session must be marked terminated. + if !st.sessions["mer-1"].IsTerminated { + t.Fatal("session must be terminated after SaveAndTeardownAll") + } +} + +// TestSaveAndTeardownAll_CleanWorktreeWritesEmptyRef verifies that a clean +// worktree (StashUncommitted returns "") still writes a worktree row (with +// empty preserved_ref). The row's presence is the shutdown-saved marker. +func TestSaveAndTeardownAll_CleanWorktreeWritesEmptyRef(t *testing.T) { + m, st, _, ws := newLifecycleManager() + ws.stashRef = "" // clean worktree + st.sessions["mer-1"] = domain.SessionRecord{ + ID: "mer-1", + ProjectID: "mer", + Kind: domain.KindWorker, + Metadata: domain.SessionMetadata{WorkspacePath: "/ws/mer-1", Branch: "ao/mer-1/root", RuntimeHandleID: "h1"}, + Activity: domain.Activity{State: domain.ActivityActive}, + } + + if err := m.SaveAndTeardownAll(ctx); err != nil { + t.Fatalf("SaveAndTeardownAll err = %v", err) + } + + rows := st.worktrees["mer-1"] + if len(rows) == 0 { + t.Fatal("clean worktree must still write a session_worktrees row as the shutdown-saved marker") + } + if rows[0].PreservedRef != "" { + t.Fatalf("preserved_ref = %q, want empty for clean worktree", rows[0].PreservedRef) + } +} + +// TestSaveAndTeardownAll_SkipsNoWorkspacePath: sessions without a workspace +// path are skipped (spawn failed before workspace.Create). +func TestSaveAndTeardownAll_SkipsNoWorkspacePath(t *testing.T) { + m, st, _, ws := newLifecycleManager() + st.sessions["mer-1"] = domain.SessionRecord{ + ID: "mer-1", + ProjectID: "mer", + Kind: domain.KindWorker, + Metadata: domain.SessionMetadata{}, // no workspace path + Activity: domain.Activity{State: domain.ActivityActive}, + } + + if err := m.SaveAndTeardownAll(ctx); err != nil { + t.Fatalf("SaveAndTeardownAll err = %v", err) + } + + if len(ws.calls) != 0 { + t.Fatalf("no workspace calls expected for sessions with no workspace path, got %v", ws.calls) + } + if len(st.worktrees["mer-1"]) != 0 { + t.Fatal("no worktree row should be written for sessions with no workspace path") + } +} + +// TestSaveAndTeardownAll_SkipsAlreadyTerminated: already-terminated sessions +// are skipped. +func TestSaveAndTeardownAll_SkipsAlreadyTerminated(t *testing.T) { + m, st, _, ws := newLifecycleManager() + st.sessions["mer-1"] = domain.SessionRecord{ + ID: "mer-1", + ProjectID: "mer", + Kind: domain.KindWorker, + IsTerminated: true, + Metadata: domain.SessionMetadata{WorkspacePath: "/ws/mer-1", Branch: "ao/mer-1/root"}, + Activity: domain.Activity{State: domain.ActivityExited}, + } + + if err := m.SaveAndTeardownAll(ctx); err != nil { + t.Fatalf("SaveAndTeardownAll err = %v", err) + } + if len(ws.calls) != 0 { + t.Fatalf("already-terminated sessions must be skipped, got calls %v", ws.calls) + } +} + +// TestSaveAndTeardownAll_NoKindFilter: both worker and orchestrator sessions +// are saved (no kind filter). +func TestSaveAndTeardownAll_NoKindFilter(t *testing.T) { + m, st, _, _ := newLifecycleManager() + st.sessions["mer-1"] = domain.SessionRecord{ + ID: "mer-1", ProjectID: "mer", Kind: domain.KindWorker, + Metadata: domain.SessionMetadata{WorkspacePath: "/ws/mer-1", Branch: "ao/mer-1/root", RuntimeHandleID: "h1"}, + Activity: domain.Activity{State: domain.ActivityActive}, + } + st.sessions["mer-2"] = domain.SessionRecord{ + ID: "mer-2", ProjectID: "mer", Kind: domain.KindOrchestrator, + Metadata: domain.SessionMetadata{WorkspacePath: "/ws/mer-2", Branch: "ao/mer-orchestrator", RuntimeHandleID: "h2"}, + Activity: domain.Activity{State: domain.ActivityActive}, + } + + if err := m.SaveAndTeardownAll(ctx); err != nil { + t.Fatalf("SaveAndTeardownAll err = %v", err) + } + + if len(st.worktrees["mer-1"]) == 0 { + t.Error("worker session mer-1 must be saved") + } + if len(st.worktrees["mer-2"]) == 0 { + t.Error("orchestrator session mer-2 must be saved") + } + if !st.sessions["mer-1"].IsTerminated { + t.Error("worker session mer-1 must be terminated") + } + if !st.sessions["mer-2"].IsTerminated { + t.Error("orchestrator session mer-2 must be terminated") + } +} + +// TestRestoreAll_RestoresBothWorkerAndOrchestrator verifies (b): RestoreAll +// restores both a worker and an orchestrator session saved by SaveAndTeardownAll. +func TestRestoreAll_RestoresBothWorkerAndOrchestrator(t *testing.T) { + m, st, rt, _ := newLifecycleManager() + + // Seed two terminated sessions that were saved by SaveAndTeardownAll + // (presence of session_worktrees rows is the marker). + st.sessions["mer-1"] = domain.SessionRecord{ + ID: "mer-1", + ProjectID: "mer", + Kind: domain.KindWorker, + Harness: domain.HarnessClaudeCode, + IsTerminated: true, + Metadata: domain.SessionMetadata{WorkspacePath: "/ws/mer-1", Branch: "ao/mer-1/root", AgentSessionID: "agent-w"}, + Activity: domain.Activity{State: domain.ActivityExited}, + } + st.sessions["mer-2"] = domain.SessionRecord{ + ID: "mer-2", + ProjectID: "mer", + Kind: domain.KindOrchestrator, + Harness: domain.HarnessClaudeCode, + IsTerminated: true, + Metadata: domain.SessionMetadata{WorkspacePath: "/ws/mer-2", Branch: "ao/mer-orchestrator", AgentSessionID: "agent-o"}, + Activity: domain.Activity{State: domain.ActivityExited}, + } + // Write the shutdown-saved marker rows. + st.worktrees["mer-1"] = []domain.SessionWorktreeRecord{{SessionID: "mer-1", RepoName: "__root__", PreservedRef: ""}} + st.worktrees["mer-2"] = []domain.SessionWorktreeRecord{{SessionID: "mer-2", RepoName: "__root__", PreservedRef: ""}} + + if err := m.RestoreAll(ctx); err != nil { + t.Fatalf("RestoreAll err = %v", err) + } + + if rt.created != 2 { + t.Fatalf("RestoreAll must relaunch both sessions, runtime.Create called %d times", rt.created) + } + if st.sessions["mer-1"].IsTerminated { + t.Error("worker session mer-1 must be live after RestoreAll") + } + if st.sessions["mer-2"].IsTerminated { + t.Error("orchestrator session mer-2 must be live after RestoreAll") + } +} + +// TestRestoreAll_SkipsSessionsKilledBeforeShutdown verifies (c): a session +// the user killed BEFORE shutdown has no session_worktrees row and must NOT +// be resurrected. +func TestRestoreAll_SkipsSessionsKilledBeforeShutdown(t *testing.T) { + m, st, rt, _ := newLifecycleManager() + + // This session was killed by the user before shutdown: IsTerminated=true, + // but no session_worktrees row (SaveAndTeardownAll skipped it). + st.sessions["mer-1"] = domain.SessionRecord{ + ID: "mer-1", + ProjectID: "mer", + Kind: domain.KindWorker, + Harness: domain.HarnessClaudeCode, + IsTerminated: true, + Metadata: domain.SessionMetadata{WorkspacePath: "/ws/mer-1", Branch: "ao/mer-1/root", Prompt: "do it"}, + Activity: domain.Activity{State: domain.ActivityExited}, + } + // Deliberately no entry in st.worktrees for mer-1. + + if err := m.RestoreAll(ctx); err != nil { + t.Fatalf("RestoreAll err = %v", err) + } + + if rt.created != 0 { + t.Fatalf("user-killed session must not be restored, runtime.Create called %d times", rt.created) + } + if !st.sessions["mer-1"].IsTerminated { + t.Error("user-killed session must remain terminated") + } +} + +// TestRestoreAll_AppliesPreservedRef: when the session_worktrees row has a +// non-empty preserved_ref, RestoreAll calls ApplyPreserved after workspace +// restore but before relaunching. +func TestRestoreAll_AppliesPreservedRef(t *testing.T) { + m, st, rt, ws := newLifecycleManager() + + st.sessions["mer-1"] = domain.SessionRecord{ + ID: "mer-1", + ProjectID: "mer", + Kind: domain.KindWorker, + Harness: domain.HarnessClaudeCode, + IsTerminated: true, + Metadata: domain.SessionMetadata{WorkspacePath: "/ws/mer-1", Branch: "ao/mer-1/root", AgentSessionID: "agent-w"}, + Activity: domain.Activity{State: domain.ActivityExited}, + } + st.worktrees["mer-1"] = []domain.SessionWorktreeRecord{ + {SessionID: "mer-1", RepoName: "__root__", PreservedRef: "refs/ao/preserved/mer-1"}, + } + + if err := m.RestoreAll(ctx); err != nil { + t.Fatalf("RestoreAll err = %v", err) + } + + applied := false + for _, c := range ws.calls { + if c == "ApplyPreserved:mer-1" { + applied = true + } + } + if !applied { + t.Fatal("ApplyPreserved was not called for session with preserved_ref") + } + if rt.created != 1 { + t.Fatal("session must still be relaunched even after ApplyPreserved") + } +} + +// TestRestoreAll_ConflictLogsAndContinues: when ApplyPreserved returns +// ErrPreservedConflict, RestoreAll logs and continues (still relaunches). +func TestRestoreAll_ConflictLogsAndContinues(t *testing.T) { + st := newFakeStore() + st.projects["mer"] = domain.ProjectRecord{ID: "mer", Config: testRoleAgents()} + rt := &fakeRuntime{} + ws := &fakeWorkspace{applyErr: fmt.Errorf("conflict: %w", ports.ErrPreservedConflict)} + var logBuf bytes.Buffer + lookPath := func(string) (string, error) { return "/bin/true", nil } + m := New(Deps{ + Runtime: rt, + Agents: fakeAgents{}, + Workspace: ws, + Store: st, + Messenger: &fakeMessenger{}, + Lifecycle: &fakeLCM{store: st}, + LookPath: lookPath, + Logger: slog.New(slog.NewTextHandler(&logBuf, nil)), + }) + + st.sessions["mer-1"] = domain.SessionRecord{ + ID: "mer-1", + ProjectID: "mer", + Kind: domain.KindWorker, + Harness: domain.HarnessClaudeCode, + IsTerminated: true, + Metadata: domain.SessionMetadata{WorkspacePath: "/ws/mer-1", Branch: "ao/mer-1/root", AgentSessionID: "agent-w"}, + Activity: domain.Activity{State: domain.ActivityExited}, + } + st.worktrees["mer-1"] = []domain.SessionWorktreeRecord{ + {SessionID: "mer-1", RepoName: "__root__", PreservedRef: "refs/ao/preserved/mer-1"}, + } + + if err := m.RestoreAll(ctx); err != nil { + t.Fatalf("RestoreAll err = %v; conflict must not abort", err) + } + if rt.created != 1 { + t.Fatalf("session must still relaunch after conflict, runtime.Create called %d times", rt.created) + } +} + +func TestReconcileLive_DeadSessionStashedAndTerminated(t *testing.T) { + st := newFakeStore() + rt := &fakeRuntime{aliveByHandle: map[string]bool{}} // handle not alive + ws := &fakeWorkspace{stashRef: "refs/ao/preserved/s1"} + lcm := &fakeLCM{store: st} + lookPath := func(string) (string, error) { return "/bin/true", nil } + m := New(Deps{Runtime: rt, Agents: fakeAgents{}, Workspace: ws, Store: st, Messenger: &fakeMessenger{}, Lifecycle: lcm, LookPath: lookPath}) + + rec := domain.SessionRecord{ + ID: "s1", + ProjectID: "p1", + IsTerminated: false, + Metadata: domain.SessionMetadata{ + Branch: "ao/s1/root", WorkspacePath: "/wt/s1", RuntimeHandleID: "s1", + }, + } + + if err := m.reconcileLive(context.Background(), rec); err != nil { + t.Fatalf("reconcileLive: %v", err) + } + if ws.stashCalls != 1 { + t.Fatalf("StashUncommitted calls = %d, want 1", ws.stashCalls) + } + if lcm.terminated["s1"] != 1 { + t.Fatalf("MarkTerminated(s1) = %d, want 1", lcm.terminated["s1"]) + } + if rt.destroyed != 0 { + t.Fatalf("Destroy calls = %d, want 0 (dead session: no tmux to kill)", rt.destroyed) + } + // The crash-orphaned session must be saved for restore, exactly like a + // graceful shutdown: a session_worktrees marker carrying the preserve ref, + // and the worktree torn down so RestoreAll re-creates it clean. + rows := st.worktrees["s1"] + if len(rows) != 1 || rows[0].PreservedRef != "refs/ao/preserved/s1" { + t.Fatalf("session_worktrees marker for s1 = %+v, want one row with the preserve ref", rows) + } + foundForceDestroy := false + for _, c := range ws.calls { + if c == "ForceDestroy:s1" { + foundForceDestroy = true + } + } + if !foundForceDestroy { + t.Fatalf("reconcileLive must ForceDestroy the worktree after capturing work; calls = %v", ws.calls) + } +} + +func TestReconcileLive_AliveSessionAdoptedNoop(t *testing.T) { + st := newFakeStore() + rt := &fakeRuntime{aliveByHandle: map[string]bool{"s2": true}} + ws := &fakeWorkspace{} + lcm := &fakeLCM{store: st} + lookPath := func(string) (string, error) { return "/bin/true", nil } + m := New(Deps{Runtime: rt, Agents: fakeAgents{}, Workspace: ws, Store: st, Messenger: &fakeMessenger{}, Lifecycle: lcm, LookPath: lookPath}) + + rec := domain.SessionRecord{ + ID: "s2", ProjectID: "p1", IsTerminated: false, + Metadata: domain.SessionMetadata{Branch: "ao/s2/root", WorkspacePath: "/wt/s2", RuntimeHandleID: "s2"}, + } + + if err := m.reconcileLive(context.Background(), rec); err != nil { + t.Fatalf("reconcileLive: %v", err) + } + if ws.stashCalls != 0 || lcm.terminated["s2"] != 0 || rt.destroyed != 0 { + t.Fatalf("adopt should be a no-op: stash=%d term=%d destroy=%d", ws.stashCalls, lcm.terminated["s2"], rt.destroyed) + } +} + +// TestReconcileLive_ProbeErrorIsNotDeath locks the invariant that a failed +// IsAlive probe is NOT treated as proof that the session is dead. reconcileLive +// must propagate the error and must NOT stash, terminate, or destroy. +func TestReconcileLive_ProbeErrorIsNotDeath(t *testing.T) { + st := newFakeStore() + rt := &fakeRuntime{aliveErr: errors.New("probe boom")} + ws := &fakeWorkspace{} + lcm := &fakeLCM{store: st} + lookPath := func(string) (string, error) { return "/bin/true", nil } + m := New(Deps{Runtime: rt, Agents: fakeAgents{}, Workspace: ws, Store: st, Messenger: &fakeMessenger{}, Lifecycle: lcm, LookPath: lookPath}) + + rec := domain.SessionRecord{ + ID: "s3", + ProjectID: "p1", + IsTerminated: false, + Metadata: domain.SessionMetadata{ + Branch: "ao/s3/root", WorkspacePath: "/wt/s3", RuntimeHandleID: "s3", + }, + } + + err := m.reconcileLive(context.Background(), rec) + if err == nil { + t.Fatal("reconcileLive: expected non-nil error on probe failure, got nil") + } + if ws.stashCalls != 0 { + t.Fatalf("StashUncommitted calls = %d, want 0 (probe error is not death)", ws.stashCalls) + } + if lcm.terminated["s3"] != 0 { + t.Fatalf("MarkTerminated(s3) = %d, want 0 (probe error is not death)", lcm.terminated["s3"]) + } + if rt.destroyed != 0 { + t.Fatalf("Destroy calls = %d, want 0 (probe error is not death)", rt.destroyed) + } +} + +func TestReconcileReap_TerminatedButAliveTmuxDestroyed(t *testing.T) { + st := newFakeStore() + rt := &fakeRuntime{aliveByHandle: map[string]bool{"t1": true}} + ws := &fakeWorkspace{} + lcm := &fakeLCM{store: st} + lookPath := func(string) (string, error) { return "/bin/true", nil } + m := New(Deps{Runtime: rt, Agents: fakeAgents{}, Workspace: ws, Store: st, Messenger: &fakeMessenger{}, Lifecycle: lcm, LookPath: lookPath}) + + rec := domain.SessionRecord{ + ID: "t1", ProjectID: "p1", IsTerminated: true, + Metadata: domain.SessionMetadata{RuntimeHandleID: "t1"}, + } + + if err := m.reconcileReap(context.Background(), rec); err != nil { + t.Fatalf("reconcileReap: %v", err) + } + if len(rt.destroyedIDs) != 1 || rt.destroyedIDs[0] != "t1" { + t.Fatalf("destroyedIDs = %v, want [t1]", rt.destroyedIDs) + } +} + +func TestReconcileReap_TerminatedAndDeadTmuxLeftAlone(t *testing.T) { + st := newFakeStore() + rt := &fakeRuntime{aliveByHandle: map[string]bool{}} // t2 not alive + ws := &fakeWorkspace{} + lcm := &fakeLCM{store: st} + lookPath := func(string) (string, error) { return "/bin/true", nil } + m := New(Deps{Runtime: rt, Agents: fakeAgents{}, Workspace: ws, Store: st, Messenger: &fakeMessenger{}, Lifecycle: lcm, LookPath: lookPath}) + + rec := domain.SessionRecord{ + ID: "t2", ProjectID: "p1", IsTerminated: true, + Metadata: domain.SessionMetadata{RuntimeHandleID: "t2"}, + } + if err := m.reconcileReap(context.Background(), rec); err != nil { + t.Fatalf("reconcileReap: %v", err) + } + if rt.destroyed != 0 { + t.Fatalf("Destroy calls = %d, want 0", rt.destroyed) + } +} diff --git a/backend/internal/storage/sqlite/store/session_worktree_store.go b/backend/internal/storage/sqlite/store/session_worktree_store.go index 04cba5cc..9b6e1a14 100644 --- a/backend/internal/storage/sqlite/store/session_worktree_store.go +++ b/backend/internal/storage/sqlite/store/session_worktree_store.go @@ -14,6 +14,16 @@ import ( func (s *Store) UpsertSessionWorktree(ctx context.Context, row domain.SessionWorktreeRecord) error { s.writeMu.Lock() defer s.writeMu.Unlock() + // ponytail: session_worktrees.state is unused multi-repo scaffolding; no + // live code path sets domain.SessionWorktreeRecord.State, so it arrives + // here as "". The generated upsert includes state in the INSERT column list + // and the CHECK constraint rejects "". Default to 'active' (the column + // default) so the row stays valid without touching the schema or gen code. + // Wire a real value when multi-repo worktree lifecycle states ship. + state := row.State + if state == "" { + state = "active" + } return s.qw.UpsertSessionWorktree(ctx, gen.UpsertSessionWorktreeParams{ SessionID: row.SessionID, RepoName: row.RepoName, @@ -21,7 +31,7 @@ func (s *Store) UpsertSessionWorktree(ctx context.Context, row domain.SessionWor BaseSha: row.BaseSHA, WorktreePath: row.WorktreePath, PreservedRef: row.PreservedRef, - State: row.State, + State: state, }) } @@ -65,6 +75,8 @@ func sessionWorktreeFromGen(row gen.SessionWorktree) domain.SessionWorktreeRecor BaseSHA: row.BaseSha, WorktreePath: row.WorktreePath, PreservedRef: row.PreservedRef, - State: row.State, + // ponytail: state is read back from the DB but no caller uses it; + // it is unused multi-repo scaffolding (see UpsertSessionWorktree above). + State: row.State, } } diff --git a/backend/internal/storage/sqlite/store/store_test.go b/backend/internal/storage/sqlite/store/store_test.go index 830ca88a..c6a56477 100644 --- a/backend/internal/storage/sqlite/store/store_test.go +++ b/backend/internal/storage/sqlite/store/store_test.go @@ -783,3 +783,41 @@ func TestSessionWorktreesRoundTrip(t *testing.T) { t.Fatalf("after delete = %#v err=%v", got, err) } } + +// TestUpsertSessionWorktreeEmptyStateDefaultsToActive exercises the guard in +// UpsertSessionWorktree: when State is left at its zero value "", the store +// must default it to "active" so the SQLite CHECK constraint is satisfied. +// Without the guard, the generated upsert would insert "" and the CHECK would +// reject it. This test catches any regression that removes that guard. +func TestUpsertSessionWorktreeEmptyStateDefaultsToActive(t *testing.T) { + s := newTestStore(t) + ctx := context.Background() + seedProject(t, s, "sw") + rec, err := s.CreateSession(ctx, sampleRecord("sw")) + if err != nil { + t.Fatalf("create session: %v", err) + } + + // State is intentionally left at zero value "" to exercise the guard. + row := domain.SessionWorktreeRecord{ + SessionID: rec.ID, + RepoName: domain.RootWorkspaceRepoName, + Branch: "ao/sw-1", + BaseSHA: "abc123", + WorktreePath: "/managed/sw/sw-1", + } + if err := s.UpsertSessionWorktree(ctx, row); err != nil { + t.Fatalf("upsert with empty State: %v", err) + } + + got, ok, err := s.GetSessionWorktree(ctx, rec.ID, domain.RootWorkspaceRepoName) + if err != nil { + t.Fatalf("get worktree: %v", err) + } + if !ok { + t.Fatal("worktree row not found after upsert") + } + if got.State != "active" { + t.Fatalf("State = %q, want %q", got.State, "active") + } +} diff --git a/backend/internal/terminal/attachment.go b/backend/internal/terminal/attachment.go index e45bca6c..35268989 100644 --- a/backend/internal/terminal/attachment.go +++ b/backend/internal/terminal/attachment.go @@ -3,7 +3,6 @@ package terminal import ( "context" "errors" - "io" "log/slog" "sync" "time" @@ -11,38 +10,17 @@ import ( "github.com/aoagents/agent-orchestrator/backend/internal/ports" ) -// PTYSource is what a terminal needs from the runtime: the argv that attaches a -// PTY to a session's pane (plus any env that argv needs but is not in the -// daemon's process env — e.g. a per-session ZELLIJ_SOCKET_DIR on Windows), -// and a liveness check used to decide whether a dropped PTY should be -// re-attached or treated as a clean exit. The Zellij runtime adapter -// satisfies this via AttachCommand/IsAlive; the interface lives here, next to -// its only consumer, so terminal does not depend on a concrete adapter. -type PTYSource interface { - AttachCommand(handle ports.RuntimeHandle) (argv []string, env []string, err error) +// Source is what the terminal needs from the runtime: open an attach Stream and +// a liveness check used to decide whether a dropped Stream should be re-attached +// or treated as a clean exit. The runtime adapters (tmux/zellij/conpty) satisfy +// it via Attach/IsAlive; the interface lives here, next to its only consumer, so +// terminal does not depend on a concrete adapter. +type Source interface { + ports.Attacher IsAlive(ctx context.Context, handle ports.RuntimeHandle) (bool, error) } -// ptyProcess is a started PTY-backed attach process. It is the injection seam -// that keeps the attach loop testable without a real process: unit tests supply -// a scripted in-memory implementation; production uses a creack/pty-backed one -// (see pty_unix.go). -type ptyProcess interface { - io.ReadWriteCloser - Resize(rows, cols uint16) error -} - -// spawnFunc starts a PTY for argv, sized rows×cols from the start (zero means -// no size was recorded yet — kernel default). Spawning at the client's grid -// matters: the attach process reads the tty size once at startup, and sizing -// the PTY only after exec relies on SIGWINCH delivery that can race the -// process installing its handler — a missed signal leaves the zellij client -// laid out for a stale size. env, when non-nil, is the full env block for the -// child (mirrors exec.Cmd.Env: nil means inherit). ctx cancellation must -// terminate the process. -type spawnFunc func(ctx context.Context, argv []string, env []string, rows, cols uint16) (ptyProcess, error) - -// reattach policy: a PTY that drops is re-attached while the underlying Zellij +// reattach policy: a Stream that drops is re-attached while the underlying // session is still alive, up to maxReattach consecutive failures. An attach that // survived longer than reattachResetGrace before dropping resets the counter, so // a long-lived pane that blips recovers but a tight crash-loop gives up. @@ -51,25 +29,24 @@ const ( defaultReattachResetTime = 5 * time.Second ) -// attachment is ONE client's hold on a pane: a private `zellij attach` PTY -// spawned per mux open, streaming to a single sink. Zellij is the multiplexer — -// it owns the session's screen state and scrollback, and answers every fresh -// attach with its init handshake (alt screen, bracketed paste, and other terminal -// modes enabled by the embedded client options) followed by a faithful repaint. -// That handshake is why the PTY is per-client and there is no server-side replay -// buffer: a byte ring can replay recent output, but the one-time mode negotiation -// at the head of the stream scrolls out of any bounded buffer. A fresh attach per -// client makes Zellij re-send it, every time, by construction. +// attachment is ONE client's hold on a pane: a private attach Stream opened per +// mux open, streaming to a single sink. The runtime is the multiplexer — it owns +// the session's screen state and scrollback, and answers every fresh attach with +// its init handshake (alt screen, bracketed paste, scrollback replay) followed by +// a faithful repaint. That handshake is why the Stream is per-client and there is +// no terminal-layer replay buffer: a byte ring can replay recent output, but the +// one-time mode negotiation at the head of the stream scrolls out of any bounded +// buffer. A fresh attach per client makes the runtime re-send it, every time, by +// construction. // -// onOpen fires once the attach PTY is actually ready to accept input. onData +// onOpen fires once the attach Stream is actually ready to accept input. onData // must not block: the WS layer funnels frames onto its own buffered writer. // onExit fires at most once, when the attach loop gives up (runtime dead, // attach failure cap) — never on close(). type attachment struct { id string handle ports.RuntimeHandle - src PTYSource - spawn spawnFunc + src Source log *slog.Logger onOpen func() onData func(data []byte) @@ -79,7 +56,7 @@ type attachment struct { resetGrace time.Duration mu sync.Mutex - pty ptyProcess + pty ports.Stream cancel context.CancelFunc rows uint16 // last size the client asked for; re-applied on every attach cols uint16 @@ -90,7 +67,7 @@ type attachment struct { pendingInput [][]byte } -func newAttachment(id string, handle ports.RuntimeHandle, src PTYSource, spawn spawnFunc, onOpen func(), onData func([]byte), onExit func(), log *slog.Logger) *attachment { +func newAttachment(id string, handle ports.RuntimeHandle, src Source, onOpen func(), onData func([]byte), onExit func(), log *slog.Logger) *attachment { if log == nil { log = slog.Default() } @@ -101,7 +78,6 @@ func newAttachment(id string, handle ports.RuntimeHandle, src PTYSource, spawn s id: id, handle: handle, src: src, - spawn: spawn, log: log, onOpen: onOpen, onData: onData, @@ -128,7 +104,7 @@ func (a *attachment) run(ctx context.Context) { } // Gate EVERY attach (including the first) on the runtime actually - // being alive. `zellij attach` resurrects EXITED sessions — re-running + // being alive. A mux attach resurrects EXITED sessions — re-running // the serialized agent command — so attaching to a dead handle would // re-create a runtime the daemon already destroyed, outside lifecycle // control. A definitive "not alive" is a clean exit. A probe ERROR is @@ -154,19 +130,11 @@ func (a *attachment) run(ctx context.Context) { return } - argv, env, err := a.src.AttachCommand(a.handle) - if a.shouldStop(ctx) { - return - } - if err != nil { - a.fail("attach command: " + err.Error()) - return - } rows, cols := a.size() if a.shouldStop(ctx) { return } - p, err := a.spawn(ctx, argv, env, rows, cols) + p, err := a.src.Attach(ctx, a.handle, rows, cols) if a.shouldStop(ctx) { if p != nil { _ = p.Close() @@ -176,7 +144,7 @@ func (a *attachment) run(ctx context.Context) { if err != nil { failures++ if failures > a.maxReattach { - a.fail("spawn pty: " + err.Error()) + a.fail("attach: " + err.Error()) return } if !a.backoff(ctx, failures) { @@ -214,7 +182,7 @@ func (a *attachment) run(ctx context.Context) { } // copyOut pumps PTY output to the sink until the PTY closes or errors. -func (a *attachment) copyOut(p ptyProcess) { +func (a *attachment) copyOut(p ports.Stream) { buf := make([]byte, 32*1024) for { n, err := p.Read(buf) @@ -291,19 +259,19 @@ func (a *attachment) resize(rows, cols uint16) error { } // size returns the client's last requested grid (zero before the first -// open/resize recorded one). The spawn path reads it so the PTY starts at the -// client's grid instead of the kernel default. +// open/resize recorded one). The attach path reads it so the Stream starts at +// the client's grid instead of the kernel default. func (a *attachment) size() (rows, cols uint16) { a.mu.Lock() defer a.mu.Unlock() return a.rows, a.cols } -// setPTY publishes a freshly attached PTY and replays the client's last -// requested size onto it (see resize) — the spawn already started at the size +// setPTY publishes a freshly attached Stream and replays the client's last +// requested size onto it (see resize) — the attach already started at the size // read in run, but a resize frame can land between that read and registration -// here; the replay (Setsize + explicit WINCH) converges the late case. -func (a *attachment) setPTY(p ptyProcess) bool { +// here; the replay (Resize) converges the late case. +func (a *attachment) setPTY(p ports.Stream) bool { a.mu.Lock() if a.closed || a.exited { a.mu.Unlock() @@ -345,7 +313,7 @@ func (a *attachment) setPTY(p ptyProcess) bool { } } -func (a *attachment) clearPTY(p ptyProcess) { +func (a *attachment) clearPTY(p ports.Stream) { a.mu.Lock() if a.pty == p { a.pty = nil @@ -355,7 +323,7 @@ func (a *attachment) clearPTY(p ptyProcess) { } // close detaches this client: stop re-attaching and kill the attach PTY. It -// never touches the Zellij session itself, which the zellij server keeps alive +// never touches the runtime session itself, which the mux server keeps alive // for other clients. func (a *attachment) close() { a.mu.Lock() diff --git a/backend/internal/terminal/attachment_integration_test.go b/backend/internal/terminal/attachment_integration_test.go index c2eb899d..a21e87c8 100644 --- a/backend/internal/terminal/attachment_integration_test.go +++ b/backend/internal/terminal/attachment_integration_test.go @@ -6,32 +6,28 @@ import ( "context" "os" "os/exec" - "path/filepath" "strconv" "strings" "testing" "time" - "github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/zellij" + "github.com/aoagents/agent-orchestrator/backend/internal/adapters/runtime/tmux" "github.com/aoagents/agent-orchestrator/backend/internal/domain" "github.com/aoagents/agent-orchestrator/backend/internal/ports" ) -// TestAttachmentStreamsRealZellijPane attaches a real PTY to a real Zellij -// session and asserts output streams back, then that killing the pane stops the -// attachment without a re-attach storm. Skipped when Zellij is unavailable. -func TestAttachmentStreamsRealZellijPane(t *testing.T) { - zellijBin, err := exec.LookPath("zellij") - if err != nil { - t.Skip("zellij unavailable") +// TestAttachmentStreamsRealTmuxPane attaches a real PTY to a real tmux session +// and asserts output streams back, then that killing the session stops the +// attachment without a re-attach storm. Skipped when tmux is unavailable. +func TestAttachmentStreamsRealTmuxPane(t *testing.T) { + if _, err := exec.LookPath("tmux"); err != nil { + t.Skip("tmux unavailable") } + // See TestAttachmentReattachAdoptsNewSize: tmux needs a usable TERM to attach. + t.Setenv("TERM", "xterm-256color") name := "ao-term-it-" + strconv.Itoa(os.Getpid()) - socketDir := filepath.Join("/tmp", name+"-socket") - if err := os.MkdirAll(socketDir, 0o755); err != nil { - t.Fatalf("mkdir socket dir: %v", err) - } - rt := zellij.New(zellij.Options{Binary: zellijBin, SocketDir: socketDir, ConfigDir: t.TempDir(), Timeout: 5 * time.Second}) + rt := tmux.New(tmux.Options{Timeout: 10 * time.Second}) handle, err := rt.Create(context.Background(), ports.RuntimeConfig{ SessionID: domain.SessionID(name), WorkspacePath: t.TempDir(), @@ -43,7 +39,7 @@ func TestAttachmentStreamsRealZellijPane(t *testing.T) { t.Cleanup(func() { _ = rt.Destroy(context.Background(), handle) }) var got safeBytes - a := newAttachment(name, handle, rt, defaultSpawn, nil, got.add, nil, testLogger()) + a := newAttachment(name, handle, rt, nil, got.add, nil, testLogger()) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go a.run(ctx) @@ -52,14 +48,6 @@ func TestAttachmentStreamsRealZellijPane(t *testing.T) { eventually(t, 3*time.Second, func() bool { return a.write([]byte("echo AO_MARKER_42\n")) == nil }) eventually(t, 5*time.Second, func() bool { return strings.Contains(got.string(), "AO_MARKER_42") }) - // A fresh attach must carry zellij's alt-screen init handshake. Mouse - // reporting is deliberately disabled for AO's embedded client, so this test - // should not require SGR mouse mode. - eventually(t, 5*time.Second, func() bool { - out := got.string() - return strings.Contains(out, "\x1b[?1049h") - }) - // Kill the session: the attachment must observe it as gone and not re-attach. if err := rt.Destroy(context.Background(), handle); err != nil { t.Fatalf("Destroy: %v", err) @@ -70,21 +58,19 @@ func TestAttachmentStreamsRealZellijPane(t *testing.T) { // TestAttachmentReattachAdoptsNewSize is the end-to-end regression for the // stale-size desync: client A holds the session at one grid, detaches, and // client B immediately attaches at a different grid (the frontend's -// remount/reconnect flow). B's zellij client must adopt B's size — the inner -// pane's tty must report it — not stay laid out for A's. This is where the -// spawn-at-size + explicit-WINCH + SIGTERM-detach fixes meet a real zellij. +// remount/reconnect flow). B's tmux client must adopt B's size, not A's. func TestAttachmentReattachAdoptsNewSize(t *testing.T) { - zellijBin, err := exec.LookPath("zellij") - if err != nil { - t.Skip("zellij unavailable") + if _, err := exec.LookPath("tmux"); err != nil { + t.Skip("tmux unavailable") } + // tmux refuses to attach a client without a usable TERM, printing + // "open terminal failed: terminal does not support clear". The daemon sets a + // default TERM in production (Finder-launched attach fix); CI runners have + // none, so set it here to match the real environment. + t.Setenv("TERM", "xterm-256color") name := "ao-term-size-it-" + strconv.Itoa(os.Getpid()) - socketDir := filepath.Join("/tmp", name+"-socket") - if err := os.MkdirAll(socketDir, 0o755); err != nil { - t.Fatalf("mkdir socket dir: %v", err) - } - rt := zellij.New(zellij.Options{Binary: zellijBin, SocketDir: socketDir, ConfigDir: t.TempDir(), Timeout: 5 * time.Second}) + rt := tmux.New(tmux.Options{Timeout: 10 * time.Second}) handle, err := rt.Create(context.Background(), ports.RuntimeConfig{ SessionID: domain.SessionID(name), WorkspacePath: t.TempDir(), @@ -98,7 +84,7 @@ func TestAttachmentReattachAdoptsNewSize(t *testing.T) { attachAt := func(rows, cols uint16) (*attachment, *safeBytes, <-chan struct{}, context.CancelFunc) { var got safeBytes opened := make(chan struct{}) - a := newAttachment(name, handle, rt, defaultSpawn, func() { close(opened) }, got.add, nil, testLogger()) + a := newAttachment(name, handle, rt, func() { close(opened) }, got.add, nil, testLogger()) if err := a.resize(rows, cols); err != nil { t.Fatalf("record size: %v", err) } @@ -111,40 +97,55 @@ func TestAttachmentReattachAdoptsNewSize(t *testing.T) { a, _, openedA, cancelA := attachAt(37, 115) select { case <-openedA: - case <-time.After(5 * time.Second): + case <-time.After(10 * time.Second): t.Fatal("client A did not attach") } a.close() cancelA() - // Client B re-attaches immediately at 148x40 — no settle gap, same as the - // frontend reconnecting. The inner pane must see B's grid (zellij chrome - // shaves a couple rows/cols, so assert the reported cols land near 148 and - // far from 115). + // Client B re-attaches immediately at 148x40. The inner pane must see B's + // grid (tmux may shave a row/col; assert cols land near 148 and far from 115). b, gotB, openedB, cancelB := attachAt(40, 148) defer cancelB() defer b.close() select { case <-openedB: - case <-time.After(5 * time.Second): + case <-time.After(10 * time.Second): t.Fatal("client B did not attach") } - eventually(t, 5*time.Second, func() bool { return b.write([]byte("echo SIZE:$(stty size)\n")) == nil }) - eventually(t, 10*time.Second, func() bool { - out := gotB.string() - i := strings.LastIndex(out, "SIZE:") + // Drive the reattached shell until it reports its width. We RESEND the probe + // each iteration: onOpen means the stream accepts input, not that the inner + // `sh -i` is already at a prompt reading stdin after the reattach, so an early + // keystroke can be dropped; retrying covers that. Real tmux + shell output is + // also slow under -race on CI, hence the long deadline. On timeout we dump + // exactly what the pane produced so the failure is self-explaining (e.g. the + // probe echoed but never executed, or stty errored). + var captured string + gotWidth := false + deadline := time.Now().Add(30 * time.Second) + for time.Now().Before(deadline) { + _ = b.write([]byte("echo SIZE:$(stty size)\n")) + time.Sleep(250 * time.Millisecond) + captured = gotB.string() + i := strings.LastIndex(captured, "SIZE:") if i < 0 { - return false + continue } - fields := strings.Fields(strings.TrimPrefix(out[i:], "SIZE:")) + fields := strings.Fields(strings.TrimPrefix(captured[i:], "SIZE:")) if len(fields) < 2 { - return false + continue } cols, err := strconv.Atoi(strings.TrimFunc(fields[1], func(r rune) bool { return r < '0' || r > '9' })) if err != nil { - return false + continue } - return cols > 130 // B's 148 minus zellij chrome; a stale A-layout reports ≤115 - }) + if cols > 130 { // B's 148 minus any tmux chrome; a stale A-layout reports <=115 + gotWidth = true + break + } + } + if !gotWidth { + t.Fatalf("reattached pane never reported B's width (cols>130) within 30s; captured:\n%q", captured) + } } diff --git a/backend/internal/terminal/attachment_test.go b/backend/internal/terminal/attachment_test.go index a0851948..634edfc4 100644 --- a/backend/internal/terminal/attachment_test.go +++ b/backend/internal/terminal/attachment_test.go @@ -13,27 +13,27 @@ import ( func testLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) } -func newTestAttachment(src PTYSource, spawn spawnFunc, onData func([]byte), onExit func()) *attachment { - return newTestAttachmentWithOpen(src, spawn, nil, onData, onExit) +func newTestAttachment(src Source, onData func([]byte), onExit func()) *attachment { + return newTestAttachmentWithOpen(src, nil, onData, onExit) } -func newTestAttachmentWithOpen(src PTYSource, spawn spawnFunc, onOpen func(), onData func([]byte), onExit func()) *attachment { - return newAttachment("t1", ports.RuntimeHandle{ID: "t1"}, src, spawn, onOpen, onData, onExit, testLogger()) +func newTestAttachmentWithOpen(src Source, onOpen func(), onData func([]byte), onExit func()) *attachment { + return newAttachment("t1", ports.RuntimeHandle{ID: "t1"}, src, onOpen, onData, onExit, testLogger()) } -func currentPTY(a *attachment) ptyProcess { +func currentPTY(a *attachment) ports.Stream { a.mu.Lock() defer a.mu.Unlock() return a.pty } func TestAttachmentStreamsOutputToSink(t *testing.T) { - src := &fakeSource{alive: true} pty := newFakePTY() sp := &fakeSpawner{ptys: []*fakePTY{pty}} + src := &fakeSource{alive: true, spawner: sp} var sink safeBytes - a := newTestAttachment(src, sp.spawn, sink.add, nil) + a := newTestAttachment(src, sink.add, nil) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -44,10 +44,10 @@ func TestAttachmentStreamsOutputToSink(t *testing.T) { } func TestAttachmentWriteAndResizeReachPTY(t *testing.T) { - src := &fakeSource{alive: true} pty := newFakePTY() sp := &fakeSpawner{ptys: []*fakePTY{pty}} - a := newTestAttachment(src, sp.spawn, nil, nil) + src := &fakeSource{alive: true, spawner: sp} + a := newTestAttachment(src, nil, nil) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -66,13 +66,13 @@ func TestAttachmentWriteAndResizeReachPTY(t *testing.T) { } func TestAttachmentSignalsOpenOnlyAfterPTYIsPublished(t *testing.T) { - src := &fakeSource{alive: true} pty := newFakePTY() sp := &fakeSpawner{ptys: []*fakePTY{pty}} + src := &fakeSource{alive: true, spawner: sp} opened := make(chan bool, 1) var a *attachment - a = newTestAttachmentWithOpen(src, sp.spawn, func() { + a = newTestAttachmentWithOpen(src, func() { opened <- currentPTY(a) == pty }, nil, nil) @@ -91,16 +91,15 @@ func TestAttachmentSignalsOpenOnlyAfterPTYIsPublished(t *testing.T) { } func TestAttachmentBuffersInputUntilPTYReady(t *testing.T) { - src := &fakeSource{alive: true} pty := newFakePTY() spawnStarted := make(chan struct{}) releaseSpawn := make(chan struct{}) - spawn := func(context.Context, []string, []string, uint16, uint16) (ptyProcess, error) { + src := &fakeSource{alive: true, attachFn: func(context.Context, uint16, uint16) (ports.Stream, error) { close(spawnStarted) <-releaseSpawn return pty, nil - } - a := newTestAttachment(src, spawn, nil, nil) + }} + a := newTestAttachment(src, nil, nil) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -123,10 +122,10 @@ func TestAttachmentBuffersInputUntilPTYReady(t *testing.T) { // resize racing the attach) must not be lost: the attach applies it the moment // the PTY is up, instead of leaving the pane at the kernel default grid. func TestAttachmentAppliesRequestedSizeOnAttach(t *testing.T) { - src := &fakeSource{alive: true} pty := newFakePTY() sp := &fakeSpawner{ptys: []*fakePTY{pty}} - a := newTestAttachment(src, sp.spawn, nil, nil) + src := &fakeSource{alive: true, spawner: sp} + a := newTestAttachment(src, nil, nil) if err := a.resize(30, 100); err != nil { t.Fatalf("resize before attach: %v", err) @@ -142,18 +141,18 @@ func TestAttachmentAppliesRequestedSizeOnAttach(t *testing.T) { }) } -// The PTY must be SPAWNED at the recorded grid, not sized after exec: the -// attach process (zellij) reads the tty size once at startup, and a post-spawn -// TIOCSWINSZ depends on SIGWINCH delivery that can race the client installing -// its handler — a missed signal left the session laid out for the previous -// client's size (the "terminal doesn't repaint after a resize" desync). Also -// covers re-attach: a later resize must reach the NEXT spawn, not the first -// grid forever. +// The Stream must be OPENED at the recorded grid, not sized after attach: the +// attach client reads the tty size once at startup, and a post-attach resize +// depends on SIGWINCH delivery that can race the client installing its handler +// — a missed signal left the session laid out for the previous client's size +// (the "terminal doesn't repaint after a resize" desync). Also covers +// re-attach: a later resize must reach the NEXT attach, not the first grid +// forever. func TestAttachmentSpawnsPTYAtRecordedSize(t *testing.T) { - src := &fakeSource{alive: true} first := newFakePTY() sp := &fakeSpawner{ptys: []*fakePTY{first}} - a := newTestAttachment(src, sp.spawn, nil, nil) + src := &fakeSource{alive: true, spawner: sp} + a := newTestAttachment(src, nil, nil) a.resetGrace = time.Hour // keep the failure counter deterministic if err := a.resize(37, 115); err != nil { @@ -183,19 +182,19 @@ func TestAttachmentSpawnsPTYAtRecordedSize(t *testing.T) { } func TestAttachmentSkipsReattachOnCleanExit(t *testing.T) { - src := &fakeSource{alive: true} // alive for the first attach pty := newFakePTY() sp := &fakeSpawner{ptys: []*fakePTY{pty}} + src := &fakeSource{alive: true, spawner: sp} // alive for the first attach exited := make(chan struct{}) - a := newTestAttachment(src, sp.spawn, nil, func() { close(exited) }) + a := newTestAttachment(src, nil, func() { close(exited) }) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go a.run(ctx) eventually(t, time.Second, func() bool { return sp.calls() == 1 }) - src.setAlive(false) // Zellij session gone -> no re-attach + src.setAlive(false) // runtime session gone -> no re-attach pty.Close() // pane ends select { case <-exited: @@ -207,17 +206,17 @@ func TestAttachmentSkipsReattachOnCleanExit(t *testing.T) { } } -// TestAttachmentNeverAttachesToDeadRuntime covers the resurrection bug: `zellij -// attach` on a killed-but-cached session resurrects it, re-running the agent +// TestAttachmentNeverAttachesToDeadRuntime covers the resurrection bug: a mux +// attach on a killed-but-cached session resurrects it, re-running the agent // command. An attachment whose runtime probes definitively dead must therefore -// report exited WITHOUT ever spawning an attach PTY — even on the very first +// report exited WITHOUT ever opening an attach Stream — even on the very first // open (the original code only checked liveness on re-attach). func TestAttachmentNeverAttachesToDeadRuntime(t *testing.T) { - src := &fakeSource{alive: false} sp := &fakeSpawner{} + src := &fakeSource{alive: false, spawner: sp} exited := make(chan struct{}) - a := newTestAttachment(src, sp.spawn, nil, func() { close(exited) }) + a := newTestAttachment(src, nil, func() { close(exited) }) go a.run(context.Background()) select { @@ -226,7 +225,7 @@ func TestAttachmentNeverAttachesToDeadRuntime(t *testing.T) { t.Fatal("expected exit when runtime is dead before first attach") } if got := sp.calls(); got != 0 { - t.Fatalf("attach must never run against a dead runtime, got %d spawns", got) + t.Fatalf("attach must never run against a dead runtime, got %d attaches", got) } } @@ -235,10 +234,10 @@ func TestAttachmentNeverAttachesToDeadRuntime(t *testing.T) { // not flip the terminal to exited, and the attach proceeds once the probe // recovers. func TestAttachmentRetriesProbeErrorsBeforeAttaching(t *testing.T) { - src := &fakeSource{aliveErr: io.ErrUnexpectedEOF} pty := newFakePTY() sp := &fakeSpawner{ptys: []*fakePTY{pty}} - a := newTestAttachment(src, sp.spawn, nil, nil) + src := &fakeSource{aliveErr: io.ErrUnexpectedEOF, spawner: sp} + a := newTestAttachment(src, nil, nil) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -250,7 +249,7 @@ func TestAttachmentRetriesProbeErrorsBeforeAttaching(t *testing.T) { t.Fatal("probe error must not be treated as runtime death") } if got := sp.calls(); got != 0 { - t.Fatalf("attach must wait for a successful probe, got %d spawns", got) + t.Fatalf("attach must wait for a successful probe, got %d attaches", got) } // Probe recovers -> the attach goes through. @@ -262,10 +261,10 @@ func TestAttachmentRetriesProbeErrorsBeforeAttaching(t *testing.T) { } func TestAttachmentReattachesWhileSessionAlive(t *testing.T) { - src := &fakeSource{alive: true} // session still alive -> re-attach on drop p1, p2 := newFakePTY(), newFakePTY() sp := &fakeSpawner{ptys: []*fakePTY{p1, p2}} - a := newTestAttachment(src, sp.spawn, nil, nil) + src := &fakeSource{alive: true, spawner: sp} // session still alive -> re-attach on drop + a := newTestAttachment(src, nil, nil) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -295,34 +294,36 @@ func TestAttachmentReattachesWhileSessionAlive(t *testing.T) { eventually(t, 2*time.Second, func() bool { return a.isExited() }) } -func TestAttachmentFailsWhenAttachCommandErrors(t *testing.T) { +// A persistent Attach error is treated like the old spawn-error path: it backs +// off and retries up to the failure cap, then reports exited. (The old +// AttachCommand-error path failed immediately; folding the argv build into +// Attach means an attach failure now shares the spawn-failure retry policy, +// which is the correct behavior for a transient dial/exec failure.) +func TestAttachmentFailsWhenAttachErrors(t *testing.T) { src := &fakeSource{alive: true, attachErr: io.ErrUnexpectedEOF} - sp := &fakeSpawner{} exited := make(chan struct{}) - a := newTestAttachment(src, sp.spawn, nil, func() { close(exited) }) + a := newTestAttachment(src, nil, func() { close(exited) }) + a.maxReattach = 2 // keep the retry budget small so the test is fast go a.run(context.Background()) select { case <-exited: - case <-time.After(time.Second): - t.Fatal("expected exit when attach command fails") - } - if sp.calls() != 0 { - t.Fatalf("spawn should not run when attach command errors, got %d calls", sp.calls()) + case <-time.After(3 * time.Second): + t.Fatal("expected exit after attach errors exhaust the retry budget") } } // close() is a detach, not a pane death: it must stop the attach loop and kill -// the client's PTY without firing onExit — the Zellij session is still alive +// the client's PTY without firing onExit — the runtime session is still alive // and an exited frame would wrongly flip the client UI to its terminal state. func TestAttachmentCloseDoesNotFireExit(t *testing.T) { - src := &fakeSource{alive: true} pty := newFakePTY() sp := &fakeSpawner{ptys: []*fakePTY{pty}} + src := &fakeSource{alive: true, spawner: sp} exited := make(chan struct{}) - a := newTestAttachment(src, sp.spawn, nil, func() { close(exited) }) + a := newTestAttachment(src, nil, func() { close(exited) }) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -346,7 +347,7 @@ func TestAttachmentCloseDoesNotFireExit(t *testing.T) { default: } if got := sp.calls(); got != 1 { - t.Fatalf("close must stop re-attaching, got %d spawns", got) + t.Fatalf("close must stop re-attaching, got %d attaches", got) } } @@ -371,11 +372,10 @@ func (p *closeOrderPTY) Close() error { } func TestAttachmentCloseClosesPTYBeforeCancel(t *testing.T) { - src := &fakeSource{alive: true} beforeCancel := make(chan struct{}) afterCancel := make(chan struct{}) var spawnCtx context.Context - spawn := func(ctx context.Context, _ []string, _ []string, _, _ uint16) (ptyProcess, error) { + src := &fakeSource{alive: true, attachFn: func(ctx context.Context, _, _ uint16) (ports.Stream, error) { spawnCtx = ctx return &closeOrderPTY{ fakePTY: newFakePTY(), @@ -383,8 +383,8 @@ func TestAttachmentCloseClosesPTYBeforeCancel(t *testing.T) { before: beforeCancel, after: afterCancel, }, nil - } - a := newTestAttachment(src, spawn, nil, nil) + }} + a := newTestAttachment(src, nil, nil) done := make(chan struct{}) go func() { diff --git a/backend/internal/terminal/fakes_test.go b/backend/internal/terminal/fakes_test.go index c0818f3d..b196aec4 100644 --- a/backend/internal/terminal/fakes_test.go +++ b/backend/internal/terminal/fakes_test.go @@ -10,23 +10,29 @@ import ( "github.com/aoagents/agent-orchestrator/backend/internal/ports" ) -// fakeSource is a scripted PTYSource. +// fakeSource is a scripted terminal Source: Attach hands out fake Streams from +// an embedded spawner (or a custom attachFn closure); IsAlive is scriptable. +// attachErr makes Attach fail. type fakeSource struct { - argv []string + spawner *fakeSpawner + attachFn func(ctx context.Context, rows, cols uint16) (ports.Stream, error) mu sync.Mutex alive bool aliveErr error attachErr error } -func (f *fakeSource) AttachCommand(ports.RuntimeHandle) ([]string, []string, error) { +func (f *fakeSource) Attach(ctx context.Context, _ ports.RuntimeHandle, rows, cols uint16) (ports.Stream, error) { if f.attachErr != nil { - return nil, nil, f.attachErr + return nil, f.attachErr } - if f.argv == nil { - return []string{"zellij", "attach"}, nil, nil + if f.attachFn != nil { + return f.attachFn(ctx, rows, cols) } - return f.argv, nil, nil + if f.spawner == nil { + f.spawner = &fakeSpawner{} + } + return f.spawner.spawn(rows, cols) } func (f *fakeSource) IsAlive(context.Context, ports.RuntimeHandle) (bool, error) { @@ -48,8 +54,8 @@ func (f *fakeSource) setAliveResult(v bool, err error) { f.mu.Unlock() } -// fakePTY is a scripted ptyProcess: Read drains the out channel, Write records, -// Resize records, and Close unblocks reads. +// fakePTY is a scripted ports.Stream: Read drains the out channel, Write +// records, Resize records, and Close unblocks reads. type fakePTY struct { out chan []byte closed chan struct{} @@ -110,16 +116,17 @@ func (p *fakePTY) resizeCalls() [][2]uint16 { // fakeSpawner hands out pre-built fakePTYs in order; once exhausted it returns // idle PTYs that block until closed (so a re-attach loop does not busy-spin). +// It is the attach seam the fakeSource backs: each Attach call is one spawn. type fakeSpawner struct { mu sync.Mutex ptys []*fakePTY n int err error created []*fakePTY - sizes [][2]uint16 // rows×cols passed to each spawn call, in order + sizes [][2]uint16 // rows×cols passed to each attach call, in order } -func (f *fakeSpawner) spawn(_ context.Context, _ []string, _ []string, rows, cols uint16) (ptyProcess, error) { +func (f *fakeSpawner) spawn(rows, cols uint16) (ports.Stream, error) { f.mu.Lock() defer f.mu.Unlock() if f.err != nil { diff --git a/backend/internal/terminal/logger_test.go b/backend/internal/terminal/logger_test.go index ba08a2db..1586e458 100644 --- a/backend/internal/terminal/logger_test.go +++ b/backend/internal/terminal/logger_test.go @@ -7,12 +7,12 @@ import ( ) func TestNilLoggerFallsBackToDefault(t *testing.T) { - mgr := NewManager(&fakeSource{}, nil, nil, WithSpawn((&fakeSpawner{}).spawn)) + mgr := NewManager(&fakeSource{}, nil, nil) defer mgr.Close() if mgr.log == nil { t.Fatal("manager logger is nil") } - a := newAttachment("t1", ports.RuntimeHandle{ID: "t1"}, &fakeSource{}, (&fakeSpawner{}).spawn, nil, nil, nil, nil) + a := newAttachment("t1", ports.RuntimeHandle{ID: "t1"}, &fakeSource{}, nil, nil, nil, nil) if a.log == nil { t.Fatal("attachment logger is nil") } diff --git a/backend/internal/terminal/manager.go b/backend/internal/terminal/manager.go index 88ff36ed..6dccfaee 100644 --- a/backend/internal/terminal/manager.go +++ b/backend/internal/terminal/manager.go @@ -34,15 +34,14 @@ const ( defaultWriteBuffer = 1024 ) -// Manager serves WebSocket clients, spawning one attach PTY per opened pane +// Manager serves WebSocket clients, opening one attach Stream per opened pane // per connection. There is no shared per-pane state to outlive a connection: -// the Zellij server owns the session (screen, scrollback, modes), and every -// fresh attach gets its full handshake + repaint. A client reconnect simply -// attaches again. +// the runtime owns the session (screen, scrollback, modes), and every fresh +// attach gets its full handshake + repaint. A client reconnect simply attaches +// again. type Manager struct { - src PTYSource + src Source events EventSource - spawn spawnFunc log *slog.Logger heartbeat time.Duration @@ -58,15 +57,12 @@ type Manager struct { // Option configures a Manager. type Option func(*Manager) -// WithSpawn overrides the PTY spawner (tests inject a fake). -func WithSpawn(fn spawnFunc) Option { return func(m *Manager) { m.spawn = fn } } - // WithHeartbeat overrides the ping interval. func WithHeartbeat(d time.Duration) Option { return func(m *Manager) { m.heartbeat = d } } -// NewManager builds a Manager. src attaches PTYs; events feeds the session +// NewManager builds a Manager. src opens attach Streams; events feeds the session // channel (may be nil to disable it). A nil logger falls back to slog.Default. -func NewManager(src PTYSource, events EventSource, log *slog.Logger, opts ...Option) *Manager { +func NewManager(src Source, events EventSource, log *slog.Logger, opts ...Option) *Manager { if log == nil { log = slog.Default() } @@ -74,7 +70,6 @@ func NewManager(src PTYSource, events EventSource, log *slog.Logger, opts ...Opt m := &Manager{ src: src, events: events, - spawn: defaultSpawn, log: log, heartbeat: defaultHeartbeat, ctx: ctx, @@ -205,8 +200,8 @@ func (c *connState) handleTerminal(msg clientMsg) { } } -// openTerminal spawns this connection's own attach PTY for the pane. rows/cols -// are the client's grid from the open frame, applied as the PTY's initial size +// openTerminal opens this connection's own attach Stream for the pane. rows/cols +// are the client's grid from the open frame, applied as the Stream's initial size // (a resize that raced ahead of the attach would otherwise be lost). func (c *connState) openTerminal(id string, rows, cols uint16) { if id == "" { @@ -223,7 +218,7 @@ func (c *connState) openTerminal(id string, rows, cols uint16) { // a is captured by onExit before assignment; safe because the attach loop — // the only thing that fires onExit — starts after the registration below. var a *attachment - a = newAttachment(id, ports.RuntimeHandle{ID: id}, c.mgr.src, c.mgr.spawn, + a = newAttachment(id, ports.RuntimeHandle{ID: id}, c.mgr.src, func() { c.enqueue(serverMsg{Ch: chTerminal, ID: id, Type: msgOpened}) }, diff --git a/backend/internal/terminal/manager_test.go b/backend/internal/terminal/manager_test.go index 8c3e3ee1..3bb2134b 100644 --- a/backend/internal/terminal/manager_test.go +++ b/backend/internal/terminal/manager_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/aoagents/agent-orchestrator/backend/internal/cdc" + "github.com/aoagents/agent-orchestrator/backend/internal/ports" ) // fakeConn is an in-memory wsConn driven by channels. @@ -68,10 +69,10 @@ func recv(t *testing.T, c *fakeConn, ch, typ string, d time.Duration) serverMsg } func TestServeOpenStreamsAndWritesTerminal(t *testing.T) { - src := &fakeSource{alive: true} pty := newFakePTY() sp := &fakeSpawner{ptys: []*fakePTY{pty}} - mgr := NewManager(src, nil, testLogger(), WithSpawn(sp.spawn), WithHeartbeat(0)) + src := &fakeSource{alive: true, spawner: sp} + mgr := NewManager(src, nil, testLogger(), WithHeartbeat(0)) defer mgr.Close() conn := newFakeConn() @@ -100,16 +101,15 @@ func TestServeOpenStreamsAndWritesTerminal(t *testing.T) { } func TestServeBuffersInputUntilAttachReady(t *testing.T) { - src := &fakeSource{alive: true} pty := newFakePTY() spawnStarted := make(chan struct{}) releaseSpawn := make(chan struct{}) - spawn := func(context.Context, []string, []string, uint16, uint16) (ptyProcess, error) { + src := &fakeSource{alive: true, attachFn: func(context.Context, uint16, uint16) (ports.Stream, error) { close(spawnStarted) <-releaseSpawn return pty, nil - } - mgr := NewManager(src, nil, testLogger(), WithSpawn(spawn), WithHeartbeat(0)) + }} + mgr := NewManager(src, nil, testLogger(), WithHeartbeat(0)) defer mgr.Close() conn := newFakeConn() @@ -148,9 +148,9 @@ func nextTerminal(t *testing.T, c *fakeConn) serverMsg { // same id on this connection is served instead of being silently dropped by the // already-open guard. func TestServeOpenDeadRuntimeReportsExitedAndAllowsReopen(t *testing.T) { - src := &fakeSource{alive: false} sp := &fakeSpawner{ptys: []*fakePTY{newFakePTY()}} - mgr := NewManager(src, nil, testLogger(), WithSpawn(sp.spawn), WithHeartbeat(0)) + src := &fakeSource{alive: false, spawner: sp} + mgr := NewManager(src, nil, testLogger(), WithHeartbeat(0)) defer mgr.Close() conn := newFakeConn() @@ -163,7 +163,7 @@ func TestServeOpenDeadRuntimeReportsExitedAndAllowsReopen(t *testing.T) { t.Fatalf("first frame = %q, want exited", m.Type) } if got := sp.calls(); got != 0 { - t.Fatalf("attach must never run against a dead runtime, got %d spawns", got) + t.Fatalf("attach must never run against a dead runtime, got %d attaches", got) } src.setAlive(true) @@ -177,10 +177,10 @@ func TestServeOpenDeadRuntimeReportsExitedAndAllowsReopen(t *testing.T) { // exit, so a later open for the same id is served rather than dropped by the // already-open guard. func TestServeExitAfterOpenClearsEntryAllowingReopen(t *testing.T) { - src := &fakeSource{alive: true} // alive for the first attach p := newFakePTY() sp := &fakeSpawner{ptys: []*fakePTY{p}} - mgr := NewManager(src, nil, testLogger(), WithSpawn(sp.spawn), WithHeartbeat(0)) + src := &fakeSource{alive: true, spawner: sp} // alive for the first attach + mgr := NewManager(src, nil, testLogger(), WithHeartbeat(0)) defer mgr.Close() conn := newFakeConn() @@ -209,10 +209,10 @@ func TestServeExitAfterOpenClearsEntryAllowingReopen(t *testing.T) { // to shake the exit/reopen interleavings out. func TestServeReopenAfterImmediateExitNeverStuck(t *testing.T) { for i := 0; i < 400; i++ { - src := &fakeSource{} - src.setAlive(false) // dead runtime -> the open exits without attaching sp := &fakeSpawner{} - mgr := NewManager(src, nil, testLogger(), WithSpawn(sp.spawn), WithHeartbeat(0)) + src := &fakeSource{spawner: sp} + src.setAlive(false) // dead runtime -> the open exits without attaching + mgr := NewManager(src, nil, testLogger(), WithHeartbeat(0)) conn := newFakeConn() ctx, cancel := context.WithCancel(context.Background()) @@ -233,7 +233,7 @@ func TestServeReopenAfterImmediateExitNeverStuck(t *testing.T) { } func TestServeRejectsOpenWithoutID(t *testing.T) { - mgr := NewManager(&fakeSource{}, nil, testLogger(), WithSpawn((&fakeSpawner{}).spawn), WithHeartbeat(0)) + mgr := NewManager(&fakeSource{}, nil, testLogger(), WithHeartbeat(0)) defer mgr.Close() conn := newFakeConn() ctx, cancel := context.WithCancel(context.Background()) @@ -249,7 +249,7 @@ func TestServeRejectsOpenWithoutID(t *testing.T) { func TestServeForwardsSessionChannelFromCDC(t *testing.T) { bc := cdc.NewBroadcaster() - mgr := NewManager(&fakeSource{}, bc, testLogger(), WithSpawn((&fakeSpawner{}).spawn), WithHeartbeat(0)) + mgr := NewManager(&fakeSource{}, bc, testLogger(), WithHeartbeat(0)) defer mgr.Close() conn := newFakeConn() @@ -271,7 +271,7 @@ func TestServeForwardsSessionChannelFromCDC(t *testing.T) { } func TestServeSystemPingGetsPong(t *testing.T) { - mgr := NewManager(&fakeSource{}, nil, testLogger(), WithSpawn((&fakeSpawner{}).spawn), WithHeartbeat(0)) + mgr := NewManager(&fakeSource{}, nil, testLogger(), WithHeartbeat(0)) defer mgr.Close() conn := newFakeConn() ctx, cancel := context.WithCancel(context.Background()) @@ -283,7 +283,7 @@ func TestServeSystemPingGetsPong(t *testing.T) { } func TestServeHeartbeatPings(t *testing.T) { - mgr := NewManager(&fakeSource{}, nil, testLogger(), WithSpawn((&fakeSpawner{}).spawn), WithHeartbeat(10*time.Millisecond)) + mgr := NewManager(&fakeSource{}, nil, testLogger(), WithHeartbeat(10*time.Millisecond)) defer mgr.Close() conn := newFakeConn() ctx, cancel := context.WithCancel(context.Background()) @@ -294,7 +294,7 @@ func TestServeHeartbeatPings(t *testing.T) { } func TestServeClosesConnOnReadEnd(t *testing.T) { - mgr := NewManager(&fakeSource{}, nil, testLogger(), WithSpawn((&fakeSpawner{}).spawn), WithHeartbeat(0)) + mgr := NewManager(&fakeSource{}, nil, testLogger(), WithHeartbeat(0)) defer mgr.Close() conn := newFakeConn() ctx, cancel := context.WithCancel(context.Background()) @@ -309,15 +309,15 @@ func TestServeClosesConnOnReadEnd(t *testing.T) { } // Each connection opening the same pane gets its OWN attach PTY — that is the -// per-client model: zellij replays its init handshake + full repaint to every +// per-client model: the runtime replays its init handshake + full repaint to every // fresh attach, so no client depends on bytes another client consumed. Output // pushed to one client's PTY must reach only that client, and closing one // client's terminal must not touch the other's PTY. func TestServePerClientAttachIsolation(t *testing.T) { - src := &fakeSource{alive: true} p1, p2 := newFakePTY(), newFakePTY() sp := &fakeSpawner{ptys: []*fakePTY{p1, p2}} - mgr := NewManager(src, nil, testLogger(), WithSpawn(sp.spawn), WithHeartbeat(0)) + src := &fakeSource{alive: true, spawner: sp} + mgr := NewManager(src, nil, testLogger(), WithHeartbeat(0)) defer mgr.Close() connA, connB := newFakeConn(), newFakeConn() @@ -334,7 +334,7 @@ func TestServePerClientAttachIsolation(t *testing.T) { recv(t, connB, chTerminal, msgOpened, time.Second) eventually(t, time.Second, func() bool { return sp.calls() == 2 }) - // Zellij fans output out per attach; here each fake PTY stands in for one + // The runtime fans output out per attach; here each fake PTY stands in for one // attach process, so its bytes must reach exactly its own connection. p1.push([]byte("for-A")) data := recv(t, connA, chTerminal, msgData, time.Second) @@ -368,10 +368,10 @@ func TestServePerClientAttachIsolation(t *testing.T) { // The open frame carries the client's grid; the PTY must start at that size // rather than the kernel default, even though the attach is asynchronous. func TestServeOpenAppliesInitialSize(t *testing.T) { - src := &fakeSource{alive: true} pty := newFakePTY() sp := &fakeSpawner{ptys: []*fakePTY{pty}} - mgr := NewManager(src, nil, testLogger(), WithSpawn(sp.spawn), WithHeartbeat(0)) + src := &fakeSource{alive: true, spawner: sp} + mgr := NewManager(src, nil, testLogger(), WithHeartbeat(0)) defer mgr.Close() conn := newFakeConn() @@ -390,10 +390,10 @@ func TestServeOpenAppliesInitialSize(t *testing.T) { // Manager.Close must kill every live attach PTY: a PTY left open keeps its // attach process running and deadlocks daemon shutdown. func TestManagerCloseKillsLiveAttachments(t *testing.T) { - src := &fakeSource{alive: true} pty := newFakePTY() sp := &fakeSpawner{ptys: []*fakePTY{pty}} - mgr := NewManager(src, nil, testLogger(), WithSpawn(sp.spawn), WithHeartbeat(0)) + src := &fakeSource{alive: true, spawner: sp} + mgr := NewManager(src, nil, testLogger(), WithHeartbeat(0)) conn := newFakeConn() ctx, cancel := context.WithCancel(context.Background()) diff --git a/docs/plans/session-lifecycle-persistence.md b/docs/plans/session-lifecycle-persistence.md new file mode 100644 index 00000000..7dc83393 --- /dev/null +++ b/docs/plans/session-lifecycle-persistence.md @@ -0,0 +1,213 @@ +# Plan: Save-on-Close / Restore-on-Open Session Lifecycle + +## Goal + +Make the intended lifecycle real and lean: on app close, save every running +session (worker AND orchestrator, no filtering) plus its uncommitted work, then +force-remove the worktrees. On app launch, recreate the worktrees, replay the +saved uncommitted work, and restore all sessions. The daemon already starts on +launch and shuts down + frees its port on quit; this plan fills the missing +save/restore middle. + +## Core architectural decisions (settled) + +1. **All save/restore logic lives in the daemon**, not the frontend. The daemon + owns the store, the gitworktree adapter, and the `git` binary. The frontend's + only new responsibility: call the existing `POST /shutdown` endpoint before + it kills the daemon, so the save runs gracefully (SIGTERM remains the + fallback and triggers the same daemon-side save path). +2. **The "last-stop manifest" is the existing SQLite state, not a new file.** + `ListAllSessions` already records id, kind (worker/orchestrator), harness, + `is_terminated`, and `Metadata{branch, workspacePath, agentSessionId, + prompt}`. The `session_worktrees` table already has a `preserved_ref` column + (migration 0009) that nothing currently writes. No manifest.json, no new + migration, no new format. The manifest is a query. +3. **Uncommitted work is captured as a git commit object pointed to by a ref** + `refs/ao/preserved/`. Reject the user's original + `refs/{worktree-path}/uncomit/` naming (worktree paths contain `/`, are not + valid single ref components, and are not stable identity). The session id is + the stable key the rest of the system already uses. +4. **Untracked files: respect `.gitignore`.** Build the preserve commit through + a temp index (`GIT_INDEX_FILE= git add -A; git write-tree; git + commit-tree`) so tracked + staged + new (non-ignored) files are captured, + side-effect-free, without mutating the working tree or the stash stack. + Ignored paths (`node_modules/`, build output, ignored `.env`) are skipped. + Log a one-line count of skipped ignored paths so it is never silent. (Chosen + over `git stash create`, which silently drops all untracked files, and over + `git stash push -u`, which mutates the worktree and the global stash stack.) +5. **Do not weaken the existing dirty-worktree refusal** used by interactive + `ao session kill` / `ao cleanup`. Add a separate `ForceDestroy` that the + shutdown path calls only AFTER the work is captured. Adding `--force` to the + shared remove path would silently destroy work in the interactive flows. + +## Global Constraints (binding — reviewers enforce verbatim) + +- App state resolves under `~/.ao` only (`AO_DATA_DIR`/`AO_RUN_FILE` + overridable). Never `~/Library/Application Support`. The manifest is the + existing SQLite DB at the configured data dir; preserve refs live in each + project repo's `.git`. +- Preserve ref name is exactly `refs/ao/preserved/`. +- Untracked capture respects `.gitignore` (no `-f`, no force-include). Skipped + ignored paths are logged with a count. +- No kind filtering anywhere in the save or restore loops: orchestrator and + worker sessions are both saved and both restored. +- Save is strictly capture-then-destroy, per session, with the DB write + committed before the worktree is removed (crash-safety invariant). +- Never delete a preserve ref except immediately after a successful clean + apply. A failed apply keeps the ref and leaves conflict markers for the agent. +- No new manifest file, no new migration, no new HTTP endpoint (reuse the + existing `POST /shutdown`). +- The existing single-session `POST /sessions/{id}/restore` endpoint and the + interactive dirty-refusal removal path stay behaviorally unchanged. +- No em dashes anywhere (prose, comments, commit messages). + +## Key files + +- `backend/internal/adapters/workspace/gitworktree/workspace.go` — Destroy, + Restore, isDirty, findWorktree (re-add logic lives here) +- `backend/internal/adapters/workspace/gitworktree/commands.go` — git arg + builders (`worktreeRemoveArgs` deliberately omits `--force`) +- `backend/internal/ports/outbound.go` — `Workspace` interface (~line 120) +- `backend/internal/session_manager/manager.go` — Kill (~411-446), Cleanup + (~556-588), Restore (~451), dirty-refusal translation +- `backend/internal/daemon/daemon.go` — boot/shutdown sequence (startSession + ~112, `srv.Run(ctx)` ~144) +- `backend/internal/storage/sqlite/store/session_store.go` — `ListAllSessions` + (~173) +- `backend/internal/storage/sqlite/store/session_worktree_store.go` — + `preserved_ref` CRUD (`UpsertSessionWorktree`) +- `backend/internal/domain/session.go`, `domain/project.go` — record + worktree + domain types +- `frontend/src/main.ts` — `before-quit` (~694-700), running.json port read + (~338) + +## Tasks (smallest coherent diff first; each ends with ONE runnable check) + +### Task 1 — `ForceDestroy` on the workspace port + gitworktree adapter +Add `ForceDestroy(ctx, info) error` to the `ports.Workspace` interface and the +gitworktree adapter. It runs `git worktree remove --force `, then prune, +then `os.RemoveAll` as a backstop. New arg builder in `commands.go`; leave the +existing safe `Destroy`/`worktreeRemoveArgs` untouched. Add the `ponytail:` +comment that ForceDestroy is only safe after the work is captured. +**Check:** Go test in `gitworktree` that creates a worktree, dirties it, calls +`ForceDestroy`, and asserts the path is gone and the worktree is deregistered. + +### Task 2 — `StashUncommitted` + `ApplyPreserved` on the gitworktree adapter +- `StashUncommitted(ctx, info) (ref string, err error)`: build the preserve + commit via a temp index that respects `.gitignore` + (`GIT_INDEX_FILE= git add -A` → `git write-tree` → `git commit-tree`), + point `refs/ao/preserved/` at it via `git update-ref`, return the ref name + (empty if the worktree is clean — nothing to preserve). Log count of ignored + paths skipped. +- `ApplyPreserved(ctx, info, ref) error`: apply the preserve commit's tree onto + the worktree (`git stash apply ` style, or `git read-tree`/checkout from + the commit). On clean success delete the ref (`git update-ref -d`); on + conflict, keep the ref, leave conflict markers, return a sentinel the caller + logs. +**Check:** Go test that round-trips a tracked edit AND a new non-ignored file +through StashUncommitted → ForceDestroy → re-add → ApplyPreserved and asserts +both reappear; and that a path matched by `.gitignore` does NOT reappear. + +### Task 3 — `SaveAndTeardownAll` + `RestoreAll` on the session manager +- `SaveAndTeardownAll(ctx)`: `ListAllSessions`; for each live (non-terminated) + session with a non-empty `Metadata.WorkspacePath`: `StashUncommitted` → + `UpsertSessionWorktree(preserved_ref=...)` (commit) → `MarkTerminated` + (reuse the LCM path Kill uses) → runtime teardown → `ForceDestroy`. Mirror + `Kill` but swap refuse-on-dirty for capture-then-force. No kind filter. +- `RestoreAll(ctx)`: `ListAllSessions`; for each terminated session that the + shutdown save actually processed: ensure worktree via the existing + `workspace.Restore`, `ApplyPreserved` if a preserve ref is recorded, then + `manager.Restore(ctx, id)`. Reuse existing `Restore`; do not duplicate its + argv/resume logic. + - **The "shutdown-saved" marker is the presence of a `session_worktrees` + row for that session.** Today nothing else writes `session_worktrees` + rows, so a row existing == "this session was saved by SaveAndTeardownAll". + A session the user killed earlier (already terminated when the save ran) + is skipped by the save and has no row, so RestoreAll skips it too. Do NOT + gate on `preserved_ref` being non-empty: a clean worktree at shutdown + writes a row with an empty `preserved_ref` and must still be restored. + No new column is needed (consistent with Task 6 leaving `state` alone). +**Check:** Go test with fakes asserting (a) save calls capture-then-force in +order and writes preserved_ref before ForceDestroy, (b) RestoreAll restores BOTH +a worker and an orchestrator, (c) a session the user killed before shutdown is +not resurrected. + +### Task 4 — Wire into daemon boot/shutdown (`daemon.go`) +- After `startSession` returns and before `srv.Run(ctx)`: call `RestoreAll` + (best-effort; log failures; never block boot). +- After `srv.Run(ctx)` returns and before the store closes: call + `SaveAndTeardownAll` with a fresh bounded context (not the cancelled `ctx`). +- Expose the manager (or a minimal `LifecycleSaver`/`LifecycleRestorer` seam) + from the wiring up to `Run`. +**Check:** Manual run documented in report — spawn a session, edit a tracked +file + add a new file, `POST /shutdown`; assert worktree removed and +`refs/ao/preserved/` exists; restart daemon; assert worktree re-created and +both edits reapplied. Plus `go build ./backend/...` green. + +### Task 5 — Frontend: call `/shutdown` before kill (`main.ts`) +In `before-quit`: `event.preventDefault()` once, `await fetch( +http://127.0.0.1:/shutdown, {method:'POST'})` with an ~8s bounded timeout +(port from the running.json the app already reads), then `killDaemon` + +`app.exit()`. Keep the `process.on('exit')` SIGTERM fallback intact. +**Check:** `cd frontend && ` green; manual: quit the app, daemon +log shows the save ran and exited cleanly (not just SIGTERM-killed). + +### Task 6 — Trim the over-built `session_worktrees.state` enum usage +No schema change. Ensure the save/restore code reads/writes only `preserved_ref` +and leaves `state` at its default; add `ponytail:` comments noting the enum is +unused multi-repo scaffolding. +**Check:** `go test ./backend/internal/storage/...` still green. + +## Edge cases the lean version must still handle + +1. Crash mid-shutdown: per-session capture-then-destroy with DB commit as the + commit point. Processed sessions recover via ref; unprocessed keep live + worktrees. No third lossy state. +2. User manually deleted a worktree dir: `workspace.Restore` re-adds from the + branch; stray non-worktree dir → it refuses, restore loop logs and skips. +3. Base branch moved: worktree re-added on the session's own branch; restores + to the agent's last state regardless of base. +4. Orchestrator vs workers: no kind filter in either loop. +5. Preserved diff conflicts on apply: keep the ref, leave conflict markers, + still relaunch the agent. Never delete the ref on failed apply. +6. Incomplete session (no branch/path): skipped on both save and restore. + +## Net change + +Added: 2 adapter methods (`ForceDestroy`, `StashUncommitted`/`ApplyPreserved`), +2 manager methods (`SaveAndTeardownAll`, `RestoreAll`), 2 daemon call sites, +1 frontend fetch. Reuses `ListAllSessions`, `session_worktrees.preserved_ref`, +`manager.Restore`, the LCM terminate path, and the existing `/shutdown` +endpoint. No new file, migration, format, or endpoint. + +## Build & verify commands (from repo root; see AGENTS.md for the full list) + +- `npm run lint` — backend `go test ./...` + golangci-lint v2.12.2 +- `cd backend && go build ./...` / `go test ./...` / `go test -race ./...` / + `go vet ./...` +- `npm run frontend:typecheck` — frontend TypeScript check (Task 5) +- Do NOT hand-edit `backend/internal/storage/sqlite/gen/*`. This plan adds no + new queries/migrations, so `npm run sqlc` should not be needed; if a task + finds it does need a new query, change `queries/*` and run `npm run sqlc`. +- This plan adds NO new HTTP routes, so the OpenAPI/`npm run api` flow and the + `internal/httpd` spec-drift tests should stay green untouched. If a reviewer + sees spec drift, a task wrongly added a route. + +## Starting point for the implementing session + +- Baseline: this plan and the cleanup are committed on `main` (the plan file + lives at `docs/plans/session-lifecycle-persistence.md`). Branch off `main` + as `feat/session-lifecycle-persistence`. +- The file:line references above are approximate (prefixed `~`). Verify each + with codegraph or grep before editing; the daemon is loopback-only and the + store is sqlc-generated, so confirm signatures rather than assuming. +- Use the `superpowers:subagent-driven-development` skill to execute: fresh + implementer subagent per task, task review (spec + quality) per task, then a + final whole-branch review. Subagents follow TDD. + +## Execution order + +Tasks are sequential where coupled: Task 2 shares the gitworktree adapter with +Task 1 (do 1 then 2, same package); Task 3 depends on 1 + 2; Task 4 depends on +3. Task 5 (frontend) and Task 6 (storage cleanup) are independent and can run +anytime. Suggested order: 1 → 2 → 3 → 4, then 5 and 6. diff --git a/docs/superpowers/plans/2026-06-24-crash-proof-session-reconcile.md b/docs/superpowers/plans/2026-06-24-crash-proof-session-reconcile.md new file mode 100644 index 00000000..1ee9bc3b --- /dev/null +++ b/docs/superpowers/plans/2026-06-24-crash-proof-session-reconcile.md @@ -0,0 +1,628 @@ +# Crash-proof Session Reconcile Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** On every daemon boot, reconcile live tmux + DB state so a SIGKILL/crash/force-quit that skips `SaveAndTeardownAll` no longer leaks an orphaned daemon, tmux sessions, or worktrees. + +**Architecture:** Add `Manager.Reconcile(ctx)` to the session manager: a live pass (adopt alive sessions, stash+terminate dead ones), a reap pass (`Destroy` tmux of terminated sessions whose pane survived), then the existing `RestoreAll` body. Wire it in place of the bare `RestoreAll` call at daemon boot. On the frontend, add a kill+replace branch for a wedged orphan daemon on launch. tmux is the persistence layer, so adopting a crash-surviving session is a no-op. + +**Tech Stack:** Go 1.x (backend, `go test`), TypeScript/Electron (frontend, `npm test` in `frontend/`). tmux runtime adapter. SQLite store. + +## Global Constraints + +- No em dashes (`—`) or en dashes (`–`) anywhere: prose, code comments, commit messages. Use a period, comma, colon, semicolon, or parentheses. +- Go: run `gofmt`/`goimports`; keep `golangci-lint` clean (the repo's CI gates on it). +- Git author email: `dev@theharshitsingh.com`. Commit with `git -c user.email=dev@theharshitsingh.com commit ...`. +- TDD: write the failing test first, watch it fail, implement minimally, watch it pass, commit. +- Reconcile is best-effort: per-session failures log and never abort the pass or block boot (same contract as the existing `RestoreAll` call site at `backend/internal/daemon/daemon.go:147`). +- Reconcile never deletes worktree directories and never spawns a new agent. Dirty worktrees are always preserved. + +--- + +## File Structure + +- `backend/internal/session_manager/manager.go` — add `Reconcile`, `reconcileLive`, `reconcileReap` methods; widen the `runtimeController` interface with `IsAlive`. The existing `RestoreAll` body is reused (called as the restore phase). +- `backend/internal/session_manager/manager_test.go` — add `IsAlive` to `fakeRuntime` (scriptable per handle); add `Reconcile` unit tests. +- `backend/internal/daemon/lifecycle_wiring.go` — add `Reconcile` to the `sessionLifecycle` interface. +- `backend/internal/daemon/daemon.go` — replace the `RestoreAll(ctx)` boot call with `Reconcile(ctx)`. +- `backend/internal/daemon/wiring_test.go` — update the `sessionLifecycle` fake/mock if it asserts the interface. +- `backend/internal/integration/lifecycle_sqlite_test.go` — add a reconcile integration case. +- `frontend/src/main.ts` — add the wedged-orphan kill+replace branch in `startDaemonInner`. +- `frontend/src/main.test.ts` (or the existing main-process test file) — test the kill+replace decision. + +--- + +## Task 1: Widen `runtimeController` with `IsAlive` and adopt-alive live pass + +**Files:** +- Modify: `backend/internal/session_manager/manager.go:64-67` (interface), add methods near `manager.go:558-623` +- Test: `backend/internal/session_manager/manager_test.go:138-152` (fake), new test fn + +**Interfaces:** +- Consumes: `domain.SessionRecord` (`.IsTerminated`, `.Metadata.WorkspacePath`, `.Metadata.Branch`, `.Metadata.RuntimeHandleID`); `runtimeHandle(meta)` -> `ports.RuntimeHandle`; `workspaceInfo(rec)` -> `ports.WorkspaceInfo`; `m.workspace.StashUncommitted`, `m.lcm.MarkTerminated`, `m.store.ListAllSessions`. +- Produces: `func (m *Manager) reconcileLive(ctx context.Context, rec domain.SessionRecord) error`; widened `runtimeController` with `IsAlive(ctx context.Context, handle ports.RuntimeHandle) (bool, error)`. + +- [ ] **Step 1: Add `IsAlive` to the test `fakeRuntime`** + +In `manager_test.go`, extend the fake so `IsAlive` is scriptable per handle id and records calls: + +```go +type fakeRuntime struct { + createErr error + created, destroyed int + lastCfg ports.RuntimeConfig + // aliveByHandle maps a RuntimeHandle.ID to its liveness; missing = false. + aliveByHandle map[string]bool + aliveErr error + destroyedIDs []string +} + +func (r *fakeRuntime) IsAlive(_ context.Context, handle ports.RuntimeHandle) (bool, error) { + if r.aliveErr != nil { + return false, r.aliveErr + } + return r.aliveByHandle[handle.ID], nil +} +``` + +Also record the destroyed handle id in the existing `Destroy`: + +```go +func (r *fakeRuntime) Destroy(_ context.Context, handle ports.RuntimeHandle) error { + r.destroyed++ + r.destroyedIDs = append(r.destroyedIDs, handle.ID) + return nil +} +``` + +- [ ] **Step 2: Write the failing test for the live pass** + +Add to `manager_test.go`. A live (`is_terminated=0`) session whose tmux is GONE must be stashed and marked terminated; an ALIVE one must be left untouched. + +```go +func TestReconcileLive_DeadSessionStashedAndTerminated(t *testing.T) { + st := newFakeStore() + rt := &fakeRuntime{aliveByHandle: map[string]bool{}} // handle not alive + ws := &fakeWorkspace{stashRef: "refs/ao/preserved/s1"} + lcm := &fakeLCM{} + m := newManager(t, st, rt, ws, lcm) + + rec := domain.SessionRecord{ + ID: "s1", + ProjectID: "p1", + IsTerminated: false, + Metadata: domain.SessionMetadata{ + Branch: "ao/s1/root", WorkspacePath: "/wt/s1", RuntimeHandleID: "s1", + }, + } + + if err := m.reconcileLive(context.Background(), rec); err != nil { + t.Fatalf("reconcileLive: %v", err) + } + if ws.stashCalls != 1 { + t.Fatalf("StashUncommitted calls = %d, want 1", ws.stashCalls) + } + if lcm.terminated["s1"] != 1 { + t.Fatalf("MarkTerminated(s1) = %d, want 1", lcm.terminated["s1"]) + } + if rt.destroyed != 0 { + t.Fatalf("Destroy calls = %d, want 0 (dead session: no tmux to kill)", rt.destroyed) + } +} + +func TestReconcileLive_AliveSessionAdoptedNoop(t *testing.T) { + st := newFakeStore() + rt := &fakeRuntime{aliveByHandle: map[string]bool{"s2": true}} + ws := &fakeWorkspace{} + lcm := &fakeLCM{} + m := newManager(t, st, rt, ws, lcm) + + rec := domain.SessionRecord{ + ID: "s2", ProjectID: "p1", IsTerminated: false, + Metadata: domain.SessionMetadata{Branch: "ao/s2/root", WorkspacePath: "/wt/s2", RuntimeHandleID: "s2"}, + } + + if err := m.reconcileLive(context.Background(), rec); err != nil { + t.Fatalf("reconcileLive: %v", err) + } + if ws.stashCalls != 0 || lcm.terminated["s2"] != 0 || rt.destroyed != 0 { + t.Fatalf("adopt should be a no-op: stash=%d term=%d destroy=%d", ws.stashCalls, lcm.terminated["s2"], rt.destroyed) + } +} +``` + +> If `fakeWorkspace` lacks a `stashCalls` counter or `fakeLCM` lacks a `terminated` map, add them: increment `stashCalls` inside `fakeWorkspace.StashUncommitted`, and `l.terminated[id]++` (init the map in the fake) inside `fakeLCM.MarkTerminated`. If `newManager` has a different signature in this file, match the existing constructor used by other tests rather than inventing one. + +- [ ] **Step 3: Run the test, verify it fails** + +Run: `cd backend && go test ./internal/session_manager/ -run TestReconcileLive -v` +Expected: FAIL — `m.reconcileLive` undefined, and `IsAlive` not in `runtimeController`. + +- [ ] **Step 4: Widen the interface and implement `reconcileLive`** + +In `manager.go`, widen the interface (around line 64): + +```go +type runtimeController interface { + Create(ctx context.Context, cfg ports.RuntimeConfig) (ports.RuntimeHandle, error) + Destroy(ctx context.Context, handle ports.RuntimeHandle) error + // IsAlive reports whether the handle's runtime session still exists. Used by + // Reconcile on boot to adopt crash-surviving sessions and reap leaked ones. + IsAlive(ctx context.Context, handle ports.RuntimeHandle) (bool, error) +} +``` + +Add the method (place it near `saveAndTeardownOne`, around line 623): + +```go +// reconcileLive handles a single non-terminated session on boot. If its runtime +// session is still alive (tmux is the persistence layer, so it survives a daemon +// crash) we adopt it: a no-op, the agent keeps running. If the runtime is gone, +// the agent died with the daemon, so we capture any uncommitted work into a +// preserve ref (best-effort) and mark the session terminated. We never relaunch +// here (that is spawn policy) and never delete the worktree. +func (m *Manager) reconcileLive(ctx context.Context, rec domain.SessionRecord) error { + if rec.Metadata.WorkspacePath == "" || rec.Metadata.Branch == "" { + return nil + } + handle := runtimeHandle(rec.Metadata) + if handle.ID != "" { + alive, err := m.runtime.IsAlive(ctx, handle) + if err != nil { + // A failed probe is not proof of death: leave the session as-is. + return fmt.Errorf("reconcile %s: probe: %w", rec.ID, err) + } + if alive { + return nil // adopt: the session survived the crash. + } + } + // Runtime is gone: preserve work (best-effort) then mark terminated. + if _, err := m.workspace.StashUncommitted(ctx, workspaceInfo(rec)); err != nil { + m.logger.Warn("reconcile: stash uncommitted failed; marking terminated anyway", "sessionID", rec.ID, "error", err) + } + if err := m.lcm.MarkTerminated(ctx, rec.ID); err != nil { + return fmt.Errorf("reconcile %s: mark terminated: %w", rec.ID, err) + } + return nil +} +``` + +- [ ] **Step 5: Run the tests, verify they pass** + +Run: `cd backend && go test ./internal/session_manager/ -run TestReconcileLive -v` +Expected: PASS (both cases). + +- [ ] **Step 6: Build to confirm the widened interface still satisfies the concrete runtime** + +Run: `cd backend && go build ./...` +Expected: success (the concrete `runtimeselect.Runtime`/`tmux.Runtime` already implement `IsAlive`). + +- [ ] **Step 7: Commit** + +```bash +cd backend && gofmt -w internal/session_manager/manager.go internal/session_manager/manager_test.go +git add internal/session_manager/manager.go internal/session_manager/manager_test.go +git -c user.email=dev@theharshitsingh.com commit -m "feat(session): reconcile live pass (adopt alive, stash+terminate dead)" +``` + +--- + +## Task 2: Reap pass and the `Reconcile` entry point + +**Files:** +- Modify: `backend/internal/session_manager/manager.go` (add `reconcileReap`, `Reconcile`; the latter reuses the existing `RestoreAll` body) +- Test: `backend/internal/session_manager/manager_test.go` + +**Interfaces:** +- Consumes: `m.store.ListAllSessions`, `m.runtime.IsAlive`, `m.runtime.Destroy`, `reconcileLive` (Task 1), the existing `RestoreAll` method (`manager.go:637`). +- Produces: `func (m *Manager) Reconcile(ctx context.Context) error`; `func (m *Manager) reconcileReap(ctx context.Context, rec domain.SessionRecord) error`. + +- [ ] **Step 1: Write the failing reap test** + +Add to `manager_test.go`. A terminated session whose tmux is still alive must have its tmux `Destroy`d; a terminated session whose tmux is gone must not. + +```go +func TestReconcileReap_TerminatedButAliveTmuxDestroyed(t *testing.T) { + st := newFakeStore() + rt := &fakeRuntime{aliveByHandle: map[string]bool{"t1": true}} + ws := &fakeWorkspace{} + lcm := &fakeLCM{} + m := newManager(t, st, rt, ws, lcm) + + rec := domain.SessionRecord{ + ID: "t1", ProjectID: "p1", IsTerminated: true, + Metadata: domain.SessionMetadata{RuntimeHandleID: "t1"}, + } + + if err := m.reconcileReap(context.Background(), rec); err != nil { + t.Fatalf("reconcileReap: %v", err) + } + if len(rt.destroyedIDs) != 1 || rt.destroyedIDs[0] != "t1" { + t.Fatalf("destroyedIDs = %v, want [t1]", rt.destroyedIDs) + } +} + +func TestReconcileReap_TerminatedAndDeadTmuxLeftAlone(t *testing.T) { + st := newFakeStore() + rt := &fakeRuntime{aliveByHandle: map[string]bool{}} // t2 not alive + m := newManager(t, st, rt, &fakeWorkspace{}, &fakeLCM{}) + + rec := domain.SessionRecord{ + ID: "t2", ProjectID: "p1", IsTerminated: true, + Metadata: domain.SessionMetadata{RuntimeHandleID: "t2"}, + } + if err := m.reconcileReap(context.Background(), rec); err != nil { + t.Fatalf("reconcileReap: %v", err) + } + if rt.destroyed != 0 { + t.Fatalf("Destroy calls = %d, want 0", rt.destroyed) + } +} +``` + +- [ ] **Step 2: Run the test, verify it fails** + +Run: `cd backend && go test ./internal/session_manager/ -run TestReconcileReap -v` +Expected: FAIL — `m.reconcileReap` undefined. + +- [ ] **Step 3: Implement `reconcileReap` and `Reconcile`** + +Add `reconcileReap` near `reconcileLive`: + +```go +// reconcileReap kills the leaked tmux session of a session the DB already marks +// terminated. This covers the teardown that marked the row terminated but failed +// to kill the runtime (e.g. ForceDestroy/Destroy errored after MarkTerminated). +// Destroy is idempotent, so an already-gone session is a no-op. +func (m *Manager) reconcileReap(ctx context.Context, rec domain.SessionRecord) error { + handle := runtimeHandle(rec.Metadata) + if handle.ID == "" { + return nil + } + alive, err := m.runtime.IsAlive(ctx, handle) + if err != nil { + return fmt.Errorf("reconcile reap %s: probe: %w", rec.ID, err) + } + if !alive { + return nil + } + if err := m.runtime.Destroy(ctx, handle); err != nil { + return fmt.Errorf("reconcile reap %s: destroy: %w", rec.ID, err) + } + return nil +} +``` + +Add the entry point. Place it just above `RestoreAll` (manager.go:625) and have it call the existing `RestoreAll` as the restore phase: + +```go +// Reconcile is the boot-time consistency pass. It replaces the bare RestoreAll +// call so that however the previous daemon died (clean shutdown, SIGKILL, or +// crash), live reality matches the DB: +// +// 1. Live pass: for each non-terminated session, adopt it if its runtime +// survived, else capture work and mark terminated (reconcileLive). +// 2. Reap pass: for each terminated session whose runtime leaked, kill it +// (reconcileReap). Runs before restore so a restored session does not +// collide with a leaked tmux of the same name. +// 3. Restore pass: relaunch shutdown-saved sessions (existing RestoreAll). +// +// Best-effort throughout: a per-session failure is logged and never aborts the +// pass or blocks boot. +func (m *Manager) Reconcile(ctx context.Context) error { + recs, err := m.store.ListAllSessions(ctx) + if err != nil { + return fmt.Errorf("reconcile: list sessions: %w", err) + } + for _, rec := range recs { + if rec.IsTerminated { + continue + } + if err := m.reconcileLive(ctx, rec); err != nil { + m.logger.Error("reconcile: live pass failed, skipping", "sessionID", rec.ID, "error", err) + } + } + for _, rec := range recs { + if !rec.IsTerminated { + continue + } + if err := m.reconcileReap(ctx, rec); err != nil { + m.logger.Error("reconcile: reap pass failed, skipping", "sessionID", rec.ID, "error", err) + } + } + return m.RestoreAll(ctx) +} +``` + +> Note: the live pass re-reads `rec.IsTerminated` from the pre-pass snapshot, so a session terminated *by* the live pass is not also reaped in the same run. That is fine: its tmux is already gone (that is why it was terminated), so reaping would be a no-op anyway. + +- [ ] **Step 4: Run the tests, verify they pass** + +Run: `cd backend && go test ./internal/session_manager/ -run 'TestReconcile' -v` +Expected: PASS (live + reap tests). + +- [ ] **Step 5: Commit** + +```bash +cd backend && gofmt -w internal/session_manager/manager.go internal/session_manager/manager_test.go +git add internal/session_manager/manager.go internal/session_manager/manager_test.go +git -c user.email=dev@theharshitsingh.com commit -m "feat(session): reconcile reap pass and Reconcile entry point" +``` + +--- + +## Task 3: Wire `Reconcile` into daemon boot + +**Files:** +- Modify: `backend/internal/daemon/lifecycle_wiring.go:64-67` (interface) +- Modify: `backend/internal/daemon/daemon.go:144-149` (boot call) +- Test: `backend/internal/daemon/wiring_test.go` + +**Interfaces:** +- Consumes: `Manager.Reconcile` (Task 2). +- Produces: `sessionLifecycle` interface gains `Reconcile(ctx context.Context) error`. + +- [ ] **Step 1: Update the wiring test/mock** + +In `wiring_test.go`, find the type used as a `sessionLifecycle` test double (it implements `RestoreAll` and `SaveAndTeardownAll`). Add a `Reconcile` method and, if the test asserts boot behavior, assert `Reconcile` is the method called on boot: + +```go +func (m *fakeSessionLifecycle) Reconcile(ctx context.Context) error { + m.reconcileCalls++ + return m.reconcileErr +} +``` + +(If `wiring_test.go` has no such double and only checks construction, add a compile-time assertion instead: `var _ sessionLifecycle = (*sessionmanager.Manager)(nil)` in the test, which fails to compile until both the interface and the concrete method exist.) + +- [ ] **Step 2: Run the test, verify it fails** + +Run: `cd backend && go test ./internal/daemon/ -run Wiring -v` +Expected: FAIL — `Reconcile` not in the interface / not asserted. + +- [ ] **Step 3: Add `Reconcile` to the interface** + +In `lifecycle_wiring.go`: + +```go +type sessionLifecycle interface { + Reconcile(ctx context.Context) error + RestoreAll(ctx context.Context) error + SaveAndTeardownAll(ctx context.Context) error +} +``` + +- [ ] **Step 4: Replace the boot call** + +In `daemon.go`, change the boot restore call (currently lines 144-149) to call `Reconcile`: + +```go + // Reconcile sessions on boot: adopt crash-surviving runtimes, capture and + // terminate dead ones, reap leaked tmux, then restore shutdown-saved + // sessions. Best-effort: a failure is logged but never blocks boot. Placed + // before srv.Run so sessions are consistent before the server serves. + if reconcileErr := sessMgr.Reconcile(ctx); reconcileErr != nil { + log.Error("reconcile sessions on boot failed", "err", reconcileErr) + } +``` + +- [ ] **Step 5: Run the tests, verify they pass** + +Run: `cd backend && go test ./internal/daemon/ -v` +Expected: PASS. + +- [ ] **Step 6: Build everything** + +Run: `cd backend && go build ./... && go vet ./internal/daemon/ ./internal/session_manager/` +Expected: success. + +- [ ] **Step 7: Commit** + +```bash +cd backend && gofmt -w internal/daemon/daemon.go internal/daemon/lifecycle_wiring.go internal/daemon/wiring_test.go +git add internal/daemon/daemon.go internal/daemon/lifecycle_wiring.go internal/daemon/wiring_test.go +git -c user.email=dev@theharshitsingh.com commit -m "feat(daemon): run Reconcile on boot in place of bare RestoreAll" +``` + +--- + +## Task 4: Integration test over the sqlite store + +**Files:** +- Modify: `backend/internal/integration/lifecycle_sqlite_test.go` + +**Interfaces:** +- Consumes: the real `Manager.Reconcile`, a real sqlite store, and the test's runtime fake (find how this file already fakes the runtime; reuse it, scripting `IsAlive` per handle). + +- [ ] **Step 1: Read the existing integration harness** + +Open `backend/internal/integration/lifecycle_sqlite_test.go`. Identify how it constructs a `Manager` with a real `sqlite.Store` and what runtime double it injects. Reuse that exact wiring; only add `IsAlive` scripting to the double if it is missing. + +- [ ] **Step 2: Write the failing integration test** + +Add a test that seeds two sessions through the store, runs `Reconcile`, and asserts the resulting DB state. Use the file's existing seeding helpers and constructor names (match them; do not invent new ones): + +```go +func TestReconcile_TerminatesDeadLiveSessionAndReapsLeakedTmux(t *testing.T) { + // ... build store + manager the same way other tests in this file do ... + + // Seed A: is_terminated=0 but its runtime is gone (crash-killed agent). + // Seed B: is_terminated=1 but its tmux is still alive (leaked teardown). + // Script the runtime double: A's handle -> not alive, B's handle -> alive. + + if err := mgr.Reconcile(ctx); err != nil { + t.Fatalf("Reconcile: %v", err) + } + + // A is now terminated in the store. + a, _, _ := store.GetSession(ctx, "A") + if !a.IsTerminated { + t.Fatalf("session A: want terminated after reconcile") + } + // B's leaked tmux was destroyed. + if !runtimeDouble.wasDestroyed("B") { + t.Fatalf("session B: want leaked tmux destroyed") + } +} +``` + +> Replace `mgr`, `store`, `ctx`, `runtimeDouble`, and the seeding with the file's actual identifiers. If the file's runtime double cannot report `wasDestroyed`, assert via the store/observable side effects it already uses. + +- [ ] **Step 3: Run it, verify it fails** + +Run: `cd backend && go test ./internal/integration/ -run TestReconcile_TerminatesDeadLiveSessionAndReapsLeakedTmux -v` +Expected: FAIL (until seeding + assertions match real helpers; iterate until it compiles and fails for the right reason, then passes once Reconcile runs). + +- [ ] **Step 4: Make it pass** + +The production code from Tasks 1-3 already implements the behavior. Adjust only the test scaffolding (identifiers, seeding) until it passes. + +Run: `cd backend && go test ./internal/integration/ -run TestReconcile -v` +Expected: PASS. + +- [ ] **Step 5: Run the full backend suite** + +Run: `cd backend && go test ./...` +Expected: PASS (no regressions in session_manager, daemon, integration). + +- [ ] **Step 6: Commit** + +```bash +cd backend && gofmt -w internal/integration/lifecycle_sqlite_test.go +git add internal/integration/lifecycle_sqlite_test.go +git -c user.email=dev@theharshitsingh.com commit -m "test(integration): reconcile terminates dead-live sessions and reaps leaked tmux" +``` + +--- + +## Task 5: Frontend wedged-orphan kill+replace branch + +**Files:** +- Modify: `frontend/src/main.ts` (in `startDaemonInner`, around lines 457-495) +- Test: `frontend/src/main.test.ts` or the existing main-process test file + +**Interfaces:** +- Consumes: existing `inspectExistingDaemon`, `resolveDaemonFromPort`, `readDaemonProbe`, `killDaemon`, `parseRunFile`/`defaultRunFilePath`, `expectedDaemonPort`. +- Produces: a pure decision helper, e.g. `function planDaemonTakeover(probe: DaemonProbe | null): "reuse" | "replace"`, unit-testable without spawning. + +- [ ] **Step 1: Read the current launch flow** + +Read `frontend/src/main.ts:432-512`. Confirm: `inspectExistingDaemon` returns a status when the run-file agrees with a live daemon; `resolveDaemonFromPort` attaches when a daemon answers the port. The gap: when a process holds the port but is unhealthy (no `/healthz` + `/readyz`) or identity-mismatched, today the code falls through to `spawn`, and the Go child then refuses the port and exits 1. We add: detect that case and kill the holder first. + +- [ ] **Step 2: Write the failing unit test for the decision helper** + +In the frontend test file: + +```ts +import { planDaemonTakeover } from "./main"; + +test("healthy probe -> reuse", () => { + expect(planDaemonTakeover({ healthy: true, pid: 123, port: 3001 })).toBe("reuse"); +}); + +test("port held but unhealthy probe -> replace", () => { + expect(planDaemonTakeover({ healthy: false, pid: 123, port: 3001 })).toBe("replace"); +}); + +test("no probe (nothing on port) -> replace (spawn fresh)", () => { + expect(planDaemonTakeover(null)).toBe("replace"); +}); +``` + +> Match `DaemonProbe`'s real shape from `frontend/src/shared/` (the `readDaemonProbe` return type). If it exposes health via a different field (e.g. presence of both `healthz` and `readyz`), encode that in `planDaemonTakeover` and the test rather than a `healthy` boolean. + +- [ ] **Step 3: Run it, verify it fails** + +Run: `cd frontend && npm test -- planDaemonTakeover` +Expected: FAIL — `planDaemonTakeover` not exported. + +- [ ] **Step 4: Implement the helper and wire the branch** + +Add the pure helper (top-level, exported) in `main.ts`: + +```ts +// planDaemonTakeover decides what to do with whatever currently holds the daemon +// port on launch. A healthy daemon is reused (it kept sessions alive across a +// crash). Anything else - an unhealthy/wedged holder, or nothing answering - means +// spawn fresh; the caller kills a live-but-unhealthy holder first. +export function planDaemonTakeover(probe: DaemonProbe | null): "reuse" | "replace" { + return probe?.healthy ? "reuse" : "replace"; +} +``` + +Then, in `startDaemonInner`, after the existing `inspectExistingDaemon` + `resolveDaemonFromPort` attach attempts fail (i.e. just before `spawn`), add: probe the expected port; if something answers but is unhealthy, SIGTERM the holder via the run-file PID and wait for the port to free before spawning. Concretely, before the `spawn(...)` at line 505: + +```ts + // A process may hold the port without being a healthy daemon we can attach to + // (wedged orphan from a crash, or a PID-dead-but-port-held run-file). Spawning + // then would make the Go child collide and exit 1. Detect it and clear it. + const holderProbe = await readDaemonProbe(expectedDaemonPort(process.env)); + if (planDaemonTakeover(holderProbe) === "replace" && holderProbe) { + const runFile = parseRunFile(await readRunFileSafe(defaultRunFilePath())); + if (runFile?.pid) { + try { + process.kill(-runFile.pid, "SIGTERM"); + } catch { + try { process.kill(runFile.pid, "SIGTERM"); } catch { /* already gone */ } + } + } + await waitForPortFree(expectedDaemonPort(process.env), 8_000); + await rmRunFileSafe(defaultRunFilePath()); + } +``` + +> Use the file's existing run-file read/parse helpers (`parseRunFile`, `defaultRunFilePath`). If `readRunFileSafe`/`rmRunFileSafe`/`waitForPortFree` do not exist, add small local helpers: `readRunFileSafe` wraps `fs.readFile` returning `""` on ENOENT; `rmRunFileSafe` wraps `fs.rm` ignoring ENOENT; `waitForPortFree` polls `readDaemonProbe` until it returns null or the timeout elapses. Keep each to a few lines, matching the file's existing async style. + +- [ ] **Step 5: Run the tests, verify they pass** + +Run: `cd frontend && npm test -- planDaemonTakeover` +Expected: PASS. + +- [ ] **Step 6: Type-check and lint the frontend** + +Run: `cd frontend && npm run typecheck && npm run lint` +Expected: success (commands per `frontend/package.json`; if names differ, use the repo's configured equivalents). + +- [ ] **Step 7: Commit** + +```bash +cd frontend +git add src/main.ts src/main.test.ts +git -c user.email=dev@theharshitsingh.com commit -m "feat(frontend): kill+replace a wedged orphan daemon on launch" +``` + +--- + +## Task 6: Full verification and branch wrap-up + +- [ ] **Step 1: Backend suite + lint** + +Run: `cd backend && go test ./... && gofmt -l . && go vet ./...` +Expected: tests PASS, `gofmt -l` prints nothing. If `golangci-lint` is installed: `golangci-lint run ./internal/session_manager/... ./internal/daemon/...` clean. + +- [ ] **Step 2: Frontend suite** + +Run: `cd frontend && npm test` +Expected: PASS. + +- [ ] **Step 3: Manual smoke (optional, real hardware)** + +With the app/daemon running and at least one session live, `kill -9` the daemon PID (from `~/.ao/running.json`), then relaunch. Expect: the live session's tmux is adopted (still listed, agent intact), no duplicate daemon, `running.json` repointed to the new PID. Then kill a session's agent and `kill -9` the daemon: expect that session marked terminated with its work in a `refs/ao/preserved/` ref, and no leaked tmux. + +- [ ] **Step 4: Review the diff against the spec** + +Confirm every spec section maps to a task: live pass (T1), reap + entry point (T2), boot wiring (T3), integration (T4), frontend takeover (T5). Confirm no worktree directory is ever deleted by reconcile and no agent is relaunched outside the existing `RestoreAll`. + +- [ ] **Step 5: Push the branch** + +```bash +git push -u origin feat/crash-proof-session-reconcile +``` + +--- + +## Self-Review notes (planning) + +- **Spec coverage:** Component 1 (live + reap matrix) -> Tasks 1-2; Component 2 (order of phases) -> Task 2 `Reconcile`; Component 3 (frontend kill+replace) -> Task 5; error-handling contract -> best-effort logging in every pass; testing section -> Tasks 1, 2, 4, 5. Deferred `ListSessions` is explicitly not implemented (matches spec Deferred). +- **Type consistency:** `IsAlive(ctx, ports.RuntimeHandle) (bool, error)` matches the concrete tmux/conpty signature (`tmux.go:176`). `Reconcile(ctx) error`, `reconcileLive(ctx, domain.SessionRecord) error`, `reconcileReap(ctx, domain.SessionRecord) error` are used identically across tasks. `runtimeHandle`/`workspaceInfo` helpers exist at `manager.go:1135,1139`. +- **Placeholder scan:** test bodies that depend on existing fakes' field names (`stashCalls`, `terminated`, `newManager`) carry an explicit instruction to match the file's real identifiers; this is unavoidable without the fakes in front of the implementer and is called out at each use, not left as a silent TODO. diff --git a/docs/superpowers/plans/2026-06-24-restore-recreate-orchestrator.md b/docs/superpowers/plans/2026-06-24-restore-recreate-orchestrator.md new file mode 100644 index 00000000..6811fa9f --- /dev/null +++ b/docs/superpowers/plans/2026-06-24-restore-recreate-orchestrator.md @@ -0,0 +1,367 @@ +# Restore Recreate Orchestrator Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Turn the opaque 500 on restoring an un-resumable session into a typed 409, and add a popup (shown only after a failed restore) that offers to recreate a fresh orchestrator on the same branch. + +**Architecture:** One small backend change (a typed sentinel error + its 409 mapping) plus a frontend popup. The recreate action reuses the EXISTING `POST /api/v1/orchestrators {clean:true}` endpoint, which already kills the dead orchestrator and re-spawns one on the canonical branch (reattaching the existing branch with history). No new backend route, manager method, or OpenAPI regeneration. + +**Tech Stack:** Go backend (session_manager + service/session), React + TypeScript renderer (Radix Dialog, openapi-fetch api client, vitest). + +## Global Constraints + +- No em dashes or en dashes anywhere (prose, comments, commit messages). Use periods, commas, colons, semicolons, parentheses. +- The renderer clones the agent-orchestrator web app; build UI from shadcn primitives (`components/ui/*`) and the Radix Dialog pattern already used by `NewTaskDialog.tsx`. +- App state under `~/.ao` only (not touched here). +- The existing resume path and the interactive dirty-refusal removal path stay behaviorally unchanged. +- Do not hand-edit generated sqlc/OpenAPI output. This feature adds no routes, so no regeneration is needed. +- Git author is already configured (dev@theharshitsingh.com); add `Co-Authored-By: Claude Opus 4.8 ` to commits. + +--- + +### Task 1: Typed error for un-resumable restore (fixes the 500) + +**Files:** +- Modify: `backend/internal/session_manager/manager.go` (sentinel near line 25; the "nothing to resume from" return at line 480) +- Modify: `backend/internal/service/session/service.go` (`toAPIError`, near line 450) +- Test: `backend/internal/service/session/service_test.go` (new test for the mapping) + +**Interfaces:** +- Produces: `sessionmanager.ErrNotResumable` (a sentinel `error`), and the wire contract `409` with code `SESSION_NOT_RESUMABLE` from `POST /api/v1/sessions/{id}/restore` when a terminated session has neither `agent_session_id` nor `prompt`. Task 2 (frontend) consumes the `SESSION_NOT_RESUMABLE` code. + +- [ ] **Step 1: Write the failing test** + +In `backend/internal/service/session/service_test.go`, add (mirror the package's existing test style and imports; `apierr` is `backend/internal/apierr`, `sessionmanager` is the session_manager package alias already used in `service.go`): + +```go +func TestToAPIError_NotResumable(t *testing.T) { + err := toAPIError(fmt.Errorf("restore foo: %w", sessionmanager.ErrNotResumable)) + var ae *apierr.Error + if !errors.As(err, &ae) { + t.Fatalf("want *apierr.Error, got %T: %v", err, err) + } + if ae.Kind != apierr.KindConflict { + t.Errorf("kind = %v, want %v", ae.Kind, apierr.KindConflict) + } + if ae.Code != "SESSION_NOT_RESUMABLE" { + t.Errorf("code = %q, want SESSION_NOT_RESUMABLE", ae.Code) + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `cd backend && go test ./internal/service/session/ -run TestToAPIError_NotResumable` +Expected: FAIL to COMPILE with `undefined: sessionmanager.ErrNotResumable`. + +- [ ] **Step 3: Add the sentinel** + +In `backend/internal/session_manager/manager.go`, in the `var (...)` error block near line 25 (next to `ErrNotRestorable`, `ErrIncompleteHandle`), add: + +```go + // ErrNotResumable means a terminated session has no saved agent session id + // and no prompt, so there is nothing for Restore to relaunch from. Distinct + // from ErrNotRestorable (which is "not terminal yet"). + ErrNotResumable = errors.New("session: nothing to resume from") +``` + +- [ ] **Step 4: Return the sentinel from Restore** + +In `backend/internal/session_manager/manager.go`, change the plain error at line 480 (inside `Restore`) from: + +```go + if meta.AgentSessionID == "" && meta.Prompt == "" { + return domain.SessionRecord{}, fmt.Errorf("restore %s: nothing to resume from", id) + } +``` + +to: + +```go + if meta.AgentSessionID == "" && meta.Prompt == "" { + return domain.SessionRecord{}, fmt.Errorf("restore %s: %w", id, ErrNotResumable) + } +``` + +- [ ] **Step 5: Map the sentinel in `toAPIError`** + +In `backend/internal/service/session/service.go`, inside `toAPIError`, add a case alongside the sibling cases (after the `ErrIncompleteHandle` case, around line 455): + +```go + case errors.Is(err, sessionmanager.ErrNotResumable): + return apierr.Conflict("SESSION_NOT_RESUMABLE", + "This session has no saved agent session or prompt to resume from", nil) +``` + +- [ ] **Step 6: Run the test to verify it passes** + +Run: `cd backend && go test ./internal/service/session/ -run TestToAPIError_NotResumable` +Expected: PASS. + +- [ ] **Step 7: Build, vet, and run the touched packages** + +Run: `cd backend && go build ./... && go vet ./internal/session_manager/... ./internal/service/session/... && go test ./internal/session_manager/... ./internal/service/session/...` +Expected: build clean, vet clean, all tests PASS (no behavior change to existing restore tests since the error value still wraps the same condition). + +- [ ] **Step 8: Commit** + +```bash +git add backend/internal/session_manager/manager.go backend/internal/service/session/service.go backend/internal/service/session/service_test.go +git commit -m "fix(session): return typed SESSION_NOT_RESUMABLE instead of 500 on un-resumable restore + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 2: Restore-unavailable popup + recreate via existing orchestrator endpoint + +**Files:** +- Modify: `frontend/src/renderer/lib/spawn-orchestrator.ts` (optional `clean` param) +- Create: `frontend/src/renderer/components/RestoreUnavailableDialog.tsx` (the popup) +- Modify: `frontend/src/renderer/components/TerminalPane.tsx` (route `SESSION_NOT_RESUMABLE` to the dialog) +- Test: `frontend/src/renderer/lib/spawn-orchestrator.test.ts` (new; clean param) + +**Interfaces:** +- Consumes from Task 1: the restore response error envelope `{ code: "SESSION_NOT_RESUMABLE", message, ... }`. +- Consumes existing: `spawnOrchestrator(projectId, clean?)` (extended here), `isOrchestrator(session)` from `frontend/src/renderer/types/workspace.ts`, `apiClient`/`apiErrorMessage` from `lib/api-client`, `workspaceQueryKey` already imported in `TerminalPane.tsx`. +- Produces: `RestoreUnavailableDialog` React component with props `{ open: boolean; session: SessionView; onOpenChange: (open: boolean) => void; onRecreated: (newOrchestratorId: string) => void }`. + +- [ ] **Step 1: Write the failing test for the `clean` param** + +Create `frontend/src/renderer/lib/spawn-orchestrator.test.ts` (mirror the mocking style in existing `frontend/src/renderer/**/*.test.ts(x)`; vitest is already configured): + +```ts +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { spawnOrchestrator } from "./spawn-orchestrator"; +import { apiClient } from "./api-client"; + +vi.mock("./api-client", () => ({ + apiClient: { POST: vi.fn() }, +})); + +describe("spawnOrchestrator", () => { + beforeEach(() => vi.clearAllMocks()); + + it("sends clean:true through to the request body when asked", async () => { + (apiClient.POST as ReturnType).mockResolvedValue({ + data: { orchestrator: { id: "proj-9" } }, + error: undefined, + response: { status: 201 }, + }); + const id = await spawnOrchestrator("proj", true); + expect(id).toBe("proj-9"); + expect(apiClient.POST).toHaveBeenCalledWith("/api/v1/orchestrators", { + body: { projectId: "proj", clean: true }, + }); + }); + + it("defaults clean to false / omitted for the existing call sites", async () => { + (apiClient.POST as ReturnType).mockResolvedValue({ + data: { orchestrator: { id: "proj-1" } }, + error: undefined, + response: { status: 201 }, + }); + await spawnOrchestrator("proj"); + expect(apiClient.POST).toHaveBeenCalledWith("/api/v1/orchestrators", { + body: { projectId: "proj", clean: false }, + }); + }); +}); +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `cd frontend && npx vitest run src/renderer/lib/spawn-orchestrator.test.ts` +Expected: FAIL (current helper sends `{ projectId }` with no `clean`). + +- [ ] **Step 3: Add the `clean` param to the helper** + +Edit `frontend/src/renderer/lib/spawn-orchestrator.ts`: + +```ts +import { apiClient } from "./api-client"; + +/** Spawn the project's orchestrator session via the daemon API. When clean is + * true the daemon first tears down any active orchestrator for the project, then + * re-spawns one on the canonical branch (reattaching the existing branch). */ +export async function spawnOrchestrator(projectId: string, clean = false): Promise { + const { data, error, response } = await apiClient.POST("/api/v1/orchestrators", { + body: { projectId, clean }, + }); + + if (error || !data?.orchestrator?.id) { + const message = + error && typeof error === "object" && "message" in error && typeof error.message === "string" + ? error.message + : `Failed to spawn orchestrator (${response.status})`; + throw new Error(message); + } + + return data.orchestrator.id; +} +``` + +- [ ] **Step 4: Run the test to verify it passes** + +Run: `cd frontend && npx vitest run src/renderer/lib/spawn-orchestrator.test.ts` +Expected: PASS (both cases). + +- [ ] **Step 5: Create the popup component** + +Create `frontend/src/renderer/components/RestoreUnavailableDialog.tsx` (mirror the Radix Dialog structure/styling of `NewTaskDialog.tsx`; reuse `Button` from `./ui/button`): + +```tsx +import * as Dialog from "@radix-ui/react-dialog"; +import { Loader2 } from "lucide-react"; +import { useState } from "react"; +import { Button } from "./ui/button"; +import { spawnOrchestrator } from "../lib/spawn-orchestrator"; +import { isOrchestrator } from "../types/workspace"; +import type { SessionView } from "../types/workspace"; + +type RestoreUnavailableDialogProps = { + open: boolean; + session: SessionView; + onOpenChange: (open: boolean) => void; + onRecreated: (newOrchestratorId: string) => void; +}; + +export function RestoreUnavailableDialog({ open, session, onOpenChange, onRecreated }: RestoreUnavailableDialogProps) { + const [busy, setBusy] = useState(false); + const [error, setError] = useState(); + const orchestrator = isOrchestrator(session); + + const recreate = async () => { + setBusy(true); + setError(undefined); + try { + const id = await spawnOrchestrator(session.projectId, true); + onOpenChange(false); + onRecreated(id); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to create orchestrator"); + } finally { + setBusy(false); + } + }; + + return ( + + + + + + Session can no longer be restored + + + {orchestrator + ? "This orchestrator has no saved agent session to resume. You can create a new orchestrator on the same branch; its committed work is preserved and the old worktree is cleaned." + : "This session has no saved agent session or prompt to resume from."} + + {error &&
{error}
} +
+ + {orchestrator && ( + + )} +
+
+
+
+ ); +} +``` + +Note: confirm `Button` supports the `variant="ghost"` prop (check `./ui/button`); if its variant names differ, use the existing equivalent for a secondary/cancel button. Confirm `SessionView` exposes `projectId` and `kind`; if `projectId` is named differently on the view type, use the actual field. + +- [ ] **Step 6: Wire the restore handler in `TerminalPane.tsx`** + +In `frontend/src/renderer/components/TerminalPane.tsx`, add state and a dialog mount, and branch the restore error on the `SESSION_NOT_RESUMABLE` code. The existing handler is `restoreSession` (around lines 85-100) and the error is `restoreError` from `apiClient.POST(".../restore")`. + +Add state near the other `useState` hooks in `AttachedTerminal`: + +```tsx + const [restoreUnavailable, setRestoreUnavailable] = useState(false); +``` + +Replace the `catch`/error handling inside `restoreSession` so a `SESSION_NOT_RESUMABLE` code opens the dialog instead of setting the inline error. The `restoreError` returned by `apiClient.POST` is the parsed error envelope, so read its `code`: + +```tsx + try { + const { error: restoreError } = await apiClient.POST("/api/v1/sessions/{sessionId}/restore", { + params: { path: { sessionId: session.id } }, + }); + if (restoreError) { + const code = (restoreError as { code?: string }).code; + if (code === "SESSION_NOT_RESUMABLE") { + setRestoreUnavailable(true); + return; + } + throw new Error(apiErrorMessage(restoreError, "Unable to restore session")); + } + await queryClient.invalidateQueries({ queryKey: workspaceQueryKey }); + } catch (err) { + setRestoreError(err instanceof Error ? err.message : "Unable to restore session"); + } finally { + setIsRestoring(false); + } +``` + +Mount the dialog inside the component's returned JSX (e.g. just before the closing tag of the root `div` in `AttachedTerminal`, alongside the other absolutely-positioned children): + +```tsx + {session && ( + { + await queryClient.invalidateQueries({ queryKey: workspaceQueryKey }); + }} + /> + )} +``` + +Add the import at the top of the file: + +```tsx +import { RestoreUnavailableDialog } from "./RestoreUnavailableDialog"; +``` + +Note: `onRecreated` here just refreshes the workspace so the new orchestrator appears in the list. If the renderer has an existing "select session" mechanism reachable from this component, call it with the new id; otherwise the invalidate is sufficient and the user picks the new orchestrator from the refreshed list. Do not invent a selection API that does not exist. + +- [ ] **Step 7: Typecheck and run the frontend tests** + +Run: `cd frontend && npx tsc --noEmit 2>&1 | grep -v "forge.config" ; npx vitest run src/renderer/lib/spawn-orchestrator.test.ts` +Expected: no NEW typecheck errors (only the pre-existing `forge.config.ts` `osxNotarize` error is acceptable); vitest PASS. + +- [ ] **Step 8: Commit** + +```bash +git add frontend/src/renderer/lib/spawn-orchestrator.ts frontend/src/renderer/lib/spawn-orchestrator.test.ts frontend/src/renderer/components/RestoreUnavailableDialog.tsx frontend/src/renderer/components/TerminalPane.tsx +git commit -m "feat(renderer): offer recreate-orchestrator popup when a session cannot be restored + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +## Manual verification (after both tasks, requires a rebuild of the packaged app) + +1. `cd frontend && export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"; nvm use 22.17.0; npm run make` (signed/notarized build per the release runbook) or run the dev app. +2. Terminate an orchestrator that has no saved agent session (e.g. a stale one), so the UI shows its "Restore session" button. +3. Click "Restore session". Expected: a popup appears (NOT "Internal server error"), titled "Session can no longer be restored". +4. Click "Create new orchestrator". Expected: a fresh orchestrator launches on the same `ao/-orchestrator` branch with its committed history intact, and appears in the session list. +5. Confirm a worker that cannot be restored shows the same popup with a Close-only button (no recreate). + +## Self-review notes + +- Spec coverage: Task 1 covers the typed-error fix (spec Backend #1); Task 2 covers the popup + recreate via the existing `/orchestrators` endpoint (spec Backend #2 reuse + Frontend). The spec's "no new endpoint / no OpenAPI regen" is honored. +- Type consistency: `ErrNotResumable` (Task 1) is the symbol consumed by `toAPIError`; `SESSION_NOT_RESUMABLE` is the wire code consumed by Task 2's handler; `spawnOrchestrator(projectId, clean)` signature is defined in Task 2 Step 3 and consumed in Step 5. +- The two `Note:` callouts (Button variant name, SessionView `projectId` field, selection API) flag the only spots where the exact local name must be confirmed against the codebase during implementation; the implementer verifies rather than guessing. diff --git a/docs/superpowers/specs/2026-06-24-crash-proof-session-reconcile-design.md b/docs/superpowers/specs/2026-06-24-crash-proof-session-reconcile-design.md new file mode 100644 index 00000000..2c0c41fd --- /dev/null +++ b/docs/superpowers/specs/2026-06-24-crash-proof-session-reconcile-design.md @@ -0,0 +1,152 @@ +# Crash-proof session reconcile — design + +Date: 2026-06-24 +Status: approved (brainstorming), pending implementation plan +Branch: `feat/crash-proof-session-reconcile` + +## Problem + +Closing the app can leave orphaned state behind: a detached daemon still +holding its port, live tmux sessions, and worktrees on disk. Observed +directly: app closed, `running.json` pointed at a dead PID, two tmux sessions +(`ao-agents-11`, the orchestrator `ao-agents-12`) still alive, and three +worktrees on disk. + +### Root cause + +`SaveAndTeardownAll` (the save-on-close teardown) is gated entirely behind +`srv.Run` returning (`backend/internal/daemon/daemon.go:151,163`). `srv.Run` +only returns on a catchable signal (`signal.NotifyContext` for SIGINT/SIGTERM) +or `POST /shutdown`. A **SIGKILL, a crash, or the AppTranslocation mount +vanishing** satisfies none of these: `srv.Run` never returns, so teardown +never runs. The DB confirmed it for the incident: sessions 11 and 12 were +still `is_terminated=0` with no termination or marker writes after the last +activity. + +The daemon is spawned `detached` (`frontend/src/main.ts:509`), so on a +non-clean app exit it is orphaned (reparented to launchd), keeps holding the +port and its tmux sessions, and later dies by SIGKILL without ever tearing +down. + +### Key principle + +You cannot guarantee a clean shutdown. Any fix that only hardens the shutdown +path leaves the SIGKILL/crash hole open. Correctness must come from +**idempotent boot-time reconcile**: every daemon start makes live reality +(tmux + worktrees) match the DB, regardless of how the previous run ended. + +## Scope + +In scope: a no-leak guarantee. After any app exit (clean, force-quit, crash), +the next boot reconciles so there are no orphaned daemon/tmux/worktrees, and +every live session is either adopted or cleanly terminated. + +Out of scope (deliberately unchanged — separate decision): + +- Orchestrator re-spawn-vs-restore policy and stale `session_worktrees` marker + cleanup (the "orchestrator spam" bug). +- Auto-relaunching crash-killed agents. Reconcile preserves work and marks + terminated; it never spawns a new agent. + +## Design + +### Component 1 — `Manager.Reconcile(ctx)` (daemon side, the core) + +A single idempotent pass that **replaces** the bare `RestoreAll` call at +`daemon.go:147`, run before the server starts serving. It folds the existing +restore logic in as one branch. Iterating `ListAllSessions`: + +Reconcile iterates `ListAllSessions` and acts per session: + +| DB state | tmux via `IsAlive(handle)` | Action | +| ----------------------------- | -------------------------- | ------------------------------------------------------------------ | +| `is_terminated=0` | alive | **Adopt** — no-op, leave live. Agent keeps running. | +| `is_terminated=0` | gone | `StashUncommitted` (best-effort) -> `MarkTerminated`. No relaunch. | +| `is_terminated=1` | alive | **Reap** — `Destroy` the leaked tmux session. | +| `is_terminated=1`, has marker | gone | Existing `RestoreAll` restore branch, unchanged. | +| `is_terminated=1`, no marker | gone | Leave terminated (user-killed before shutdown; untouched). | + +Adoption is safe and lossless because tmux is the persistence layer: the +detached tmux session survives a daemon crash, and the session's +`runtime_handle_id` (the tmux session name) is in the DB. A matching live +handle means the session genuinely survived; adopting is a no-op. + +The **reap** of a terminated-but-still-alive tmux session uses the existing +per-handle `IsAlive` + `Destroy`; no session enumeration is needed because +every leak tied to a session has a DB row (confirmed for the incident: 9, 11, +12 all have rows). Reap must run **before** the restore branch so a restored +session gets a fresh runtime rather than colliding with a leaked tmux of the +same name. + +Worktrees: dirty worktrees are **always preserved** (this is why an +intentionally-preserved dirty worktree like session 9 survives — correct, by +design; matches the interactive `Destroy` `ErrWorkspaceDirty` refusal). +Reconcile does not delete worktree directories; worktree lifecycle stays with +the existing teardown/restore/cleanup paths. + +### Component 2 — order of operations + +`Reconcile` runs three phases over `ListAllSessions`, in order: + +1. **Live pass** (`is_terminated=0`): adopt if `IsAlive`, else stash + + `MarkTerminated`. +2. **Reap pass** (`is_terminated=1` with live tmux): `Destroy` the leaked + session. +3. **Restore pass**: the existing `RestoreAll` body (terminated + marked + sessions), unchanged. + +Deferred (YAGNI): reaping a tmux session that has **no DB row at all** (a true +orphan). Not observed in the incident and not reachable through normal spawn +(every tmux session is created for a DB-backed session). If it ever appears, it +is a follow-up that adds a `Runtime.ListSessions` enumerator scoped to this +daemon's session-id namespace (so a co-resident AO install's sessions — +observed: `aa-107`, `aa-109` — stay untouched). Out of scope here. + +### Component 3 — Frontend "replace wedged orphan" branch + +The healthy-attach path already exists: `inspectExistingDaemon` + +`resolveDaemonFromPort` (`frontend/src/main.ts:457-485`) attach to a healthy +existing daemon. The gap is the failure branch. Add: when the port is held but +the daemon is unhealthy / identity-mismatched / PID-dead-but-port-held, +SIGTERM the process group, wait for the port to free, clear the stale +`running.json`, then spawn fresh (which runs Reconcile). A healthy orphan is +reconnected exactly as today, untouched. + +## Behaviour for the observed incident + +- 11 & 12 (alive tmux) -> **adopted**, nothing lost. +- A future crash where tmux also died -> work stashed, marked terminated, no + orphan left. +- Orphan daemon on next launch -> reused if healthy, else killed + replaced. +- A terminated session whose tmux survived teardown -> reaped (`Destroy`). +- Dirty worktrees (like 9) -> preserved. + +## Error handling + +- Per-session reconcile failures are logged and never abort the pass (same + pattern as `SaveAndTeardownAll` / `RestoreAll`). +- `Reconcile` is best-effort and must never block boot: a failure is logged, + boot continues (same contract as the current `RestoreAll` call site). +- `StashUncommitted` on a crash-dead worktree is best-effort; a failure logs + and still proceeds to `MarkTerminated` (no work is destroyed — the worktree + stays on disk). +- Orphan-reap `Destroy` failures are logged and do not abort the loop. + +## Testing + +- Unit: table-test `Reconcile` over each matrix row with a fake runtime whose + `IsAlive` is scriptable per handle (alive / gone), asserting DB transitions + (`MarkTerminated`), `StashUncommitted` calls, and runtime `Destroy` (reap) + calls. +- Unit: assert the live pass adopts (no `Destroy`, no `MarkTerminated`) when + `IsAlive` is true. +- Integration: extend the sqlite lifecycle test with a seeded + `is_terminated=0`-but-dead session and a `is_terminated=1`-but-alive session; + assert the post-reconcile DB state and the reap `Destroy` call. + +## Open question (resolved during planning) + +Orphan-reap is done per-session via `IsAlive` over DB rows, so there is no +enumeration and no namespace-matching risk in this iteration. The riskier +"reap a tmux session with no DB row" case is deferred (see Component 2, +Deferred), which removes the original namespace-scoping question from scope. diff --git a/docs/superpowers/specs/2026-06-24-restore-recreate-orchestrator-design.md b/docs/superpowers/specs/2026-06-24-restore-recreate-orchestrator-design.md new file mode 100644 index 00000000..3adcef75 --- /dev/null +++ b/docs/superpowers/specs/2026-06-24-restore-recreate-orchestrator-design.md @@ -0,0 +1,211 @@ +# Design: Graceful Restore + Post-Failure Orchestrator Recreate + +## Problem + +Clicking "Restore session" on a terminated session that has no resumable state +returns an opaque **HTTP 500** and the UI shows "Internal server error". Root +cause, traced through the running build: + +- `manager.Restore` (`backend/internal/session_manager/manager.go:479-480`) + returns a **plain** error when a session has neither an agent session id nor a + prompt: + ```go + if meta.AgentSessionID == "" && meta.Prompt == "" { + return ..., fmt.Errorf("restore %s: nothing to resume from", id) + } + ``` +- `toAPIError` (`backend/internal/service/session/service.go:444`) maps known + sentinels (`ErrNotRestorable`, `ErrIncompleteHandle`, ...) to clean 4xx codes, + but an unrecognized error "passes through and surfaces as a 500" (its own + comment). "nothing to resume from" is not a sentinel, so the user gets a 500. + +Observed on `ao-agents-8`: a terminated **orchestrator** with empty +`agent_session_id` and empty `prompt` (a stale pre-lifecycle-feature orphan). +The branch `ao/ao-agents-orchestrator` still exists with its committed history; +only the resumable agent state is gone. + +This is a **pre-existing** bug (the single-session restore endpoint predates the +session-lifecycle feature). It is now visible because terminating such a session +makes the UI offer its Restore button. + +## Goals + +1. A restore that cannot succeed returns a clear, typed client error, never a 500. +2. When restore is confirmed impossible for an **orchestrator**, the user is + offered, via a **popup that appears only after clicking Restore**, the option + to create a fresh orchestrator on the same branch (preserving committed + history), cleaning the old worktree. +3. Restore is offered/attempted normally for sessions that CAN be restored; the + recreate path never fires unless a restore attempt was made and the backend + confirmed it is not resumable. No orchestrator spam when restore works. + +## Non-Goals + +- Workers get the clear error + popup explanation, but **no** recreate action + (scope decision: orchestrators only). +- No change to how restorable sessions resume (the existing resume path stays + behaviorally unchanged). +- No upfront `restorable` flag on the session DTO: the flow is driven by the + restore attempt's response, so a precomputed flag is unnecessary (YAGNI). + +## Core reframe + +Two distinct operations on a terminated session share worktree machinery but +differ at launch: + +- **Restore** = re-attach a worktree on the existing branch + **resume** the + agent (requires `agent_session_id` or `prompt`). +- **Recreate orchestrator** = re-attach a worktree on the existing branch + + launch a **fresh** orchestrator agent (no resume state needed). + +`worktree add` has two arg builders in +`backend/internal/adapters/workspace/gitworktree/commands.go`: +`worktreeAddBranchArgs` (existing branch, no `-b`, used by `Restore`) and +`worktreeAddNewBranchArgs` (`-b`, new branch, used by `Create`/Spawn). Recreate +must REUSE the existing branch, so it goes through the existing-branch attach +(the `Restore` path), NOT Spawn's `-b` path. + +## Design + +### Backend + +#### 1. Typed error for un-resumable restore (fixes the 500) +- Add sentinel in `session_manager` (next to the existing sentinels near + `manager.go:25`): + ```go + ErrNotResumable = errors.New("session: nothing to resume from") + ``` +- Use it at `manager.go:480`: + ```go + return domain.SessionRecord{}, fmt.Errorf("restore %s: %w", id, ErrNotResumable) + ``` +- Map it in `toAPIError` (`service/session/service.go`), alongside the sibling + cases, as a **409**: + ```go + case errors.Is(err, sessionmanager.ErrNotResumable): + return apierr.Conflict("SESSION_NOT_RESUMABLE", + "This session has no saved agent session or prompt to resume from", nil) + ``` + +#### 2. Recreate: REUSE the existing `POST /api/v1/orchestrators` (clean=true) +**Discovery during planning:** the recreate capability already ships. No new +endpoint or manager method is needed. + +- `SessionsController.spawnOrchestrator` already handles `POST /api/v1/orchestrators` + with body `{projectId, clean}` (`httpd/controllers/sessions.go`). +- `Service.SpawnOrchestrator(ctx, projectID, clean)` + (`service/session/service.go:263`): when `clean` is true it kills any active + orchestrators for the project, then `Spawn(SpawnConfig{ProjectID, Kind: + orchestrator})`. +- `Spawn` with no branch defaults to the canonical orchestrator branch + `ao/-orchestrator` (`defaultSessionBranch`). That is the SAME branch + the dead orchestrator used. +- `workspace.Create` -> `addWorktree` + (`adapters/workspace/gitworktree/workspace.go`) already detects an EXISTING + local branch (`refExists("refs/heads/"+branch)`) and attaches it with the + no-`-b` `worktreeAddBranchArgs` (preserving committed history); it only uses + `-b` for a genuinely new branch, and refuses with `ErrBranchCheckedOutElsewhere` + (409) if the branch is live in another worktree. + +So "create a new orchestrator on the same branch, cleaning the old worktree" = +`POST /api/v1/orchestrators {projectId, clean:true}`. The `clean` kill frees the +dead orchestrator's worktree; the re-spawn reattaches the existing branch. The +old session row stays terminated; a new orchestrator session id is returned. +Orchestrator uniqueness is already enforced by the `clean` kill-then-spawn rule. + +The ONLY backend change in this feature is item #1 (the typed error). No new +route, no `RecreateOrchestrator`, no OpenAPI/spec regen. + +### Frontend + +`frontend/src/renderer/components/TerminalPane.tsx`: + +- The **"Restore session"** button stays on every terminated non-reviewer + session (the existing `canRestoreSession` trigger is unchanged). +- `restoreSession` handler, after `POST /api/v1/sessions/{id}/restore`: + - success → invalidate workspace queries + attach (existing behavior). + - error whose API code is **`SESSION_NOT_RESUMABLE`** → open a new dialog + component instead of showing the inline error. + - any other error → existing inline error display. +- New `RestoreUnavailableDialog` component (Radix Dialog, mirroring + `NewTaskDialog.tsx`; primitives from `components/ui/*`): + - Title: "Session can no longer be restored". + - Body: explains there is no saved agent session/prompt to resume from. + - If the session `kind === "orchestrator"`: primary button **"Create new + orchestrator"** → calls the existing `spawnOrchestrator` helper + (`frontend/src/renderer/lib/spawn-orchestrator.ts`) extended with a `clean` + argument: `spawnOrchestrator(projectId, true)` → `POST /api/v1/orchestrators + {projectId, clean:true}`, with a loading state; on success, invalidate + workspace queries and select the returned new orchestrator id; "Cancel" + closes. + - If `kind === "worker"`: explanatory text + "Close" only (no recreate). +- Detect the code via the API error body `code === "SESSION_NOT_RESUMABLE"` + (same envelope `apiErrorMessage`/error-shape the renderer already reads). +- `spawn-orchestrator.ts` gains an optional `clean = false` parameter passed + through to the request body; the existing single-arg call sites are unchanged. + +## Data flow + +``` +User clicks "Restore session" + -> POST /sessions/{id}/restore + restorable -> 200, terminal attaches + not resumable -> 409 SESSION_NOT_RESUMABLE + -> popup opens + orchestrator -> "Create new orchestrator" + -> POST /api/v1/orchestrators {projectId, clean:true} + (existing endpoint: kills active orchestrator, + re-spawns on canonical branch, reattaches + existing branch with history) + -> 201, select new orchestrator + worker -> explanatory close-only popup +``` + +## Error handling + +- All restore/recreate failures are typed `apierr` values → correct 4xx, never a + 500 for a client-actionable condition. +- Recreate is best-effort-validated up front (kind, terminated, branch present) + so the common rejections are clean 409s, not deep wrapped errors. +- Worktree attach failures during recreate surface as the existing workspace + error kinds (e.g. branch-checked-out-elsewhere) already mapped in `toAPIError`. + +## Testing + +- **Backend unit (session_manager):** restore of a terminated session with empty + `agent_session_id`+`prompt` returns `ErrNotResumable`. +- **Backend service:** `toAPIError(ErrNotResumable)` → 409 `SESSION_NOT_RESUMABLE`. +- **Frontend:** typecheck green; the `restoreSession` handler routes a + `SESSION_NOT_RESUMABLE` response to the dialog and a success to attach; the + dialog shows the orchestrator create button only for `kind === "orchestrator"`; + `spawnOrchestrator(projectId, true)` sends `clean:true`. +- **Manual:** on the packaged build, terminate an orchestrator that has no + resume state, click Restore, confirm the popup appears (not a 500), click + "Create new orchestrator", confirm a fresh orchestrator launches on the same + branch with history intact. + +## Files touched + +- `backend/internal/session_manager/manager.go` — `ErrNotResumable` sentinel + + use it at the "nothing to resume from" return. +- `backend/internal/service/session/service.go` — `toAPIError` case for + `ErrNotResumable` → 409 `SESSION_NOT_RESUMABLE`. +- `frontend/src/renderer/lib/spawn-orchestrator.ts` — optional `clean` param. +- `frontend/src/renderer/components/TerminalPane.tsx` — restore handler routes + `SESSION_NOT_RESUMABLE` to the dialog. +- `frontend/src/renderer/components/RestoreUnavailableDialog.tsx` — new dialog. + +No new backend route, manager method, or OpenAPI regeneration: the recreate +reuses the existing `POST /api/v1/orchestrators` (clean=true) path. + +## Constraints (binding) + +- No em dashes or en dashes anywhere (prose, comments, commit messages). +- Renderer clones the agent-orchestrator web app; build the dialog from shadcn + primitives (`components/ui/*`) and the Radix Dialog pattern already used by + `NewTaskDialog.tsx`. (See `DESIGN.md`.) +- App state under `~/.ao` only (not directly touched here). +- Do not hand-edit generated sqlc or OpenAPI output; regenerate via the npm + scripts. +- The existing resume path and the interactive dirty-refusal removal path stay + behaviorally unchanged. diff --git a/frontend/forge.config.ts b/frontend/forge.config.ts index d7b32402..99c3ab70 100644 --- a/frontend/forge.config.ts +++ b/frontend/forge.config.ts @@ -14,17 +14,30 @@ const config: ForgeConfig = { // deb/rpm makers below, and the runtime window icon from src/main.ts. icon: "assets/icon", extraResource: ["daemon", "assets/icon.png"], - // macOS signing + notarization — set CSC_LINK/CSC_KEY_PASSWORD and - // APPLE_ID/APPLE_APP_SPECIFIC_PASSWORD/APPLE_TEAM_ID in CI. + // macOS signing + notarization. Two paths are supported: + // - CI: set CSC_LINK/CSC_KEY_PASSWORD and + // APPLE_ID/APPLE_APP_SPECIFIC_PASSWORD/APPLE_TEAM_ID. + // - Local keychain: set APPLE_SIGNING_IDENTITY (a Developer ID Application + // identity in the login keychain) and AO_NOTARY_PROFILE (a notarytool + // keychain profile created with `notarytool store-credentials`). // See frontend/docs/desktop-release.md. - osxSign: process.env.CSC_LINK ? {} : undefined, - osxNotarize: process.env.APPLE_ID - ? { - appleId: process.env.APPLE_ID, - appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD!, - teamId: process.env.APPLE_TEAM_ID!, - } - : undefined, + osxSign: process.env.APPLE_SIGNING_IDENTITY + ? { identity: process.env.APPLE_SIGNING_IDENTITY } + : process.env.CSC_LINK + ? {} + : undefined, + osxNotarize: process.env.AO_NOTARY_PROFILE + ? ({ + tool: "notarytool", + keychainProfile: process.env.AO_NOTARY_PROFILE, + } as unknown as ForgeConfig["packagerConfig"]["osxNotarize"]) + : process.env.APPLE_ID + ? { + appleId: process.env.APPLE_ID, + appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD!, + teamId: process.env.APPLE_TEAM_ID!, + } + : undefined, }, rebuildConfig: {}, makers: [ diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 348392a2..d4f8a3da 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -1,49 +1,50 @@ -lockfileVersion: "9.0" +lockfileVersion: '9.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false importers: + .: dependencies: - "@radix-ui/react-dialog": + '@radix-ui/react-dialog': specifier: ^1.1.16 version: 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-slot": + '@radix-ui/react-slot': specifier: ^1.2.5 version: 1.2.5(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-tabs": + '@radix-ui/react-tabs': specifier: ^1.1.14 version: 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-tooltip": + '@radix-ui/react-tooltip': specifier: ^1.2.9 version: 1.2.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@tanstack/react-query": + '@tanstack/react-query': specifier: ^5.101.0 version: 5.101.0(react@19.2.7) - "@tanstack/react-router": + '@tanstack/react-router': specifier: ^1.170.15 version: 1.170.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@xterm/addon-canvas": + '@xterm/addon-canvas': specifier: ^0.7.0 version: 0.7.0(@xterm/xterm@5.5.0) - "@xterm/addon-fit": + '@xterm/addon-fit': specifier: ^0.10.0 version: 0.10.0(@xterm/xterm@5.5.0) - "@xterm/addon-search": + '@xterm/addon-search': specifier: ^0.15.0 version: 0.15.0(@xterm/xterm@5.5.0) - "@xterm/addon-unicode11": + '@xterm/addon-unicode11': specifier: ^0.9.0 version: 0.9.0 - "@xterm/addon-web-links": + '@xterm/addon-web-links': specifier: ^0.11.0 version: 0.11.0(@xterm/xterm@5.5.0) - "@xterm/addon-webgl": + '@xterm/addon-webgl': specifier: ^0.19.0 version: 0.19.0 - "@xterm/xterm": + '@xterm/xterm': specifier: ^5.5.0 version: 5.5.0 class-variance-authority: @@ -58,6 +59,9 @@ importers: openapi-fetch: specifier: ^0.17.0 version: 0.17.0 + posthog-js: + specifier: ^1.390.2 + version: 1.393.0 radix-ui: specifier: ^1.5.0 version: 1.5.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -80,60 +84,63 @@ importers: specifier: ^5.0.14 version: 5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)) devDependencies: - "@electron-forge/cli": + '@electron-forge/cli': specifier: ^7.8.0 version: 7.11.2(encoding@0.1.13) - "@electron-forge/maker-deb": + '@electron-forge/maker-base': specifier: ^7.8.0 version: 7.11.2 - "@electron-forge/maker-rpm": + '@electron-forge/maker-deb': specifier: ^7.8.0 version: 7.11.2 - "@electron-forge/maker-squirrel": + '@electron-forge/maker-rpm': specifier: ^7.8.0 version: 7.11.2 - "@electron-forge/maker-zip": + '@electron-forge/maker-zip': specifier: ^7.8.0 version: 7.11.2 - "@electron-forge/plugin-vite": + '@electron-forge/plugin-vite': specifier: ^7.8.0 version: 7.11.2 - "@electron-forge/publisher-github": + '@electron-forge/publisher-github': specifier: ^7.8.0 version: 7.11.2 - "@playwright/test": + '@playwright/test': specifier: ^1.60.0 version: 1.60.0 - "@tailwindcss/vite": + '@tailwindcss/vite': specifier: ^4.3.0 version: 4.3.0(vite@8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0)) - "@tanstack/router-plugin": + '@tanstack/router-plugin': specifier: ^1.168.18 version: 1.168.18(@tanstack/react-router@1.170.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0))(webpack@5.107.2) - "@testing-library/jest-dom": + '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 - "@testing-library/react": + '@testing-library/react': specifier: ^16.3.2 version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@testing-library/user-event": + '@testing-library/user-event': specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.1) - "@types/react": + '@types/react': specifier: ^19.2.17 version: 19.2.17 - "@types/react-dom": + '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.17) - "@vitejs/plugin-react": + '@vitejs/plugin-react': specifier: ^6.0.2 version: 6.0.2(vite@8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0)) + app-builder-lib: + specifier: ^26.15.3 + version: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3) electron: specifier: ^33.0.0 version: 33.4.11 jsdom: specifier: ^29.1.1 - version: 29.1.1 + version: 29.1.1(@noble/hashes@2.2.0) openapi-typescript: specifier: ^7.13.0 version: 7.13.0(typescript@5.9.3) @@ -151,1661 +158,1478 @@ importers: version: 8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0) vitest: specifier: ^4.1.8 - version: 4.1.8(@types/node@25.9.2)(jsdom@29.1.1)(vite@8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0)) + version: 4.1.8(@types/node@25.9.2)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0)) + optionalDependencies: + electron-installer-debian: + specifier: ^3.2.0 + version: 3.2.0 + electron-installer-redhat: + specifier: ^3.4.0 + version: 3.4.0 packages: - "@adobe/css-tools@4.5.0": - resolution: - { integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q== } - - "@asamuzakjp/css-color@5.1.11": - resolution: - { integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg== } - engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } - - "@asamuzakjp/dom-selector@7.1.1": - resolution: - { integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ== } - engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } - - "@asamuzakjp/generational-cache@1.0.1": - resolution: - { integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg== } - engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } - - "@asamuzakjp/nwsapi@2.3.9": - resolution: - { integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q== } - - "@babel/code-frame@7.29.7": - resolution: - { integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw== } - engines: { node: ">=6.9.0" } - - "@babel/compat-data@7.29.7": - resolution: - { integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg== } - engines: { node: ">=6.9.0" } - - "@babel/core@7.29.7": - resolution: - { integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA== } - engines: { node: ">=6.9.0" } - - "@babel/generator@7.29.7": - resolution: - { integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ== } - engines: { node: ">=6.9.0" } - - "@babel/helper-compilation-targets@7.29.7": - resolution: - { integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g== } - engines: { node: ">=6.9.0" } - - "@babel/helper-globals@7.29.7": - resolution: - { integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA== } - engines: { node: ">=6.9.0" } - - "@babel/helper-module-imports@7.29.7": - resolution: - { integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g== } - engines: { node: ">=6.9.0" } - - "@babel/helper-module-transforms@7.29.7": - resolution: - { integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg== } - engines: { node: ">=6.9.0" } + + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} peerDependencies: - "@babel/core": ^7.0.0 - - "@babel/helper-string-parser@7.29.7": - resolution: - { integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw== } - engines: { node: ">=6.9.0" } - - "@babel/helper-validator-identifier@7.29.7": - resolution: - { integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg== } - engines: { node: ">=6.9.0" } - - "@babel/helper-validator-option@7.29.7": - resolution: - { integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw== } - engines: { node: ">=6.9.0" } - - "@babel/helpers@7.29.7": - resolution: - { integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg== } - engines: { node: ">=6.9.0" } - - "@babel/parser@7.29.7": - resolution: - { integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg== } - engines: { node: ">=6.0.0" } + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} hasBin: true - "@babel/runtime@7.29.7": - resolution: - { integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw== } - engines: { node: ">=6.9.0" } - - "@babel/template@7.29.7": - resolution: - { integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg== } - engines: { node: ">=6.9.0" } - - "@babel/traverse@7.29.7": - resolution: - { integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw== } - engines: { node: ">=6.9.0" } - - "@babel/types@7.29.7": - resolution: - { integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA== } - engines: { node: ">=6.9.0" } - - "@bramus/specificity@2.4.2": - resolution: - { integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw== } + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} hasBin: true - "@csstools/color-helpers@6.0.2": - resolution: - { integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q== } - engines: { node: ">=20.19.0" } + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} - "@csstools/css-calc@3.2.1": - resolution: - { integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg== } - engines: { node: ">=20.19.0" } + '@csstools/css-calc@3.2.1': + resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} + engines: {node: '>=20.19.0'} peerDependencies: - "@csstools/css-parser-algorithms": ^4.0.0 - "@csstools/css-tokenizer": ^4.0.0 + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 - "@csstools/css-color-parser@4.1.1": - resolution: - { integrity: sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g== } - engines: { node: ">=20.19.0" } + '@csstools/css-color-parser@4.1.1': + resolution: {integrity: sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==} + engines: {node: '>=20.19.0'} peerDependencies: - "@csstools/css-parser-algorithms": ^4.0.0 - "@csstools/css-tokenizer": ^4.0.0 + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 - "@csstools/css-parser-algorithms@4.0.0": - resolution: - { integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w== } - engines: { node: ">=20.19.0" } + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} peerDependencies: - "@csstools/css-tokenizer": ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 - "@csstools/css-syntax-patches-for-csstree@1.1.5": - resolution: - { integrity: sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A== } + '@csstools/css-syntax-patches-for-csstree@1.1.5': + resolution: {integrity: sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==} peerDependencies: css-tree: ^3.2.1 peerDependenciesMeta: css-tree: optional: true - "@csstools/css-tokenizer@4.0.0": - resolution: - { integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA== } - engines: { node: ">=20.19.0" } + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} - "@electron-forge/cli@7.11.2": - resolution: - { integrity: sha512-c+C4ndLfHbxwZuCn9G8iT9wD/woLdaVkoSVjAIbj+0nJhi8UmiVsz/+Gxlj4cvhMRTzBMBxudstLU7RocMikfg== } - engines: { node: ">= 16.4.0" } + '@electron-forge/cli@7.11.2': + resolution: {integrity: sha512-c+C4ndLfHbxwZuCn9G8iT9wD/woLdaVkoSVjAIbj+0nJhi8UmiVsz/+Gxlj4cvhMRTzBMBxudstLU7RocMikfg==} + engines: {node: '>= 16.4.0'} hasBin: true - "@electron-forge/core-utils@7.11.2": - resolution: - { integrity: sha512-/Fpwo44an6ulUdq94co5OOcbRCohgYNci/E6eoZZuTO9f72X+PqJkMkghqkMX3iQ8Aq2QRLkGKFwrKWJNTjL7Q== } - engines: { node: ">= 16.4.0" } - - "@electron-forge/core@7.11.2": - resolution: - { integrity: sha512-RbOvlCahSlYBkY1XFgD5QuoifZltEY3ezYGqJYnV1z6RiUK1DfUXwdidmclBLI9d6u8NNr9xWPv79LHVc9ZA3Q== } - engines: { node: ">= 16.4.0" } - - "@electron-forge/maker-base@7.11.2": - resolution: - { integrity: sha512-9934zYu9WVdgCYQXvtS+eL1oyLagsY8JlWhZmoK8yWTYftSAydH7jb3seVpfy6n85SYmY/yjcAy2lvOTy5dUwA== } - engines: { node: ">= 16.4.0" } - - "@electron-forge/maker-deb@7.11.2": - resolution: - { integrity: sha512-MYSdCTsqzKNmsmaq7CIFh2kJdBWUZ4njxnVGrIRClzueVITk5Kots3+eQo+e5QQLvXTVn2XTNDc2nYjvtBh+Mw== } - engines: { node: ">= 16.4.0" } - - "@electron-forge/maker-rpm@7.11.2": - resolution: - { integrity: sha512-BEj/DcW6bSpmOyKUa3UsOgT7Hm3ZuP0Wa6OuQEunjxeCWn7yoDTDtjuYA0xRvzk+T4NCyDO3RBGjy6nYNSPU2Q== } - engines: { node: ">= 16.4.0" } - - "@electron-forge/maker-squirrel@7.11.2": - resolution: - { integrity: sha512-4CILo57ZDEQH1mJxjhYCSXuv+WaU7oPq67KqiTLEUOEzmiPg9u9/z7FXE34H/Tn5aKWN3dy+ngAETzv6iERCGg== } - engines: { node: ">= 16.4.0" } - - "@electron-forge/maker-zip@7.11.2": - resolution: - { integrity: sha512-FWnOm2MORX/nt8psnEtID3Vnt8Blby1NkzjU3KjXBPF9kave71C3lI8KbBbCeKKyTQ/S00i2FiglKdRWQ1WNTw== } - engines: { node: ">= 16.4.0" } - - "@electron-forge/plugin-base@7.11.2": - resolution: - { integrity: sha512-tIFzEE2+D9NnCAn/rLwSkh8H59IqN+G973JNl7xmCzquO6qa7/veitZOQFGO79Zmmgkc8R/fmiCbh7LIdLS9Tg== } - engines: { node: ">= 16.4.0" } - - "@electron-forge/plugin-vite@7.11.2": - resolution: - { integrity: sha512-QagRgjXfMBeyP+NkMdUMqke/E0ldfcBycjkgCb2FEH3VnS+Llk5RE2716H3quTuUtRhX2gdRuUDdLsstHFuGWg== } - engines: { node: ">= 16.4.0" } - - "@electron-forge/publisher-base@7.11.2": - resolution: - { integrity: sha512-YwK4ZF3+uW7PBEV/ho59NVTriP3fCahskORrztUaFIdG0QP3hqMsfmo01euv98FDsBEW9UXo7/EW8t5jpmYZ0Q== } - engines: { node: ">= 16.4.0" } - - "@electron-forge/publisher-github@7.11.2": - resolution: - { integrity: sha512-1kandHpGPRg2+Lfo6AyI6DtKVMmrc0yyOdJJmo1A7eJO6U8icMGrypSPwOIiTsN6OYJds/vZBzFQ6Vs9rvRsVg== } - engines: { node: ">= 16.4.0" } - - "@electron-forge/shared-types@7.11.2": - resolution: - { integrity: sha512-Tcles7y74xy3jN5dEC+Pt1duJYk4c7W2xu98tjWW8RewmfKD2uHkie6I1I3yifPFZXZ/QfTlaFOOoKIQ9ENZjg== } - engines: { node: ">= 16.4.0" } - - "@electron-forge/template-base@7.11.2": - resolution: - { integrity: sha512-l10I+XZRbbxFGiDLMnuXmlOppmLYmimKj6FWjEGUvft4VJFXW2BIDrLIugIGdM1nbrl/0aYjen2xRg0nZlcWzg== } - engines: { node: ">= 16.4.0" } - - "@electron-forge/template-vite-typescript@7.11.2": - resolution: - { integrity: sha512-QvvdmO9Gdv+3aISI9+bBLKPBTyKaucs6HhXxz+IDALcdykIL9wVN0/BrWuwwgbwuw4BiJTyXGSPNXuJ+EWnP6g== } - engines: { node: ">= 16.4.0" } - - "@electron-forge/template-vite@7.11.2": - resolution: - { integrity: sha512-yFSDSu3IdyNpgLXzrwODSUyaWniHRSZI82gwcXdnJLx7D7DIDLtbx6KzEoy7QBmWZRULO3F7rLsYG+Ur7orvyA== } - engines: { node: ">= 16.4.0" } - - "@electron-forge/template-webpack-typescript@7.11.2": - resolution: - { integrity: sha512-2lwK+OrCeZgYM8WqsUXJzk94rdF0z/kA7WnAf79U3COEmAAMcFIwJtwF8c/n+52UecP3yrEE70LIGmM1sjGZJQ== } - engines: { node: ">= 16.4.0" } - - "@electron-forge/template-webpack@7.11.2": - resolution: - { integrity: sha512-JjG8XIZctrSZvTlii7Hqvt/pHDKigRk4PoLTQCs1TiT05ZWsn40itBm8cbja3L7bfm0ccDd3JTWWOl2G7PhlmA== } - engines: { node: ">= 16.4.0" } - - "@electron-forge/tracer@7.11.2": - resolution: - { integrity: sha512-U8j5Hyj2Zt7I5PciJvPJfmEv69Gb/Da9v+k655z3Jj1cuY0UnToEJ61IhXrzlTYqo+jUKC+fgAjDJ6vltJTS0A== } - engines: { node: ">= 14.17.5" } - - "@electron/asar@3.4.1": - resolution: - { integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA== } - engines: { node: ">=10.12.0" } + '@electron-forge/core-utils@7.11.2': + resolution: {integrity: sha512-/Fpwo44an6ulUdq94co5OOcbRCohgYNci/E6eoZZuTO9f72X+PqJkMkghqkMX3iQ8Aq2QRLkGKFwrKWJNTjL7Q==} + engines: {node: '>= 16.4.0'} + + '@electron-forge/core@7.11.2': + resolution: {integrity: sha512-RbOvlCahSlYBkY1XFgD5QuoifZltEY3ezYGqJYnV1z6RiUK1DfUXwdidmclBLI9d6u8NNr9xWPv79LHVc9ZA3Q==} + engines: {node: '>= 16.4.0'} + + '@electron-forge/maker-base@7.11.2': + resolution: {integrity: sha512-9934zYu9WVdgCYQXvtS+eL1oyLagsY8JlWhZmoK8yWTYftSAydH7jb3seVpfy6n85SYmY/yjcAy2lvOTy5dUwA==} + engines: {node: '>= 16.4.0'} + + '@electron-forge/maker-deb@7.11.2': + resolution: {integrity: sha512-MYSdCTsqzKNmsmaq7CIFh2kJdBWUZ4njxnVGrIRClzueVITk5Kots3+eQo+e5QQLvXTVn2XTNDc2nYjvtBh+Mw==} + engines: {node: '>= 16.4.0'} + + '@electron-forge/maker-rpm@7.11.2': + resolution: {integrity: sha512-BEj/DcW6bSpmOyKUa3UsOgT7Hm3ZuP0Wa6OuQEunjxeCWn7yoDTDtjuYA0xRvzk+T4NCyDO3RBGjy6nYNSPU2Q==} + engines: {node: '>= 16.4.0'} + + '@electron-forge/maker-zip@7.11.2': + resolution: {integrity: sha512-FWnOm2MORX/nt8psnEtID3Vnt8Blby1NkzjU3KjXBPF9kave71C3lI8KbBbCeKKyTQ/S00i2FiglKdRWQ1WNTw==} + engines: {node: '>= 16.4.0'} + + '@electron-forge/plugin-base@7.11.2': + resolution: {integrity: sha512-tIFzEE2+D9NnCAn/rLwSkh8H59IqN+G973JNl7xmCzquO6qa7/veitZOQFGO79Zmmgkc8R/fmiCbh7LIdLS9Tg==} + engines: {node: '>= 16.4.0'} + + '@electron-forge/plugin-vite@7.11.2': + resolution: {integrity: sha512-QagRgjXfMBeyP+NkMdUMqke/E0ldfcBycjkgCb2FEH3VnS+Llk5RE2716H3quTuUtRhX2gdRuUDdLsstHFuGWg==} + engines: {node: '>= 16.4.0'} + + '@electron-forge/publisher-base@7.11.2': + resolution: {integrity: sha512-YwK4ZF3+uW7PBEV/ho59NVTriP3fCahskORrztUaFIdG0QP3hqMsfmo01euv98FDsBEW9UXo7/EW8t5jpmYZ0Q==} + engines: {node: '>= 16.4.0'} + + '@electron-forge/publisher-github@7.11.2': + resolution: {integrity: sha512-1kandHpGPRg2+Lfo6AyI6DtKVMmrc0yyOdJJmo1A7eJO6U8icMGrypSPwOIiTsN6OYJds/vZBzFQ6Vs9rvRsVg==} + engines: {node: '>= 16.4.0'} + + '@electron-forge/shared-types@7.11.2': + resolution: {integrity: sha512-Tcles7y74xy3jN5dEC+Pt1duJYk4c7W2xu98tjWW8RewmfKD2uHkie6I1I3yifPFZXZ/QfTlaFOOoKIQ9ENZjg==} + engines: {node: '>= 16.4.0'} + + '@electron-forge/template-base@7.11.2': + resolution: {integrity: sha512-l10I+XZRbbxFGiDLMnuXmlOppmLYmimKj6FWjEGUvft4VJFXW2BIDrLIugIGdM1nbrl/0aYjen2xRg0nZlcWzg==} + engines: {node: '>= 16.4.0'} + + '@electron-forge/template-vite-typescript@7.11.2': + resolution: {integrity: sha512-QvvdmO9Gdv+3aISI9+bBLKPBTyKaucs6HhXxz+IDALcdykIL9wVN0/BrWuwwgbwuw4BiJTyXGSPNXuJ+EWnP6g==} + engines: {node: '>= 16.4.0'} + + '@electron-forge/template-vite@7.11.2': + resolution: {integrity: sha512-yFSDSu3IdyNpgLXzrwODSUyaWniHRSZI82gwcXdnJLx7D7DIDLtbx6KzEoy7QBmWZRULO3F7rLsYG+Ur7orvyA==} + engines: {node: '>= 16.4.0'} + + '@electron-forge/template-webpack-typescript@7.11.2': + resolution: {integrity: sha512-2lwK+OrCeZgYM8WqsUXJzk94rdF0z/kA7WnAf79U3COEmAAMcFIwJtwF8c/n+52UecP3yrEE70LIGmM1sjGZJQ==} + engines: {node: '>= 16.4.0'} + + '@electron-forge/template-webpack@7.11.2': + resolution: {integrity: sha512-JjG8XIZctrSZvTlii7Hqvt/pHDKigRk4PoLTQCs1TiT05ZWsn40itBm8cbja3L7bfm0ccDd3JTWWOl2G7PhlmA==} + engines: {node: '>= 16.4.0'} + + '@electron-forge/tracer@7.11.2': + resolution: {integrity: sha512-U8j5Hyj2Zt7I5PciJvPJfmEv69Gb/Da9v+k655z3Jj1cuY0UnToEJ61IhXrzlTYqo+jUKC+fgAjDJ6vltJTS0A==} + engines: {node: '>= 14.17.5'} + + '@electron/asar@3.4.1': + resolution: {integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==} + engines: {node: '>=10.12.0'} hasBin: true - "@electron/get@2.0.3": - resolution: - { integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ== } - engines: { node: ">=12" } - - "@electron/get@3.1.0": - resolution: - { integrity: sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ== } - engines: { node: ">=14" } - - "@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2": - resolution: - { - gitHosted: true, - tarball: https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2, - } + '@electron/fuses@1.8.0': + resolution: {integrity: sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==} + hasBin: true + + '@electron/get@2.0.3': + resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} + engines: {node: '>=12'} + + '@electron/get@3.1.0': + resolution: {integrity: sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==} + engines: {node: '>=14'} + + '@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2': + resolution: {tarball: https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2} version: 10.2.0-electron.1 - engines: { node: ">=12.13.0" } + engines: {node: '>=12.13.0'} hasBin: true - "@electron/notarize@2.5.0": - resolution: - { integrity: sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A== } - engines: { node: ">= 10.0.0" } + '@electron/notarize@2.5.0': + resolution: {integrity: sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==} + engines: {node: '>= 10.0.0'} - "@electron/osx-sign@1.3.3": - resolution: - { integrity: sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg== } - engines: { node: ">=12.0.0" } + '@electron/osx-sign@1.3.3': + resolution: {integrity: sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==} + engines: {node: '>=12.0.0'} hasBin: true - "@electron/packager@18.4.4": - resolution: - { integrity: sha512-fTUCmgL25WXTcFpM1M72VmFP8w3E4d+KNzWxmTDRpvwkfn/S206MAtM2cy0GF78KS9AwASMOUmlOIzCHeNxcGQ== } - engines: { node: ">= 16.13.0" } + '@electron/packager@18.4.4': + resolution: {integrity: sha512-fTUCmgL25WXTcFpM1M72VmFP8w3E4d+KNzWxmTDRpvwkfn/S206MAtM2cy0GF78KS9AwASMOUmlOIzCHeNxcGQ==} + engines: {node: '>= 16.13.0'} hasBin: true - "@electron/rebuild@3.7.2": - resolution: - { integrity: sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg== } - engines: { node: ">=12.13.0" } + '@electron/rebuild@3.7.2': + resolution: {integrity: sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg==} + engines: {node: '>=12.13.0'} hasBin: true - "@electron/universal@2.0.3": - resolution: - { integrity: sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g== } - engines: { node: ">=16.4" } + '@electron/rebuild@4.0.4': + resolution: {integrity: sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==} + engines: {node: '>=22.12.0'} + hasBin: true + + '@electron/universal@2.0.3': + resolution: {integrity: sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==} + engines: {node: '>=16.4'} - "@electron/windows-sign@1.2.2": - resolution: - { integrity: sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ== } - engines: { node: ">=14.14" } + '@electron/windows-sign@1.2.2': + resolution: {integrity: sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==} + engines: {node: '>=14.14'} hasBin: true - "@emnapi/core@1.10.0": - resolution: - { integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw== } + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - "@emnapi/runtime@1.10.0": - resolution: - { integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA== } + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - "@emnapi/wasi-threads@1.2.1": - resolution: - { integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w== } + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - "@exodus/bytes@1.15.1": - resolution: - { integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q== } - engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - "@noble/hashes": ^1.8.0 || ^2.0.0 + '@noble/hashes': ^1.8.0 || ^2.0.0 peerDependenciesMeta: - "@noble/hashes": + '@noble/hashes': optional: true - "@floating-ui/core@1.7.5": - resolution: - { integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ== } + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} - "@floating-ui/dom@1.7.6": - resolution: - { integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ== } + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} - "@floating-ui/react-dom@2.1.8": - resolution: - { integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A== } + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} peerDependencies: - react: ">=16.8.0" - react-dom: ">=16.8.0" - - "@floating-ui/utils@0.2.11": - resolution: - { integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg== } - - "@gar/promisify@1.1.3": - resolution: - { integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== } - - "@inquirer/checkbox@3.0.1": - resolution: - { integrity: sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ== } - engines: { node: ">=18" } - - "@inquirer/confirm@4.0.1": - resolution: - { integrity: sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w== } - engines: { node: ">=18" } - - "@inquirer/core@9.2.1": - resolution: - { integrity: sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg== } - engines: { node: ">=18" } - - "@inquirer/editor@3.0.1": - resolution: - { integrity: sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q== } - engines: { node: ">=18" } - - "@inquirer/expand@3.0.1": - resolution: - { integrity: sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ== } - engines: { node: ">=18" } - - "@inquirer/figures@1.0.15": - resolution: - { integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g== } - engines: { node: ">=18" } - - "@inquirer/input@3.0.1": - resolution: - { integrity: sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg== } - engines: { node: ">=18" } - - "@inquirer/number@2.0.1": - resolution: - { integrity: sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ== } - engines: { node: ">=18" } - - "@inquirer/password@3.0.1": - resolution: - { integrity: sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ== } - engines: { node: ">=18" } - - "@inquirer/prompts@6.0.1": - resolution: - { integrity: sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A== } - engines: { node: ">=18" } - - "@inquirer/rawlist@3.0.1": - resolution: - { integrity: sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ== } - engines: { node: ">=18" } - - "@inquirer/search@2.0.1": - resolution: - { integrity: sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg== } - engines: { node: ">=18" } - - "@inquirer/select@3.0.1": - resolution: - { integrity: sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q== } - engines: { node: ">=18" } - - "@inquirer/type@1.5.5": - resolution: - { integrity: sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA== } - engines: { node: ">=18" } - - "@inquirer/type@2.0.0": - resolution: - { integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag== } - engines: { node: ">=18" } - - "@jridgewell/gen-mapping@0.3.13": - resolution: - { integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== } - - "@jridgewell/remapping@2.3.5": - resolution: - { integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== } - - "@jridgewell/resolve-uri@3.1.2": - resolution: - { integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== } - engines: { node: ">=6.0.0" } - - "@jridgewell/source-map@0.3.11": - resolution: - { integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA== } - - "@jridgewell/sourcemap-codec@1.5.5": - resolution: - { integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== } - - "@jridgewell/trace-mapping@0.3.31": - resolution: - { integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== } - - "@listr2/prompt-adapter-inquirer@2.0.22": - resolution: - { integrity: sha512-hV36ZoY+xKL6pYOt1nPNnkciFkn89KZwqLhAFzJvYysAvL5uBQdiADZx/8bIDXIukzzwG0QlPYolgMzQUtKgpQ== } - engines: { node: ">=18.0.0" } + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@gar/promisify@1.1.3': + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + + '@inquirer/checkbox@3.0.1': + resolution: {integrity: sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==} + engines: {node: '>=18'} + + '@inquirer/confirm@4.0.1': + resolution: {integrity: sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==} + engines: {node: '>=18'} + + '@inquirer/core@9.2.1': + resolution: {integrity: sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==} + engines: {node: '>=18'} + + '@inquirer/editor@3.0.1': + resolution: {integrity: sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==} + engines: {node: '>=18'} + + '@inquirer/expand@3.0.1': + resolution: {integrity: sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==} + engines: {node: '>=18'} + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@3.0.1': + resolution: {integrity: sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==} + engines: {node: '>=18'} + + '@inquirer/number@2.0.1': + resolution: {integrity: sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==} + engines: {node: '>=18'} + + '@inquirer/password@3.0.1': + resolution: {integrity: sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==} + engines: {node: '>=18'} + + '@inquirer/prompts@6.0.1': + resolution: {integrity: sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==} + engines: {node: '>=18'} + + '@inquirer/rawlist@3.0.1': + resolution: {integrity: sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==} + engines: {node: '>=18'} + + '@inquirer/search@2.0.1': + resolution: {integrity: sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==} + engines: {node: '>=18'} + + '@inquirer/select@3.0.1': + resolution: {integrity: sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==} + engines: {node: '>=18'} + + '@inquirer/type@1.5.5': + resolution: {integrity: sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==} + engines: {node: '>=18'} + + '@inquirer/type@2.0.0': + resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==} + engines: {node: '>=18'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@listr2/prompt-adapter-inquirer@2.0.22': + resolution: {integrity: sha512-hV36ZoY+xKL6pYOt1nPNnkciFkn89KZwqLhAFzJvYysAvL5uBQdiADZx/8bIDXIukzzwG0QlPYolgMzQUtKgpQ==} + engines: {node: '>=18.0.0'} peerDependencies: - "@inquirer/prompts": ">= 3 < 8" + '@inquirer/prompts': '>= 3 < 8' - "@malept/cross-spawn-promise@1.1.1": - resolution: - { integrity: sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ== } - engines: { node: ">= 10" } + '@malept/cross-spawn-promise@1.1.1': + resolution: {integrity: sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==} + engines: {node: '>= 10'} - "@malept/cross-spawn-promise@2.0.0": - resolution: - { integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg== } - engines: { node: ">= 12.13.0" } + '@malept/cross-spawn-promise@2.0.0': + resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==} + engines: {node: '>= 12.13.0'} - "@napi-rs/wasm-runtime@1.1.4": - resolution: - { integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow== } + '@malept/flatpak-bundler@0.4.0': + resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} + engines: {node: '>= 10.0.0'} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: - "@emnapi/core": ^1.7.1 - "@emnapi/runtime": ^1.7.1 - - "@nodelib/fs.scandir@2.1.5": - resolution: - { integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== } - engines: { node: ">= 8" } - - "@nodelib/fs.stat@2.0.5": - resolution: - { integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== } - engines: { node: ">= 8" } - - "@nodelib/fs.walk@1.2.8": - resolution: - { integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== } - engines: { node: ">= 8" } - - "@npmcli/fs@2.1.2": - resolution: - { integrity: sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ== } - engines: { node: ^12.13.0 || ^14.15.0 || >=16.0.0 } - - "@npmcli/move-file@2.0.1": - resolution: - { integrity: sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ== } - engines: { node: ^12.13.0 || ^14.15.0 || >=16.0.0 } + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} + + '@noble/hashes@2.2.0': + resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==} + engines: {node: '>= 20.19.0'} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@npmcli/fs@2.1.2': + resolution: {integrity: sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + '@npmcli/move-file@2.0.1': + resolution: {integrity: sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} deprecated: This functionality has been moved to @npmcli/fs - "@octokit/auth-token@4.0.0": - resolution: - { integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA== } - engines: { node: ">= 18" } - - "@octokit/core@5.2.2": - resolution: - { integrity: sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg== } - engines: { node: ">= 18" } - - "@octokit/endpoint@9.0.6": - resolution: - { integrity: sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw== } - engines: { node: ">= 18" } - - "@octokit/graphql@7.1.1": - resolution: - { integrity: sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g== } - engines: { node: ">= 18" } - - "@octokit/openapi-types@12.11.0": - resolution: - { integrity: sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ== } - - "@octokit/openapi-types@24.2.0": - resolution: - { integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg== } - - "@octokit/plugin-paginate-rest@11.4.4-cjs.2": - resolution: - { integrity: sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw== } - engines: { node: ">= 18" } + '@octokit/auth-token@4.0.0': + resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} + engines: {node: '>= 18'} + + '@octokit/core@5.2.2': + resolution: {integrity: sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==} + engines: {node: '>= 18'} + + '@octokit/endpoint@9.0.6': + resolution: {integrity: sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==} + engines: {node: '>= 18'} + + '@octokit/graphql@7.1.1': + resolution: {integrity: sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==} + engines: {node: '>= 18'} + + '@octokit/openapi-types@12.11.0': + resolution: {integrity: sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==} + + '@octokit/openapi-types@24.2.0': + resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==} + + '@octokit/plugin-paginate-rest@11.4.4-cjs.2': + resolution: {integrity: sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==} + engines: {node: '>= 18'} peerDependencies: - "@octokit/core": "5" + '@octokit/core': '5' - "@octokit/plugin-request-log@4.0.1": - resolution: - { integrity: sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA== } - engines: { node: ">= 18" } + '@octokit/plugin-request-log@4.0.1': + resolution: {integrity: sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==} + engines: {node: '>= 18'} peerDependencies: - "@octokit/core": "5" + '@octokit/core': '5' - "@octokit/plugin-rest-endpoint-methods@13.3.2-cjs.1": - resolution: - { integrity: sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ== } - engines: { node: ">= 18" } + '@octokit/plugin-rest-endpoint-methods@13.3.2-cjs.1': + resolution: {integrity: sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==} + engines: {node: '>= 18'} peerDependencies: - "@octokit/core": ^5 + '@octokit/core': ^5 - "@octokit/plugin-retry@6.1.0": - resolution: - { integrity: sha512-WrO3bvq4E1Xh1r2mT9w6SDFg01gFmP81nIG77+p/MqW1JeXXgL++6umim3t6x0Zj5pZm3rXAN+0HEjmmdhIRig== } - engines: { node: ">= 18" } + '@octokit/plugin-retry@6.1.0': + resolution: {integrity: sha512-WrO3bvq4E1Xh1r2mT9w6SDFg01gFmP81nIG77+p/MqW1JeXXgL++6umim3t6x0Zj5pZm3rXAN+0HEjmmdhIRig==} + engines: {node: '>= 18'} peerDependencies: - "@octokit/core": "5" - - "@octokit/request-error@5.1.1": - resolution: - { integrity: sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g== } - engines: { node: ">= 18" } - - "@octokit/request@8.4.1": - resolution: - { integrity: sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw== } - engines: { node: ">= 18" } - - "@octokit/rest@20.1.2": - resolution: - { integrity: sha512-GmYiltypkHHtihFwPRxlaorG5R9VAHuk/vbszVoRTGXnAsY60wYLkh/E2XiFmdZmqrisw+9FaazS1i5SbdWYgA== } - engines: { node: ">= 18" } - - "@octokit/types@13.10.0": - resolution: - { integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA== } - - "@octokit/types@6.41.0": - resolution: - { integrity: sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg== } - - "@oxc-project/types@0.133.0": - resolution: - { integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA== } - - "@playwright/test@1.60.0": - resolution: - { integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag== } - engines: { node: ">=18" } + '@octokit/core': '5' + + '@octokit/request-error@5.1.1': + resolution: {integrity: sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==} + engines: {node: '>= 18'} + + '@octokit/request@8.4.1': + resolution: {integrity: sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==} + engines: {node: '>= 18'} + + '@octokit/rest@20.1.2': + resolution: {integrity: sha512-GmYiltypkHHtihFwPRxlaorG5R9VAHuk/vbszVoRTGXnAsY60wYLkh/E2XiFmdZmqrisw+9FaazS1i5SbdWYgA==} + engines: {node: '>= 18'} + + '@octokit/types@13.10.0': + resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} + + '@octokit/types@6.41.0': + resolution: {integrity: sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==} + + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + + '@peculiar/asn1-schema@2.8.0': + resolution: {integrity: sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==} + + '@peculiar/json-schema@1.1.12': + resolution: {integrity: sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==} + engines: {node: '>=8.0.0'} + + '@peculiar/utils@2.0.3': + resolution: {integrity: sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==} + + '@peculiar/webcrypto@1.7.1': + resolution: {integrity: sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==} + engines: {node: '>=14.18.0'} + + '@playwright/test@1.60.0': + resolution: {integrity: sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==} + engines: {node: '>=18'} hasBin: true - "@radix-ui/number@1.1.2": - resolution: - { integrity: sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig== } + '@posthog/core@1.37.1': + resolution: {integrity: sha512-KRBuxF/XBm3tNpqWlXpWE82XxsYsJb0jSyEic14LMXMvqDv5iApK1jfV0+seikDb9SpPs3tPkWUfHdwaUtFBtQ==} + + '@posthog/types@1.391.0': + resolution: {integrity: sha512-oJ6jkqVMq+T4ax9F0rUllJc0KHpSgpaMwTNYWkE70iBiyXDVyhcNBmYnNKzSODgpzsaQNI6VfK8JrRYbkSJZZw==} + + '@radix-ui/number@1.1.2': + resolution: {integrity: sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==} - "@radix-ui/primitive@1.1.4": - resolution: - { integrity: sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ== } + '@radix-ui/primitive@1.1.4': + resolution: {integrity: sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==} - "@radix-ui/react-accessible-icon@1.1.9": - resolution: - { integrity: sha512-5W9KzJz/3DeYbGJHbZv8Q6AkxMOKUmALfc+PRg9dWwJZMk6zD37Sz8sZrF7UD6CBkiJvn7dNeRzn5G7XiCMyig== } + '@radix-ui/react-accessible-icon@1.1.9': + resolution: {integrity: sha512-5W9KzJz/3DeYbGJHbZv8Q6AkxMOKUmALfc+PRg9dWwJZMk6zD37Sz8sZrF7UD6CBkiJvn7dNeRzn5G7XiCMyig==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-accordion@1.2.13": - resolution: - { integrity: sha512-xITxBB2p5m5tAe7M0F95kb4uAh7jSIKGlExMEm93HlW+XxZHV2eXFbPWLktd4JhRiwcnXNbO7iekcrbZy6ZCvA== } + '@radix-ui/react-accordion@1.2.13': + resolution: {integrity: sha512-xITxBB2p5m5tAe7M0F95kb4uAh7jSIKGlExMEm93HlW+XxZHV2eXFbPWLktd4JhRiwcnXNbO7iekcrbZy6ZCvA==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-alert-dialog@1.1.16": - resolution: - { integrity: sha512-vPaIgo0mxYlvcFaM9jB2Uot9TjGXMuAPEvrc6BOLeV+I5U8s1dkIoouYaa6lmSfc5SPMo5x5djOTOTvaigdGMQ== } + '@radix-ui/react-alert-dialog@1.1.16': + resolution: {integrity: sha512-vPaIgo0mxYlvcFaM9jB2Uot9TjGXMuAPEvrc6BOLeV+I5U8s1dkIoouYaa6lmSfc5SPMo5x5djOTOTvaigdGMQ==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-arrow@1.1.9": - resolution: - { integrity: sha512-yqHW5WQ/cTpU/un7dqqIKNy2iRU8BC0JB78PEzTfCCYvZu1U6W9KwObAniMk9nhSfyotKPQTYaUD/HB0f5muig== } + '@radix-ui/react-arrow@1.1.9': + resolution: {integrity: sha512-yqHW5WQ/cTpU/un7dqqIKNy2iRU8BC0JB78PEzTfCCYvZu1U6W9KwObAniMk9nhSfyotKPQTYaUD/HB0f5muig==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-aspect-ratio@1.1.9": - resolution: - { integrity: sha512-Xy+Dpxt/5n9rVTdPrNFmf8GwG1NlT1pzCF/z1MgOGZMLZWdWl+km+ZRWGQAPEhbkzSwYEsfYmTca8NhUtVxqnw== } + '@radix-ui/react-aspect-ratio@1.1.9': + resolution: {integrity: sha512-Xy+Dpxt/5n9rVTdPrNFmf8GwG1NlT1pzCF/z1MgOGZMLZWdWl+km+ZRWGQAPEhbkzSwYEsfYmTca8NhUtVxqnw==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-avatar@1.1.12": - resolution: - { integrity: sha512-NQCQyWC7QrDPhjMn8hUqFeU0lUrprIgm1AyMgLbzuQJibNnatdc3SSMo3/UGFu/eUkJUU1cEcKCnyhXTQzq6tA== } + '@radix-ui/react-avatar@1.1.12': + resolution: {integrity: sha512-NQCQyWC7QrDPhjMn8hUqFeU0lUrprIgm1AyMgLbzuQJibNnatdc3SSMo3/UGFu/eUkJUU1cEcKCnyhXTQzq6tA==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-checkbox@1.3.4": - resolution: - { integrity: sha512-m3JmIOAX5ZzZ6VPjxEU2dbTOhoHi0nT5riwcDwe8idocsWf4a5DXJLDtZ6LfJwMBx7W+A2b7kp2TgPEKtaiF6A== } + '@radix-ui/react-checkbox@1.3.4': + resolution: {integrity: sha512-m3JmIOAX5ZzZ6VPjxEU2dbTOhoHi0nT5riwcDwe8idocsWf4a5DXJLDtZ6LfJwMBx7W+A2b7kp2TgPEKtaiF6A==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-collapsible@1.1.13": - resolution: - { integrity: sha512-F0s8+p2XNpfc3k02zBfB0jPWbkHVG162+p7BdUMyJ2308QMqZ+oaclX+FAzKFovgL5OqRU+Rvy6f/vbdlJVaqA== } + '@radix-ui/react-collapsible@1.1.13': + resolution: {integrity: sha512-F0s8+p2XNpfc3k02zBfB0jPWbkHVG162+p7BdUMyJ2308QMqZ+oaclX+FAzKFovgL5OqRU+Rvy6f/vbdlJVaqA==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-collection@1.1.9": - resolution: - { integrity: sha512-zuSVi7ziP7uQRqc+yGxsKJfNkdyHv3ZKDaHe0gzg4dRgws96TPKWIiz84tVHP4GEcEl8bC0mdt17NkcxaJHmaQ== } + '@radix-ui/react-collection@1.1.9': + resolution: {integrity: sha512-zuSVi7ziP7uQRqc+yGxsKJfNkdyHv3ZKDaHe0gzg4dRgws96TPKWIiz84tVHP4GEcEl8bC0mdt17NkcxaJHmaQ==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-compose-refs@1.1.3": - resolution: - { integrity: sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA== } + '@radix-ui/react-compose-refs@1.1.3': + resolution: {integrity: sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@radix-ui/react-context-menu@2.3.0": - resolution: - { integrity: sha512-d7CouXhAW+CGmFOqmB+IEvd3E9GcaqfgvfjCc3hfulp2pkaUCEVEGa0SN5nNWYA+IvQ6g1Pt+S5dpNn1AoY9hg== } + '@radix-ui/react-context-menu@2.3.0': + resolution: {integrity: sha512-d7CouXhAW+CGmFOqmB+IEvd3E9GcaqfgvfjCc3hfulp2pkaUCEVEGa0SN5nNWYA+IvQ6g1Pt+S5dpNn1AoY9hg==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-context@1.1.4": - resolution: - { integrity: sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg== } + '@radix-ui/react-context@1.1.4': + resolution: {integrity: sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@radix-ui/react-dialog@1.1.16": - resolution: - { integrity: sha512-l9ok83YBclEZhbjgzt76Hw733e6cvRKPNgO6GJ/IETlufXG9p+fRu2wlvpImQvR6xdJ8h7J8J2DBvsPEiEsKMw== } + '@radix-ui/react-dialog@1.1.16': + resolution: {integrity: sha512-l9ok83YBclEZhbjgzt76Hw733e6cvRKPNgO6GJ/IETlufXG9p+fRu2wlvpImQvR6xdJ8h7J8J2DBvsPEiEsKMw==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-direction@1.1.2": - resolution: - { integrity: sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA== } + '@radix-ui/react-direction@1.1.2': + resolution: {integrity: sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@radix-ui/react-dismissable-layer@1.1.12": - resolution: - { integrity: sha512-MhoruH6xEzsbvOmo4TNgMfmtvRGyDZw4MDSdf4ybMHfezjqwzv6hyd4lsMzBp8K9Sn6sGzCF62x1I7BYUECXOg== } + '@radix-ui/react-dismissable-layer@1.1.12': + resolution: {integrity: sha512-MhoruH6xEzsbvOmo4TNgMfmtvRGyDZw4MDSdf4ybMHfezjqwzv6hyd4lsMzBp8K9Sn6sGzCF62x1I7BYUECXOg==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-dropdown-menu@2.1.17": - resolution: - { integrity: sha512-S6b3Jm57sY5EdDyOMLkacbB0qMnKhy1RCKZCt795ZkmtUOAvojYIZ5p7dXHIh5Cyr3jCLLI5/g64V3FKLudZmw== } + '@radix-ui/react-dropdown-menu@2.1.17': + resolution: {integrity: sha512-S6b3Jm57sY5EdDyOMLkacbB0qMnKhy1RCKZCt795ZkmtUOAvojYIZ5p7dXHIh5Cyr3jCLLI5/g64V3FKLudZmw==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-focus-guards@1.1.4": - resolution: - { integrity: sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q== } + '@radix-ui/react-focus-guards@1.1.4': + resolution: {integrity: sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@radix-ui/react-focus-scope@1.1.9": - resolution: - { integrity: sha512-9Se8t+Zry+1rEOL7Y6l/4ANYU/TOtAtf8O2fKdwLltcaMcm6kOqYGbzO4tMFQ0bvzO920pRAoHpFZ4W85S3keQ== } + '@radix-ui/react-focus-scope@1.1.9': + resolution: {integrity: sha512-9Se8t+Zry+1rEOL7Y6l/4ANYU/TOtAtf8O2fKdwLltcaMcm6kOqYGbzO4tMFQ0bvzO920pRAoHpFZ4W85S3keQ==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-form@0.1.9": - resolution: - { integrity: sha512-eTPyThIKDacJ3mJDvYwf/PSmsEYlOyA2Qcb+aGyWwYv+P5w57VPUkMVA2XJ9z0Du2KBY1HoHQzhPV9iYL/r4hg== } + '@radix-ui/react-form@0.1.9': + resolution: {integrity: sha512-eTPyThIKDacJ3mJDvYwf/PSmsEYlOyA2Qcb+aGyWwYv+P5w57VPUkMVA2XJ9z0Du2KBY1HoHQzhPV9iYL/r4hg==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-hover-card@1.1.16": - resolution: - { integrity: sha512-hAileDBtd6CX7nlZOarOnISQ6PP4q0e16BX51ulzdZ+7IzjL0sDTVpFdmSYrIjw6zVNsfQBao5gG6AWr3qwfvA== } + '@radix-ui/react-hover-card@1.1.16': + resolution: {integrity: sha512-hAileDBtd6CX7nlZOarOnISQ6PP4q0e16BX51ulzdZ+7IzjL0sDTVpFdmSYrIjw6zVNsfQBao5gG6AWr3qwfvA==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-id@1.1.2": - resolution: - { integrity: sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA== } + '@radix-ui/react-id@1.1.2': + resolution: {integrity: sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@radix-ui/react-label@2.1.9": - resolution: - { integrity: sha512-rDoTeMbCwRVcnmo7NGT9IlPo1yXmEI+xc1URP3oeewwZEV4mdTp1dYUhYbQdo4D1q2SjKVvv4N1gNY77QAQtjA== } + '@radix-ui/react-label@2.1.9': + resolution: {integrity: sha512-rDoTeMbCwRVcnmo7NGT9IlPo1yXmEI+xc1URP3oeewwZEV4mdTp1dYUhYbQdo4D1q2SjKVvv4N1gNY77QAQtjA==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-menu@2.1.17": - resolution: - { integrity: sha512-fmbNnFyf+JYCN0DhhWnEdUTDnZD1mXaPQWivdsPIb8oOSbARfD3LIQJbLCG8a8QLCwoMxiJ7GVPIFcC8Dw8v2Q== } + '@radix-ui/react-menu@2.1.17': + resolution: {integrity: sha512-fmbNnFyf+JYCN0DhhWnEdUTDnZD1mXaPQWivdsPIb8oOSbARfD3LIQJbLCG8a8QLCwoMxiJ7GVPIFcC8Dw8v2Q==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-menubar@1.1.17": - resolution: - { integrity: sha512-AKtZ4O782yO7qwIyq73WpulYt1IHhQ0htDb6wNcxzxnSDCcSWMVBiU9ycpcA90XzQO4IVIxIErtak6Kg/Vt0rQ== } + '@radix-ui/react-menubar@1.1.17': + resolution: {integrity: sha512-AKtZ4O782yO7qwIyq73WpulYt1IHhQ0htDb6wNcxzxnSDCcSWMVBiU9ycpcA90XzQO4IVIxIErtak6Kg/Vt0rQ==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-navigation-menu@1.2.15": - resolution: - { integrity: sha512-/fS8hKCcRt4DwCGa5QIB3juRXmfYSOk4a2AEe/BDIyy7Hm+eje2Y13oUx5zejl+wFt1owrM7E8NWlbaEl5EGpg== } + '@radix-ui/react-navigation-menu@1.2.15': + resolution: {integrity: sha512-/fS8hKCcRt4DwCGa5QIB3juRXmfYSOk4a2AEe/BDIyy7Hm+eje2Y13oUx5zejl+wFt1owrM7E8NWlbaEl5EGpg==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-one-time-password-field@0.1.9": - resolution: - { integrity: sha512-fvCzA9hm7yN5xxTPJIi4VhSmH5gv+76ILsxguBK3cm3icD5BR4vW7POQmu8Zio0yh91uuouG/Kang40IbMkaSQ== } + '@radix-ui/react-one-time-password-field@0.1.9': + resolution: {integrity: sha512-fvCzA9hm7yN5xxTPJIi4VhSmH5gv+76ILsxguBK3cm3icD5BR4vW7POQmu8Zio0yh91uuouG/Kang40IbMkaSQ==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-password-toggle-field@0.1.4": - resolution: - { integrity: sha512-qoDSkObZ9faJlsjlwyBH6ia7kq9vaJ2QwWTowT3nQpzPvUTAKesmWuGJYpd91HIoJqS+5ZPXy5uFPp+HlwdaAg== } + '@radix-ui/react-password-toggle-field@0.1.4': + resolution: {integrity: sha512-qoDSkObZ9faJlsjlwyBH6ia7kq9vaJ2QwWTowT3nQpzPvUTAKesmWuGJYpd91HIoJqS+5ZPXy5uFPp+HlwdaAg==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-popover@1.1.16": - resolution: - { integrity: sha512-8brVpAU5Uq7Bh0c8EFc4ZTf2JJTYn0o+1L+CUJB3UYIOkTjKGMgoHvduylrahdmNlr3DfH0rFq2DrbNZXgaspw== } + '@radix-ui/react-popover@1.1.16': + resolution: {integrity: sha512-8brVpAU5Uq7Bh0c8EFc4ZTf2JJTYn0o+1L+CUJB3UYIOkTjKGMgoHvduylrahdmNlr3DfH0rFq2DrbNZXgaspw==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-popper@1.3.0": - resolution: - { integrity: sha512-9PB589e1aWZbrlFUHdz6WiPCL+xLZHQFX7oibqG/6Q0SwOkxDyQX9W/cyPa+sAPPKuC8cpLCpRczE5a/1DiwVQ== } + '@radix-ui/react-popper@1.3.0': + resolution: {integrity: sha512-9PB589e1aWZbrlFUHdz6WiPCL+xLZHQFX7oibqG/6Q0SwOkxDyQX9W/cyPa+sAPPKuC8cpLCpRczE5a/1DiwVQ==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-portal@1.1.11": - resolution: - { integrity: sha512-UEytdjgEh2tJGgD/gZK4FUx6t1rNIlM3U0DENhSrG7I75FGm1DnaDuVUWF1pWAWUwGmn1sCJ1VGHn8LhN1aTOw== } + '@radix-ui/react-portal@1.1.11': + resolution: {integrity: sha512-UEytdjgEh2tJGgD/gZK4FUx6t1rNIlM3U0DENhSrG7I75FGm1DnaDuVUWF1pWAWUwGmn1sCJ1VGHn8LhN1aTOw==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-presence@1.1.6": - resolution: - { integrity: sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ== } + '@radix-ui/react-presence@1.1.6': + resolution: {integrity: sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-primitive@2.1.5": - resolution: - { integrity: sha512-zifXeB8Y88qCYx8PLZ5oQb32KwZub+s925mMoZsBBq9KUQqWKkREubTfs6ASjRPPBe7Jt9O8OHH89+95VG+grA== } + '@radix-ui/react-primitive@2.1.5': + resolution: {integrity: sha512-zifXeB8Y88qCYx8PLZ5oQb32KwZub+s925mMoZsBBq9KUQqWKkREubTfs6ASjRPPBe7Jt9O8OHH89+95VG+grA==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-progress@1.1.9": - resolution: - { integrity: sha512-+EOkvg1Zn1vI1+fRDfRSAiJ7BWfcDAo5ASMmbqrcLZ4s4USk2FGkoHgeb2X+CkUgo2zJMiyObwf1k44CrRWsyw== } + '@radix-ui/react-progress@1.1.9': + resolution: {integrity: sha512-+EOkvg1Zn1vI1+fRDfRSAiJ7BWfcDAo5ASMmbqrcLZ4s4USk2FGkoHgeb2X+CkUgo2zJMiyObwf1k44CrRWsyw==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-radio-group@1.4.0": - resolution: - { integrity: sha512-eHdV5bLx9sH+tBnbDjkIBdvQEH/c6MEtQYhTbxkaDK9qsIFFLtmJYEQFVdwhnruWotLfQmIuWEL/J+L3utE8rQ== } + '@radix-ui/react-radio-group@1.4.0': + resolution: {integrity: sha512-eHdV5bLx9sH+tBnbDjkIBdvQEH/c6MEtQYhTbxkaDK9qsIFFLtmJYEQFVdwhnruWotLfQmIuWEL/J+L3utE8rQ==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-roving-focus@1.1.12": - resolution: - { integrity: sha512-FvgPt1bRmg8Xt2QpF7NUZW3dE0ZQHGm41dAdgT2J2GJPoIXz+9Em3NobAxf4fupcxhgHu03E5CRiU2MWvObXyg== } + '@radix-ui/react-roving-focus@1.1.12': + resolution: {integrity: sha512-FvgPt1bRmg8Xt2QpF7NUZW3dE0ZQHGm41dAdgT2J2GJPoIXz+9Em3NobAxf4fupcxhgHu03E5CRiU2MWvObXyg==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-scroll-area@1.2.11": - resolution: - { integrity: sha512-DS39ziOgea75U/TrXKU2/oKp0be2jrDHnzFLvahg/0iNAT1Zq16e4Uw0WXwyXvsK+mG3BRyMb7A3NRZMDuEXtQ== } + '@radix-ui/react-scroll-area@1.2.11': + resolution: {integrity: sha512-DS39ziOgea75U/TrXKU2/oKp0be2jrDHnzFLvahg/0iNAT1Zq16e4Uw0WXwyXvsK+mG3BRyMb7A3NRZMDuEXtQ==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-select@2.3.0": - resolution: - { integrity: sha512-mENc7WpJvJcW8hlMpzfFcHcEhTvYS5JMBmi9HVC1Q00uhBwML086MHYUV8QQdQv6lcu0Wg8dzd1RB8AFADcG/g== } + '@radix-ui/react-select@2.3.0': + resolution: {integrity: sha512-mENc7WpJvJcW8hlMpzfFcHcEhTvYS5JMBmi9HVC1Q00uhBwML086MHYUV8QQdQv6lcu0Wg8dzd1RB8AFADcG/g==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-separator@1.1.9": - resolution: - { integrity: sha512-gvgW+JV/Mbjj6darztTetnmElpQEzZrXpJvfj+dOxNAxiyHEAyUvEjjl4zxblvmjmKmi3jfPoy7ZdxzCuUBJSA== } + '@radix-ui/react-separator@1.1.9': + resolution: {integrity: sha512-gvgW+JV/Mbjj6darztTetnmElpQEzZrXpJvfj+dOxNAxiyHEAyUvEjjl4zxblvmjmKmi3jfPoy7ZdxzCuUBJSA==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-slider@1.4.0": - resolution: - { integrity: sha512-RHcPlLOThRJM51DSIC33ZnpDEBYhyEFroVWkd2P54PGGjkmAt14RboYUU9E1MFst666zFHM0tGtWvMjSOtU1pw== } + '@radix-ui/react-slider@1.4.0': + resolution: {integrity: sha512-RHcPlLOThRJM51DSIC33ZnpDEBYhyEFroVWkd2P54PGGjkmAt14RboYUU9E1MFst666zFHM0tGtWvMjSOtU1pw==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-slot@1.2.5": - resolution: - { integrity: sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg== } + '@radix-ui/react-slot@1.2.5': + resolution: {integrity: sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg==} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@radix-ui/react-switch@1.3.0": - resolution: - { integrity: sha512-GP1EZwhoZO/GGnhM1P5/2Vpm8iN8EnngyU0oezn2l78kN8tj25pyrvjIaT7azBhK615KSt+P2w39y57YV5jVkA== } + '@radix-ui/react-switch@1.3.0': + resolution: {integrity: sha512-GP1EZwhoZO/GGnhM1P5/2Vpm8iN8EnngyU0oezn2l78kN8tj25pyrvjIaT7azBhK615KSt+P2w39y57YV5jVkA==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-tabs@1.1.14": - resolution: - { integrity: sha512-D5jwp9JNuwDeCw3CYD2Fz+sSHo0droQjC8u75dJHe4aWr5q6yBiXZU+hurXnKudRgEpUkD5TsI6bjHPo5ThUxA== } + '@radix-ui/react-tabs@1.1.14': + resolution: {integrity: sha512-D5jwp9JNuwDeCw3CYD2Fz+sSHo0droQjC8u75dJHe4aWr5q6yBiXZU+hurXnKudRgEpUkD5TsI6bjHPo5ThUxA==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-toast@1.2.16": - resolution: - { integrity: sha512-WUymDDiN2DpoGudRN1aW4wF5O3BNQjZZO/5nngPoNiEVqjyOzirvZZNO0R6dC1ifucSINVaSv8JX1aq47VGgiA== } + '@radix-ui/react-toast@1.2.16': + resolution: {integrity: sha512-WUymDDiN2DpoGudRN1aW4wF5O3BNQjZZO/5nngPoNiEVqjyOzirvZZNO0R6dC1ifucSINVaSv8JX1aq47VGgiA==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-toggle-group@1.1.12": - resolution: - { integrity: sha512-TEgECgJaWGAHJJZGzNNEYTNBdIXqX7LchANycpyP7DkfjmuiSN7ISt1k/ZRGVJgVJonsgP4vwaiKMn5utrcwWQ== } + '@radix-ui/react-toggle-group@1.1.12': + resolution: {integrity: sha512-TEgECgJaWGAHJJZGzNNEYTNBdIXqX7LchANycpyP7DkfjmuiSN7ISt1k/ZRGVJgVJonsgP4vwaiKMn5utrcwWQ==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-toggle@1.1.11": - resolution: - { integrity: sha512-FikrKJemoBGZQ6uRID0HJqSPBP6D7OppdD2OhLl0ZYLlAyPXI7MezoYGmumwNkrAoRm35xXkb4C8JPfJZZzcaw== } + '@radix-ui/react-toggle@1.1.11': + resolution: {integrity: sha512-FikrKJemoBGZQ6uRID0HJqSPBP6D7OppdD2OhLl0ZYLlAyPXI7MezoYGmumwNkrAoRm35xXkb4C8JPfJZZzcaw==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-toolbar@1.1.12": - resolution: - { integrity: sha512-4wHtJVdIgqMmEwUvxA0BYg/2JMRbt0L3+8UD8Ml/nhKkfXtiZcM8u/S15gQ5xj9YEd/0qlrm5bE805LsjQ+J8A== } + '@radix-ui/react-toolbar@1.1.12': + resolution: {integrity: sha512-4wHtJVdIgqMmEwUvxA0BYg/2JMRbt0L3+8UD8Ml/nhKkfXtiZcM8u/S15gQ5xj9YEd/0qlrm5bE805LsjQ+J8A==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-tooltip@1.2.9": - resolution: - { integrity: sha512-u6F9MmTtBSLkiXNVDrtB/yPCZarM9smNswC24YYLV/M+bth6J3Gs3vlJezEoFwKZvPvxhCpUYdUnOsNG/0XOlA== } + '@radix-ui/react-tooltip@1.2.9': + resolution: {integrity: sha512-u6F9MmTtBSLkiXNVDrtB/yPCZarM9smNswC24YYLV/M+bth6J3Gs3vlJezEoFwKZvPvxhCpUYdUnOsNG/0XOlA==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/react-use-callback-ref@1.1.2": - resolution: - { integrity: sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw== } + '@radix-ui/react-use-callback-ref@1.1.2': + resolution: {integrity: sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@radix-ui/react-use-controllable-state@1.2.3": - resolution: - { integrity: sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA== } + '@radix-ui/react-use-controllable-state@1.2.3': + resolution: {integrity: sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@radix-ui/react-use-effect-event@0.0.3": - resolution: - { integrity: sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA== } + '@radix-ui/react-use-effect-event@0.0.3': + resolution: {integrity: sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@radix-ui/react-use-escape-keydown@1.1.2": - resolution: - { integrity: sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw== } + '@radix-ui/react-use-escape-keydown@1.1.2': + resolution: {integrity: sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@radix-ui/react-use-is-hydrated@0.1.1": - resolution: - { integrity: sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A== } + '@radix-ui/react-use-is-hydrated@0.1.1': + resolution: {integrity: sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@radix-ui/react-use-layout-effect@1.1.2": - resolution: - { integrity: sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA== } + '@radix-ui/react-use-layout-effect@1.1.2': + resolution: {integrity: sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@radix-ui/react-use-previous@1.1.2": - resolution: - { integrity: sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw== } + '@radix-ui/react-use-previous@1.1.2': + resolution: {integrity: sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@radix-ui/react-use-rect@1.1.2": - resolution: - { integrity: sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw== } + '@radix-ui/react-use-rect@1.1.2': + resolution: {integrity: sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@radix-ui/react-use-size@1.1.2": - resolution: - { integrity: sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w== } + '@radix-ui/react-use-size@1.1.2': + resolution: {integrity: sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@radix-ui/react-visually-hidden@1.2.5": - resolution: - { integrity: sha512-tPcHNI3FajdDBFpl/Ez1m2WL0ufJqBKyHxMDBvKitopamK36WwBGOMicuMEZKkM5Wce41QxUyv6BsiqfrWBiGg== } + '@radix-ui/react-visually-hidden@1.2.5': + resolution: {integrity: sha512-tPcHNI3FajdDBFpl/Ez1m2WL0ufJqBKyHxMDBvKitopamK36WwBGOMicuMEZKkM5Wce41QxUyv6BsiqfrWBiGg==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@radix-ui/rect@1.1.2": - resolution: - { integrity: sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA== } + '@radix-ui/rect@1.1.2': + resolution: {integrity: sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==} - "@redocly/ajv@8.11.2": - resolution: - { integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg== } + '@redocly/ajv@8.11.2': + resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} - "@redocly/config@0.22.0": - resolution: - { integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ== } + '@redocly/config@0.22.0': + resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==} - "@redocly/openapi-core@1.34.15": - resolution: - { integrity: sha512-HAwCnNyKcs5XGQqms+9t7OdAPM/5TDstmhF+0i7tdCFato2QKuYIlyWETwkXd8c5zbltr1oB+6y9NTeQLr2d6Q== } - engines: { node: ">=18.17.0", npm: ">=9.5.0" } + '@redocly/openapi-core@1.34.15': + resolution: {integrity: sha512-HAwCnNyKcs5XGQqms+9t7OdAPM/5TDstmhF+0i7tdCFato2QKuYIlyWETwkXd8c5zbltr1oB+6y9NTeQLr2d6Q==} + engines: {node: '>=18.17.0', npm: '>=9.5.0'} - "@rolldown/binding-android-arm64@1.0.3": - resolution: - { integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw== } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-android-arm64@1.0.3': + resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - "@rolldown/binding-darwin-arm64@1.0.3": - resolution: - { integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA== } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-darwin-arm64@1.0.3': + resolution: {integrity: sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - "@rolldown/binding-darwin-x64@1.0.3": - resolution: - { integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg== } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-darwin-x64@1.0.3': + resolution: {integrity: sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - "@rolldown/binding-freebsd-x64@1.0.3": - resolution: - { integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g== } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-freebsd-x64@1.0.3': + resolution: {integrity: sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - "@rolldown/binding-linux-arm-gnueabihf@1.0.3": - resolution: - { integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw== } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': + resolution: {integrity: sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - "@rolldown/binding-linux-arm64-gnu@1.0.3": - resolution: - { integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw== } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-linux-arm64-gnu@1.0.3': + resolution: {integrity: sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] - "@rolldown/binding-linux-arm64-musl@1.0.3": - resolution: - { integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q== } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-linux-arm64-musl@1.0.3': + resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] - "@rolldown/binding-linux-ppc64-gnu@1.0.3": - resolution: - { integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg== } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-linux-ppc64-gnu@1.0.3': + resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] - "@rolldown/binding-linux-s390x-gnu@1.0.3": - resolution: - { integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg== } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-linux-s390x-gnu@1.0.3': + resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] - "@rolldown/binding-linux-x64-gnu@1.0.3": - resolution: - { integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg== } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-linux-x64-gnu@1.0.3': + resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] - "@rolldown/binding-linux-x64-musl@1.0.3": - resolution: - { integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow== } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-linux-x64-musl@1.0.3': + resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] - "@rolldown/binding-openharmony-arm64@1.0.3": - resolution: - { integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg== } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-openharmony-arm64@1.0.3': + resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - "@rolldown/binding-wasm32-wasi@1.0.3": - resolution: - { integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg== } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-wasm32-wasi@1.0.3': + resolution: {integrity: sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - "@rolldown/binding-win32-arm64-msvc@1.0.3": - resolution: - { integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g== } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-win32-arm64-msvc@1.0.3': + resolution: {integrity: sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - "@rolldown/binding-win32-x64-msvc@1.0.3": - resolution: - { integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA== } - engines: { node: ^20.19.0 || >=22.12.0 } + '@rolldown/binding-win32-x64-msvc@1.0.3': + resolution: {integrity: sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - "@rolldown/pluginutils@1.0.1": - resolution: - { integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw== } + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - "@sindresorhus/is@4.6.0": - resolution: - { integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== } - engines: { node: ">=10" } + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} - "@standard-schema/spec@1.1.0": - resolution: - { integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== } + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - "@szmarczak/http-timer@4.0.6": - resolution: - { integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w== } - engines: { node: ">=10" } + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} - "@tailwindcss/node@4.3.0": - resolution: - { integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g== } + '@tailwindcss/node@4.3.0': + resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} - "@tailwindcss/oxide-android-arm64@4.3.0": - resolution: - { integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng== } - engines: { node: ">= 20" } + '@tailwindcss/oxide-android-arm64@4.3.0': + resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + engines: {node: '>= 20'} cpu: [arm64] os: [android] - "@tailwindcss/oxide-darwin-arm64@4.3.0": - resolution: - { integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ== } - engines: { node: ">= 20" } + '@tailwindcss/oxide-darwin-arm64@4.3.0': + resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + engines: {node: '>= 20'} cpu: [arm64] os: [darwin] - "@tailwindcss/oxide-darwin-x64@4.3.0": - resolution: - { integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA== } - engines: { node: ">= 20" } + '@tailwindcss/oxide-darwin-x64@4.3.0': + resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + engines: {node: '>= 20'} cpu: [x64] os: [darwin] - "@tailwindcss/oxide-freebsd-x64@4.3.0": - resolution: - { integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ== } - engines: { node: ">= 20" } + '@tailwindcss/oxide-freebsd-x64@4.3.0': + resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + engines: {node: '>= 20'} cpu: [x64] os: [freebsd] - "@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0": - resolution: - { integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA== } - engines: { node: ">= 20" } + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + engines: {node: '>= 20'} cpu: [arm] os: [linux] - "@tailwindcss/oxide-linux-arm64-gnu@4.3.0": - resolution: - { integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg== } - engines: { node: ">= 20" } + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] - "@tailwindcss/oxide-linux-arm64-musl@4.3.0": - resolution: - { integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ== } - engines: { node: ">= 20" } + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] - "@tailwindcss/oxide-linux-x64-gnu@4.3.0": - resolution: - { integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ== } - engines: { node: ">= 20" } + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] - "@tailwindcss/oxide-linux-x64-musl@4.3.0": - resolution: - { integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg== } - engines: { node: ">= 20" } + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] - "@tailwindcss/oxide-wasm32-wasi@4.3.0": - resolution: - { integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA== } - engines: { node: ">=14.0.0" } + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: - - "@napi-rs/wasm-runtime" - - "@emnapi/core" - - "@emnapi/runtime" - - "@tybys/wasm-util" - - "@emnapi/wasi-threads" + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' - tslib - "@tailwindcss/oxide-win32-arm64-msvc@4.3.0": - resolution: - { integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ== } - engines: { node: ">= 20" } + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + engines: {node: '>= 20'} cpu: [arm64] os: [win32] - "@tailwindcss/oxide-win32-x64-msvc@4.3.0": - resolution: - { integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA== } - engines: { node: ">= 20" } + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + engines: {node: '>= 20'} cpu: [x64] os: [win32] - "@tailwindcss/oxide@4.3.0": - resolution: - { integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg== } - engines: { node: ">= 20" } + '@tailwindcss/oxide@4.3.0': + resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + engines: {node: '>= 20'} - "@tailwindcss/vite@4.3.0": - resolution: - { integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw== } + '@tailwindcss/vite@4.3.0': + resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 - "@tanstack/history@1.162.0": - resolution: - { integrity: sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA== } - engines: { node: ">=20.19" } + '@tanstack/history@1.162.0': + resolution: {integrity: sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==} + engines: {node: '>=20.19'} - "@tanstack/query-core@5.101.0": - resolution: - { integrity: sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow== } + '@tanstack/query-core@5.101.0': + resolution: {integrity: sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==} - "@tanstack/react-query@5.101.0": - resolution: - { integrity: sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg== } + '@tanstack/react-query@5.101.0': + resolution: {integrity: sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==} peerDependencies: react: ^18 || ^19 - "@tanstack/react-router@1.170.15": - resolution: - { integrity: sha512-GawYz7HEjj8rTUUDoT/SemDEVm63pZUO+2mOcXHY9Jl3EwMS5gFBnPu/2UvcrwRm1jN1k79fokc0d4aFmrLatg== } - engines: { node: ">=20.19" } + '@tanstack/react-router@1.170.15': + resolution: {integrity: sha512-GawYz7HEjj8rTUUDoT/SemDEVm63pZUO+2mOcXHY9Jl3EwMS5gFBnPu/2UvcrwRm1jN1k79fokc0d4aFmrLatg==} + engines: {node: '>=20.19'} peerDependencies: - react: ">=18.0.0 || >=19.0.0" - react-dom: ">=18.0.0 || >=19.0.0" + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' - "@tanstack/react-store@0.9.3": - resolution: - { integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg== } + '@tanstack/react-store@0.9.3': + resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - "@tanstack/router-core@1.171.13": - resolution: - { integrity: sha512-+NOwEj1kO/6IGmpHRIZHasYxYWpyBQGNIZAST9aNrk9Q3YlU9SgqVnl1pbLa9qAKfeNdXQIRve0RQb/0kyDeDA== } - engines: { node: ">=20.19" } + '@tanstack/router-core@1.171.13': + resolution: {integrity: sha512-+NOwEj1kO/6IGmpHRIZHasYxYWpyBQGNIZAST9aNrk9Q3YlU9SgqVnl1pbLa9qAKfeNdXQIRve0RQb/0kyDeDA==} + engines: {node: '>=20.19'} - "@tanstack/router-generator@1.167.17": - resolution: - { integrity: sha512-xtB9tB2Ws0tWR6Pi7nc3Qk9IYgoh1mQCKWjHqIl9tf6BNUpKoqniJoPAQ4+LGrK8FeZYU0o0p/qlZEyj9FAulA== } - engines: { node: ">=20.19" } + '@tanstack/router-generator@1.167.17': + resolution: {integrity: sha512-xtB9tB2Ws0tWR6Pi7nc3Qk9IYgoh1mQCKWjHqIl9tf6BNUpKoqniJoPAQ4+LGrK8FeZYU0o0p/qlZEyj9FAulA==} + engines: {node: '>=20.19'} - "@tanstack/router-plugin@1.168.18": - resolution: - { integrity: sha512-MofS28/axfnfnhOD2RSgJEaU882aX5RsAzhGz5Vc4XhAmvCjy919u9JrNs4QsTWFbTD1P7IJ8WFlFVsrg0pStg== } - engines: { node: ">=20.19" } + '@tanstack/router-plugin@1.168.18': + resolution: {integrity: sha512-MofS28/axfnfnhOD2RSgJEaU882aX5RsAzhGz5Vc4XhAmvCjy919u9JrNs4QsTWFbTD1P7IJ8WFlFVsrg0pStg==} + engines: {node: '>=20.19'} peerDependencies: - "@rsbuild/core": ">=1.0.2 || ^2.0.0" - "@tanstack/react-router": ^1.170.15 - vite: ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0" + '@rsbuild/core': '>=1.0.2 || ^2.0.0' + '@tanstack/react-router': ^1.170.15 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0' vite-plugin-solid: ^2.11.10 || ^3.0.0-0 - webpack: ">=5.92.0" + webpack: '>=5.92.0' peerDependenciesMeta: - "@rsbuild/core": + '@rsbuild/core': optional: true - "@tanstack/react-router": + '@tanstack/react-router': optional: true vite: optional: true @@ -1814,157 +1638,136 @@ packages: webpack: optional: true - "@tanstack/router-utils@1.162.2": - resolution: - { integrity: sha512-hTWqJtqIFFdvuCl8WXNyrodp2L9zo2G37xKRrcVmVRWpAB2h+U1LuRAfS4tsFTiWOIoE/B+WDVFB8JpoEdw6jQ== } - engines: { node: ">=20.19" } + '@tanstack/router-utils@1.162.2': + resolution: {integrity: sha512-hTWqJtqIFFdvuCl8WXNyrodp2L9zo2G37xKRrcVmVRWpAB2h+U1LuRAfS4tsFTiWOIoE/B+WDVFB8JpoEdw6jQ==} + engines: {node: '>=20.19'} - "@tanstack/store@0.9.3": - resolution: - { integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw== } + '@tanstack/store@0.9.3': + resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} - "@tanstack/virtual-file-routes@1.162.0": - resolution: - { integrity: sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA== } - engines: { node: ">=20.19" } + '@tanstack/virtual-file-routes@1.162.0': + resolution: {integrity: sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA==} + engines: {node: '>=20.19'} - "@testing-library/dom@10.4.1": - resolution: - { integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg== } - engines: { node: ">=18" } + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} - "@testing-library/jest-dom@6.9.1": - resolution: - { integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA== } - engines: { node: ">=14", npm: ">=6", yarn: ">=1" } + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} - "@testing-library/react@16.3.2": - resolution: - { integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g== } - engines: { node: ">=18" } + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} peerDependencies: - "@testing-library/dom": ^10.0.0 - "@types/react": ^18.0.0 || ^19.0.0 - "@types/react-dom": ^18.0.0 || ^19.0.0 + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true - "@testing-library/user-event@14.6.1": - resolution: - { integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw== } - engines: { node: ">=12", npm: ">=6" } + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} peerDependencies: - "@testing-library/dom": ">=7.21.4" + '@testing-library/dom': '>=7.21.4' - "@tootallnate/once@2.0.1": - resolution: - { integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ== } - engines: { node: ">= 10" } + '@tootallnate/once@2.0.1': + resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==} + engines: {node: '>= 10'} - "@tybys/wasm-util@0.10.2": - resolution: - { integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg== } + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} - "@types/aria-query@5.0.4": - resolution: - { integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== } + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - "@types/cacheable-request@6.0.3": - resolution: - { integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw== } + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} - "@types/chai@5.2.3": - resolution: - { integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA== } + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - "@types/deep-eql@4.0.2": - resolution: - { integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw== } + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} - "@types/estree@1.0.9": - resolution: - { integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg== } + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - "@types/fs-extra@9.0.13": - resolution: - { integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA== } + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - "@types/http-cache-semantics@4.2.0": - resolution: - { integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q== } + '@types/fs-extra@9.0.13': + resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} - "@types/json-schema@7.0.15": - resolution: - { integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== } + '@types/http-cache-semantics@4.2.0': + resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} - "@types/keyv@3.1.4": - resolution: - { integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg== } + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - "@types/mute-stream@0.0.4": - resolution: - { integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow== } + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} - "@types/node@20.19.42": - resolution: - { integrity: sha512-5L7SUaFC1RyDraj2yRhyBzHTobyXHmohD100CChNtyPyleoq37Mqab5Gn8XEKI04dfN/oqPdpHk38MgcQWHbZg== } + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - "@types/node@22.19.20": - resolution: - { integrity: sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw== } + '@types/mute-stream@0.0.4': + resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} - "@types/node@25.9.2": - resolution: - { integrity: sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw== } + '@types/node@20.19.42': + resolution: {integrity: sha512-5L7SUaFC1RyDraj2yRhyBzHTobyXHmohD100CChNtyPyleoq37Mqab5Gn8XEKI04dfN/oqPdpHk38MgcQWHbZg==} - "@types/react-dom@19.2.3": - resolution: - { integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ== } + '@types/node@22.19.20': + resolution: {integrity: sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==} + + '@types/node@25.9.2': + resolution: {integrity: sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: - "@types/react": ^19.2.0 + '@types/react': ^19.2.0 - "@types/react@19.2.17": - resolution: - { integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw== } + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} - "@types/responselike@1.0.3": - resolution: - { integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw== } + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} - "@types/wrap-ansi@3.0.0": - resolution: - { integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g== } + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - "@types/yauzl@2.10.3": - resolution: - { integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q== } + '@types/wrap-ansi@3.0.0': + resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} - "@vitejs/plugin-react@6.0.2": - resolution: - { integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg== } - engines: { node: ^20.19.0 || >=22.12.0 } + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@vitejs/plugin-react@6.0.2': + resolution: {integrity: sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==} + engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - "@rolldown/plugin-babel": ^0.1.7 || ^0.2.0 + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 babel-plugin-react-compiler: ^1.0.0 vite: ^8.0.0 peerDependenciesMeta: - "@rolldown/plugin-babel": + '@rolldown/plugin-babel': optional: true babel-plugin-react-compiler: optional: true - "@vitest/expect@4.1.8": - resolution: - { integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ== } + '@vitest/expect@4.1.8': + resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==} - "@vitest/mocker@4.1.8": - resolution: - { integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw== } + '@vitest/mocker@4.1.8': + resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1974,179 +1777,148 @@ packages: vite: optional: true - "@vitest/pretty-format@4.1.8": - resolution: - { integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA== } + '@vitest/pretty-format@4.1.8': + resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} + + '@vitest/runner@4.1.8': + resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==} - "@vitest/runner@4.1.8": - resolution: - { integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg== } + '@vitest/snapshot@4.1.8': + resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==} - "@vitest/snapshot@4.1.8": - resolution: - { integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ== } + '@vitest/spy@4.1.8': + resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==} - "@vitest/spy@4.1.8": - resolution: - { integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA== } + '@vitest/utils@4.1.8': + resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} - "@vitest/utils@4.1.8": - resolution: - { integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg== } + '@vscode/sudo-prompt@9.3.2': + resolution: {integrity: sha512-gcXoCN00METUNFeQOFJ+C9xUI0DKB+0EGMVg7wbVYRHBw2Eq3fKisDZOkRdOz3kqXRKOENMfShPOmypw1/8nOw==} - "@vscode/sudo-prompt@9.3.2": - resolution: - { integrity: sha512-gcXoCN00METUNFeQOFJ+C9xUI0DKB+0EGMVg7wbVYRHBw2Eq3fKisDZOkRdOz3kqXRKOENMfShPOmypw1/8nOw== } + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} - "@webassemblyjs/ast@1.14.1": - resolution: - { integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== } + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} - "@webassemblyjs/floating-point-hex-parser@1.13.2": - resolution: - { integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== } + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} - "@webassemblyjs/helper-api-error@1.13.2": - resolution: - { integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== } + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} - "@webassemblyjs/helper-buffer@1.14.1": - resolution: - { integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== } + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} - "@webassemblyjs/helper-numbers@1.13.2": - resolution: - { integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== } + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} - "@webassemblyjs/helper-wasm-bytecode@1.13.2": - resolution: - { integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== } + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} - "@webassemblyjs/helper-wasm-section@1.14.1": - resolution: - { integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== } + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} - "@webassemblyjs/ieee754@1.13.2": - resolution: - { integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== } + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} - "@webassemblyjs/leb128@1.13.2": - resolution: - { integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== } + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} - "@webassemblyjs/utf8@1.13.2": - resolution: - { integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== } + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} - "@webassemblyjs/wasm-edit@1.14.1": - resolution: - { integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== } + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} - "@webassemblyjs/wasm-gen@1.14.1": - resolution: - { integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== } + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} - "@webassemblyjs/wasm-opt@1.14.1": - resolution: - { integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== } + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} - "@webassemblyjs/wasm-parser@1.14.1": - resolution: - { integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== } + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} - "@webassemblyjs/wast-printer@1.14.1": - resolution: - { integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== } + '@xmldom/xmldom@0.8.13': + resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} + engines: {node: '>=10.0.0'} - "@xmldom/xmldom@0.9.10": - resolution: - { integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw== } - engines: { node: ">=14.6" } + '@xmldom/xmldom@0.9.10': + resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} + engines: {node: '>=14.6'} - "@xterm/addon-canvas@0.7.0": - resolution: - { integrity: sha512-LF5LYcfvefJuJ7QotNRdRSPc9YASAVDeoT5uyXS/nZshZXjYplGXRECBGiznwvhNL2I8bq1Lf5MzRwstsYQ2Iw== } + '@xterm/addon-canvas@0.7.0': + resolution: {integrity: sha512-LF5LYcfvefJuJ7QotNRdRSPc9YASAVDeoT5uyXS/nZshZXjYplGXRECBGiznwvhNL2I8bq1Lf5MzRwstsYQ2Iw==} peerDependencies: - "@xterm/xterm": ^5.0.0 + '@xterm/xterm': ^5.0.0 - "@xterm/addon-fit@0.10.0": - resolution: - { integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ== } + '@xterm/addon-fit@0.10.0': + resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==} peerDependencies: - "@xterm/xterm": ^5.0.0 + '@xterm/xterm': ^5.0.0 - "@xterm/addon-search@0.15.0": - resolution: - { integrity: sha512-ZBZKLQ+EuKE83CqCmSSz5y1tx+aNOCUaA7dm6emgOX+8J9H1FWXZyrKfzjwzV+V14TV3xToz1goIeRhXBS5qjg== } + '@xterm/addon-search@0.15.0': + resolution: {integrity: sha512-ZBZKLQ+EuKE83CqCmSSz5y1tx+aNOCUaA7dm6emgOX+8J9H1FWXZyrKfzjwzV+V14TV3xToz1goIeRhXBS5qjg==} peerDependencies: - "@xterm/xterm": ^5.0.0 + '@xterm/xterm': ^5.0.0 - "@xterm/addon-unicode11@0.9.0": - resolution: - { integrity: sha512-FxDnYcyuXhNl+XSqGZL/t0U9eiNb/q3EWT5rYkQT/zuig8Gz/VagnQANKHdDWFM2lTMk9ly0EFQxxxtZUoRetw== } + '@xterm/addon-unicode11@0.9.0': + resolution: {integrity: sha512-FxDnYcyuXhNl+XSqGZL/t0U9eiNb/q3EWT5rYkQT/zuig8Gz/VagnQANKHdDWFM2lTMk9ly0EFQxxxtZUoRetw==} - "@xterm/addon-web-links@0.11.0": - resolution: - { integrity: sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q== } + '@xterm/addon-web-links@0.11.0': + resolution: {integrity: sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q==} peerDependencies: - "@xterm/xterm": ^5.0.0 + '@xterm/xterm': ^5.0.0 - "@xterm/addon-webgl@0.19.0": - resolution: - { integrity: sha512-b3fMOsyLVuCeNJWxolACEUED0vm7qC0cy4wRvf3oURSzDTYVQiGPhTnhWZwIHdvC48Y+oLhvYXnY4XDXPoJo6A== } + '@xterm/addon-webgl@0.19.0': + resolution: {integrity: sha512-b3fMOsyLVuCeNJWxolACEUED0vm7qC0cy4wRvf3oURSzDTYVQiGPhTnhWZwIHdvC48Y+oLhvYXnY4XDXPoJo6A==} - "@xterm/xterm@5.5.0": - resolution: - { integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A== } + '@xterm/xterm@5.5.0': + resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==} - "@xtuc/ieee754@1.2.0": - resolution: - { integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== } + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} - "@xtuc/long@4.2.2": - resolution: - { integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== } + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} abbrev@1.1.1: - resolution: - { integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== } + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + abbrev@4.0.0: + resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} + engines: {node: ^20.17.0 || >=22.9.0} acorn-import-phases@1.0.4: - resolution: - { integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ== } - engines: { node: ">=10.13.0" } + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} peerDependencies: acorn: ^8.14.0 acorn@8.16.0: - resolution: - { integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== } - engines: { node: ">=0.4.0" } + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} hasBin: true agent-base@6.0.2: - resolution: - { integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== } - engines: { node: ">= 6.0.0" } + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} agent-base@7.1.4: - resolution: - { integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== } - engines: { node: ">= 14" } + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} agentkeepalive@4.6.0: - resolution: - { integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ== } - engines: { node: ">= 8.0.0" } + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} aggregate-error@3.1.0: - resolution: - { integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== } - engines: { node: ">=8" } + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} ajv-formats@2.1.1: - resolution: - { integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== } + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: ajv: ^8.0.0 peerDependenciesMeta: @@ -2154,1045 +1926,998 @@ packages: optional: true ajv-keywords@5.1.0: - resolution: - { integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== } + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} peerDependencies: ajv: ^8.8.2 ajv@8.20.0: - resolution: - { integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA== } + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} ansi-colors@4.1.3: - resolution: - { integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== } - engines: { node: ">=6" } + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} ansi-escapes@4.3.2: - resolution: - { integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== } - engines: { node: ">=8" } + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} ansi-escapes@5.0.0: - resolution: - { integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA== } - engines: { node: ">=12" } + resolution: {integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==} + engines: {node: '>=12'} ansi-regex@5.0.1: - resolution: - { integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== } - engines: { node: ">=8" } + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} ansi-regex@6.2.2: - resolution: - { integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== } - engines: { node: ">=12" } + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} ansi-styles@4.3.0: - resolution: - { integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== } - engines: { node: ">=8" } + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} ansi-styles@5.2.0: - resolution: - { integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== } - engines: { node: ">=10" } + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} ansi-styles@6.2.3: - resolution: - { integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== } - engines: { node: ">=12" } + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} ansis@4.3.1: - resolution: - { integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA== } - engines: { node: ">=14" } + resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} + engines: {node: '>=14'} + + app-builder-lib@26.15.3: + resolution: {integrity: sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==} + engines: {node: '>=14.0.0'} + peerDependencies: + dmg-builder: 26.15.3 + electron-builder-squirrel-windows: 26.15.3 argparse@2.0.1: - resolution: - { integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== } + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} aria-hidden@1.2.6: - resolution: - { integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA== } - engines: { node: ">=10" } + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} aria-query@5.3.0: - resolution: - { integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== } + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} aria-query@5.3.2: - resolution: - { integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + asn1js@3.0.10: + resolution: {integrity: sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==} + engines: {node: '>=12.0.0'} assertion-error@2.0.1: - resolution: - { integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== } - engines: { node: ">=12" } + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + async-exit-hook@2.0.1: + resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} + engines: {node: '>=0.12.0'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} at-least-node@1.0.0: - resolution: - { integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== } - engines: { node: ">= 4.0.0" } + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} author-regex@1.0.0: - resolution: - { integrity: sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g== } - engines: { node: ">=0.8" } + resolution: {integrity: sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==} + engines: {node: '>=0.8'} + + aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} babel-dead-code-elimination@1.0.12: - resolution: - { integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig== } + resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} balanced-match@1.0.2: - resolution: - { integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== } + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} base64-js@1.5.1: - resolution: - { integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== } + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} baseline-browser-mapping@2.10.35: - resolution: - { integrity: sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg== } - engines: { node: ">=6.0.0" } + resolution: {integrity: sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg==} + engines: {node: '>=6.0.0'} hasBin: true before-after-hook@2.2.3: - resolution: - { integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== } + resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} bidi-js@1.0.3: - resolution: - { integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw== } + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} bl@4.1.0: - resolution: - { integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== } + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} bluebird@3.7.2: - resolution: - { integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== } + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} boolean@3.2.0: - resolution: - { integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== } + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. bottleneck@2.19.5: - resolution: - { integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw== } + resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} brace-expansion@1.1.15: - resolution: - { integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg== } + resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} brace-expansion@2.1.1: - resolution: - { integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA== } + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} braces@3.0.3: - resolution: - { integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== } - engines: { node: ">=8" } + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} browserslist@4.28.2: - resolution: - { integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg== } - engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true buffer-crc32@0.2.13: - resolution: - { integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== } + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} buffer-from@1.1.2: - resolution: - { integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== } + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} buffer@5.7.1: - resolution: - { integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== } + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + builder-util-runtime@9.7.0: + resolution: {integrity: sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==} + engines: {node: '>=12.0.0'} + + builder-util@26.15.3: + resolution: {integrity: sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==} + engines: {node: '>=14.0.0'} + + bytestreamjs@2.0.1: + resolution: {integrity: sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==} + engines: {node: '>=6.0.0'} cacache@16.1.3: - resolution: - { integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ== } - engines: { node: ^12.13.0 || ^14.15.0 || >=16.0.0 } + resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} cacheable-lookup@5.0.4: - resolution: - { integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== } - engines: { node: ">=10.6.0" } + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} cacheable-request@7.0.4: - resolution: - { integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg== } - engines: { node: ">=8" } + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} caniuse-lite@1.0.30001797: - resolution: - { integrity: sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w== } + resolution: {integrity: sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==} chai@6.2.2: - resolution: - { integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg== } - engines: { node: ">=18" } + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} chalk@4.1.2: - resolution: - { integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== } - engines: { node: ">=10" } + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} change-case@5.4.4: - resolution: - { integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w== } + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} chardet@0.7.0: - resolution: - { integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== } + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} chokidar@5.0.0: - resolution: - { integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw== } - engines: { node: ">= 20.19.0" } + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} chownr@2.0.0: - resolution: - { integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== } - engines: { node: ">=10" } + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} chrome-trace-event@1.0.4: - resolution: - { integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== } - engines: { node: ">=6.0" } + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + chromium-pickle-js@0.2.0: + resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==} + + ci-info@4.3.1: + resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} + engines: {node: '>=8'} class-variance-authority@0.7.1: - resolution: - { integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg== } + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} clean-stack@2.2.0: - resolution: - { integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== } - engines: { node: ">=6" } + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} cli-cursor@3.1.0: - resolution: - { integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== } - engines: { node: ">=8" } + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} cli-cursor@4.0.0: - resolution: - { integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg== } - engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} cli-spinners@2.9.2: - resolution: - { integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== } - engines: { node: ">=6" } + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} cli-truncate@3.1.0: - resolution: - { integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA== } - engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + resolution: {integrity: sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} cli-width@4.1.0: - resolution: - { integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ== } - engines: { node: ">= 12" } + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} cliui@7.0.4: - resolution: - { integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== } + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} cliui@8.0.1: - resolution: - { integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== } - engines: { node: ">=12" } + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} clone-response@1.0.3: - resolution: - { integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== } + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} clone@1.0.4: - resolution: - { integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== } - engines: { node: ">=0.8" } + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} clsx@2.1.1: - resolution: - { integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== } - engines: { node: ">=6" } + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} color-convert@2.0.1: - resolution: - { integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== } - engines: { node: ">=7.0.0" } + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} color-name@1.1.4: - resolution: - { integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== } + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} colorette@1.4.0: - resolution: - { integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== } + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} colorette@2.0.20: - resolution: - { integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== } + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} commander@11.1.0: - resolution: - { integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== } - engines: { node: ">=16" } + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} commander@2.20.3: - resolution: - { integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== } + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} commander@5.1.0: - resolution: - { integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== } - engines: { node: ">= 6" } + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} commander@9.5.0: - resolution: - { integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== } - engines: { node: ^12.20.0 || >=14 } + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} compare-version@0.1.2: - resolution: - { integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} + engines: {node: '>=0.10.0'} concat-map@0.0.1: - resolution: - { integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== } + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} convert-source-map@2.0.0: - resolution: - { integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== } + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} cookie-es@3.1.1: - resolution: - { integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg== } + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + + core-js@3.49.0: + resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} cross-dirname@0.1.0: - resolution: - { integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q== } + resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==} cross-spawn@6.0.6: - resolution: - { integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw== } - engines: { node: ">=4.8" } + resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} + engines: {node: '>=4.8'} cross-spawn@7.0.6: - resolution: - { integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== } - engines: { node: ">= 8" } + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} cross-zip@4.0.1: - resolution: - { integrity: sha512-n63i0lZ0rvQ6FXiGQ+/JFCKAUyPFhLQYJIqKaa+tSJtfKeULF/IDNDAbdnSIxgS4NTuw2b0+lj8LzfITuq+ZxQ== } - engines: { node: ">=12.10" } + resolution: {integrity: sha512-n63i0lZ0rvQ6FXiGQ+/JFCKAUyPFhLQYJIqKaa+tSJtfKeULF/IDNDAbdnSIxgS4NTuw2b0+lj8LzfITuq+ZxQ==} + engines: {node: '>=12.10'} css-tree@3.2.1: - resolution: - { integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA== } - engines: { node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0 } + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} css.escape@1.5.1: - resolution: - { integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== } + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} csstype@3.2.3: - resolution: - { integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== } + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} data-urls@7.0.0: - resolution: - { integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA== } - engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} debug@2.6.9: - resolution: - { integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== } + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: - supports-color: "*" + supports-color: '*' peerDependenciesMeta: supports-color: optional: true debug@4.4.3: - resolution: - { integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== } - engines: { node: ">=6.0" } + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} peerDependencies: - supports-color: "*" + supports-color: '*' peerDependenciesMeta: supports-color: optional: true decimal.js@10.6.0: - resolution: - { integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg== } + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} decompress-response@6.0.0: - resolution: - { integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== } - engines: { node: ">=10" } + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} defaults@1.0.4: - resolution: - { integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== } + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} defer-to-connect@2.0.1: - resolution: - { integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== } - engines: { node: ">=10" } + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} define-data-property@1.1.4: - resolution: - { integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} define-properties@1.2.1: - resolution: - { integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} deprecation@2.3.1: - resolution: - { integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== } + resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} dequal@2.0.3: - resolution: - { integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== } - engines: { node: ">=6" } + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} detect-libc@2.1.2: - resolution: - { integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== } - engines: { node: ">=8" } + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} detect-node-es@1.1.0: - resolution: - { integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== } + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} detect-node@2.1.0: - resolution: - { integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== } + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} diff@8.0.4: - resolution: - { integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw== } - engines: { node: ">=0.3.1" } + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} dir-compare@4.2.0: - resolution: - { integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ== } + resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} + + dmg-builder@26.15.3: + resolution: {integrity: sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==} dom-accessibility-api@0.5.16: - resolution: - { integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== } + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} dom-accessibility-api@0.6.3: - resolution: - { integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== } + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dompurify@3.4.11: + resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==} + + dotenv-expand@11.0.7: + resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} eastasianwidth@0.2.0: - resolution: - { integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== } + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-builder-squirrel-windows@26.15.3: + resolution: {integrity: sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==} electron-installer-common@0.10.4: - resolution: - { integrity: sha512-8gMNPXfAqUE5CfXg8RL0vXpLE9HAaPkgLXVoHE3BMUzogMWenf4LmwQ27BdCUrEhkjrKl+igs2IHJibclR3z3Q== } - engines: { node: ">= 10.0.0" } + resolution: {integrity: sha512-8gMNPXfAqUE5CfXg8RL0vXpLE9HAaPkgLXVoHE3BMUzogMWenf4LmwQ27BdCUrEhkjrKl+igs2IHJibclR3z3Q==} + engines: {node: '>= 10.0.0'} electron-installer-debian@3.2.0: - resolution: - { integrity: sha512-58ZrlJ1HQY80VucsEIG9tQ//HrTlG6sfofA3nRGr6TmkX661uJyu4cMPPh6kXW+aHdq/7+q25KyQhDrXvRL7jw== } - engines: { node: ">= 10.0.0" } + resolution: {integrity: sha512-58ZrlJ1HQY80VucsEIG9tQ//HrTlG6sfofA3nRGr6TmkX661uJyu4cMPPh6kXW+aHdq/7+q25KyQhDrXvRL7jw==} + engines: {node: '>= 10.0.0'} os: [darwin, linux] hasBin: true electron-installer-redhat@3.4.0: - resolution: - { integrity: sha512-gEISr3U32Sgtj+fjxUAlSDo3wyGGq6OBx7rF5UdpIgbnpUvMN4W5uYb0ThpnAZ42VEJh/3aODQXHbFS4f5J3Iw== } - engines: { node: ">= 10.0.0" } + resolution: {integrity: sha512-gEISr3U32Sgtj+fjxUAlSDo3wyGGq6OBx7rF5UdpIgbnpUvMN4W5uYb0ThpnAZ42VEJh/3aODQXHbFS4f5J3Iw==} + engines: {node: '>= 10.0.0'} os: [darwin, linux] hasBin: true + electron-publish@26.15.3: + resolution: {integrity: sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==} + electron-to-chromium@1.5.371: - resolution: - { integrity: sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w== } + resolution: {integrity: sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w==} electron-winstaller@5.4.0: - resolution: - { integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg== } - engines: { node: ">=8.0.0" } + resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} + engines: {node: '>=8.0.0'} electron@33.4.11: - resolution: - { integrity: sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg== } - engines: { node: ">= 12.20.55" } + resolution: {integrity: sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==} + engines: {node: '>= 12.20.55'} hasBin: true emoji-regex@8.0.0: - resolution: - { integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== } + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emoji-regex@9.2.2: - resolution: - { integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== } + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} encoding@0.1.13: - resolution: - { integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== } + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} end-of-stream@1.4.5: - resolution: - { integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg== } + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} enhanced-resolve@5.23.0: - resolution: - { integrity: sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA== } - engines: { node: ">=10.13.0" } + resolution: {integrity: sha512-yJN/BOOLxcOW2aQgeif9mSnaUB8KtvmMMp56oA1kx1CRfBKbhZm2pJ+NBY+3eOboHxix8lfjWpHE0Ei5U8RbSA==} + engines: {node: '>=10.13.0'} entities@8.0.0: - resolution: - { integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA== } - engines: { node: ">=20.19.0" } + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} env-paths@2.2.1: - resolution: - { integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== } - engines: { node: ">=6" } + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} err-code@2.0.3: - resolution: - { integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== } + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} error-ex@1.3.4: - resolution: - { integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== } + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} es-define-property@1.0.1: - resolution: - { integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} es-errors@1.3.0: - resolution: - { integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} es-module-lexer@2.1.0: - resolution: - { integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ== } + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} es6-error@4.1.1: - resolution: - { integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== } + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} escalade@3.2.0: - resolution: - { integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== } - engines: { node: ">=6" } + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} escape-string-regexp@1.0.5: - resolution: - { integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== } - engines: { node: ">=0.8.0" } + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} escape-string-regexp@4.0.0: - resolution: - { integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== } - engines: { node: ">=10" } + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} eslint-scope@5.1.1: - resolution: - { integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== } - engines: { node: ">=8.0.0" } + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} esrecurse@4.3.0: - resolution: - { integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== } - engines: { node: ">=4.0" } + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} estraverse@4.3.0: - resolution: - { integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== } - engines: { node: ">=4.0" } + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} estraverse@5.3.0: - resolution: - { integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== } - engines: { node: ">=4.0" } + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} estree-walker@3.0.3: - resolution: - { integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== } + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} eta@3.5.0: - resolution: - { integrity: sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug== } - engines: { node: ">=6.0.0" } + resolution: {integrity: sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug==} + engines: {node: '>=6.0.0'} eventemitter3@5.0.4: - resolution: - { integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw== } + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} events@3.3.0: - resolution: - { integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== } - engines: { node: ">=0.8.x" } + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} execa@1.0.0: - resolution: - { integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== } - engines: { node: ">=6" } + resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} + engines: {node: '>=6'} expect-type@1.3.0: - resolution: - { integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA== } - engines: { node: ">=12.0.0" } + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} exponential-backoff@3.1.3: - resolution: - { integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA== } + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} external-editor@3.1.0: - resolution: - { integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== } - engines: { node: ">=4" } + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} extract-zip@2.0.1: - resolution: - { integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== } - engines: { node: ">= 10.17.0" } + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} hasBin: true fast-deep-equal@3.1.3: - resolution: - { integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== } + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} fast-glob@3.3.3: - resolution: - { integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== } - engines: { node: ">=8.6.0" } + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} fast-uri@3.1.2: - resolution: - { integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ== } + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} fastq@1.20.1: - resolution: - { integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw== } + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} fd-slicer@1.1.0: - resolution: - { integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== } + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} fdir@6.5.0: - resolution: - { integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== } - engines: { node: ">=12.0.0" } + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: picomatch: optional: true + fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + + filelist@1.0.6: + resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} + filename-reserved-regex@2.0.0: - resolution: - { integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ== } - engines: { node: ">=4" } + resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==} + engines: {node: '>=4'} filenamify@4.3.0: - resolution: - { integrity: sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg== } - engines: { node: ">=8" } + resolution: {integrity: sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==} + engines: {node: '>=8'} fill-range@7.1.1: - resolution: - { integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== } - engines: { node: ">=8" } + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} find-up@2.1.0: - resolution: - { integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ== } - engines: { node: ">=4" } + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} + engines: {node: '>=4'} find-up@5.0.0: - resolution: - { integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== } - engines: { node: ">=10" } + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} flora-colossus@2.0.0: - resolution: - { integrity: sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA== } - engines: { node: ">= 12" } + resolution: {integrity: sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA==} + engines: {node: '>= 12'} + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} fs-extra@10.1.0: - resolution: - { integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== } - engines: { node: ">=12" } + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.3.1: + resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==} + engines: {node: '>=14.14'} fs-extra@11.3.5: - resolution: - { integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg== } - engines: { node: ">=14.14" } + resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==} + engines: {node: '>=14.14'} fs-extra@7.0.1: - resolution: - { integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== } - engines: { node: ">=6 <7 || >=8" } + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} fs-extra@8.1.0: - resolution: - { integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== } - engines: { node: ">=6 <7 || >=8" } + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} fs-extra@9.1.0: - resolution: - { integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== } - engines: { node: ">=10" } + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} fs-minipass@2.1.0: - resolution: - { integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== } - engines: { node: ">= 8" } + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} fs.realpath@1.0.0: - resolution: - { integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== } + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} fsevents@2.3.2: - resolution: - { integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== } - engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] fsevents@2.3.3: - resolution: - { integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== } - engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] function-bind@1.1.2: - resolution: - { integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== } + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} galactus@1.0.0: - resolution: - { integrity: sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ== } - engines: { node: ">= 12" } + resolution: {integrity: sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ==} + engines: {node: '>= 12'} gar@1.0.4: - resolution: - { integrity: sha512-w4n9cPWyP7aHxKxYHFQMegj7WIAsL/YX/C4Bs5Rr8s1H9M1rNtRWRsw+ovYMkXDQ5S4ZbYHsHAPmevPjPgw44w== } + resolution: {integrity: sha512-w4n9cPWyP7aHxKxYHFQMegj7WIAsL/YX/C4Bs5Rr8s1H9M1rNtRWRsw+ovYMkXDQ5S4ZbYHsHAPmevPjPgw44w==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. gensync@1.0.0-beta.2: - resolution: - { integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== } - engines: { node: ">=6.9.0" } + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} get-caller-file@2.0.5: - resolution: - { integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== } - engines: { node: 6.* || 8.* || >= 10.* } + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} get-folder-size@2.0.1: - resolution: - { integrity: sha512-+CEb+GDCM7tkOS2wdMKTn9vU7DgnKUTuDlehkNJKNSovdCOVxs14OfKCk4cvSaR3za4gj+OBdl9opPN9xrJ0zA== } + resolution: {integrity: sha512-+CEb+GDCM7tkOS2wdMKTn9vU7DgnKUTuDlehkNJKNSovdCOVxs14OfKCk4cvSaR3za4gj+OBdl9opPN9xrJ0zA==} hasBin: true + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-nonce@1.0.1: - resolution: - { integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== } - engines: { node: ">=6" } + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} get-package-info@1.0.0: - resolution: - { integrity: sha512-SCbprXGAPdIhKAXiG+Mk6yeoFH61JlYunqdFQFHDtLjJlDjFf6x07dsS8acO+xWt52jpdVo49AlVDnUVK1sDNw== } - engines: { node: ">= 4.0" } + resolution: {integrity: sha512-SCbprXGAPdIhKAXiG+Mk6yeoFH61JlYunqdFQFHDtLjJlDjFf6x07dsS8acO+xWt52jpdVo49AlVDnUVK1sDNw==} + engines: {node: '>= 4.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} get-stream@4.1.0: - resolution: - { integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== } - engines: { node: ">=6" } + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} get-stream@5.2.0: - resolution: - { integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== } - engines: { node: ">=8" } + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} github-url-to-object@4.0.6: - resolution: - { integrity: sha512-NaqbYHMUAlPcmWFdrAB7bcxrNIiiJWJe8s/2+iOc9vlcHlwHqSGrPk+Yi3nu6ebTwgsZEa7igz+NH2vEq3gYwQ== } + resolution: {integrity: sha512-NaqbYHMUAlPcmWFdrAB7bcxrNIiiJWJe8s/2+iOc9vlcHlwHqSGrPk+Yi3nu6ebTwgsZEa7igz+NH2vEq3gYwQ==} glob-parent@5.1.2: - resolution: - { integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== } - engines: { node: ">= 6" } + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} glob-to-regexp@0.4.1: - resolution: - { integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== } + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} glob@7.2.3: - resolution: - { integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== } + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@8.1.0: - resolution: - { integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== } - engines: { node: ">=12" } + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-agent@3.0.0: - resolution: - { integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q== } - engines: { node: ">=10.0" } + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} global-dirs@3.0.1: - resolution: - { integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA== } - engines: { node: ">=10" } + resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} + engines: {node: '>=10'} globalthis@1.0.4: - resolution: - { integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} gopd@1.2.0: - resolution: - { integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} got@11.8.6: - resolution: - { integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g== } - engines: { node: ">=10.19.0" } + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} graceful-fs@4.2.11: - resolution: - { integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== } + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} has-flag@4.0.0: - resolution: - { integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== } - engines: { node: ">=8" } + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} has-property-descriptors@1.0.2: - resolution: - { integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== } + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} hasown@2.0.4: - resolution: - { integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A== } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} hosted-git-info@2.8.9: - resolution: - { integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== } + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} html-encoding-sniffer@6.0.0: - resolution: - { integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg== } - engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} http-cache-semantics@4.2.0: - resolution: - { integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ== } + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} http-proxy-agent@5.0.0: - resolution: - { integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== } - engines: { node: ">= 6" } + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} http2-wrapper@1.0.3: - resolution: - { integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg== } - engines: { node: ">=10.19.0" } + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} https-proxy-agent@5.0.1: - resolution: - { integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== } - engines: { node: ">= 6" } + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} https-proxy-agent@7.0.6: - resolution: - { integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== } - engines: { node: ">= 14" } + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} humanize-ms@1.2.1: - resolution: - { integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== } + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} iconv-lite@0.4.24: - resolution: - { integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} iconv-lite@0.6.3: - resolution: - { integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} ieee754@1.2.1: - resolution: - { integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== } + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} imurmurhash@0.1.4: - resolution: - { integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== } - engines: { node: ">=0.8.19" } + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} indent-string@4.0.0: - resolution: - { integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== } - engines: { node: ">=8" } + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} index-to-position@1.2.0: - resolution: - { integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw== } - engines: { node: ">=18" } + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + engines: {node: '>=18'} infer-owner@1.0.4: - resolution: - { integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== } + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} inflight@1.0.6: - resolution: - { integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== } + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.4: - resolution: - { integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== } + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} ini@2.0.0: - resolution: - { integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== } - engines: { node: ">=10" } + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} interpret@3.1.1: - resolution: - { integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== } - engines: { node: ">=10.13.0" } + resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} + engines: {node: '>=10.13.0'} ip-address@10.2.0: - resolution: - { integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA== } - engines: { node: ">= 12" } + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} is-arrayish@0.2.1: - resolution: - { integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== } + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} is-core-module@2.16.2: - resolution: - { integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA== } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} is-extglob@2.1.1: - resolution: - { integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} is-fullwidth-code-point@3.0.0: - resolution: - { integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== } - engines: { node: ">=8" } + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} is-fullwidth-code-point@4.0.0: - resolution: - { integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== } - engines: { node: ">=12" } + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} is-glob@4.0.3: - resolution: - { integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} is-interactive@1.0.0: - resolution: - { integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== } - engines: { node: ">=8" } + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} is-lambda@1.0.1: - resolution: - { integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== } + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} is-number@7.0.0: - resolution: - { integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== } - engines: { node: ">=0.12.0" } + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} is-potential-custom-element-name@1.0.1: - resolution: - { integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== } + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} is-stream@1.1.0: - resolution: - { integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} is-unicode-supported@0.1.0: - resolution: - { integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== } - engines: { node: ">=10" } + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} is-url@1.2.4: - resolution: - { integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== } + resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} isbinaryfile@4.0.10: - resolution: - { integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== } - engines: { node: ">= 8.0.0" } + resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} + engines: {node: '>= 8.0.0'} + + isbinaryfile@5.0.7: + resolution: {integrity: sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==} + engines: {node: '>= 18.0.0'} isbot@5.1.42: - resolution: - { integrity: sha512-/SXsVh7KpPRISrD4ffrGSxnTLlUBzEQUfWIusaJPrpJ93FW1P0YEZri5vAUkFsA0m2HRUhQRQadk2wJ+EeKowQ== } - engines: { node: ">=18" } + resolution: {integrity: sha512-/SXsVh7KpPRISrD4ffrGSxnTLlUBzEQUfWIusaJPrpJ93FW1P0YEZri5vAUkFsA0m2HRUhQRQadk2wJ+EeKowQ==} + engines: {node: '>=18'} isexe@2.0.0: - resolution: - { integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== } + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + + isexe@4.0.0: + resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} + engines: {node: '>=20'} + + jake@10.9.4: + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} + engines: {node: '>=10'} + hasBin: true jest-worker@27.5.1: - resolution: - { integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== } - engines: { node: ">= 10.13.0" } + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} jiti@2.7.0: - resolution: - { integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ== } + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true js-levenshtein@1.1.6: - resolution: - { integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} js-tokens@4.0.0: - resolution: - { integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== } + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} js-yaml@4.1.1: - resolution: - { integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== } + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true jsdom@29.1.1: - resolution: - { integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q== } - engines: { node: ^20.19.0 || ^22.13.0 || >=24.0.0 } + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} peerDependencies: canvas: ^3.0.0 peerDependenciesMeta: @@ -3200,1159 +2925,1054 @@ packages: optional: true jsesc@3.1.0: - resolution: - { integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== } - engines: { node: ">=6" } + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} hasBin: true json-buffer@3.0.1: - resolution: - { integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== } + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} json-schema-traverse@1.0.0: - resolution: - { integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== } + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} json-stringify-safe@5.0.1: - resolution: - { integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== } + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} json5@2.2.3: - resolution: - { integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== } - engines: { node: ">=6" } + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} hasBin: true jsonfile@4.0.0: - resolution: - { integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== } + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} jsonfile@6.2.1: - resolution: - { integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q== } + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} junk@3.1.0: - resolution: - { integrity: sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ== } - engines: { node: ">=8" } + resolution: {integrity: sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==} + engines: {node: '>=8'} keyv@4.5.4: - resolution: - { integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== } + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + lazy-val@1.0.5: + resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} lightningcss-android-arm64@1.32.0: - resolution: - { integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg== } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} cpu: [arm64] os: [android] lightningcss-darwin-arm64@1.32.0: - resolution: - { integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ== } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] lightningcss-darwin-x64@1.32.0: - resolution: - { integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w== } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] lightningcss-freebsd-x64@1.32.0: - resolution: - { integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig== } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: - { integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw== } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] lightningcss-linux-arm64-gnu@1.32.0: - resolution: - { integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ== } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: - resolution: - { integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg== } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: - resolution: - { integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA== } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: - resolution: - { integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg== } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: - resolution: - { integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw== } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] lightningcss-win32-x64-msvc@1.32.0: - resolution: - { integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q== } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] lightningcss@1.32.0: - resolution: - { integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ== } - engines: { node: ">= 12.0.0" } + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} listr2@7.0.2: - resolution: - { integrity: sha512-rJysbR9GKIalhTbVL2tYbF2hVyDnrf7pFUZBwjPaMIdadYHmeT+EVi/Bu3qd7ETQPahTotg2WRCatXwRBW554g== } - engines: { node: ">=16.0.0" } + resolution: {integrity: sha512-rJysbR9GKIalhTbVL2tYbF2hVyDnrf7pFUZBwjPaMIdadYHmeT+EVi/Bu3qd7ETQPahTotg2WRCatXwRBW554g==} + engines: {node: '>=16.0.0'} load-json-file@2.0.0: - resolution: - { integrity: sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ== } - engines: { node: ">=4" } + resolution: {integrity: sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ==} + engines: {node: '>=4'} loader-runner@4.3.2: - resolution: - { integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w== } - engines: { node: ">=6.11.5" } + resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} + engines: {node: '>=6.11.5'} locate-path@2.0.0: - resolution: - { integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA== } - engines: { node: ">=4" } + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} + engines: {node: '>=4'} locate-path@6.0.0: - resolution: - { integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== } - engines: { node: ">=10" } + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} lodash.get@4.4.2: - resolution: - { integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== } + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. lodash@4.18.1: - resolution: - { integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== } + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} log-symbols@4.1.0: - resolution: - { integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== } - engines: { node: ">=10" } + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} log-update@5.0.1: - resolution: - { integrity: sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw== } - engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + resolution: {integrity: sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} lowercase-keys@2.0.0: - resolution: - { integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== } - engines: { node: ">=8" } + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} lru-cache@11.5.1: - resolution: - { integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A== } - engines: { node: 20 || >=22 } + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} lru-cache@5.1.1: - resolution: - { integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== } + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} lru-cache@7.18.3: - resolution: - { integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== } - engines: { node: ">=12" } + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} lucide-react@1.17.0: - resolution: - { integrity: sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w== } + resolution: {integrity: sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 lz-string@1.5.0: - resolution: - { integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== } + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true magic-string@0.30.21: - resolution: - { integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== } + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} make-fetch-happen@10.2.1: - resolution: - { integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w== } - engines: { node: ^12.13.0 || ^14.15.0 || >=16.0.0 } + resolution: {integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} map-age-cleaner@0.1.3: - resolution: - { integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== } - engines: { node: ">=6" } + resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} + engines: {node: '>=6'} matcher@3.0.0: - resolution: - { integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng== } - engines: { node: ">=10" } + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} mdn-data@2.27.1: - resolution: - { integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ== } + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} mem@4.3.0: - resolution: - { integrity: sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== } - engines: { node: ">=6" } + resolution: {integrity: sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==} + engines: {node: '>=6'} merge-stream@2.0.0: - resolution: - { integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== } + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} merge2@1.4.1: - resolution: - { integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== } - engines: { node: ">= 8" } + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} micromatch@4.0.8: - resolution: - { integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== } - engines: { node: ">=8.6" } + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} mime-db@1.52.0: - resolution: - { integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} mime-db@1.54.0: - resolution: - { integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} mime-types@2.1.35: - resolution: - { integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true mimic-fn@2.1.0: - resolution: - { integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== } - engines: { node: ">=6" } + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} mimic-response@1.0.1: - resolution: - { integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== } - engines: { node: ">=4" } + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} mimic-response@3.1.0: - resolution: - { integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== } - engines: { node: ">=10" } + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} min-indent@1.0.1: - resolution: - { integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== } - engines: { node: ">=4" } + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} minimatch@3.1.5: - resolution: - { integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== } + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} minimatch@5.1.9: - resolution: - { integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw== } - engines: { node: ">=10" } + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} minimatch@9.0.9: - resolution: - { integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg== } - engines: { node: ">=16 || 14 >=14.17" } + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} minimist@1.2.8: - resolution: - { integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== } + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} minipass-collect@1.0.2: - resolution: - { integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== } - engines: { node: ">= 8" } + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} minipass-fetch@2.1.2: - resolution: - { integrity: sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA== } - engines: { node: ^12.13.0 || ^14.15.0 || >=16.0.0 } + resolution: {integrity: sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} minipass-flush@1.0.7: - resolution: - { integrity: sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA== } - engines: { node: ">= 8" } + resolution: {integrity: sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==} + engines: {node: '>= 8'} minipass-pipeline@1.2.4: - resolution: - { integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== } - engines: { node: ">=8" } + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} minipass-sized@1.0.3: - resolution: - { integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== } - engines: { node: ">=8" } + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} minipass@3.3.6: - resolution: - { integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== } - engines: { node: ">=8" } + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} minipass@5.0.0: - resolution: - { integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== } - engines: { node: ">=8" } + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} minizlib@2.1.2: - resolution: - { integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== } - engines: { node: ">= 8" } + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} mkdirp@0.5.6: - resolution: - { integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== } + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true mkdirp@1.0.4: - resolution: - { integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== } - engines: { node: ">=10" } + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} hasBin: true ms@2.0.0: - resolution: - { integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== } + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} ms@2.1.3: - resolution: - { integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== } + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} mute-stream@1.0.0: - resolution: - { integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA== } - engines: { node: ^14.17.0 || ^16.13.0 || >=18.0.0 } + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} nanoid@3.3.12: - resolution: - { integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ== } - engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true negotiator@0.6.4: - resolution: - { integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== } - engines: { node: ">= 0.6" } + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} neo-async@2.6.2: - resolution: - { integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== } + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} nice-try@1.0.5: - resolution: - { integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== } + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} node-abi@3.92.0: - resolution: - { integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ== } - engines: { node: ">=10" } + resolution: {integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==} + engines: {node: '>=10'} + + node-abi@4.31.0: + resolution: {integrity: sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw==} + engines: {node: '>=22.12.0'} node-api-version@0.2.1: - resolution: - { integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q== } + resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==} node-fetch@2.7.0: - resolution: - { integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== } - engines: { node: 4.x || >=6.0.0 } + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} peerDependencies: encoding: ^0.1.0 peerDependenciesMeta: encoding: optional: true + node-gyp@12.4.0: + resolution: {integrity: sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + node-releases@2.0.47: - resolution: - { integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og== } - engines: { node: ">=18" } + resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==} + engines: {node: '>=18'} nopt@6.0.0: - resolution: - { integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g== } - engines: { node: ^12.13.0 || ^14.15.0 || >=16.0.0 } + resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + hasBin: true + + nopt@9.0.0: + resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==} + engines: {node: ^20.17.0 || >=22.9.0} hasBin: true normalize-package-data@2.5.0: - resolution: - { integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== } + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} normalize-url@6.1.0: - resolution: - { integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== } - engines: { node: ">=10" } + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} npm-run-path@2.0.2: - resolution: - { integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw== } - engines: { node: ">=4" } + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} object-keys@1.1.1: - resolution: - { integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} obug@2.1.2: - resolution: - { integrity: sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg== } - engines: { node: ">=12.20.0" } + resolution: {integrity: sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==} + engines: {node: '>=12.20.0'} once@1.4.0: - resolution: - { integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== } + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} onetime@5.1.2: - resolution: - { integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== } - engines: { node: ">=6" } + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} openapi-fetch@0.17.0: - resolution: - { integrity: sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig== } + resolution: {integrity: sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig==} openapi-typescript-helpers@0.1.0: - resolution: - { integrity: sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw== } + resolution: {integrity: sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw==} openapi-typescript@7.13.0: - resolution: - { integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ== } + resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==} hasBin: true peerDependencies: typescript: ^5.x ora@5.4.1: - resolution: - { integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== } - engines: { node: ">=10" } + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} os-tmpdir@1.0.2: - resolution: - { integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} p-cancelable@2.1.1: - resolution: - { integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== } - engines: { node: ">=8" } + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} p-defer@1.0.0: - resolution: - { integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw== } - engines: { node: ">=4" } + resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==} + engines: {node: '>=4'} p-finally@1.0.0: - resolution: - { integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== } - engines: { node: ">=4" } + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} p-is-promise@2.1.0: - resolution: - { integrity: sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== } - engines: { node: ">=6" } + resolution: {integrity: sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==} + engines: {node: '>=6'} p-limit@1.3.0: - resolution: - { integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== } - engines: { node: ">=4" } + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} p-limit@3.1.0: - resolution: - { integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== } - engines: { node: ">=10" } + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} p-locate@2.0.0: - resolution: - { integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg== } - engines: { node: ">=4" } + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} + engines: {node: '>=4'} p-locate@5.0.0: - resolution: - { integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== } - engines: { node: ">=10" } + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} p-map@4.0.0: - resolution: - { integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== } - engines: { node: ">=10" } + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} p-try@1.0.0: - resolution: - { integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww== } - engines: { node: ">=4" } + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} + engines: {node: '>=4'} parse-author@2.0.0: - resolution: - { integrity: sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==} + engines: {node: '>=0.10.0'} parse-json@2.2.0: - resolution: - { integrity: sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==} + engines: {node: '>=0.10.0'} parse-json@8.3.0: - resolution: - { integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ== } - engines: { node: ">=18" } + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} parse5@8.0.1: - resolution: - { integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw== } + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} path-exists@3.0.0: - resolution: - { integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== } - engines: { node: ">=4" } + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} path-exists@4.0.0: - resolution: - { integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== } - engines: { node: ">=8" } + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} path-is-absolute@1.0.1: - resolution: - { integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} path-key@2.0.1: - resolution: - { integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== } - engines: { node: ">=4" } + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} path-key@3.1.1: - resolution: - { integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== } - engines: { node: ">=8" } + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} path-parse@1.0.7: - resolution: - { integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== } + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} path-type@2.0.0: - resolution: - { integrity: sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ== } - engines: { node: ">=4" } + resolution: {integrity: sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==} + engines: {node: '>=4'} pathe@2.0.3: - resolution: - { integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== } + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pe-library@0.4.1: + resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} + engines: {node: '>=12', npm: '>=6'} pe-library@1.0.1: - resolution: - { integrity: sha512-nh39Mo1eGWmZS7y+mK/dQIqg7S1lp38DpRxkyoHf0ZcUs/HDc+yyTjuOtTvSMZHmfSLuSQaX945u05Y2Q6UWZg== } - engines: { node: ">=14", npm: ">=7" } + resolution: {integrity: sha512-nh39Mo1eGWmZS7y+mK/dQIqg7S1lp38DpRxkyoHf0ZcUs/HDc+yyTjuOtTvSMZHmfSLuSQaX945u05Y2Q6UWZg==} + engines: {node: '>=14', npm: '>=7'} pend@1.2.0: - resolution: - { integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== } + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} picocolors@1.1.1: - resolution: - { integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== } + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} picomatch@2.3.2: - resolution: - { integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA== } - engines: { node: ">=8.6" } + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} picomatch@4.0.4: - resolution: - { integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== } - engines: { node: ">=12" } + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} pify@2.3.0: - resolution: - { integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pkijs@3.4.0: + resolution: {integrity: sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==} + engines: {node: '>=16.0.0'} playwright-core@1.60.0: - resolution: - { integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA== } - engines: { node: ">=18" } + resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} + engines: {node: '>=18'} hasBin: true playwright@1.60.0: - resolution: - { integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA== } - engines: { node: ">=18" } + resolution: {integrity: sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==} + engines: {node: '>=18'} hasBin: true + plist@3.1.0: + resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} + engines: {node: '>=10.4.0'} + plist@3.1.1: - resolution: - { integrity: sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA== } - engines: { node: ">=10.4.0" } + resolution: {integrity: sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==} + engines: {node: '>=10.4.0'} pluralize@8.0.0: - resolution: - { integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== } - engines: { node: ">=4" } + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} postcss@8.5.15: - resolution: - { integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A== } - engines: { node: ^10 || ^12 || >=14 } + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + posthog-js@1.393.0: + resolution: {integrity: sha512-BNu62XUNkFEIq7ZQJwvgtZIgWUfn0HozVcYHO8P1WMq2Crx+d+/l7TJsO6YHf3aUUiJn+L8L8NX7XgFZxW3/tw==} postject@1.0.0-alpha.6: - resolution: - { integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A== } - engines: { node: ">=14.0.0" } + resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==} + engines: {node: '>=14.0.0'} hasBin: true + preact@10.29.2: + resolution: {integrity: sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==} + prettier@3.8.4: - resolution: - { integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q== } - engines: { node: ">=14" } + resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==} + engines: {node: '>=14'} hasBin: true pretty-format@27.5.1: - resolution: - { integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== } - engines: { node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0 } + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} proc-log@2.0.1: - resolution: - { integrity: sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw== } - engines: { node: ^12.13.0 || ^14.15.0 || >=16.0.0 } + resolution: {integrity: sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + proc-log@6.1.0: + resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} progress@2.0.3: - resolution: - { integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== } - engines: { node: ">=0.4.0" } + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} promise-inflight@1.0.1: - resolution: - { integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== } + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} peerDependencies: - bluebird: "*" + bluebird: '*' peerDependenciesMeta: bluebird: optional: true promise-retry@2.0.1: - resolution: - { integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== } - engines: { node: ">=10" } + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} pump@3.0.4: - resolution: - { integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA== } + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} punycode@2.3.1: - resolution: - { integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== } - engines: { node: ">=6" } + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pvtsutils@1.3.6: + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} + + pvutils@1.1.5: + resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==} + engines: {node: '>=16.0.0'} + + query-selector-shadow-dom@1.0.1: + resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} queue-microtask@1.2.3: - resolution: - { integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== } + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} quick-lru@5.1.1: - resolution: - { integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== } - engines: { node: ">=10" } + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} radix-ui@1.5.0: - resolution: - { integrity: sha512-Nzh2HNpClgB31FBHRqt2xG8XNUfVfQRpf34hACC5PNrXTd5JdXdqOXwLs3BL+D8CNYiNQiJiT8QGr5Q4vq+00w== } + resolution: {integrity: sha512-Nzh2HNpClgB31FBHRqt2xG8XNUfVfQRpf34hACC5PNrXTd5JdXdqOXwLs3BL+D8CNYiNQiJiT8QGr5Q4vq+00w==} peerDependencies: - "@types/react": "*" - "@types/react-dom": "*" + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true - "@types/react-dom": + '@types/react-dom': optional: true react-dom@19.2.7: - resolution: - { integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ== } + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} peerDependencies: react: ^19.2.7 react-is@17.0.2: - resolution: - { integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== } + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} react-remove-scroll-bar@2.3.8: - resolution: - { integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q== } - engines: { node: ">=10" } + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: - "@types/react": + '@types/react': optional: true react-remove-scroll@2.7.2: - resolution: - { integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q== } - engines: { node: ">=10" } + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true react-resizable-panels@4.11.2: - resolution: - { integrity: sha512-+kfFbDZ8mygc7g0vxOcDzCVGuwiIUOnILqPoUHo6/uP+Mmyx6HzZU+kj1aOPDlktXuobYbr6BtQekvJwHRX4Eg== } + resolution: {integrity: sha512-+kfFbDZ8mygc7g0vxOcDzCVGuwiIUOnILqPoUHo6/uP+Mmyx6HzZU+kj1aOPDlktXuobYbr6BtQekvJwHRX4Eg==} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 react-style-singleton@2.2.3: - resolution: - { integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ== } - engines: { node: ">=10" } + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true react@19.2.7: - resolution: - { integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} read-binary-file-arch@1.0.6: - resolution: - { integrity: sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg== } + resolution: {integrity: sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==} hasBin: true read-pkg-up@2.0.0: - resolution: - { integrity: sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w== } - engines: { node: ">=4" } + resolution: {integrity: sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w==} + engines: {node: '>=4'} read-pkg@2.0.0: - resolution: - { integrity: sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA== } - engines: { node: ">=4" } + resolution: {integrity: sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA==} + engines: {node: '>=4'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} readable-stream@3.6.2: - resolution: - { integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== } - engines: { node: ">= 6" } + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} readdirp@5.0.0: - resolution: - { integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ== } - engines: { node: ">= 20.19.0" } + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} rechoir@0.8.0: - resolution: - { integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== } - engines: { node: ">= 10.13.0" } + resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} + engines: {node: '>= 10.13.0'} redent@3.0.0: - resolution: - { integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== } - engines: { node: ">=8" } + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} require-directory@2.1.1: - resolution: - { integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} require-from-string@2.0.2: - resolution: - { integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resedit@1.7.2: + resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} + engines: {node: '>=12', npm: '>=6'} resedit@2.0.3: - resolution: - { integrity: sha512-oTeemxwoMuxxTYxXUwjkrOPfngTQehlv0/HoYFNkB4uzsP1Un1A9nI8JQKGOFkxpqkC7qkMs0lUsGrvUlbLNUA== } - engines: { node: ">=14", npm: ">=7" } + resolution: {integrity: sha512-oTeemxwoMuxxTYxXUwjkrOPfngTQehlv0/HoYFNkB4uzsP1Un1A9nI8JQKGOFkxpqkC7qkMs0lUsGrvUlbLNUA==} + engines: {node: '>=14', npm: '>=7'} resolve-alpn@1.2.1: - resolution: - { integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== } + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} resolve@1.22.12: - resolution: - { integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA== } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} hasBin: true responselike@2.0.1: - resolution: - { integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw== } + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} restore-cursor@3.1.0: - resolution: - { integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== } - engines: { node: ">=8" } + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} restore-cursor@4.0.0: - resolution: - { integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg== } - engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} retry@0.12.0: - resolution: - { integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== } - engines: { node: ">= 4" } + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} reusify@1.1.0: - resolution: - { integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== } - engines: { iojs: ">=1.0.0", node: ">=0.10.0" } + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} rfdc@1.4.1: - resolution: - { integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== } + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} rimraf@2.6.3: - resolution: - { integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== } + resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rimraf@3.0.2: - resolution: - { integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== } + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true roarr@2.15.4: - resolution: - { integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A== } - engines: { node: ">=8.0" } + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} rolldown@1.0.3: - resolution: - { integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g== } - engines: { node: ^20.19.0 || >=22.12.0 } + resolution: {integrity: sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true run-parallel@1.2.0: - resolution: - { integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== } + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} safe-buffer@5.2.1: - resolution: - { integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== } + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} safer-buffer@2.1.2: - resolution: - { integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== } + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sanitize-filename@1.6.4: + resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==} + + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} saxes@6.0.0: - resolution: - { integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== } - engines: { node: ">=v12.22.7" } + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} scheduler@0.27.0: - resolution: - { integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== } + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} schema-utils@4.3.3: - resolution: - { integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA== } - engines: { node: ">= 10.13.0" } + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} semver-compare@1.0.0: - resolution: - { integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== } + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} semver@5.7.2: - resolution: - { integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== } + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true semver@6.3.1: - resolution: - { integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== } + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} hasBin: true semver@7.8.4: - resolution: - { integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA== } - engines: { node: ">=10" } + resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} + engines: {node: '>=10'} hasBin: true serialize-error@7.0.1: - resolution: - { integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw== } - engines: { node: ">=10" } + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} seroval-plugins@1.5.4: - resolution: - { integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw== } - engines: { node: ">=10" } + resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==} + engines: {node: '>=10'} peerDependencies: seroval: ^1.0 seroval@1.5.4: - resolution: - { integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw== } - engines: { node: ">=10" } + resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==} + engines: {node: '>=10'} shebang-command@1.2.0: - resolution: - { integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} shebang-command@2.0.0: - resolution: - { integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== } - engines: { node: ">=8" } + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} shebang-regex@1.0.0: - resolution: - { integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} shebang-regex@3.0.0: - resolution: - { integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== } - engines: { node: ">=8" } + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} siginfo@2.0.0: - resolution: - { integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== } + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} signal-exit@3.0.7: - resolution: - { integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== } + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} signal-exit@4.1.0: - resolution: - { integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== } - engines: { node: ">=14" } + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} slice-ansi@5.0.0: - resolution: - { integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== } - engines: { node: ">=12" } + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} smart-buffer@4.2.0: - resolution: - { integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== } - engines: { node: ">= 6.0.0", npm: ">= 3.0.0" } + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} socks-proxy-agent@7.0.0: - resolution: - { integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww== } - engines: { node: ">= 10" } + resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==} + engines: {node: '>= 10'} socks@2.8.9: - resolution: - { integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw== } - engines: { node: ">= 10.0.0", npm: ">= 3.0.0" } + resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} source-map-js@1.2.1: - resolution: - { integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} source-map-support@0.5.21: - resolution: - { integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== } + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} source-map@0.6.1: - resolution: - { integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} spdx-correct@3.2.0: - resolution: - { integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== } + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} spdx-exceptions@2.5.0: - resolution: - { integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== } + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} spdx-expression-parse@3.0.1: - resolution: - { integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== } + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} spdx-license-ids@3.0.23: - resolution: - { integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw== } + resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} sprintf-js@1.1.3: - resolution: - { integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== } + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} ssri@9.0.1: - resolution: - { integrity: sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q== } - engines: { node: ^12.13.0 || ^14.15.0 || >=16.0.0 } + resolution: {integrity: sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} stackback@0.0.2: - resolution: - { integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== } + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + stat-mode@1.0.0: + resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} + engines: {node: '>= 6'} std-env@4.1.0: - resolution: - { integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ== } + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} string-width@4.2.3: - resolution: - { integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== } - engines: { node: ">=8" } + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} string-width@5.1.2: - resolution: - { integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== } - engines: { node: ">=12" } + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} string_decoder@1.3.0: - resolution: - { integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== } + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} strip-ansi@6.0.1: - resolution: - { integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== } - engines: { node: ">=8" } + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} strip-ansi@7.2.0: - resolution: - { integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w== } - engines: { node: ">=12" } + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} strip-bom@3.0.0: - resolution: - { integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== } - engines: { node: ">=4" } + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} strip-eof@1.0.0: - resolution: - { integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} strip-indent@3.0.0: - resolution: - { integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== } - engines: { node: ">=8" } + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} strip-outer@1.0.1: - resolution: - { integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==} + engines: {node: '>=0.10.0'} sumchecker@3.0.1: - resolution: - { integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg== } - engines: { node: ">= 8.0" } + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} supports-color@10.2.2: - resolution: - { integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g== } - engines: { node: ">=18" } + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} supports-color@7.2.0: - resolution: - { integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== } - engines: { node: ">=8" } + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} supports-color@8.1.1: - resolution: - { integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== } - engines: { node: ">=10" } + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} supports-preserve-symlinks-flag@1.0.0: - resolution: - { integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== } - engines: { node: ">= 0.4" } + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} symbol-tree@3.2.4: - resolution: - { integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== } + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} tailwind-merge@3.6.0: - resolution: - { integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w== } + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} tailwindcss@4.3.0: - resolution: - { integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q== } + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} tapable@2.3.3: - resolution: - { integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A== } - engines: { node: ">=6" } + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} tar@6.2.1: - resolution: - { integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== } - engines: { node: ">=10" } + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + tar@7.5.16: + resolution: {integrity: sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==} + engines: {node: '>=18'} + + temp-file@3.4.0: + resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} + temp@0.9.4: - resolution: - { integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA== } - engines: { node: ">=6.0.0" } + resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} + engines: {node: '>=6.0.0'} terser-webpack-plugin@5.6.1: - resolution: - { integrity: sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ== } - engines: { node: ">= 10.13.0" } + resolution: {integrity: sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==} + engines: {node: '>= 10.13.0'} peerDependencies: - "@minify-html/node": "*" - "@swc/core": "*" - "@swc/css": "*" - "@swc/html": "*" - clean-css: "*" - cssnano: "*" - csso: "*" - esbuild: "*" - html-minifier-terser: "*" - lightningcss: "*" - postcss: "*" - uglify-js: "*" + '@minify-html/node': '*' + '@swc/core': '*' + '@swc/css': '*' + '@swc/html': '*' + clean-css: '*' + cssnano: '*' + csso: '*' + esbuild: '*' + html-minifier-terser: '*' + lightningcss: '*' + postcss: '*' + uglify-js: '*' webpack: ^5.1.0 peerDependenciesMeta: - "@minify-html/node": + '@minify-html/node': optional: true - "@swc/core": + '@swc/core': optional: true - "@swc/css": + '@swc/css': optional: true - "@swc/html": + '@swc/html': optional: true clean-css: optional: true @@ -4372,237 +3992,211 @@ packages: optional: true terser@5.48.0: - resolution: - { integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q== } - engines: { node: ">=10" } + resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==} + engines: {node: '>=10'} hasBin: true + tiny-async-pool@1.3.0: + resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==} + tiny-each-async@2.0.3: - resolution: - { integrity: sha512-5ROII7nElnAirvFn8g7H7MtpfV1daMcyfTGQwsn/x2VtyV+VPiO5CjReCJtWLvoKTDEDmZocf3cNPraiMnBXLA== } + resolution: {integrity: sha512-5ROII7nElnAirvFn8g7H7MtpfV1daMcyfTGQwsn/x2VtyV+VPiO5CjReCJtWLvoKTDEDmZocf3cNPraiMnBXLA==} tinybench@2.9.0: - resolution: - { integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== } + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} tinyexec@1.2.4: - resolution: - { integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg== } - engines: { node: ">=18" } + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} tinyglobby@0.2.17: - resolution: - { integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== } - engines: { node: ">=12.0.0" } + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} tinyrainbow@3.1.0: - resolution: - { integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw== } - engines: { node: ">=14.0.0" } + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} tldts-core@7.4.2: - resolution: - { integrity: sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA== } + resolution: {integrity: sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==} tldts@7.4.2: - resolution: - { integrity: sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw== } + resolution: {integrity: sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==} hasBin: true tmp-promise@3.0.3: - resolution: - { integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ== } + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} tmp@0.0.33: - resolution: - { integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== } - engines: { node: ">=0.6.0" } + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} tmp@0.2.7: - resolution: - { integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw== } - engines: { node: ">=14.14" } + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} to-regex-range@5.0.1: - resolution: - { integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== } - engines: { node: ">=8.0" } + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} tough-cookie@6.0.1: - resolution: - { integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw== } - engines: { node: ">=16" } + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} tr46@0.0.3: - resolution: - { integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== } + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} tr46@6.0.0: - resolution: - { integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw== } - engines: { node: ">=20" } + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} trim-repeated@1.0.0: - resolution: - { integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==} + engines: {node: '>=0.10.0'} + + truncate-utf8-bytes@1.0.2: + resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} tslib@2.8.1: - resolution: - { integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== } + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} type-fest@0.13.1: - resolution: - { integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== } - engines: { node: ">=10" } + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} type-fest@0.21.3: - resolution: - { integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== } - engines: { node: ">=10" } + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} type-fest@1.4.0: - resolution: - { integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== } - engines: { node: ">=10" } + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} type-fest@4.41.0: - resolution: - { integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== } - engines: { node: ">=16" } + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} typescript@5.4.5: - resolution: - { integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== } - engines: { node: ">=14.17" } + resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} + engines: {node: '>=14.17'} hasBin: true typescript@5.9.3: - resolution: - { integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== } - engines: { node: ">=14.17" } + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} hasBin: true undici-types@6.21.0: - resolution: - { integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== } + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} undici-types@7.24.6: - resolution: - { integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg== } + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + undici@6.27.0: + resolution: {integrity: sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==} + engines: {node: '>=18.17'} undici@7.27.2: - resolution: - { integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA== } - engines: { node: ">=20.18.1" } + resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==} + engines: {node: '>=20.18.1'} unique-filename@2.0.1: - resolution: - { integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A== } - engines: { node: ^12.13.0 || ^14.15.0 || >=16.0.0 } + resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} unique-slug@3.0.0: - resolution: - { integrity: sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w== } - engines: { node: ^12.13.0 || ^14.15.0 || >=16.0.0 } + resolution: {integrity: sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} universal-user-agent@6.0.1: - resolution: - { integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ== } + resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} universalify@0.1.2: - resolution: - { integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== } - engines: { node: ">= 4.0.0" } + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} universalify@2.0.1: - resolution: - { integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== } - engines: { node: ">= 10.0.0" } + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} unplugin@3.0.0: - resolution: - { integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg== } - engines: { node: ^20.19.0 || >=22.12.0 } + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + + unzipper@0.12.5: + resolution: {integrity: sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==} update-browserslist-db@1.2.3: - resolution: - { integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== } + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: - browserslist: ">= 4.21.0" + browserslist: '>= 4.21.0' update-electron-app@3.2.0: - resolution: - { integrity: sha512-l2e7bzsW+rw70pfyyQeA9E/ofpNY2ZS99XuYxD2qWL4fEy3qMjpqwwgB0me7ESpGogIQE1CM0SaDvKGsK4Jg3Q== } + resolution: {integrity: sha512-l2e7bzsW+rw70pfyyQeA9E/ofpNY2ZS99XuYxD2qWL4fEy3qMjpqwwgB0me7ESpGogIQE1CM0SaDvKGsK4Jg3Q==} uri-js-replace@1.0.1: - resolution: - { integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g== } + resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} use-callback-ref@1.3.3: - resolution: - { integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg== } - engines: { node: ">=10" } + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true use-sidecar@1.1.3: - resolution: - { integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ== } - engines: { node: ">=10" } + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} peerDependencies: - "@types/react": "*" + '@types/react': '*' react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: - "@types/react": + '@types/react': optional: true use-sync-external-store@1.6.0: - resolution: - { integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== } + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 username@5.1.0: - resolution: - { integrity: sha512-PCKbdWw85JsYMvmCv5GH3kXmM66rCd9m1hBEDutPNv94b/pqCMT4NtcKyeWYvLFiE8b+ha1Jdl8XAaUdPn5QTg== } - engines: { node: ">=8" } + resolution: {integrity: sha512-PCKbdWw85JsYMvmCv5GH3kXmM66rCd9m1hBEDutPNv94b/pqCMT4NtcKyeWYvLFiE8b+ha1Jdl8XAaUdPn5QTg==} + engines: {node: '>=8'} + + utf8-byte-length@1.0.5: + resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} util-deprecate@1.0.2: - resolution: - { integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== } + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} validate-npm-package-license@3.0.4: - resolution: - { integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== } + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} vite@8.0.16: - resolution: - { integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw== } - engines: { node: ^20.19.0 || >=22.12.0 } + resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - "@types/node": ^20.19.0 || >=22.12.0 - "@vitejs/devtools": ^0.1.18 + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 esbuild: ^0.27.0 || ^0.28.0 - jiti: ">=1.21.0" + jiti: '>=1.21.0' less: ^4.0.0 sass: ^1.70.0 sass-embedded: ^1.70.0 - stylus: ">=0.54.8" + stylus: '>=0.54.8' sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 yaml: ^2.4.2 peerDependenciesMeta: - "@types/node": + '@types/node': optional: true - "@vitejs/devtools": + '@vitejs/devtools': optional: true esbuild: optional: true @@ -4626,41 +4220,40 @@ packages: optional: true vitest@4.1.8: - resolution: - { integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig== } - engines: { node: ^20.0.0 || ^22.0.0 || >=24.0.0 } + resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: - "@edge-runtime/vm": "*" - "@opentelemetry/api": ^1.9.0 - "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 - "@vitest/browser-playwright": 4.1.8 - "@vitest/browser-preview": 4.1.8 - "@vitest/browser-webdriverio": 4.1.8 - "@vitest/coverage-istanbul": 4.1.8 - "@vitest/coverage-v8": 4.1.8 - "@vitest/ui": 4.1.8 - happy-dom: "*" - jsdom: "*" + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.8 + '@vitest/browser-preview': 4.1.8 + '@vitest/browser-webdriverio': 4.1.8 + '@vitest/coverage-istanbul': 4.1.8 + '@vitest/coverage-v8': 4.1.8 + '@vitest/ui': 4.1.8 + happy-dom: '*' + jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: - "@edge-runtime/vm": + '@edge-runtime/vm': optional: true - "@opentelemetry/api": + '@opentelemetry/api': optional: true - "@types/node": + '@types/node': optional: true - "@vitest/browser-playwright": + '@vitest/browser-playwright': optional: true - "@vitest/browser-preview": + '@vitest/browser-preview': optional: true - "@vitest/browser-webdriverio": + '@vitest/browser-webdriverio': optional: true - "@vitest/coverage-istanbul": + '@vitest/coverage-istanbul': optional: true - "@vitest/coverage-v8": + '@vitest/coverage-v8': optional: true - "@vitest/ui": + '@vitest/ui': optional: true happy-dom: optional: true @@ -4668,183 +4261,168 @@ packages: optional: true w3c-xmlserializer@5.0.0: - resolution: - { integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA== } - engines: { node: ">=18" } + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} watchpack@2.5.1: - resolution: - { integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg== } - engines: { node: ">=10.13.0" } + resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} + engines: {node: '>=10.13.0'} wcwidth@1.0.1: - resolution: - { integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== } + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + web-vitals@5.3.0: + resolution: {integrity: sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==} + + webcrypto-core@1.9.2: + resolution: {integrity: sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==} webidl-conversions@3.0.1: - resolution: - { integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== } + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} webidl-conversions@8.0.1: - resolution: - { integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ== } - engines: { node: ">=20" } + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} webpack-sources@3.5.0: - resolution: - { integrity: sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ== } - engines: { node: ">=10.13.0" } + resolution: {integrity: sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==} + engines: {node: '>=10.13.0'} webpack-virtual-modules@0.6.2: - resolution: - { integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ== } + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} webpack@5.107.2: - resolution: - { integrity: sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ== } - engines: { node: ">=10.13.0" } + resolution: {integrity: sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==} + engines: {node: '>=10.13.0'} hasBin: true peerDependencies: - webpack-cli: "*" + webpack-cli: '*' peerDependenciesMeta: webpack-cli: optional: true whatwg-mimetype@5.0.0: - resolution: - { integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw== } - engines: { node: ">=20" } + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} whatwg-url@16.0.1: - resolution: - { integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw== } - engines: { node: ^20.19.0 || ^22.12.0 || >=24.0.0 } + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} whatwg-url@5.0.0: - resolution: - { integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== } + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} which@1.3.1: - resolution: - { integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== } + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true which@2.0.2: - resolution: - { integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== } - engines: { node: ">= 8" } + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@5.0.0: + resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + which@6.0.1: + resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} + engines: {node: ^20.17.0 || >=22.9.0} hasBin: true why-is-node-running@2.3.0: - resolution: - { integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== } - engines: { node: ">=8" } + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} hasBin: true word-wrap@1.2.5: - resolution: - { integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} wrap-ansi@6.2.0: - resolution: - { integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== } - engines: { node: ">=8" } + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} wrap-ansi@7.0.0: - resolution: - { integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== } - engines: { node: ">=10" } + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} wrap-ansi@8.1.0: - resolution: - { integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== } - engines: { node: ">=12" } + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} wrappy@1.0.2: - resolution: - { integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== } + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} xml-name-validator@5.0.0: - resolution: - { integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg== } - engines: { node: ">=18" } + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} xmlbuilder@15.1.1: - resolution: - { integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== } - engines: { node: ">=8.0" } + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} xmlchars@2.2.0: - resolution: - { integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== } + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} y18n@5.0.8: - resolution: - { integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== } - engines: { node: ">=10" } + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} yallist@3.1.1: - resolution: - { integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== } + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} yallist@4.0.0: - resolution: - { integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== } + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} yaml-ast-parser@0.0.43: - resolution: - { integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A== } + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} yargs-parser@20.2.9: - resolution: - { integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== } - engines: { node: ">=10" } + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} yargs-parser@21.1.1: - resolution: - { integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== } - engines: { node: ">=12" } + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} yargs@16.2.0: - resolution: - { integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== } - engines: { node: ">=10" } + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} yargs@17.7.2: - resolution: - { integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== } - engines: { node: ">=12" } + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} yauzl@2.10.0: - resolution: - { integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== } + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} yocto-queue@0.1.0: - resolution: - { integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== } - engines: { node: ">=10" } + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} yoctocolors-cjs@2.1.3: - resolution: - { integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw== } - engines: { node: ">=18" } + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} zod@4.4.3: - resolution: - { integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ== } + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} zustand@5.0.14: - resolution: - { integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g== } - engines: { node: ">=12.20.0" } + resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==} + engines: {node: '>=12.20.0'} peerDependencies: - "@types/react": ">=18.0.0" - immer: ">=9.0.6" - react: ">=18.0.0" - use-sync-external-store: ">=1.2.0" + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' peerDependenciesMeta: - "@types/react": + '@types/react': optional: true immer: optional: true @@ -4854,48 +4432,49 @@ packages: optional: true snapshots: - "@adobe/css-tools@4.5.0": {} - "@asamuzakjp/css-color@5.1.11": + '@adobe/css-tools@4.5.0': {} + + '@asamuzakjp/css-color@5.1.11': dependencies: - "@asamuzakjp/generational-cache": 1.0.1 - "@csstools/css-calc": 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - "@csstools/css-color-parser": 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - "@csstools/css-parser-algorithms": 4.0.0(@csstools/css-tokenizer@4.0.0) - "@csstools/css-tokenizer": 4.0.0 + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 - "@asamuzakjp/dom-selector@7.1.1": + '@asamuzakjp/dom-selector@7.1.1': dependencies: - "@asamuzakjp/generational-cache": 1.0.1 - "@asamuzakjp/nwsapi": 2.3.9 + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 bidi-js: 1.0.3 css-tree: 3.2.1 is-potential-custom-element-name: 1.0.1 - "@asamuzakjp/generational-cache@1.0.1": {} + '@asamuzakjp/generational-cache@1.0.1': {} - "@asamuzakjp/nwsapi@2.3.9": {} + '@asamuzakjp/nwsapi@2.3.9': {} - "@babel/code-frame@7.29.7": + '@babel/code-frame@7.29.7': dependencies: - "@babel/helper-validator-identifier": 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 - "@babel/compat-data@7.29.7": {} + '@babel/compat-data@7.29.7': {} - "@babel/core@7.29.7": + '@babel/core@7.29.7': dependencies: - "@babel/code-frame": 7.29.7 - "@babel/generator": 7.29.7 - "@babel/helper-compilation-targets": 7.29.7 - "@babel/helper-module-transforms": 7.29.7(@babel/core@7.29.7) - "@babel/helpers": 7.29.7 - "@babel/parser": 7.29.7 - "@babel/template": 7.29.7 - "@babel/traverse": 7.29.7 - "@babel/types": 7.29.7 - "@jridgewell/remapping": 2.3.5 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3(supports-color@10.2.2) gensync: 1.0.0-beta.2 @@ -4904,116 +4483,116 @@ snapshots: transitivePeerDependencies: - supports-color - "@babel/generator@7.29.7": + '@babel/generator@7.29.7': dependencies: - "@babel/parser": 7.29.7 - "@babel/types": 7.29.7 - "@jridgewell/gen-mapping": 0.3.13 - "@jridgewell/trace-mapping": 0.3.31 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - "@babel/helper-compilation-targets@7.29.7": + '@babel/helper-compilation-targets@7.29.7': dependencies: - "@babel/compat-data": 7.29.7 - "@babel/helper-validator-option": 7.29.7 + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 - "@babel/helper-globals@7.29.7": {} + '@babel/helper-globals@7.29.7': {} - "@babel/helper-module-imports@7.29.7": + '@babel/helper-module-imports@7.29.7': dependencies: - "@babel/traverse": 7.29.7 - "@babel/types": 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - "@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)": + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': dependencies: - "@babel/core": 7.29.7 - "@babel/helper-module-imports": 7.29.7 - "@babel/helper-validator-identifier": 7.29.7 - "@babel/traverse": 7.29.7 + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - "@babel/helper-string-parser@7.29.7": {} + '@babel/helper-string-parser@7.29.7': {} - "@babel/helper-validator-identifier@7.29.7": {} + '@babel/helper-validator-identifier@7.29.7': {} - "@babel/helper-validator-option@7.29.7": {} + '@babel/helper-validator-option@7.29.7': {} - "@babel/helpers@7.29.7": + '@babel/helpers@7.29.7': dependencies: - "@babel/template": 7.29.7 - "@babel/types": 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 - "@babel/parser@7.29.7": + '@babel/parser@7.29.7': dependencies: - "@babel/types": 7.29.7 + '@babel/types': 7.29.7 - "@babel/runtime@7.29.7": {} + '@babel/runtime@7.29.7': {} - "@babel/template@7.29.7": + '@babel/template@7.29.7': dependencies: - "@babel/code-frame": 7.29.7 - "@babel/parser": 7.29.7 - "@babel/types": 7.29.7 + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 - "@babel/traverse@7.29.7": + '@babel/traverse@7.29.7': dependencies: - "@babel/code-frame": 7.29.7 - "@babel/generator": 7.29.7 - "@babel/helper-globals": 7.29.7 - "@babel/parser": 7.29.7 - "@babel/template": 7.29.7 - "@babel/types": 7.29.7 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color - "@babel/types@7.29.7": + '@babel/types@7.29.7': dependencies: - "@babel/helper-string-parser": 7.29.7 - "@babel/helper-validator-identifier": 7.29.7 + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 - "@bramus/specificity@2.4.2": + '@bramus/specificity@2.4.2': dependencies: css-tree: 3.2.1 - "@csstools/color-helpers@6.0.2": {} + '@csstools/color-helpers@6.0.2': {} - "@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)": + '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: - "@csstools/css-parser-algorithms": 4.0.0(@csstools/css-tokenizer@4.0.0) - "@csstools/css-tokenizer": 4.0.0 + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 - "@csstools/css-color-parser@4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)": + '@csstools/css-color-parser@4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: - "@csstools/color-helpers": 6.0.2 - "@csstools/css-calc": 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - "@csstools/css-parser-algorithms": 4.0.0(@csstools/css-tokenizer@4.0.0) - "@csstools/css-tokenizer": 4.0.0 + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 - "@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)": + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': dependencies: - "@csstools/css-tokenizer": 4.0.0 + '@csstools/css-tokenizer': 4.0.0 - "@csstools/css-syntax-patches-for-csstree@1.1.5(css-tree@3.2.1)": + '@csstools/css-syntax-patches-for-csstree@1.1.5(css-tree@3.2.1)': optionalDependencies: css-tree: 3.2.1 - "@csstools/css-tokenizer@4.0.0": {} + '@csstools/css-tokenizer@4.0.0': {} - "@electron-forge/cli@7.11.2(encoding@0.1.13)": + '@electron-forge/cli@7.11.2(encoding@0.1.13)': dependencies: - "@electron-forge/core": 7.11.2(encoding@0.1.13) - "@electron-forge/core-utils": 7.11.2 - "@electron-forge/shared-types": 7.11.2 - "@electron/get": 3.1.0 - "@inquirer/prompts": 6.0.1 - "@listr2/prompt-adapter-inquirer": 2.0.22(@inquirer/prompts@6.0.1) + '@electron-forge/core': 7.11.2(encoding@0.1.13) + '@electron-forge/core-utils': 7.11.2 + '@electron-forge/shared-types': 7.11.2 + '@electron/get': 3.1.0 + '@inquirer/prompts': 6.0.1 + '@listr2/prompt-adapter-inquirer': 2.0.22(@inquirer/prompts@6.0.1) chalk: 4.1.2 commander: 11.1.0 debug: 4.4.3(supports-color@10.2.2) @@ -5022,10 +4601,10 @@ snapshots: log-symbols: 4.1.0 semver: 7.8.4 transitivePeerDependencies: - - "@minify-html/node" - - "@swc/core" - - "@swc/css" - - "@swc/html" + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' - bluebird - clean-css - cssnano @@ -5039,11 +4618,11 @@ snapshots: - uglify-js - webpack-cli - "@electron-forge/core-utils@7.11.2": + '@electron-forge/core-utils@7.11.2': dependencies: - "@electron-forge/shared-types": 7.11.2 - "@electron/rebuild": 3.7.2 - "@malept/cross-spawn-promise": 2.0.0 + '@electron-forge/shared-types': 7.11.2 + '@electron/rebuild': 3.7.2 + '@malept/cross-spawn-promise': 2.0.0 chalk: 4.1.2 debug: 4.4.3(supports-color@10.2.2) find-up: 5.0.0 @@ -5055,24 +4634,24 @@ snapshots: - bluebird - supports-color - "@electron-forge/core@7.11.2(encoding@0.1.13)": - dependencies: - "@electron-forge/core-utils": 7.11.2 - "@electron-forge/maker-base": 7.11.2 - "@electron-forge/plugin-base": 7.11.2 - "@electron-forge/publisher-base": 7.11.2 - "@electron-forge/shared-types": 7.11.2 - "@electron-forge/template-base": 7.11.2 - "@electron-forge/template-vite": 7.11.2 - "@electron-forge/template-vite-typescript": 7.11.2 - "@electron-forge/template-webpack": 7.11.2 - "@electron-forge/template-webpack-typescript": 7.11.2 - "@electron-forge/tracer": 7.11.2 - "@electron/get": 3.1.0 - "@electron/packager": 18.4.4 - "@electron/rebuild": 3.7.2 - "@malept/cross-spawn-promise": 2.0.0 - "@vscode/sudo-prompt": 9.3.2 + '@electron-forge/core@7.11.2(encoding@0.1.13)': + dependencies: + '@electron-forge/core-utils': 7.11.2 + '@electron-forge/maker-base': 7.11.2 + '@electron-forge/plugin-base': 7.11.2 + '@electron-forge/publisher-base': 7.11.2 + '@electron-forge/shared-types': 7.11.2 + '@electron-forge/template-base': 7.11.2 + '@electron-forge/template-vite': 7.11.2 + '@electron-forge/template-vite-typescript': 7.11.2 + '@electron-forge/template-webpack': 7.11.2 + '@electron-forge/template-webpack-typescript': 7.11.2 + '@electron-forge/tracer': 7.11.2 + '@electron/get': 3.1.0 + '@electron/packager': 18.4.4 + '@electron/rebuild': 3.7.2 + '@malept/cross-spawn-promise': 2.0.0 + '@vscode/sudo-prompt': 9.3.2 chalk: 4.1.2 debug: 4.4.3(supports-color@10.2.2) eta: 3.5.0 @@ -5092,10 +4671,10 @@ snapshots: source-map-support: 0.5.21 username: 5.1.0 transitivePeerDependencies: - - "@minify-html/node" - - "@swc/core" - - "@swc/css" - - "@swc/html" + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' - bluebird - clean-css - cssnano @@ -5109,50 +4688,39 @@ snapshots: - uglify-js - webpack-cli - "@electron-forge/maker-base@7.11.2": + '@electron-forge/maker-base@7.11.2': dependencies: - "@electron-forge/shared-types": 7.11.2 + '@electron-forge/shared-types': 7.11.2 fs-extra: 10.1.0 which: 2.0.2 transitivePeerDependencies: - bluebird - supports-color - "@electron-forge/maker-deb@7.11.2": + '@electron-forge/maker-deb@7.11.2': dependencies: - "@electron-forge/maker-base": 7.11.2 - "@electron-forge/shared-types": 7.11.2 + '@electron-forge/maker-base': 7.11.2 + '@electron-forge/shared-types': 7.11.2 optionalDependencies: electron-installer-debian: 3.2.0 transitivePeerDependencies: - bluebird - supports-color - "@electron-forge/maker-rpm@7.11.2": + '@electron-forge/maker-rpm@7.11.2': dependencies: - "@electron-forge/maker-base": 7.11.2 - "@electron-forge/shared-types": 7.11.2 + '@electron-forge/maker-base': 7.11.2 + '@electron-forge/shared-types': 7.11.2 optionalDependencies: electron-installer-redhat: 3.4.0 transitivePeerDependencies: - bluebird - supports-color - "@electron-forge/maker-squirrel@7.11.2": + '@electron-forge/maker-zip@7.11.2': dependencies: - "@electron-forge/maker-base": 7.11.2 - "@electron-forge/shared-types": 7.11.2 - fs-extra: 10.1.0 - optionalDependencies: - electron-winstaller: 5.4.0 - transitivePeerDependencies: - - bluebird - - supports-color - - "@electron-forge/maker-zip@7.11.2": - dependencies: - "@electron-forge/maker-base": 7.11.2 - "@electron-forge/shared-types": 7.11.2 + '@electron-forge/maker-base': 7.11.2 + '@electron-forge/shared-types': 7.11.2 cross-zip: 4.0.1 fs-extra: 10.1.0 got: 11.8.6 @@ -5160,17 +4728,17 @@ snapshots: - bluebird - supports-color - "@electron-forge/plugin-base@7.11.2": + '@electron-forge/plugin-base@7.11.2': dependencies: - "@electron-forge/shared-types": 7.11.2 + '@electron-forge/shared-types': 7.11.2 transitivePeerDependencies: - bluebird - supports-color - "@electron-forge/plugin-vite@7.11.2": + '@electron-forge/plugin-vite@7.11.2': dependencies: - "@electron-forge/plugin-base": 7.11.2 - "@electron-forge/shared-types": 7.11.2 + '@electron-forge/plugin-base': 7.11.2 + '@electron-forge/shared-types': 7.11.2 chalk: 4.1.2 debug: 4.4.3(supports-color@10.2.2) fs-extra: 10.1.0 @@ -5179,22 +4747,22 @@ snapshots: - bluebird - supports-color - "@electron-forge/publisher-base@7.11.2": + '@electron-forge/publisher-base@7.11.2': dependencies: - "@electron-forge/shared-types": 7.11.2 + '@electron-forge/shared-types': 7.11.2 transitivePeerDependencies: - bluebird - supports-color - "@electron-forge/publisher-github@7.11.2": + '@electron-forge/publisher-github@7.11.2': dependencies: - "@electron-forge/publisher-base": 7.11.2 - "@electron-forge/shared-types": 7.11.2 - "@octokit/core": 5.2.2 - "@octokit/plugin-retry": 6.1.0(@octokit/core@5.2.2) - "@octokit/request-error": 5.1.1 - "@octokit/rest": 20.1.2 - "@octokit/types": 6.41.0 + '@electron-forge/publisher-base': 7.11.2 + '@electron-forge/shared-types': 7.11.2 + '@octokit/core': 5.2.2 + '@octokit/plugin-retry': 6.1.0(@octokit/core@5.2.2) + '@octokit/request-error': 5.1.1 + '@octokit/rest': 20.1.2 + '@octokit/types': 6.41.0 chalk: 4.1.2 debug: 4.4.3(supports-color@10.2.2) fs-extra: 10.1.0 @@ -5204,21 +4772,21 @@ snapshots: - bluebird - supports-color - "@electron-forge/shared-types@7.11.2": + '@electron-forge/shared-types@7.11.2': dependencies: - "@electron-forge/tracer": 7.11.2 - "@electron/packager": 18.4.4 - "@electron/rebuild": 3.7.2 + '@electron-forge/tracer': 7.11.2 + '@electron/packager': 18.4.4 + '@electron/rebuild': 3.7.2 listr2: 7.0.2 transitivePeerDependencies: - bluebird - supports-color - "@electron-forge/template-base@7.11.2": + '@electron-forge/template-base@7.11.2': dependencies: - "@electron-forge/core-utils": 7.11.2 - "@electron-forge/shared-types": 7.11.2 - "@malept/cross-spawn-promise": 2.0.0 + '@electron-forge/core-utils': 7.11.2 + '@electron-forge/shared-types': 7.11.2 + '@malept/cross-spawn-promise': 2.0.0 debug: 4.4.3(supports-color@10.2.2) fs-extra: 10.1.0 semver: 7.8.4 @@ -5227,36 +4795,36 @@ snapshots: - bluebird - supports-color - "@electron-forge/template-vite-typescript@7.11.2": + '@electron-forge/template-vite-typescript@7.11.2': dependencies: - "@electron-forge/shared-types": 7.11.2 - "@electron-forge/template-base": 7.11.2 + '@electron-forge/shared-types': 7.11.2 + '@electron-forge/template-base': 7.11.2 fs-extra: 10.1.0 transitivePeerDependencies: - bluebird - supports-color - "@electron-forge/template-vite@7.11.2": + '@electron-forge/template-vite@7.11.2': dependencies: - "@electron-forge/shared-types": 7.11.2 - "@electron-forge/template-base": 7.11.2 + '@electron-forge/shared-types': 7.11.2 + '@electron-forge/template-base': 7.11.2 fs-extra: 10.1.0 transitivePeerDependencies: - bluebird - supports-color - "@electron-forge/template-webpack-typescript@7.11.2": + '@electron-forge/template-webpack-typescript@7.11.2': dependencies: - "@electron-forge/shared-types": 7.11.2 - "@electron-forge/template-base": 7.11.2 + '@electron-forge/shared-types': 7.11.2 + '@electron-forge/template-base': 7.11.2 fs-extra: 10.1.0 typescript: 5.4.5 webpack: 5.107.2 transitivePeerDependencies: - - "@minify-html/node" - - "@swc/core" - - "@swc/css" - - "@swc/html" + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' - bluebird - clean-css - cssnano @@ -5269,26 +4837,32 @@ snapshots: - uglify-js - webpack-cli - "@electron-forge/template-webpack@7.11.2": + '@electron-forge/template-webpack@7.11.2': dependencies: - "@electron-forge/shared-types": 7.11.2 - "@electron-forge/template-base": 7.11.2 + '@electron-forge/shared-types': 7.11.2 + '@electron-forge/template-base': 7.11.2 fs-extra: 10.1.0 transitivePeerDependencies: - bluebird - supports-color - "@electron-forge/tracer@7.11.2": + '@electron-forge/tracer@7.11.2': dependencies: chrome-trace-event: 1.0.4 - "@electron/asar@3.4.1": + '@electron/asar@3.4.1': dependencies: commander: 5.1.0 glob: 7.2.3 minimatch: 3.1.5 - "@electron/get@2.0.3": + '@electron/fuses@1.8.0': + dependencies: + chalk: 4.1.2 + fs-extra: 9.1.0 + minimist: 1.2.8 + + '@electron/get@2.0.3': dependencies: debug: 4.4.3(supports-color@10.2.2) env-paths: 2.2.1 @@ -5302,7 +4876,7 @@ snapshots: transitivePeerDependencies: - supports-color - "@electron/get@3.1.0": + '@electron/get@3.1.0': dependencies: debug: 4.4.3(supports-color@10.2.2) env-paths: 2.2.1 @@ -5316,7 +4890,7 @@ snapshots: transitivePeerDependencies: - supports-color - "@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2": + '@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2': dependencies: env-paths: 2.2.1 exponential-backoff: 3.1.3 @@ -5332,7 +4906,7 @@ snapshots: - bluebird - supports-color - "@electron/notarize@2.5.0": + '@electron/notarize@2.5.0': dependencies: debug: 4.4.3(supports-color@10.2.2) fs-extra: 9.1.0 @@ -5340,7 +4914,7 @@ snapshots: transitivePeerDependencies: - supports-color - "@electron/osx-sign@1.3.3": + '@electron/osx-sign@1.3.3': dependencies: compare-version: 0.1.2 debug: 4.4.3(supports-color@10.2.2) @@ -5351,15 +4925,15 @@ snapshots: transitivePeerDependencies: - supports-color - "@electron/packager@18.4.4": + '@electron/packager@18.4.4': dependencies: - "@electron/asar": 3.4.1 - "@electron/get": 3.1.0 - "@electron/notarize": 2.5.0 - "@electron/osx-sign": 1.3.3 - "@electron/universal": 2.0.3 - "@electron/windows-sign": 1.2.2 - "@malept/cross-spawn-promise": 2.0.0 + '@electron/asar': 3.4.1 + '@electron/get': 3.1.0 + '@electron/notarize': 2.5.0 + '@electron/osx-sign': 1.3.3 + '@electron/universal': 2.0.3 + '@electron/windows-sign': 1.2.2 + '@malept/cross-spawn-promise': 2.0.0 debug: 4.4.3(supports-color@10.2.2) extract-zip: 2.0.1 filenamify: 4.3.0 @@ -5377,10 +4951,10 @@ snapshots: transitivePeerDependencies: - supports-color - "@electron/rebuild@3.7.2": + '@electron/rebuild@3.7.2': dependencies: - "@electron/node-gyp": https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2 - "@malept/cross-spawn-promise": 2.0.0 + '@electron/node-gyp': https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2 + '@malept/cross-spawn-promise': 2.0.0 chalk: 4.1.2 debug: 4.4.3(supports-color@10.2.2) detect-libc: 2.1.2 @@ -5397,10 +4971,21 @@ snapshots: - bluebird - supports-color - "@electron/universal@2.0.3": + '@electron/rebuild@4.0.4': + dependencies: + '@malept/cross-spawn-promise': 2.0.0 + debug: 4.4.3(supports-color@10.2.2) + node-abi: 4.31.0 + node-api-version: 0.2.1 + node-gyp: 12.4.0 + read-binary-file-arch: 1.0.6 + transitivePeerDependencies: + - supports-color + + '@electron/universal@2.0.3': dependencies: - "@electron/asar": 3.4.1 - "@malept/cross-spawn-promise": 2.0.0 + '@electron/asar': 3.4.1 + '@malept/cross-spawn-promise': 2.0.0 debug: 4.4.3(supports-color@10.2.2) dir-compare: 4.2.0 fs-extra: 11.3.5 @@ -5409,7 +4994,7 @@ snapshots: transitivePeerDependencies: - supports-color - "@electron/windows-sign@1.2.2": + '@electron/windows-sign@1.2.2': dependencies: cross-dirname: 0.1.0 debug: 4.4.3(supports-color@10.2.2) @@ -5419,63 +5004,65 @@ snapshots: transitivePeerDependencies: - supports-color - "@emnapi/core@1.10.0": + '@emnapi/core@1.10.0': dependencies: - "@emnapi/wasi-threads": 1.2.1 + '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true - "@emnapi/runtime@1.10.0": + '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 optional: true - "@emnapi/wasi-threads@1.2.1": + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 optional: true - "@exodus/bytes@1.15.1": {} + '@exodus/bytes@1.15.1(@noble/hashes@2.2.0)': + optionalDependencies: + '@noble/hashes': 2.2.0 - "@floating-ui/core@1.7.5": + '@floating-ui/core@1.7.5': dependencies: - "@floating-ui/utils": 0.2.11 + '@floating-ui/utils': 0.2.11 - "@floating-ui/dom@1.7.6": + '@floating-ui/dom@1.7.6': dependencies: - "@floating-ui/core": 1.7.5 - "@floating-ui/utils": 0.2.11 + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 - "@floating-ui/react-dom@2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@floating-ui/react-dom@2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@floating-ui/dom": 1.7.6 + '@floating-ui/dom': 1.7.6 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - "@floating-ui/utils@0.2.11": {} + '@floating-ui/utils@0.2.11': {} - "@gar/promisify@1.1.3": {} + '@gar/promisify@1.1.3': {} - "@inquirer/checkbox@3.0.1": + '@inquirer/checkbox@3.0.1': dependencies: - "@inquirer/core": 9.2.1 - "@inquirer/figures": 1.0.15 - "@inquirer/type": 2.0.0 + '@inquirer/core': 9.2.1 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 2.0.0 ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.3 - "@inquirer/confirm@4.0.1": + '@inquirer/confirm@4.0.1': dependencies: - "@inquirer/core": 9.2.1 - "@inquirer/type": 2.0.0 + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 - "@inquirer/core@9.2.1": + '@inquirer/core@9.2.1': dependencies: - "@inquirer/figures": 1.0.15 - "@inquirer/type": 2.0.0 - "@types/mute-stream": 0.0.4 - "@types/node": 22.19.20 - "@types/wrap-ansi": 3.0.0 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 2.0.0 + '@types/mute-stream': 0.0.4 + '@types/node': 22.19.20 + '@types/wrap-ansi': 3.0.0 ansi-escapes: 4.3.2 cli-width: 4.1.0 mute-stream: 1.0.0 @@ -5484,985 +5071,1030 @@ snapshots: wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.3 - "@inquirer/editor@3.0.1": + '@inquirer/editor@3.0.1': dependencies: - "@inquirer/core": 9.2.1 - "@inquirer/type": 2.0.0 + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 external-editor: 3.1.0 - "@inquirer/expand@3.0.1": + '@inquirer/expand@3.0.1': dependencies: - "@inquirer/core": 9.2.1 - "@inquirer/type": 2.0.0 + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 yoctocolors-cjs: 2.1.3 - "@inquirer/figures@1.0.15": {} + '@inquirer/figures@1.0.15': {} - "@inquirer/input@3.0.1": + '@inquirer/input@3.0.1': dependencies: - "@inquirer/core": 9.2.1 - "@inquirer/type": 2.0.0 + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 - "@inquirer/number@2.0.1": + '@inquirer/number@2.0.1': dependencies: - "@inquirer/core": 9.2.1 - "@inquirer/type": 2.0.0 + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 - "@inquirer/password@3.0.1": + '@inquirer/password@3.0.1': dependencies: - "@inquirer/core": 9.2.1 - "@inquirer/type": 2.0.0 + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 ansi-escapes: 4.3.2 - "@inquirer/prompts@6.0.1": + '@inquirer/prompts@6.0.1': dependencies: - "@inquirer/checkbox": 3.0.1 - "@inquirer/confirm": 4.0.1 - "@inquirer/editor": 3.0.1 - "@inquirer/expand": 3.0.1 - "@inquirer/input": 3.0.1 - "@inquirer/number": 2.0.1 - "@inquirer/password": 3.0.1 - "@inquirer/rawlist": 3.0.1 - "@inquirer/search": 2.0.1 - "@inquirer/select": 3.0.1 + '@inquirer/checkbox': 3.0.1 + '@inquirer/confirm': 4.0.1 + '@inquirer/editor': 3.0.1 + '@inquirer/expand': 3.0.1 + '@inquirer/input': 3.0.1 + '@inquirer/number': 2.0.1 + '@inquirer/password': 3.0.1 + '@inquirer/rawlist': 3.0.1 + '@inquirer/search': 2.0.1 + '@inquirer/select': 3.0.1 - "@inquirer/rawlist@3.0.1": + '@inquirer/rawlist@3.0.1': dependencies: - "@inquirer/core": 9.2.1 - "@inquirer/type": 2.0.0 + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 yoctocolors-cjs: 2.1.3 - "@inquirer/search@2.0.1": + '@inquirer/search@2.0.1': dependencies: - "@inquirer/core": 9.2.1 - "@inquirer/figures": 1.0.15 - "@inquirer/type": 2.0.0 + '@inquirer/core': 9.2.1 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 2.0.0 yoctocolors-cjs: 2.1.3 - "@inquirer/select@3.0.1": + '@inquirer/select@3.0.1': dependencies: - "@inquirer/core": 9.2.1 - "@inquirer/figures": 1.0.15 - "@inquirer/type": 2.0.0 + '@inquirer/core': 9.2.1 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 2.0.0 ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.3 - "@inquirer/type@1.5.5": + '@inquirer/type@1.5.5': dependencies: mute-stream: 1.0.0 - "@inquirer/type@2.0.0": + '@inquirer/type@2.0.0': dependencies: mute-stream: 1.0.0 - "@jridgewell/gen-mapping@0.3.13": + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + + '@jridgewell/gen-mapping@0.3.13': dependencies: - "@jridgewell/sourcemap-codec": 1.5.5 - "@jridgewell/trace-mapping": 0.3.31 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 - "@jridgewell/remapping@2.3.5": + '@jridgewell/remapping@2.3.5': dependencies: - "@jridgewell/gen-mapping": 0.3.13 - "@jridgewell/trace-mapping": 0.3.31 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 - "@jridgewell/resolve-uri@3.1.2": {} + '@jridgewell/resolve-uri@3.1.2': {} - "@jridgewell/source-map@0.3.11": + '@jridgewell/source-map@0.3.11': dependencies: - "@jridgewell/gen-mapping": 0.3.13 - "@jridgewell/trace-mapping": 0.3.31 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 - "@jridgewell/sourcemap-codec@1.5.5": {} + '@jridgewell/sourcemap-codec@1.5.5': {} - "@jridgewell/trace-mapping@0.3.31": + '@jridgewell/trace-mapping@0.3.31': dependencies: - "@jridgewell/resolve-uri": 3.1.2 - "@jridgewell/sourcemap-codec": 1.5.5 + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 - "@listr2/prompt-adapter-inquirer@2.0.22(@inquirer/prompts@6.0.1)": + '@listr2/prompt-adapter-inquirer@2.0.22(@inquirer/prompts@6.0.1)': dependencies: - "@inquirer/prompts": 6.0.1 - "@inquirer/type": 1.5.5 + '@inquirer/prompts': 6.0.1 + '@inquirer/type': 1.5.5 - "@malept/cross-spawn-promise@1.1.1": + '@malept/cross-spawn-promise@1.1.1': dependencies: cross-spawn: 7.0.6 optional: true - "@malept/cross-spawn-promise@2.0.0": + '@malept/cross-spawn-promise@2.0.0': dependencies: cross-spawn: 7.0.6 - "@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)": + '@malept/flatpak-bundler@0.4.0': + dependencies: + debug: 4.4.3(supports-color@10.2.2) + fs-extra: 9.1.0 + lodash: 4.18.1 + tmp-promise: 3.0.3 + transitivePeerDependencies: + - supports-color + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - "@emnapi/core": 1.10.0 - "@emnapi/runtime": 1.10.0 - "@tybys/wasm-util": 0.10.2 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 optional: true - "@nodelib/fs.scandir@2.1.5": + '@noble/hashes@1.4.0': {} + + '@noble/hashes@2.2.0': {} + + '@nodelib/fs.scandir@2.1.5': dependencies: - "@nodelib/fs.stat": 2.0.5 + '@nodelib/fs.stat': 2.0.5 run-parallel: 1.2.0 - "@nodelib/fs.stat@2.0.5": {} + '@nodelib/fs.stat@2.0.5': {} - "@nodelib/fs.walk@1.2.8": + '@nodelib/fs.walk@1.2.8': dependencies: - "@nodelib/fs.scandir": 2.1.5 + '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - "@npmcli/fs@2.1.2": + '@npmcli/fs@2.1.2': dependencies: - "@gar/promisify": 1.1.3 + '@gar/promisify': 1.1.3 semver: 7.8.4 - "@npmcli/move-file@2.0.1": + '@npmcli/move-file@2.0.1': dependencies: mkdirp: 1.0.4 rimraf: 3.0.2 - "@octokit/auth-token@4.0.0": {} + '@octokit/auth-token@4.0.0': {} - "@octokit/core@5.2.2": + '@octokit/core@5.2.2': dependencies: - "@octokit/auth-token": 4.0.0 - "@octokit/graphql": 7.1.1 - "@octokit/request": 8.4.1 - "@octokit/request-error": 5.1.1 - "@octokit/types": 13.10.0 + '@octokit/auth-token': 4.0.0 + '@octokit/graphql': 7.1.1 + '@octokit/request': 8.4.1 + '@octokit/request-error': 5.1.1 + '@octokit/types': 13.10.0 before-after-hook: 2.2.3 universal-user-agent: 6.0.1 - "@octokit/endpoint@9.0.6": + '@octokit/endpoint@9.0.6': dependencies: - "@octokit/types": 13.10.0 + '@octokit/types': 13.10.0 universal-user-agent: 6.0.1 - "@octokit/graphql@7.1.1": + '@octokit/graphql@7.1.1': dependencies: - "@octokit/request": 8.4.1 - "@octokit/types": 13.10.0 + '@octokit/request': 8.4.1 + '@octokit/types': 13.10.0 universal-user-agent: 6.0.1 - "@octokit/openapi-types@12.11.0": {} + '@octokit/openapi-types@12.11.0': {} - "@octokit/openapi-types@24.2.0": {} + '@octokit/openapi-types@24.2.0': {} - "@octokit/plugin-paginate-rest@11.4.4-cjs.2(@octokit/core@5.2.2)": + '@octokit/plugin-paginate-rest@11.4.4-cjs.2(@octokit/core@5.2.2)': dependencies: - "@octokit/core": 5.2.2 - "@octokit/types": 13.10.0 + '@octokit/core': 5.2.2 + '@octokit/types': 13.10.0 - "@octokit/plugin-request-log@4.0.1(@octokit/core@5.2.2)": + '@octokit/plugin-request-log@4.0.1(@octokit/core@5.2.2)': dependencies: - "@octokit/core": 5.2.2 + '@octokit/core': 5.2.2 - "@octokit/plugin-rest-endpoint-methods@13.3.2-cjs.1(@octokit/core@5.2.2)": + '@octokit/plugin-rest-endpoint-methods@13.3.2-cjs.1(@octokit/core@5.2.2)': dependencies: - "@octokit/core": 5.2.2 - "@octokit/types": 13.10.0 + '@octokit/core': 5.2.2 + '@octokit/types': 13.10.0 - "@octokit/plugin-retry@6.1.0(@octokit/core@5.2.2)": + '@octokit/plugin-retry@6.1.0(@octokit/core@5.2.2)': dependencies: - "@octokit/core": 5.2.2 - "@octokit/request-error": 5.1.1 - "@octokit/types": 13.10.0 + '@octokit/core': 5.2.2 + '@octokit/request-error': 5.1.1 + '@octokit/types': 13.10.0 bottleneck: 2.19.5 - "@octokit/request-error@5.1.1": + '@octokit/request-error@5.1.1': dependencies: - "@octokit/types": 13.10.0 + '@octokit/types': 13.10.0 deprecation: 2.3.1 once: 1.4.0 - "@octokit/request@8.4.1": + '@octokit/request@8.4.1': dependencies: - "@octokit/endpoint": 9.0.6 - "@octokit/request-error": 5.1.1 - "@octokit/types": 13.10.0 + '@octokit/endpoint': 9.0.6 + '@octokit/request-error': 5.1.1 + '@octokit/types': 13.10.0 universal-user-agent: 6.0.1 - "@octokit/rest@20.1.2": + '@octokit/rest@20.1.2': dependencies: - "@octokit/core": 5.2.2 - "@octokit/plugin-paginate-rest": 11.4.4-cjs.2(@octokit/core@5.2.2) - "@octokit/plugin-request-log": 4.0.1(@octokit/core@5.2.2) - "@octokit/plugin-rest-endpoint-methods": 13.3.2-cjs.1(@octokit/core@5.2.2) + '@octokit/core': 5.2.2 + '@octokit/plugin-paginate-rest': 11.4.4-cjs.2(@octokit/core@5.2.2) + '@octokit/plugin-request-log': 4.0.1(@octokit/core@5.2.2) + '@octokit/plugin-rest-endpoint-methods': 13.3.2-cjs.1(@octokit/core@5.2.2) - "@octokit/types@13.10.0": + '@octokit/types@13.10.0': dependencies: - "@octokit/openapi-types": 24.2.0 + '@octokit/openapi-types': 24.2.0 - "@octokit/types@6.41.0": + '@octokit/types@6.41.0': dependencies: - "@octokit/openapi-types": 12.11.0 + '@octokit/openapi-types': 12.11.0 - "@oxc-project/types@0.133.0": {} + '@oxc-project/types@0.133.0': {} + + '@peculiar/asn1-schema@2.8.0': + dependencies: + '@peculiar/utils': 2.0.3 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/json-schema@1.1.12': + dependencies: + tslib: 2.8.1 + + '@peculiar/utils@2.0.3': + dependencies: + tslib: 2.8.1 - "@playwright/test@1.60.0": + '@peculiar/webcrypto@1.7.1': + dependencies: + '@peculiar/asn1-schema': 2.8.0 + '@peculiar/json-schema': 1.1.12 + '@peculiar/utils': 2.0.3 + tslib: 2.8.1 + webcrypto-core: 1.9.2 + + '@playwright/test@1.60.0': dependencies: playwright: 1.60.0 - "@radix-ui/number@1.1.2": {} + '@posthog/core@1.37.1': + dependencies: + '@posthog/types': 1.391.0 + + '@posthog/types@1.391.0': {} + + '@radix-ui/number@1.1.2': {} - "@radix-ui/primitive@1.1.4": {} + '@radix-ui/primitive@1.1.4': {} - "@radix-ui/react-accessible-icon@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@radix-ui/react-accessible-icon@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@radix-ui/react-visually-hidden": 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-accordion@1.2.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-collapsible": 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-collection": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-accordion@1.2.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collapsible': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/react-alert-dialog@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@radix-ui/react-alert-dialog@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-dialog": 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-slot": 1.2.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dialog': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.5(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/react-arrow@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@radix-ui/react-arrow@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/react-aspect-ratio@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@radix-ui/react-aspect-ratio@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/react-avatar@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@radix-ui/react-avatar@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-callback-ref": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-is-hydrated": 0.1.1(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-checkbox@1.3.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-previous": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-size": 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-checkbox@1.3.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-collapsible@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-collapsible@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/react-collection@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@radix-ui/react-collection@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-slot": 1.2.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.5(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/react-compose-refs@1.1.3(@types/react@19.2.17)(react@19.2.7)": + '@radix-ui/react-compose-refs@1.1.3(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 - "@radix-ui/react-context-menu@2.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@radix-ui/react-context-menu@2.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-menu": 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-menu': 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/react-context@1.1.4(@types/react@19.2.17)(react@19.2.7)": + '@radix-ui/react-context@1.1.4(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 optionalDependencies: - "@types/react": 19.2.17 - - "@radix-ui/react-dialog@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-dismissable-layer": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-focus-guards": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-focus-scope": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-portal": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-slot": 1.2.5(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + + '@radix-ui/react-dialog@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-portal': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) aria-hidden: 1.2.6 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/react-direction@1.1.2(@types/react@19.2.17)(react@19.2.7)": + '@radix-ui/react-direction@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 - "@radix-ui/react-dismissable-layer@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@radix-ui/react-dismissable-layer@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-callback-ref": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-escape-keydown": 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-escape-keydown': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-dropdown-menu@2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-menu": 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-dropdown-menu@2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-menu': 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/react-focus-guards@1.1.4(@types/react@19.2.17)(react@19.2.7)": + '@radix-ui/react-focus-guards@1.1.4(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 - "@radix-ui/react-focus-scope@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@radix-ui/react-focus-scope@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-callback-ref": 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/react-form@0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@radix-ui/react-form@0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-label": 2.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-label': 2.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-hover-card@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-dismissable-layer": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-popper": 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-portal": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-hover-card@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-popper': 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/react-id@1.1.2(@types/react@19.2.17)(react@19.2.7)": + '@radix-ui/react-id@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: - "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 - "@radix-ui/react-label@2.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@radix-ui/react-label@2.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-menu@2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-collection": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-dismissable-layer": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-focus-guards": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-focus-scope": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-popper": 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-portal": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-roving-focus": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-slot": 1.2.5(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-callback-ref": 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-menu@2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) aria-hidden: 1.2.6 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-menubar@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-collection": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-menu": 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-roving-focus": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-menubar@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-menu': 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-navigation-menu@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-collection": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-dismissable-layer": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-callback-ref": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-previous": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-visually-hidden": 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-navigation-menu@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-one-time-password-field@0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/number": 1.1.2 - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-collection": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-roving-focus": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-effect-event": 0.0.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-is-hydrated": 0.1.1(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-one-time-password-field@0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-password-toggle-field@0.1.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-effect-event": 0.0.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-is-hydrated": 0.1.1(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-password-toggle-field@0.1.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-popover@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-dismissable-layer": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-focus-guards": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-focus-scope": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-popper": 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-portal": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-slot": 1.2.5(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-popover@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) aria-hidden: 1.2.6 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-popper@1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@floating-ui/react-dom": 2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-arrow": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-callback-ref": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-rect": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-size": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/rect": 1.1.2 + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-popper@1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-arrow': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-rect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/rect': 1.1.2 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/react-portal@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@radix-ui/react-portal@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/react-presence@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@radix-ui/react-presence@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/react-primitive@2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@radix-ui/react-primitive@2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@radix-ui/react-slot": 1.2.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-slot': 1.2.5(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/react-progress@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@radix-ui/react-progress@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-radio-group@1.4.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-roving-focus": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-previous": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-size": 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-radio-group@1.4.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-roving-focus@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-collection": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-callback-ref": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-roving-focus@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-scroll-area@1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/number": 1.1.2 - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-callback-ref": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-scroll-area@1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-select@2.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/number": 1.1.2 - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-collection": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-dismissable-layer": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-focus-guards": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-focus-scope": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-popper": 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-portal": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-slot": 1.2.5(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-callback-ref": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-previous": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-visually-hidden": 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-select@2.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) aria-hidden: 1.2.6 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/react-separator@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@radix-ui/react-separator@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-slider@1.4.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/number": 1.1.2 - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-collection": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-previous": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-size": 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-slider@1.4.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/react-slot@1.2.5(@types/react@19.2.17)(react@19.2.7)": + '@radix-ui/react-slot@1.2.5(@types/react@19.2.17)(react@19.2.7)': dependencies: - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 - "@radix-ui/react-switch@1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@radix-ui/react-switch@1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-previous": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-size": 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-tabs@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-roving-focus": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-tabs@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-toast@1.2.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-collection": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-dismissable-layer": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-portal": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-callback-ref": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-visually-hidden": 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-toast@1.2.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-toggle-group@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-roving-focus": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-toggle": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-toggle-group@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toggle': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/react-toggle@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@radix-ui/react-toggle@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-toolbar@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-roving-focus": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-separator": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-toggle-group": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-toolbar@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-separator': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toggle-group': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) - - "@radix-ui/react-tooltip@1.2.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": - dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-dismissable-layer": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-id": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-popper": 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-portal": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-slot": 1.2.5(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-visually-hidden": 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-tooltip@1.2.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/react-use-callback-ref@1.1.2(@types/react@19.2.17)(react@19.2.7)": + '@radix-ui/react-use-callback-ref@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 - "@radix-ui/react-use-controllable-state@1.2.3(@types/react@19.2.17)(react@19.2.7)": + '@radix-ui/react-use-controllable-state@1.2.3(@types/react@19.2.17)(react@19.2.7)': dependencies: - "@radix-ui/react-use-effect-event": 0.0.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 - "@radix-ui/react-use-effect-event@0.0.3(@types/react@19.2.17)(react@19.2.7)": + '@radix-ui/react-use-effect-event@0.0.3(@types/react@19.2.17)(react@19.2.7)': dependencies: - "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 - "@radix-ui/react-use-escape-keydown@1.1.2(@types/react@19.2.17)(react@19.2.7)": + '@radix-ui/react-use-escape-keydown@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: - "@radix-ui/react-use-callback-ref": 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 - "@radix-ui/react-use-is-hydrated@0.1.1(@types/react@19.2.17)(react@19.2.7)": + '@radix-ui/react-use-is-hydrated@0.1.1(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 - "@radix-ui/react-use-layout-effect@1.1.2(@types/react@19.2.17)(react@19.2.7)": + '@radix-ui/react-use-layout-effect@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 - "@radix-ui/react-use-previous@1.1.2(@types/react@19.2.17)(react@19.2.7)": + '@radix-ui/react-use-previous@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: react: 19.2.7 optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 - "@radix-ui/react-use-rect@1.1.2(@types/react@19.2.17)(react@19.2.7)": + '@radix-ui/react-use-rect@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: - "@radix-ui/rect": 1.1.2 + '@radix-ui/rect': 1.1.2 react: 19.2.7 optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 - "@radix-ui/react-use-size@1.1.2(@types/react@19.2.17)(react@19.2.7)": + '@radix-ui/react-use-size@1.1.2(@types/react@19.2.17)(react@19.2.7)': dependencies: - "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) react: 19.2.7 optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 - "@radix-ui/react-visually-hidden@1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@radix-ui/react-visually-hidden@1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@radix-ui/rect@1.1.2": {} + '@radix-ui/rect@1.1.2': {} - "@redocly/ajv@8.11.2": + '@redocly/ajv@8.11.2': dependencies: fast-deep-equal: 3.1.3 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 uri-js-replace: 1.0.1 - "@redocly/config@0.22.0": {} + '@redocly/config@0.22.0': {} - "@redocly/openapi-core@1.34.15(supports-color@10.2.2)": + '@redocly/openapi-core@1.34.15(supports-color@10.2.2)': dependencies: - "@redocly/ajv": 8.11.2 - "@redocly/config": 0.22.0 + '@redocly/ajv': 8.11.2 + '@redocly/config': 0.22.0 colorette: 1.4.0 https-proxy-agent: 7.0.6(supports-color@10.2.2) js-levenshtein: 1.1.6 @@ -6473,68 +6105,68 @@ snapshots: transitivePeerDependencies: - supports-color - "@rolldown/binding-android-arm64@1.0.3": + '@rolldown/binding-android-arm64@1.0.3': optional: true - "@rolldown/binding-darwin-arm64@1.0.3": + '@rolldown/binding-darwin-arm64@1.0.3': optional: true - "@rolldown/binding-darwin-x64@1.0.3": + '@rolldown/binding-darwin-x64@1.0.3': optional: true - "@rolldown/binding-freebsd-x64@1.0.3": + '@rolldown/binding-freebsd-x64@1.0.3': optional: true - "@rolldown/binding-linux-arm-gnueabihf@1.0.3": + '@rolldown/binding-linux-arm-gnueabihf@1.0.3': optional: true - "@rolldown/binding-linux-arm64-gnu@1.0.3": + '@rolldown/binding-linux-arm64-gnu@1.0.3': optional: true - "@rolldown/binding-linux-arm64-musl@1.0.3": + '@rolldown/binding-linux-arm64-musl@1.0.3': optional: true - "@rolldown/binding-linux-ppc64-gnu@1.0.3": + '@rolldown/binding-linux-ppc64-gnu@1.0.3': optional: true - "@rolldown/binding-linux-s390x-gnu@1.0.3": + '@rolldown/binding-linux-s390x-gnu@1.0.3': optional: true - "@rolldown/binding-linux-x64-gnu@1.0.3": + '@rolldown/binding-linux-x64-gnu@1.0.3': optional: true - "@rolldown/binding-linux-x64-musl@1.0.3": + '@rolldown/binding-linux-x64-musl@1.0.3': optional: true - "@rolldown/binding-openharmony-arm64@1.0.3": + '@rolldown/binding-openharmony-arm64@1.0.3': optional: true - "@rolldown/binding-wasm32-wasi@1.0.3": + '@rolldown/binding-wasm32-wasi@1.0.3': dependencies: - "@emnapi/core": 1.10.0 - "@emnapi/runtime": 1.10.0 - "@napi-rs/wasm-runtime": 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - "@rolldown/binding-win32-arm64-msvc@1.0.3": + '@rolldown/binding-win32-arm64-msvc@1.0.3': optional: true - "@rolldown/binding-win32-x64-msvc@1.0.3": + '@rolldown/binding-win32-x64-msvc@1.0.3': optional: true - "@rolldown/pluginutils@1.0.1": {} + '@rolldown/pluginutils@1.0.1': {} - "@sindresorhus/is@4.6.0": {} + '@sindresorhus/is@4.6.0': {} - "@standard-schema/spec@1.1.0": {} + '@standard-schema/spec@1.1.0': {} - "@szmarczak/http-timer@4.0.6": + '@szmarczak/http-timer@4.0.6': dependencies: defer-to-connect: 2.0.1 - "@tailwindcss/node@4.3.0": + '@tailwindcss/node@4.3.0': dependencies: - "@jridgewell/remapping": 2.3.5 + '@jridgewell/remapping': 2.3.5 enhanced-resolve: 5.23.0 jiti: 2.7.0 lightningcss: 1.32.0 @@ -6542,102 +6174,102 @@ snapshots: source-map-js: 1.2.1 tailwindcss: 4.3.0 - "@tailwindcss/oxide-android-arm64@4.3.0": + '@tailwindcss/oxide-android-arm64@4.3.0': optional: true - "@tailwindcss/oxide-darwin-arm64@4.3.0": + '@tailwindcss/oxide-darwin-arm64@4.3.0': optional: true - "@tailwindcss/oxide-darwin-x64@4.3.0": + '@tailwindcss/oxide-darwin-x64@4.3.0': optional: true - "@tailwindcss/oxide-freebsd-x64@4.3.0": + '@tailwindcss/oxide-freebsd-x64@4.3.0': optional: true - "@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0": + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': optional: true - "@tailwindcss/oxide-linux-arm64-gnu@4.3.0": + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': optional: true - "@tailwindcss/oxide-linux-arm64-musl@4.3.0": + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': optional: true - "@tailwindcss/oxide-linux-x64-gnu@4.3.0": + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': optional: true - "@tailwindcss/oxide-linux-x64-musl@4.3.0": + '@tailwindcss/oxide-linux-x64-musl@4.3.0': optional: true - "@tailwindcss/oxide-wasm32-wasi@4.3.0": + '@tailwindcss/oxide-wasm32-wasi@4.3.0': optional: true - "@tailwindcss/oxide-win32-arm64-msvc@4.3.0": + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': optional: true - "@tailwindcss/oxide-win32-x64-msvc@4.3.0": + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': optional: true - "@tailwindcss/oxide@4.3.0": + '@tailwindcss/oxide@4.3.0': optionalDependencies: - "@tailwindcss/oxide-android-arm64": 4.3.0 - "@tailwindcss/oxide-darwin-arm64": 4.3.0 - "@tailwindcss/oxide-darwin-x64": 4.3.0 - "@tailwindcss/oxide-freebsd-x64": 4.3.0 - "@tailwindcss/oxide-linux-arm-gnueabihf": 4.3.0 - "@tailwindcss/oxide-linux-arm64-gnu": 4.3.0 - "@tailwindcss/oxide-linux-arm64-musl": 4.3.0 - "@tailwindcss/oxide-linux-x64-gnu": 4.3.0 - "@tailwindcss/oxide-linux-x64-musl": 4.3.0 - "@tailwindcss/oxide-wasm32-wasi": 4.3.0 - "@tailwindcss/oxide-win32-arm64-msvc": 4.3.0 - "@tailwindcss/oxide-win32-x64-msvc": 4.3.0 - - "@tailwindcss/vite@4.3.0(vite@8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0))": - dependencies: - "@tailwindcss/node": 4.3.0 - "@tailwindcss/oxide": 4.3.0 + '@tailwindcss/oxide-android-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-x64': 4.3.0 + '@tailwindcss/oxide-freebsd-x64': 4.3.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-x64-musl': 4.3.0 + '@tailwindcss/oxide-wasm32-wasi': 4.3.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + + '@tailwindcss/vite@4.3.0(vite@8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0))': + dependencies: + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 tailwindcss: 4.3.0 vite: 8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0) - "@tanstack/history@1.162.0": {} + '@tanstack/history@1.162.0': {} - "@tanstack/query-core@5.101.0": {} + '@tanstack/query-core@5.101.0': {} - "@tanstack/react-query@5.101.0(react@19.2.7)": + '@tanstack/react-query@5.101.0(react@19.2.7)': dependencies: - "@tanstack/query-core": 5.101.0 + '@tanstack/query-core': 5.101.0 react: 19.2.7 - "@tanstack/react-router@1.170.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@tanstack/react-router@1.170.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@tanstack/history": 1.162.0 - "@tanstack/react-store": 0.9.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@tanstack/router-core": 1.171.13 + '@tanstack/history': 1.162.0 + '@tanstack/react-store': 0.9.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tanstack/router-core': 1.171.13 isbot: 5.1.42 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - "@tanstack/react-store@0.9.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@tanstack/react-store@0.9.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@tanstack/store": 0.9.3 + '@tanstack/store': 0.9.3 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) use-sync-external-store: 1.6.0(react@19.2.7) - "@tanstack/router-core@1.171.13": + '@tanstack/router-core@1.171.13': dependencies: - "@tanstack/history": 1.162.0 + '@tanstack/history': 1.162.0 cookie-es: 3.1.1 seroval: 1.5.4 seroval-plugins: 1.5.4(seroval@1.5.4) - "@tanstack/router-generator@1.167.17": + '@tanstack/router-generator@1.167.17': dependencies: - "@babel/types": 7.29.7 - "@tanstack/router-core": 1.171.13 - "@tanstack/router-utils": 1.162.2 - "@tanstack/virtual-file-routes": 1.162.0 + '@babel/types': 7.29.7 + '@tanstack/router-core': 1.171.13 + '@tanstack/router-utils': 1.162.2 + '@tanstack/virtual-file-routes': 1.162.0 jiti: 2.7.0 magic-string: 0.30.21 prettier: 3.8.4 @@ -6645,29 +6277,29 @@ snapshots: transitivePeerDependencies: - supports-color - "@tanstack/router-plugin@1.168.18(@tanstack/react-router@1.170.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0))(webpack@5.107.2)": + '@tanstack/router-plugin@1.168.18(@tanstack/react-router@1.170.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(vite@8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0))(webpack@5.107.2)': dependencies: - "@babel/core": 7.29.7 - "@babel/template": 7.29.7 - "@babel/types": 7.29.7 - "@tanstack/router-core": 1.171.13 - "@tanstack/router-generator": 1.167.17 - "@tanstack/router-utils": 1.162.2 + '@babel/core': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + '@tanstack/router-core': 1.171.13 + '@tanstack/router-generator': 1.167.17 + '@tanstack/router-utils': 1.162.2 chokidar: 5.0.0 unplugin: 3.0.0 zod: 4.4.3 optionalDependencies: - "@tanstack/react-router": 1.170.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tanstack/react-router': 1.170.15(react-dom@19.2.7(react@19.2.7))(react@19.2.7) vite: 8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0) webpack: 5.107.2 transitivePeerDependencies: - supports-color - "@tanstack/router-utils@1.162.2": + '@tanstack/router-utils@1.162.2': dependencies: - "@babel/generator": 7.29.7 - "@babel/parser": 7.29.7 - "@babel/types": 7.29.7 + '@babel/generator': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 ansis: 4.3.1 babel-dead-code-elimination: 1.0.12 diff: 8.0.4 @@ -6676,271 +6308,283 @@ snapshots: transitivePeerDependencies: - supports-color - "@tanstack/store@0.9.3": {} + '@tanstack/store@0.9.3': {} - "@tanstack/virtual-file-routes@1.162.0": {} + '@tanstack/virtual-file-routes@1.162.0': {} - "@testing-library/dom@10.4.1": + '@testing-library/dom@10.4.1': dependencies: - "@babel/code-frame": 7.29.7 - "@babel/runtime": 7.29.7 - "@types/aria-query": 5.0.4 + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 aria-query: 5.3.0 dom-accessibility-api: 0.5.16 lz-string: 1.5.0 picocolors: 1.1.1 pretty-format: 27.5.1 - "@testing-library/jest-dom@6.9.1": + '@testing-library/jest-dom@6.9.1': dependencies: - "@adobe/css-tools": 4.5.0 + '@adobe/css-tools': 4.5.0 aria-query: 5.3.2 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 picocolors: 1.1.1 redent: 3.0.0 - "@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)": + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: - "@babel/runtime": 7.29.7 - "@testing-library/dom": 10.4.1 + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) - "@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)": + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: - "@testing-library/dom": 10.4.1 + '@testing-library/dom': 10.4.1 - "@tootallnate/once@2.0.1": {} + '@tootallnate/once@2.0.1': {} - "@tybys/wasm-util@0.10.2": + '@tybys/wasm-util@0.10.2': dependencies: tslib: 2.8.1 optional: true - "@types/aria-query@5.0.4": {} + '@types/aria-query@5.0.4': {} - "@types/cacheable-request@6.0.3": + '@types/cacheable-request@6.0.3': dependencies: - "@types/http-cache-semantics": 4.2.0 - "@types/keyv": 3.1.4 - "@types/node": 25.9.2 - "@types/responselike": 1.0.3 + '@types/http-cache-semantics': 4.2.0 + '@types/keyv': 3.1.4 + '@types/node': 25.9.2 + '@types/responselike': 1.0.3 - "@types/chai@5.2.3": + '@types/chai@5.2.3': dependencies: - "@types/deep-eql": 4.0.2 + '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 - "@types/deep-eql@4.0.2": {} + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/deep-eql@4.0.2': {} - "@types/estree@1.0.9": {} + '@types/estree@1.0.9': {} - "@types/fs-extra@9.0.13": + '@types/fs-extra@9.0.13': dependencies: - "@types/node": 25.9.2 - optional: true + '@types/node': 25.9.2 - "@types/http-cache-semantics@4.2.0": {} + '@types/http-cache-semantics@4.2.0': {} - "@types/json-schema@7.0.15": {} + '@types/json-schema@7.0.15': {} - "@types/keyv@3.1.4": + '@types/keyv@3.1.4': dependencies: - "@types/node": 25.9.2 + '@types/node': 25.9.2 + + '@types/ms@2.1.0': {} - "@types/mute-stream@0.0.4": + '@types/mute-stream@0.0.4': dependencies: - "@types/node": 22.19.20 + '@types/node': 25.9.2 - "@types/node@20.19.42": + '@types/node@20.19.42': dependencies: undici-types: 6.21.0 - "@types/node@22.19.20": + '@types/node@22.19.20': dependencies: undici-types: 6.21.0 - "@types/node@25.9.2": + '@types/node@25.9.2': dependencies: undici-types: 7.24.6 - "@types/react-dom@19.2.3(@types/react@19.2.17)": + '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 - "@types/react@19.2.17": + '@types/react@19.2.17': dependencies: csstype: 3.2.3 - "@types/responselike@1.0.3": + '@types/responselike@1.0.3': dependencies: - "@types/node": 25.9.2 + '@types/node': 25.9.2 + + '@types/trusted-types@2.0.7': + optional: true - "@types/wrap-ansi@3.0.0": {} + '@types/wrap-ansi@3.0.0': {} - "@types/yauzl@2.10.3": + '@types/yauzl@2.10.3': dependencies: - "@types/node": 20.19.42 + '@types/node': 20.19.42 optional: true - "@vitejs/plugin-react@6.0.2(vite@8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0))": + '@vitejs/plugin-react@6.0.2(vite@8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0))': dependencies: - "@rolldown/pluginutils": 1.0.1 + '@rolldown/pluginutils': 1.0.1 vite: 8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0) - "@vitest/expect@4.1.8": + '@vitest/expect@4.1.8': dependencies: - "@standard-schema/spec": 1.1.0 - "@types/chai": 5.2.3 - "@vitest/spy": 4.1.8 - "@vitest/utils": 4.1.8 + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 chai: 6.2.2 tinyrainbow: 3.1.0 - "@vitest/mocker@4.1.8(vite@8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0))": + '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0))': dependencies: - "@vitest/spy": 4.1.8 + '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: vite: 8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0) - "@vitest/pretty-format@4.1.8": + '@vitest/pretty-format@4.1.8': dependencies: tinyrainbow: 3.1.0 - "@vitest/runner@4.1.8": + '@vitest/runner@4.1.8': dependencies: - "@vitest/utils": 4.1.8 + '@vitest/utils': 4.1.8 pathe: 2.0.3 - "@vitest/snapshot@4.1.8": + '@vitest/snapshot@4.1.8': dependencies: - "@vitest/pretty-format": 4.1.8 - "@vitest/utils": 4.1.8 + '@vitest/pretty-format': 4.1.8 + '@vitest/utils': 4.1.8 magic-string: 0.30.21 pathe: 2.0.3 - "@vitest/spy@4.1.8": {} + '@vitest/spy@4.1.8': {} - "@vitest/utils@4.1.8": + '@vitest/utils@4.1.8': dependencies: - "@vitest/pretty-format": 4.1.8 + '@vitest/pretty-format': 4.1.8 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - "@vscode/sudo-prompt@9.3.2": {} + '@vscode/sudo-prompt@9.3.2': {} - "@webassemblyjs/ast@1.14.1": + '@webassemblyjs/ast@1.14.1': dependencies: - "@webassemblyjs/helper-numbers": 1.13.2 - "@webassemblyjs/helper-wasm-bytecode": 1.13.2 + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - "@webassemblyjs/floating-point-hex-parser@1.13.2": {} + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} - "@webassemblyjs/helper-api-error@1.13.2": {} + '@webassemblyjs/helper-api-error@1.13.2': {} - "@webassemblyjs/helper-buffer@1.14.1": {} + '@webassemblyjs/helper-buffer@1.14.1': {} - "@webassemblyjs/helper-numbers@1.13.2": + '@webassemblyjs/helper-numbers@1.13.2': dependencies: - "@webassemblyjs/floating-point-hex-parser": 1.13.2 - "@webassemblyjs/helper-api-error": 1.13.2 - "@xtuc/long": 4.2.2 + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 - "@webassemblyjs/helper-wasm-bytecode@1.13.2": {} + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} - "@webassemblyjs/helper-wasm-section@1.14.1": + '@webassemblyjs/helper-wasm-section@1.14.1': dependencies: - "@webassemblyjs/ast": 1.14.1 - "@webassemblyjs/helper-buffer": 1.14.1 - "@webassemblyjs/helper-wasm-bytecode": 1.13.2 - "@webassemblyjs/wasm-gen": 1.14.1 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 - "@webassemblyjs/ieee754@1.13.2": + '@webassemblyjs/ieee754@1.13.2': dependencies: - "@xtuc/ieee754": 1.2.0 + '@xtuc/ieee754': 1.2.0 - "@webassemblyjs/leb128@1.13.2": + '@webassemblyjs/leb128@1.13.2': dependencies: - "@xtuc/long": 4.2.2 + '@xtuc/long': 4.2.2 - "@webassemblyjs/utf8@1.13.2": {} + '@webassemblyjs/utf8@1.13.2': {} - "@webassemblyjs/wasm-edit@1.14.1": + '@webassemblyjs/wasm-edit@1.14.1': dependencies: - "@webassemblyjs/ast": 1.14.1 - "@webassemblyjs/helper-buffer": 1.14.1 - "@webassemblyjs/helper-wasm-bytecode": 1.13.2 - "@webassemblyjs/helper-wasm-section": 1.14.1 - "@webassemblyjs/wasm-gen": 1.14.1 - "@webassemblyjs/wasm-opt": 1.14.1 - "@webassemblyjs/wasm-parser": 1.14.1 - "@webassemblyjs/wast-printer": 1.14.1 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 - "@webassemblyjs/wasm-gen@1.14.1": + '@webassemblyjs/wasm-gen@1.14.1': dependencies: - "@webassemblyjs/ast": 1.14.1 - "@webassemblyjs/helper-wasm-bytecode": 1.13.2 - "@webassemblyjs/ieee754": 1.13.2 - "@webassemblyjs/leb128": 1.13.2 - "@webassemblyjs/utf8": 1.13.2 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 - "@webassemblyjs/wasm-opt@1.14.1": + '@webassemblyjs/wasm-opt@1.14.1': dependencies: - "@webassemblyjs/ast": 1.14.1 - "@webassemblyjs/helper-buffer": 1.14.1 - "@webassemblyjs/wasm-gen": 1.14.1 - "@webassemblyjs/wasm-parser": 1.14.1 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 - "@webassemblyjs/wasm-parser@1.14.1": + '@webassemblyjs/wasm-parser@1.14.1': dependencies: - "@webassemblyjs/ast": 1.14.1 - "@webassemblyjs/helper-api-error": 1.13.2 - "@webassemblyjs/helper-wasm-bytecode": 1.13.2 - "@webassemblyjs/ieee754": 1.13.2 - "@webassemblyjs/leb128": 1.13.2 - "@webassemblyjs/utf8": 1.13.2 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 - "@webassemblyjs/wast-printer@1.14.1": + '@webassemblyjs/wast-printer@1.14.1': dependencies: - "@webassemblyjs/ast": 1.14.1 - "@xtuc/long": 4.2.2 + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 - "@xmldom/xmldom@0.9.10": {} + '@xmldom/xmldom@0.8.13': {} - "@xterm/addon-canvas@0.7.0(@xterm/xterm@5.5.0)": + '@xmldom/xmldom@0.9.10': {} + + '@xterm/addon-canvas@0.7.0(@xterm/xterm@5.5.0)': dependencies: - "@xterm/xterm": 5.5.0 + '@xterm/xterm': 5.5.0 - "@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)": + '@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)': dependencies: - "@xterm/xterm": 5.5.0 + '@xterm/xterm': 5.5.0 - "@xterm/addon-search@0.15.0(@xterm/xterm@5.5.0)": + '@xterm/addon-search@0.15.0(@xterm/xterm@5.5.0)': dependencies: - "@xterm/xterm": 5.5.0 + '@xterm/xterm': 5.5.0 - "@xterm/addon-unicode11@0.9.0": {} + '@xterm/addon-unicode11@0.9.0': {} - "@xterm/addon-web-links@0.11.0(@xterm/xterm@5.5.0)": + '@xterm/addon-web-links@0.11.0(@xterm/xterm@5.5.0)': dependencies: - "@xterm/xterm": 5.5.0 + '@xterm/xterm': 5.5.0 - "@xterm/addon-webgl@0.19.0": {} + '@xterm/addon-webgl@0.19.0': {} - "@xterm/xterm@5.5.0": {} + '@xterm/xterm@5.5.0': {} - "@xtuc/ieee754@1.2.0": {} + '@xtuc/ieee754@1.2.0': {} - "@xtuc/long@4.2.2": {} + '@xtuc/long@4.2.2': {} abbrev@1.1.1: {} + abbrev@4.0.0: {} + acorn-import-phases@1.0.4(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -7004,6 +6648,54 @@ snapshots: ansis@4.3.1: {} + app-builder-lib@26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3): + dependencies: + '@electron/asar': 3.4.1 + '@electron/fuses': 1.8.0 + '@electron/get': 3.1.0 + '@electron/notarize': 2.5.0 + '@electron/osx-sign': 1.3.3 + '@electron/rebuild': 4.0.4 + '@electron/universal': 2.0.3 + '@malept/flatpak-bundler': 0.4.0 + '@noble/hashes': 2.2.0 + '@peculiar/webcrypto': 1.7.1 + '@types/fs-extra': 9.0.13 + ajv: 8.20.0 + asn1js: 3.0.10 + async-exit-hook: 2.0.1 + builder-util: 26.15.3 + builder-util-runtime: 9.7.0 + chromium-pickle-js: 0.2.0 + ci-info: 4.3.1 + debug: 4.4.3(supports-color@10.2.2) + dmg-builder: 26.15.3(electron-builder-squirrel-windows@26.15.3) + dotenv: 16.6.1 + dotenv-expand: 11.0.7 + ejs: 3.1.10 + electron-builder-squirrel-windows: 26.15.3(dmg-builder@26.15.3) + electron-publish: 26.15.3 + fs-extra: 10.1.0 + hosted-git-info: 4.1.0 + isbinaryfile: 5.0.7 + jiti: 2.7.0 + js-yaml: 4.1.1 + json5: 2.2.3 + lazy-val: 1.0.5 + minimatch: 10.2.5 + pkijs: 3.4.0 + plist: 3.1.0 + proper-lockfile: 4.1.2 + resedit: 1.7.2 + semver: 7.7.4 + tar: 7.5.16 + temp-file: 3.4.0 + tiny-async-pool: 1.3.0 + unzipper: 0.12.5 + which: 5.0.0 + transitivePeerDependencies: + - supports-color + argparse@2.0.1: {} aria-hidden@1.2.6: @@ -7016,23 +6708,39 @@ snapshots: aria-query@5.3.2: {} + asn1js@3.0.10: + dependencies: + pvtsutils: 1.3.6 + pvutils: 1.1.5 + tslib: 2.8.1 + assertion-error@2.0.1: {} + async-exit-hook@2.0.1: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + at-least-node@1.0.0: {} author-regex@1.0.0: {} + aws4@1.13.2: {} + babel-dead-code-elimination@1.0.12: dependencies: - "@babel/core": 7.29.7 - "@babel/parser": 7.29.7 - "@babel/traverse": 7.29.7 - "@babel/types": 7.29.7 + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.10.35: {} @@ -7065,6 +6773,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -7086,10 +6798,38 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + builder-util-runtime@9.7.0: + dependencies: + debug: 4.4.3(supports-color@10.2.2) + sax: 1.6.0 + transitivePeerDependencies: + - supports-color + + builder-util@26.15.3: + dependencies: + '@types/debug': 4.1.13 + builder-util-runtime: 9.7.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@10.2.2) + fs-extra: 10.1.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6(supports-color@10.2.2) + js-yaml: 4.1.1 + sanitize-filename: 1.6.4 + source-map-support: 0.5.21 + stat-mode: 1.0.0 + temp-file: 3.4.0 + tiny-async-pool: 1.3.0 + transitivePeerDependencies: + - supports-color + + bytestreamjs@2.0.1: {} + cacache@16.1.3: dependencies: - "@npmcli/fs": 2.1.2 - "@npmcli/move-file": 2.0.1 + '@npmcli/fs': 2.1.2 + '@npmcli/move-file': 2.0.1 chownr: 2.0.0 fs-minipass: 2.1.0 glob: 8.1.0 @@ -7121,6 +6861,11 @@ snapshots: normalize-url: 6.1.0 responselike: 2.0.1 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + caniuse-lite@1.0.30001797: {} chai@6.2.2: {} @@ -7140,8 +6885,14 @@ snapshots: chownr@2.0.0: {} + chownr@3.0.0: {} + chrome-trace-event@1.0.4: {} + chromium-pickle-js@0.2.0: {} + + ci-info@4.3.1: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -7196,6 +6947,10 @@ snapshots: colorette@2.0.20: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@11.1.0: {} commander@2.20.3: {} @@ -7212,6 +6967,10 @@ snapshots: cookie-es@3.1.1: {} + core-js@3.49.0: {} + + core-util-is@1.0.3: {} + cross-dirname@0.1.0: {} cross-spawn@6.0.6: @@ -7239,12 +6998,12 @@ snapshots: csstype@3.2.3: {} - data-urls@7.0.0: + data-urls@7.0.0(@noble/hashes@2.2.0): dependencies: whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1 + whatwg-url: 16.0.1(@noble/hashes@2.2.0) transitivePeerDependencies: - - "@noble/hashes" + - '@noble/hashes' debug@2.6.9: dependencies: @@ -7282,6 +7041,8 @@ snapshots: object-keys: 1.1.1 optional: true + delayed-stream@1.0.0: {} + deprecation@2.3.1: {} dequal@2.0.3: {} @@ -7300,16 +7061,59 @@ snapshots: minimatch: 3.1.5 p-limit: 3.1.0 + dmg-builder@26.15.3(electron-builder-squirrel-windows@26.15.3): + dependencies: + app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3) + builder-util: 26.15.3 + fs-extra: 10.1.0 + js-yaml: 4.1.1 + transitivePeerDependencies: + - electron-builder-squirrel-windows + - supports-color + dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} + dompurify@3.4.11: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + dotenv-expand@11.0.7: + dependencies: + dotenv: 16.6.1 + + dotenv@16.6.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + eastasianwidth@0.2.0: {} + ejs@3.1.10: + dependencies: + jake: 10.9.4 + + electron-builder-squirrel-windows@26.15.3(dmg-builder@26.15.3): + dependencies: + app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3) + builder-util: 26.15.3 + electron-winstaller: 5.4.0 + transitivePeerDependencies: + - dmg-builder + - supports-color + electron-installer-common@0.10.4: dependencies: - "@electron/asar": 3.4.1 - "@malept/cross-spawn-promise": 1.1.1 + '@electron/asar': 3.4.1 + '@malept/cross-spawn-promise': 1.1.1 debug: 4.4.3(supports-color@10.2.2) fs-extra: 9.1.0 glob: 7.2.3 @@ -7318,14 +7122,14 @@ snapshots: semver: 7.8.4 tmp-promise: 3.0.3 optionalDependencies: - "@types/fs-extra": 9.0.13 + '@types/fs-extra': 9.0.13 transitivePeerDependencies: - supports-color optional: true electron-installer-debian@3.2.0: dependencies: - "@malept/cross-spawn-promise": 1.1.1 + '@malept/cross-spawn-promise': 1.1.1 debug: 4.4.3(supports-color@10.2.2) electron-installer-common: 0.10.4 fs-extra: 9.1.0 @@ -7339,7 +7143,7 @@ snapshots: electron-installer-redhat@3.4.0: dependencies: - "@malept/cross-spawn-promise": 1.1.1 + '@malept/cross-spawn-promise': 1.1.1 debug: 4.4.3(supports-color@10.2.2) electron-installer-common: 0.10.4 fs-extra: 9.1.0 @@ -7350,25 +7154,38 @@ snapshots: - supports-color optional: true + electron-publish@26.15.3: + dependencies: + '@types/fs-extra': 9.0.13 + aws4: 1.13.2 + builder-util: 26.15.3 + builder-util-runtime: 9.7.0 + chalk: 4.1.2 + form-data: 4.0.6 + fs-extra: 10.1.0 + lazy-val: 1.0.5 + mime: 2.6.0 + transitivePeerDependencies: + - supports-color + electron-to-chromium@1.5.371: {} electron-winstaller@5.4.0: dependencies: - "@electron/asar": 3.4.1 + '@electron/asar': 3.4.1 debug: 4.4.3(supports-color@10.2.2) fs-extra: 7.0.1 lodash: 4.18.1 temp: 0.9.4 optionalDependencies: - "@electron/windows-sign": 1.2.2 + '@electron/windows-sign': 1.2.2 transitivePeerDependencies: - supports-color - optional: true electron@33.4.11: dependencies: - "@electron/get": 2.0.3 - "@types/node": 20.19.42 + '@electron/get': 2.0.3 + '@types/node': 20.19.42 extract-zip: 2.0.1 transitivePeerDependencies: - supports-color @@ -7401,13 +7218,23 @@ snapshots: dependencies: is-arrayish: 0.2.1 - es-define-property@1.0.1: - optional: true + es-define-property@1.0.1: {} es-errors@1.3.0: {} es-module-lexer@2.1.0: {} + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + es6-error@4.1.1: optional: true @@ -7433,7 +7260,7 @@ snapshots: estree-walker@3.0.3: dependencies: - "@types/estree": 1.0.9 + '@types/estree': 1.0.9 eta@3.5.0: {} @@ -7467,7 +7294,7 @@ snapshots: get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: - "@types/yauzl": 2.10.3 + '@types/yauzl': 2.10.3 transitivePeerDependencies: - supports-color @@ -7475,8 +7302,8 @@ snapshots: fast-glob@3.3.3: dependencies: - "@nodelib/fs.stat": 2.0.5 - "@nodelib/fs.walk": 1.2.8 + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 micromatch: 4.0.8 @@ -7495,6 +7322,12 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + fflate@0.4.8: {} + + filelist@1.0.6: + dependencies: + minimatch: 5.1.9 + filename-reserved-regex@2.0.0: {} filenamify@4.3.0: @@ -7523,12 +7356,26 @@ snapshots: transitivePeerDependencies: - supports-color + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + fs-extra@10.1.0: dependencies: graceful-fs: 4.2.11 jsonfile: 6.2.1 universalify: 2.0.1 + fs-extra@11.3.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + fs-extra@11.3.5: dependencies: graceful-fs: 4.2.11 @@ -7540,7 +7387,6 @@ snapshots: graceful-fs: 4.2.11 jsonfile: 4.0.0 universalify: 0.1.2 - optional: true fs-extra@8.1.0: dependencies: @@ -7590,6 +7436,19 @@ snapshots: tiny-each-async: 2.0.3 optional: true + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} get-package-info@1.0.0: @@ -7601,6 +7460,11 @@ snapshots: transitivePeerDependencies: - supports-color + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + get-stream@4.1.0: dependencies: pump: 3.0.4 @@ -7656,15 +7520,14 @@ snapshots: gopd: 1.2.0 optional: true - gopd@1.2.0: - optional: true + gopd@1.2.0: {} got@11.8.6: dependencies: - "@sindresorhus/is": 4.6.0 - "@szmarczak/http-timer": 4.0.6 - "@types/cacheable-request": 6.0.3 - "@types/responselike": 1.0.3 + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 cacheable-lookup: 5.0.4 cacheable-request: 7.0.4 decompress-response: 6.0.0 @@ -7682,28 +7545,45 @@ snapshots: es-define-property: 1.0.1 optional: true + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.4: dependencies: function-bind: 1.1.2 hosted-git-info@2.8.9: {} - html-encoding-sniffer@6.0.0: + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + html-encoding-sniffer@6.0.0(@noble/hashes@2.2.0): dependencies: - "@exodus/bytes": 1.15.1 + '@exodus/bytes': 1.15.1(@noble/hashes@2.2.0) transitivePeerDependencies: - - "@noble/hashes" + - '@noble/hashes' http-cache-semantics@4.2.0: {} http-proxy-agent@5.0.0: dependencies: - "@tootallnate/once": 2.0.1 + '@tootallnate/once': 2.0.1 agent-base: 6.0.2 debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + http2-wrapper@1.0.3: dependencies: quick-lru: 5.1.1 @@ -7789,15 +7669,29 @@ snapshots: is-url@1.2.4: {} + isarray@1.0.0: {} + isbinaryfile@4.0.10: {} + isbinaryfile@5.0.7: {} + isbot@5.1.42: {} isexe@2.0.0: {} + isexe@3.1.5: {} + + isexe@4.0.0: {} + + jake@10.9.4: + dependencies: + async: 3.2.6 + filelist: 1.0.6 + picocolors: 1.1.1 + jest-worker@27.5.1: dependencies: - "@types/node": 25.9.2 + '@types/node': 25.9.2 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -7811,17 +7705,17 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@29.1.1: + jsdom@29.1.1(@noble/hashes@2.2.0): dependencies: - "@asamuzakjp/css-color": 5.1.11 - "@asamuzakjp/dom-selector": 7.1.1 - "@bramus/specificity": 2.4.2 - "@csstools/css-syntax-patches-for-csstree": 1.1.5(css-tree@3.2.1) - "@exodus/bytes": 1.15.1 + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.5(css-tree@3.2.1) + '@exodus/bytes': 1.15.1(@noble/hashes@2.2.0) css-tree: 3.2.1 - data-urls: 7.0.0 + data-urls: 7.0.0(@noble/hashes@2.2.0) decimal.js: 10.6.0 - html-encoding-sniffer: 6.0.0 + html-encoding-sniffer: 6.0.0(@noble/hashes@2.2.0) is-potential-custom-element-name: 1.0.1 lru-cache: 11.5.1 parse5: 8.0.1 @@ -7832,10 +7726,10 @@ snapshots: w3c-xmlserializer: 5.0.0 webidl-conversions: 8.0.1 whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1 + whatwg-url: 16.0.1(@noble/hashes@2.2.0) xml-name-validator: 5.0.0 transitivePeerDependencies: - - "@noble/hashes" + - '@noble/hashes' jsesc@3.1.0: {} @@ -7864,6 +7758,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + lazy-val@1.0.5: {} + lightningcss-android-arm64@1.32.0: optional: true @@ -7942,8 +7838,7 @@ snapshots: lodash.get@4.4.2: {} - lodash@4.18.1: - optional: true + lodash@4.18.1: {} log-symbols@4.1.0: dependencies: @@ -7966,6 +7861,10 @@ snapshots: dependencies: yallist: 3.1.1 + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + lru-cache@7.18.3: {} lucide-react@1.17.0(react@19.2.7): @@ -7976,7 +7875,7 @@ snapshots: magic-string@0.30.21: dependencies: - "@jridgewell/sourcemap-codec": 1.5.5 + '@jridgewell/sourcemap-codec': 1.5.5 make-fetch-happen@10.2.1: dependencies: @@ -8009,6 +7908,8 @@ snapshots: escape-string-regexp: 4.0.0 optional: true + math-intrinsics@1.1.0: {} + mdn-data@2.27.1: {} mem@4.3.0: @@ -8034,6 +7935,8 @@ snapshots: dependencies: mime-db: 1.52.0 + mime@2.6.0: {} + mimic-fn@2.1.0: {} mimic-response@1.0.1: {} @@ -8042,6 +7945,10 @@ snapshots: min-indent@1.0.1: {} + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + minimatch@3.1.5: dependencies: brace-expansion: 1.1.15 @@ -8086,15 +7993,20 @@ snapshots: minipass@5.0.0: {} + minipass@7.1.3: {} + minizlib@2.1.2: dependencies: minipass: 3.3.6 yallist: 4.0.0 + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + mkdirp@0.5.6: dependencies: minimist: 1.2.8 - optional: true mkdirp@1.0.4: {} @@ -8116,6 +8028,10 @@ snapshots: dependencies: semver: 7.8.4 + node-abi@4.31.0: + dependencies: + semver: 7.8.4 + node-api-version@0.2.1: dependencies: semver: 7.8.4 @@ -8126,12 +8042,31 @@ snapshots: optionalDependencies: encoding: 0.1.13 + node-gyp@12.4.0: + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.3 + graceful-fs: 4.2.11 + nopt: 9.0.0 + proc-log: 6.1.0 + semver: 7.8.4 + tar: 7.5.16 + tinyglobby: 0.2.17 + undici: 6.27.0 + which: 6.0.1 + + node-int64@0.4.0: {} + node-releases@2.0.47: {} nopt@6.0.0: dependencies: abbrev: 1.1.1 + nopt@9.0.0: + dependencies: + abbrev: 4.0.0 + normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 @@ -8166,7 +8101,7 @@ snapshots: openapi-typescript@7.13.0(typescript@5.9.3): dependencies: - "@redocly/openapi-core": 1.34.15(supports-color@10.2.2) + '@redocly/openapi-core': 1.34.15(supports-color@10.2.2) ansi-colors: 4.1.3 change-case: 5.4.4 parse-json: 8.3.0 @@ -8228,7 +8163,7 @@ snapshots: parse-json@8.3.0: dependencies: - "@babel/code-frame": 7.29.7 + '@babel/code-frame': 7.29.7 index-to-position: 1.2.0 type-fest: 4.41.0 @@ -8254,6 +8189,8 @@ snapshots: pathe@2.0.3: {} + pe-library@0.4.1: {} + pe-library@1.0.1: {} pend@1.2.0: {} @@ -8266,6 +8203,15 @@ snapshots: pify@2.3.0: {} + pkijs@3.4.0: + dependencies: + '@noble/hashes': 1.4.0 + asn1js: 3.0.10 + bytestreamjs: 2.0.1 + pvtsutils: 1.3.6 + pvutils: 1.1.5 + tslib: 2.8.1 + playwright-core@1.60.0: {} playwright@1.60.0: @@ -8274,9 +8220,15 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + plist@3.1.0: + dependencies: + '@xmldom/xmldom': 0.8.13 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + plist@3.1.1: dependencies: - "@xmldom/xmldom": 0.9.10 + '@xmldom/xmldom': 0.9.10 base64-js: 1.5.1 xmlbuilder: 15.1.1 @@ -8288,10 +8240,23 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + posthog-js@1.393.0: + dependencies: + '@posthog/core': 1.37.1 + '@posthog/types': 1.391.0 + core-js: 3.49.0 + dompurify: 3.4.11 + fflate: 0.4.8 + preact: 10.29.2 + query-selector-shadow-dom: 1.0.1 + web-vitals: 5.3.0 + postject@1.0.0-alpha.6: dependencies: commander: 9.5.0 + preact@10.29.2: {} + prettier@3.8.4: {} pretty-format@27.5.1: @@ -8302,6 +8267,10 @@ snapshots: proc-log@2.0.1: {} + proc-log@6.1.0: {} + + process-nextick-args@2.0.1: {} + progress@2.0.3: {} promise-inflight@1.0.1: {} @@ -8311,6 +8280,12 @@ snapshots: err-code: 2.0.3 retry: 0.12.0 + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + pump@3.0.4: dependencies: end-of-stream: 1.4.5 @@ -8318,72 +8293,80 @@ snapshots: punycode@2.3.1: {} + pvtsutils@1.3.6: + dependencies: + tslib: 2.8.1 + + pvutils@1.1.5: {} + + query-selector-shadow-dom@1.0.1: {} + queue-microtask@1.2.3: {} quick-lru@5.1.1: {} radix-ui@1.5.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: - "@radix-ui/primitive": 1.1.4 - "@radix-ui/react-accessible-icon": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-accordion": 1.2.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-alert-dialog": 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-arrow": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-aspect-ratio": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-avatar": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-checkbox": 1.3.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-collapsible": 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-collection": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-compose-refs": 1.1.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-context-menu": 2.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-dialog": 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-direction": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-dismissable-layer": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-dropdown-menu": 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-focus-guards": 1.1.4(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-focus-scope": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-form": 0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-hover-card": 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-label": 2.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-menu": 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-menubar": 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-navigation-menu": 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-one-time-password-field": 0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-password-toggle-field": 0.1.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-popover": 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-popper": 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-portal": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-presence": 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-primitive": 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-progress": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-radio-group": 1.4.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-roving-focus": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-scroll-area": 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-select": 2.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-separator": 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-slider": 1.4.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-slot": 1.2.5(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-switch": 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-tabs": 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-toast": 1.2.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-toggle": 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-toggle-group": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-toolbar": 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-tooltip": 1.2.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - "@radix-ui/react-use-callback-ref": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-controllable-state": 1.2.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-effect-event": 0.0.3(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-escape-keydown": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-is-hydrated": 0.1.1(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-layout-effect": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-use-size": 1.1.2(@types/react@19.2.17)(react@19.2.7) - "@radix-ui/react-visually-hidden": 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-accessible-icon': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-accordion': 1.2.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-alert-dialog': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-arrow': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-aspect-ratio': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-avatar': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-checkbox': 1.3.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-collapsible': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-collection': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context-menu': 2.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-dialog': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-dropdown-menu': 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-form': 0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-hover-card': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-label': 2.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-menu': 2.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-menubar': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-navigation-menu': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-one-time-password-field': 0.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-password-toggle-field': 0.1.4(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-popover': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-popper': 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-progress': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-radio-group': 1.4.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-scroll-area': 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-select': 2.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-separator': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slider': 1.4.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.2.5(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-switch': 1.3.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-tabs': 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toast': 1.2.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toggle': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toggle-group': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-toolbar': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-tooltip': 1.2.9(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-escape-keydown': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 - "@types/react-dom": 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) react-dom@19.2.7(react@19.2.7): dependencies: @@ -8398,7 +8381,7 @@ snapshots: react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7) tslib: 2.8.1 optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 react-remove-scroll@2.7.2(@types/react@19.2.17)(react@19.2.7): dependencies: @@ -8409,7 +8392,7 @@ snapshots: use-callback-ref: 1.3.3(@types/react@19.2.17)(react@19.2.7) use-sidecar: 1.1.3(@types/react@19.2.17)(react@19.2.7) optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 react-resizable-panels@4.11.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: @@ -8422,7 +8405,7 @@ snapshots: react: 19.2.7 tslib: 2.8.1 optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 react@19.2.7: {} @@ -8443,6 +8426,16 @@ snapshots: normalize-package-data: 2.5.0 path-type: 2.0.0 + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -8464,6 +8457,10 @@ snapshots: require-from-string@2.0.2: {} + resedit@1.7.2: + dependencies: + pe-library: 0.4.1 + resedit@2.0.3: dependencies: pe-library: 1.0.1 @@ -8500,7 +8497,6 @@ snapshots: rimraf@2.6.3: dependencies: glob: 7.2.3 - optional: true rimraf@3.0.2: dependencies: @@ -8518,33 +8514,41 @@ snapshots: rolldown@1.0.3: dependencies: - "@oxc-project/types": 0.133.0 - "@rolldown/pluginutils": 1.0.1 + '@oxc-project/types': 0.133.0 + '@rolldown/pluginutils': 1.0.1 optionalDependencies: - "@rolldown/binding-android-arm64": 1.0.3 - "@rolldown/binding-darwin-arm64": 1.0.3 - "@rolldown/binding-darwin-x64": 1.0.3 - "@rolldown/binding-freebsd-x64": 1.0.3 - "@rolldown/binding-linux-arm-gnueabihf": 1.0.3 - "@rolldown/binding-linux-arm64-gnu": 1.0.3 - "@rolldown/binding-linux-arm64-musl": 1.0.3 - "@rolldown/binding-linux-ppc64-gnu": 1.0.3 - "@rolldown/binding-linux-s390x-gnu": 1.0.3 - "@rolldown/binding-linux-x64-gnu": 1.0.3 - "@rolldown/binding-linux-x64-musl": 1.0.3 - "@rolldown/binding-openharmony-arm64": 1.0.3 - "@rolldown/binding-wasm32-wasi": 1.0.3 - "@rolldown/binding-win32-arm64-msvc": 1.0.3 - "@rolldown/binding-win32-x64-msvc": 1.0.3 + '@rolldown/binding-android-arm64': 1.0.3 + '@rolldown/binding-darwin-arm64': 1.0.3 + '@rolldown/binding-darwin-x64': 1.0.3 + '@rolldown/binding-freebsd-x64': 1.0.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.3 + '@rolldown/binding-linux-arm64-gnu': 1.0.3 + '@rolldown/binding-linux-arm64-musl': 1.0.3 + '@rolldown/binding-linux-ppc64-gnu': 1.0.3 + '@rolldown/binding-linux-s390x-gnu': 1.0.3 + '@rolldown/binding-linux-x64-gnu': 1.0.3 + '@rolldown/binding-linux-x64-musl': 1.0.3 + '@rolldown/binding-openharmony-arm64': 1.0.3 + '@rolldown/binding-wasm32-wasi': 1.0.3 + '@rolldown/binding-win32-arm64-msvc': 1.0.3 + '@rolldown/binding-win32-x64-msvc': 1.0.3 run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safer-buffer@2.1.2: {} + sanitize-filename@1.6.4: + dependencies: + truncate-utf8-bytes: 1.0.2 + + sax@1.6.0: {} + saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -8553,7 +8557,7 @@ snapshots: schema-utils@4.3.3: dependencies: - "@types/json-schema": 7.0.15 + '@types/json-schema': 7.0.15 ajv: 8.20.0 ajv-formats: 2.1.1(ajv@8.20.0) ajv-keywords: 5.1.0(ajv@8.20.0) @@ -8565,6 +8569,8 @@ snapshots: semver@6.3.1: {} + semver@7.7.4: {} + semver@7.8.4: {} serialize-error@7.0.1: @@ -8648,6 +8654,8 @@ snapshots: stackback@0.0.2: {} + stat-mode@1.0.0: {} + std-env@4.1.0: {} string-width@4.2.3: @@ -8662,6 +8670,10 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.2.0 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -8721,15 +8733,27 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 + tar@7.5.16: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + + temp-file@3.4.0: + dependencies: + async-exit-hook: 2.0.1 + fs-extra: 10.1.0 + temp@0.9.4: dependencies: mkdirp: 0.5.6 rimraf: 2.6.3 - optional: true terser-webpack-plugin@5.6.1(webpack@5.107.2): dependencies: - "@jridgewell/trace-mapping": 0.3.31 + '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.48.0 @@ -8737,11 +8761,15 @@ snapshots: terser@5.48.0: dependencies: - "@jridgewell/source-map": 0.3.11 + '@jridgewell/source-map': 0.3.11 acorn: 8.16.0 commander: 2.20.3 source-map-support: 0.5.21 + tiny-async-pool@1.3.0: + dependencies: + semver: 5.7.2 + tiny-each-async@2.0.3: optional: true @@ -8765,14 +8793,12 @@ snapshots: tmp-promise@3.0.3: dependencies: tmp: 0.2.7 - optional: true tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 - tmp@0.2.7: - optional: true + tmp@0.2.7: {} to-regex-range@5.0.1: dependencies: @@ -8792,6 +8818,10 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 + truncate-utf8-bytes@1.0.2: + dependencies: + utf8-byte-length: 1.0.5 + tslib@2.8.1: {} type-fest@0.13.1: @@ -8811,6 +8841,8 @@ snapshots: undici-types@7.24.6: {} + undici@6.27.0: {} + undici@7.27.2: {} unique-filename@2.0.1: @@ -8829,10 +8861,18 @@ snapshots: unplugin@3.0.0: dependencies: - "@jridgewell/remapping": 2.3.5 + '@jridgewell/remapping': 2.3.5 picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 + unzipper@0.12.5: + dependencies: + bluebird: 3.7.2 + duplexer2: 0.1.4 + fs-extra: 11.3.1 + graceful-fs: 4.2.11 + node-int64: 0.4.0 + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 @@ -8851,7 +8891,7 @@ snapshots: react: 19.2.7 tslib: 2.8.1 optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 use-sidecar@1.1.3(@types/react@19.2.17)(react@19.2.7): dependencies: @@ -8859,7 +8899,7 @@ snapshots: react: 19.2.7 tslib: 2.8.1 optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 use-sync-external-store@1.6.0(react@19.2.7): dependencies: @@ -8870,6 +8910,8 @@ snapshots: execa: 1.0.0 mem: 4.3.0 + utf8-byte-length@1.0.5: {} + util-deprecate@1.0.2: {} validate-npm-package-license@3.0.4: @@ -8885,20 +8927,20 @@ snapshots: rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: - "@types/node": 25.9.2 + '@types/node': 25.9.2 fsevents: 2.3.3 jiti: 2.7.0 terser: 5.48.0 - vitest@4.1.8(@types/node@25.9.2)(jsdom@29.1.1)(vite@8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0)): + vitest@4.1.8(@types/node@25.9.2)(jsdom@29.1.1(@noble/hashes@2.2.0))(vite@8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0)): dependencies: - "@vitest/expect": 4.1.8 - "@vitest/mocker": 4.1.8(vite@8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0)) - "@vitest/pretty-format": 4.1.8 - "@vitest/runner": 4.1.8 - "@vitest/snapshot": 4.1.8 - "@vitest/spy": 4.1.8 - "@vitest/utils": 4.1.8 + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0)) + '@vitest/pretty-format': 4.1.8 + '@vitest/runner': 4.1.8 + '@vitest/snapshot': 4.1.8 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -8913,8 +8955,8 @@ snapshots: vite: 8.0.16(@types/node@25.9.2)(jiti@2.7.0)(terser@5.48.0) why-is-node-running: 2.3.0 optionalDependencies: - "@types/node": 25.9.2 - jsdom: 29.1.1 + '@types/node': 25.9.2 + jsdom: 29.1.1(@noble/hashes@2.2.0) transitivePeerDependencies: - msw @@ -8931,6 +8973,16 @@ snapshots: dependencies: defaults: 1.0.4 + web-vitals@5.3.0: {} + + webcrypto-core@1.9.2: + dependencies: + '@peculiar/asn1-schema': 2.8.0 + '@peculiar/json-schema': 1.1.12 + '@peculiar/utils': 2.0.3 + asn1js: 3.0.10 + tslib: 2.8.1 + webidl-conversions@3.0.1: {} webidl-conversions@8.0.1: {} @@ -8941,11 +8993,11 @@ snapshots: webpack@5.107.2: dependencies: - "@types/estree": 1.0.9 - "@types/json-schema": 7.0.15 - "@webassemblyjs/ast": 1.14.1 - "@webassemblyjs/wasm-edit": 1.14.1 - "@webassemblyjs/wasm-parser": 1.14.1 + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.16.0 acorn-import-phases: 1.0.4(acorn@8.16.0) browserslist: 4.28.2 @@ -8965,10 +9017,10 @@ snapshots: watchpack: 2.5.1 webpack-sources: 3.5.0 transitivePeerDependencies: - - "@minify-html/node" - - "@swc/core" - - "@swc/css" - - "@swc/html" + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' - clean-css - cssnano - csso @@ -8980,13 +9032,13 @@ snapshots: whatwg-mimetype@5.0.0: {} - whatwg-url@16.0.1: + whatwg-url@16.0.1(@noble/hashes@2.2.0): dependencies: - "@exodus/bytes": 1.15.1 + '@exodus/bytes': 1.15.1(@noble/hashes@2.2.0) tr46: 6.0.0 webidl-conversions: 8.0.1 transitivePeerDependencies: - - "@noble/hashes" + - '@noble/hashes' whatwg-url@5.0.0: dependencies: @@ -9001,6 +9053,14 @@ snapshots: dependencies: isexe: 2.0.0 + which@5.0.0: + dependencies: + isexe: 3.1.5 + + which@6.0.1: + dependencies: + isexe: 4.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -9041,6 +9101,8 @@ snapshots: yallist@4.0.0: {} + yallist@5.0.0: {} + yaml-ast-parser@0.0.43: {} yargs-parser@20.2.9: @@ -9082,6 +9144,6 @@ snapshots: zustand@5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)): optionalDependencies: - "@types/react": 19.2.17 + '@types/react': 19.2.17 react: 19.2.7 use-sync-external-store: 1.6.0(react@19.2.7) diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 3bebc74d..6f7bc180 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -14,7 +14,7 @@ import { import { updateElectronApp } from "update-electron-app"; import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import { existsSync } from "node:fs"; -import { readFile } from "node:fs/promises"; +import { readFile, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; @@ -28,6 +28,7 @@ import { resolveDaemonFromPort, resolveDaemonFromRunFile, } from "./shared/daemon-attach"; +import { shouldReplacePortHolder } from "./shared/daemon-takeover"; import { buildDaemonEnv, resolveShellEnv, type ShellRunner } from "./shared/shell-env"; import { DEFAULT_POSTHOG_HOST, DEFAULT_POSTHOG_PROJECT_KEY } from "./shared/posthog-config"; import { buildTelemetryBootstrap } from "./shared/telemetry"; @@ -484,6 +485,60 @@ async function startDaemonInner(startEpoch: number): Promise { return daemonStatus; } + // Wedged-orphan kill+replace: both attach paths returned null, but a process + // may still be holding the port. The only reachable case here is a hung/wedged + // holder whose run-file PID is still alive but is not answering /healthz (e.g. + // our own daemon that bound the port and then deadlocked). Two cases are + // intentionally NOT handled: an identity-mismatched but healthy AO daemon is + // already surfaced as an error status upstream by resolveDaemonFromPort (not + // killed here), and a foreign non-AO process holding the port with a dead + // run-file PID is not replaced (out of scope). When no holder is detectable, + // skip straight to spawn. + const orphanProbe = await readDaemonProbe(expectedDaemonPort(process.env), "healthz"); + const runFilePath_ = runFilePath(); + let runFilePid: number | null = null; + if (runFilePath_) { + try { + runFilePid = parseRunFile(await readFile(runFilePath_, "utf8"))?.pid ?? null; + } catch { + // run-file absent or unreadable; proceed without a PID. + } + } + // process.kill(pid, 0) does not kill; it throws iff the PID is not live. + let holderPidAlive = false; + if (runFilePid) { + try { process.kill(runFilePid, 0); holderPidAlive = true; } catch { holderPidAlive = false; } + } + if (shouldReplacePortHolder(orphanProbe, holderPidAlive)) { + // Use the run-file PID when available; fall back to the probe's reported + // PID as a last resort (a wedged daemon may not have written a fresh run-file). + const pidToKill = runFilePid ?? orphanProbe?.pid ?? null; + if (pidToKill) { + try { + process.kill(-pidToKill, "SIGTERM"); + } catch { + try { + process.kill(pidToKill, "SIGTERM"); + } catch { + // process already gone; proceed + } + } + } + // Poll until the port is free (probe returns null) or 8 s elapses. + const TAKEOVER_TIMEOUT_MS = 8_000; + const TAKEOVER_POLL_MS = 200; + const deadline = Date.now() + TAKEOVER_TIMEOUT_MS; + while (Date.now() < deadline) { + const still = await readDaemonProbe(expectedDaemonPort(process.env), "healthz"); + if (!still) break; + await new Promise((r) => setTimeout(r, TAKEOVER_POLL_MS)); + } + // Remove the stale run-file so the new daemon can write a fresh one. + if (runFilePath_) { + await rm(runFilePath_, { force: true }); + } + } + if (launch.source === "bundled" && !existsSync(launch.command)) { setDaemonStatus({ state: "error", @@ -691,12 +746,67 @@ app.whenReady().then(() => { }); }); -app.on("before-quit", () => { +// Re-entrancy guard: the first before-quit fires, prevents default, does async +// work, then calls app.exit(). If app.quit() is called concurrently (e.g. from +// window-all-closed on non-darwin), the second before-quit fires while the first +// is still in flight. Without a guard it would preventDefault again and loop. +// With the guard set to true, the second invocation falls through and lets the +// quit proceed. app.exit() itself does NOT re-fire before-quit, so the guard +// mainly protects against a concurrent app.quit() race. +let quitting = false; + +app.on("before-quit", (event) => { browserViewHost?.dispose(); browserViewHost = null; - if (daemonProcess) { - killDaemon(daemonProcess); + + // Re-entrancy: if we already started the async quit sequence, let this + // invocation fall through so the app actually exits. + if (quitting) return; + quitting = true; + + // Capture the current daemon handle and port before any async gap so that + // a race with stopDaemon() cannot null them out underneath us. + const child = daemonProcess; + const port = daemonStatus.state === "ready" ? daemonStatus.port : undefined; + + if (!child) { + // No daemon we own: nothing to shut down. + return; } + + // Prevent the synchronous quit so we can ask the daemon to save gracefully + // before killing it. + event.preventDefault(); + + const doQuit = async () => { + // Best-effort graceful shutdown: POST /shutdown so the daemon flushes + // its session state before exiting. An ~8s timeout prevents a hung or + // absent daemon from blocking quit indefinitely. + // Note: the daemon's internal save bound is 30s (shutdownSaveTimeout), so + // if this fetch times out and we proceed to killDaemon (SIGTERM), the first + // SIGTERM only cancels the daemon's listen context; the daemon's in-flight + // save (on a fresh context) still runs to completion or its own 30s bound. + if (port !== undefined) { + try { + await fetch(`http://127.0.0.1:${port}/shutdown`, { + method: "POST", + signal: AbortSignal.timeout(8_000), + }); + } catch { + // Timeout, network error, or daemon already gone: proceed to kill. + console.log(`AO: /shutdown fetch failed (port ${port}); proceeding with SIGTERM.`); + } + } + + // Kill the daemon process group (reaches the daemon behind any shell + // wrapper and its PTY children). + killDaemon(child); + + // Exit without re-firing before-quit (app.exit bypasses the event). + app.exit(0); + }; + + void doQuit(); }); // Last-resort teardown. before-quit covers the normal quit path, but app.exit() diff --git a/frontend/src/renderer/components/RestoreUnavailableDialog.tsx b/frontend/src/renderer/components/RestoreUnavailableDialog.tsx new file mode 100644 index 00000000..98dbda5f --- /dev/null +++ b/frontend/src/renderer/components/RestoreUnavailableDialog.tsx @@ -0,0 +1,64 @@ +import * as Dialog from "@radix-ui/react-dialog"; +import { Loader2 } from "lucide-react"; +import { useState } from "react"; +import { Button } from "./ui/button"; +import { spawnOrchestrator } from "../lib/spawn-orchestrator"; +import { isOrchestratorSession } from "../types/workspace"; +import type { WorkspaceSession } from "../types/workspace"; + +type RestoreUnavailableDialogProps = { + open: boolean; + session: WorkspaceSession; + onOpenChange: (open: boolean) => void; + onRecreated: (newOrchestratorId: string) => void; +}; + +export function RestoreUnavailableDialog({ open, session, onOpenChange, onRecreated }: RestoreUnavailableDialogProps) { + const [busy, setBusy] = useState(false); + const [error, setError] = useState(); + const orchestrator = isOrchestratorSession(session); + + const recreate = async () => { + setBusy(true); + setError(undefined); + try { + const id = await spawnOrchestrator(session.workspaceId, true); + onOpenChange(false); + onRecreated(id); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to create orchestrator"); + } finally { + setBusy(false); + } + }; + + return ( + + + + + + Session can no longer be restored + + + {orchestrator + ? "This orchestrator has no saved agent session to resume. You can create a new orchestrator on the same branch; its committed work is preserved and the old worktree is cleaned." + : "This session has no saved agent session or prompt to resume from."} + + {error &&
{error}
} +
+ + {orchestrator && ( + + )} +
+
+
+
+ ); +} diff --git a/frontend/src/renderer/components/TerminalPane.tsx b/frontend/src/renderer/components/TerminalPane.tsx index 841762e8..5e61254d 100644 --- a/frontend/src/renderer/components/TerminalPane.tsx +++ b/frontend/src/renderer/components/TerminalPane.tsx @@ -7,6 +7,7 @@ import { useTerminalSession, type AttachableTerminal, type TerminalSessionState import { apiClient, apiErrorMessage } from "../lib/api-client"; import { workspaceQueryKey } from "../hooks/useWorkspaceQuery"; import { XtermTerminal } from "./XtermTerminal"; +import { RestoreUnavailableDialog } from "./RestoreUnavailableDialog"; type TerminalPaneProps = { session?: WorkspaceSession; @@ -69,6 +70,7 @@ function AttachedTerminal({ session, theme, daemonReady, terminalTarget, fontSiz const [initFailed, setInitFailed] = useState(false); const [isRestoring, setIsRestoring] = useState(false); const [restoreError, setRestoreError] = useState(); + const [restoreUnavailable, setRestoreUnavailable] = useState(false); const queryClient = useQueryClient(); const { attach, state, error } = useTerminalSession(attachSession, { daemonReady }); const handleId = attachSession?.terminalHandleId; @@ -90,7 +92,14 @@ function AttachedTerminal({ session, theme, daemonReady, terminalTarget, fontSiz const { error: restoreError } = await apiClient.POST("/api/v1/sessions/{sessionId}/restore", { params: { path: { sessionId: session.id } }, }); - if (restoreError) throw new Error(apiErrorMessage(restoreError, "Unable to restore session")); + if (restoreError) { + const code = (restoreError as { code?: string }).code; + if (code === "SESSION_NOT_RESUMABLE") { + setRestoreUnavailable(true); + return; + } + throw new Error(apiErrorMessage(restoreError, "Unable to restore session")); + } await queryClient.invalidateQueries({ queryKey: workspaceQueryKey }); } catch (err) { setRestoreError(err instanceof Error ? err.message : "Unable to restore session"); @@ -163,6 +172,16 @@ function AttachedTerminal({ session, theme, daemonReady, terminalTarget, fontSiz )} + {session && ( + { + await queryClient.invalidateQueries({ queryKey: workspaceQueryKey }); + }} + /> + )} ); } diff --git a/frontend/src/renderer/components/XtermTerminal.test.tsx b/frontend/src/renderer/components/XtermTerminal.test.tsx index 3787148c..59892b55 100644 --- a/frontend/src/renderer/components/XtermTerminal.test.tsx +++ b/frontend/src/renderer/components/XtermTerminal.test.tsx @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { XtermTerminal } from "./XtermTerminal"; const state = vi.hoisted(() => ({ + linkHandler: null as null | ((event: MouseEvent, uri: string) => void), lastTerminal: null as null | { keyHandler?: (event: KeyboardEvent) => boolean; wheelHandler?: (event: WheelEvent) => boolean; @@ -103,7 +104,11 @@ vi.mock("@xterm/addon-unicode11", () => ({ })); vi.mock("@xterm/addon-web-links", () => ({ - WebLinksAddon: class FakeWebLinksAddon {}, + WebLinksAddon: class FakeWebLinksAddon { + constructor(handler?: (event: MouseEvent, uri: string) => void) { + state.linkHandler = handler ?? null; + } + }, })); vi.mock("@xterm/addon-canvas", () => ({ @@ -131,6 +136,7 @@ function setNavigatorPlatform(platform: string) { describe("XtermTerminal", () => { beforeEach(() => { state.lastTerminal = null; + state.linkHandler = null; setNavigatorPlatform("Linux x86_64"); window.ao!.clipboard.writeText = vi.fn().mockResolvedValue(undefined); window.ao!.clipboard.readText = vi.fn().mockResolvedValue(""); @@ -486,6 +492,19 @@ describe("XtermTerminal", () => { expect(onInput).not.toHaveBeenCalled(); }); + it("opens terminal links via window.open so Electron routes them to the OS browser", () => { + const open = vi.spyOn(window, "open").mockReturnValue(null); + render(); + + // The default WebLinksAddon handler opens an empty window first, which the + // Electron main process denies; ours must pass the matched URL directly. + expect(state.linkHandler).toBeTypeOf("function"); + state.linkHandler!({} as MouseEvent, "https://example.com"); + + expect(open).toHaveBeenCalledWith("https://example.com", "_blank", "noopener"); + open.mockRestore(); + }); + it("forces plain drag selection without raw xterm data forwarding", () => { render(); diff --git a/frontend/src/renderer/components/XtermTerminal.tsx b/frontend/src/renderer/components/XtermTerminal.tsx index 625e80fa..48de2c8d 100644 --- a/frontend/src/renderer/components/XtermTerminal.tsx +++ b/frontend/src/renderer/components/XtermTerminal.tsx @@ -70,11 +70,10 @@ function loadRenderer(term: Terminal): void { const terminalThemes = buildTerminalThemes(); const SUPPRESS_NATIVE_PASTE_MS = 100; -// Erase scrollback (3J) + display (2J) and home the cursor — yyork's -// terminalResetSequence. Deliberately NOT term.reset(): every pane PTY is a -// fresh per-client `zellij attach` whose handshake re-asserts terminal modes -// anyway, but a full RIS would drop them for the window until that handshake -// arrives. The clear only wipes pixels; modes stay up. +// Erase scrollback (3J) + display (2J) and home the cursor. Deliberately NOT +// term.reset(): every pane PTY is a fresh per-client attach whose handshake +// re-asserts terminal modes anyway, but a full RIS would drop them until that +// handshake arrives. The clear only wipes pixels; modes stay up. const CLEAR_SEQUENCE = "\x1b[3J\x1b[2J\x1b[H"; function preparePastedText(text: string): string { @@ -163,14 +162,13 @@ type XtermInternal = Terminal & { }; }; -// zellij (started with `--mouse-mode true`, see backend embeddedClientOptions) -// acts on SGR mouse-wheel reports written to its stdin and scrolls the focused -// pane, but it does NOT enable host mouse reporting, so xterm's own mouse -// protocol stays NONE and it never reports the wheel itself. With scrollback:0 -// xterm would instead convert the wheel into cursor-arrow keys (its alt-buffer -// fallback), which move the agent's cursor/history rather than scrolling. So we -// synthesize the SGR wheel reports here. SGR button 64 = wheel up, 65 = down; -// reports are 1-based and a single cell is enough for a borderless single pane. +// We never scroll locally (scrollback:0). Instead we synthesize SGR mouse-wheel +// reports and write them to the pane; tmux (with `mouse on`, set by the runtime +// adapter) acts on them and scrolls its scrollback via copy-mode. With +// scrollback:0 xterm would otherwise convert the wheel into cursor-arrow keys +// (its alt-buffer fallback), which move the agent's cursor rather than scrolling. +// SGR button 64 = wheel up, 65 = down; reports are 1-based and a single cell is +// enough for a borderless single pane. const SGR_WHEEL_UP = 64; const SGR_WHEEL_DOWN = 65; @@ -243,12 +241,12 @@ export function XtermTerminal(props: XtermTerminalProps) { // background, the way VS Code's terminal does; without it dim colors // render washed out. minimumContrastRatio: 4.5, - // The mux PTY runs `zellij attach` (backend AttachCommand), a - // full-screen alt-buffer app that owns scrollback itself — same as - // yyork. xterm's own buffer never accumulates history (the alt screen - // doesn't feed scrollback), and wheel events reach zellij as mouse - // reports instead of scrolling locally. 0 also stops FitAddon - // reserving ~14px on the right for a scrollbar that can never appear. + // The pane PTY runs a full-screen alt-buffer app (tmux attach) that + // owns scrollback itself, so xterm's own buffer never accumulates + // history (the alt screen doesn't feed scrollback) and wheel events + // are forwarded as mouse reports instead of scrolling locally. 0 also + // stops FitAddon reserving ~14px on the right for a scrollbar that can + // never appear. scrollback: 0, theme: props.theme === "dark" ? terminalThemes.dark : terminalThemes.light, }); @@ -264,7 +262,17 @@ export function XtermTerminal(props: XtermTerminalProps) { const unicode = new Unicode11Addon(); term.loadAddon(unicode); term.unicode.activeVersion = "11"; - term.loadAddon(new WebLinksAddon()); + // Open links in the OS browser. The default WebLinksAddon handler calls + // window.open() with no URL and then assigns location.href, but the + // Electron main process denies every window.open and only forwards the URL + // passed to it (main.ts setWindowOpenHandler), so the default handler's + // empty open is dropped and clicks silently no-op. Pass the matched URL to + // window.open directly so the main process routes it to shell.openExternal. + term.loadAddon( + new WebLinksAddon((_event, uri) => { + window.open(uri, "_blank", "noopener"); + }), + ); term.loadAddon(new SearchAddon()); term.open(host); @@ -379,8 +387,8 @@ export function XtermTerminal(props: XtermTerminalProps) { // 50/250ms catch the common settle; 600/1200ms are a session-bounded // backstop. By 600ms the WebGL atlas and font metrics are unambiguously // warm, so even if the convergence loop below detached at a briefly-stable - // wrong measurement, this re-measures the real cell box and corrects — - // which fires the PTY resize that makes zellij repaint cleanly (clearing + // wrong measurement, this re-measures the real cell box and corrects, + // firing the PTY resize that makes the pane repaint cleanly (clearing // any ghost frame). fit() is idempotent: a no-op when the grid is already // right, so a correct terminal never reflows. const settleTimers = [50, 250, 600, 1200].map((ms) => window.setTimeout(fitTerminal, ms)); @@ -454,7 +462,7 @@ export function XtermTerminal(props: XtermTerminalProps) { // composition, shortcuts, and wheel reports are emitted explicitly below. const keyInput = term.onKey(({ key }) => emitUserInput(key, "keyboard")); - // Translate wheel motion into SGR wheel reports for zellij (see + // Translate wheel motion into SGR wheel reports for the pane (see // sgrWheelReport), one report per scrolled line. WheelEvent.deltaMode // varies by platform/device: trackpads and normalized wheels report // pixels (mode 0, the macOS case), while many Linux/Windows mouse wheels diff --git a/frontend/src/renderer/lib/spawn-orchestrator.test.ts b/frontend/src/renderer/lib/spawn-orchestrator.test.ts new file mode 100644 index 00000000..34475155 --- /dev/null +++ b/frontend/src/renderer/lib/spawn-orchestrator.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { spawnOrchestrator } from "./spawn-orchestrator"; +import { apiClient } from "./api-client"; + +vi.mock("./api-client", () => ({ + apiClient: { POST: vi.fn() }, +})); + +describe("spawnOrchestrator", () => { + beforeEach(() => vi.clearAllMocks()); + + it("sends clean:true through to the request body when asked", async () => { + (apiClient.POST as ReturnType).mockResolvedValue({ + data: { orchestrator: { id: "proj-9" } }, + error: undefined, + response: { status: 201 }, + }); + const id = await spawnOrchestrator("proj", true); + expect(id).toBe("proj-9"); + expect(apiClient.POST).toHaveBeenCalledWith("/api/v1/orchestrators", { + body: { projectId: "proj", clean: true }, + }); + }); + + it("defaults clean to false / omitted for the existing call sites", async () => { + (apiClient.POST as ReturnType).mockResolvedValue({ + data: { orchestrator: { id: "proj-1" } }, + error: undefined, + response: { status: 201 }, + }); + await spawnOrchestrator("proj"); + expect(apiClient.POST).toHaveBeenCalledWith("/api/v1/orchestrators", { + body: { projectId: "proj", clean: false }, + }); + }); +}); diff --git a/frontend/src/renderer/lib/spawn-orchestrator.ts b/frontend/src/renderer/lib/spawn-orchestrator.ts index 15d73b86..1a0c2c81 100644 --- a/frontend/src/renderer/lib/spawn-orchestrator.ts +++ b/frontend/src/renderer/lib/spawn-orchestrator.ts @@ -1,9 +1,11 @@ import { apiClient } from "./api-client"; -/** Spawn the project's orchestrator session via the daemon API. */ -export async function spawnOrchestrator(projectId: string): Promise { +/** Spawn the project's orchestrator session via the daemon API. When clean is + * true the daemon first tears down any active orchestrator for the project, then + * re-spawns one on the canonical branch (reattaching the existing branch). */ +export async function spawnOrchestrator(projectId: string, clean = false): Promise { const { data, error, response } = await apiClient.POST("/api/v1/orchestrators", { - body: { projectId }, + body: { projectId, clean }, }); if (error || !data?.orchestrator?.id) { diff --git a/frontend/src/renderer/routeTree.gen.ts b/frontend/src/renderer/routeTree.gen.ts index ffe4cdc4..6a13a2de 100644 --- a/frontend/src/renderer/routeTree.gen.ts +++ b/frontend/src/renderer/routeTree.gen.ts @@ -8,183 +8,188 @@ // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. -import { Route as rootRouteImport } from "./routes/__root"; -import { Route as ShellRouteImport } from "./routes/_shell"; -import { Route as ShellIndexRouteImport } from "./routes/_shell.index"; -import { Route as ShellPrsRouteImport } from "./routes/_shell.prs"; -import { Route as ShellSessionsSessionIdRouteImport } from "./routes/_shell.sessions.$sessionId"; -import { Route as ShellProjectsProjectIdRouteImport } from "./routes/_shell.projects.$projectId"; -import { Route as ShellProjectsProjectIdSettingsRouteImport } from "./routes/_shell.projects.$projectId_.settings"; -import { Route as ShellProjectsProjectIdSessionsSessionIdRouteImport } from "./routes/_shell.projects.$projectId_.sessions.$sessionId"; +import { Route as rootRouteImport } from './routes/__root' +import { Route as ShellRouteImport } from './routes/_shell' +import { Route as ShellIndexRouteImport } from './routes/_shell.index' +import { Route as ShellPrsRouteImport } from './routes/_shell.prs' +import { Route as ShellSessionsSessionIdRouteImport } from './routes/_shell.sessions.$sessionId' +import { Route as ShellProjectsProjectIdRouteImport } from './routes/_shell.projects.$projectId' +import { Route as ShellProjectsProjectIdSettingsRouteImport } from './routes/_shell.projects.$projectId_.settings' +import { Route as ShellProjectsProjectIdSessionsSessionIdRouteImport } from './routes/_shell.projects.$projectId_.sessions.$sessionId' const ShellRoute = ShellRouteImport.update({ - id: "/_shell", - getParentRoute: () => rootRouteImport, -} as any); + id: '/_shell', + getParentRoute: () => rootRouteImport, +} as any) const ShellIndexRoute = ShellIndexRouteImport.update({ - id: "/", - path: "/", - getParentRoute: () => ShellRoute, -} as any); + id: '/', + path: '/', + getParentRoute: () => ShellRoute, +} as any) const ShellPrsRoute = ShellPrsRouteImport.update({ - id: "/prs", - path: "/prs", - getParentRoute: () => ShellRoute, -} as any); + id: '/prs', + path: '/prs', + getParentRoute: () => ShellRoute, +} as any) const ShellSessionsSessionIdRoute = ShellSessionsSessionIdRouteImport.update({ - id: "/sessions/$sessionId", - path: "/sessions/$sessionId", - getParentRoute: () => ShellRoute, -} as any); + id: '/sessions/$sessionId', + path: '/sessions/$sessionId', + getParentRoute: () => ShellRoute, +} as any) const ShellProjectsProjectIdRoute = ShellProjectsProjectIdRouteImport.update({ - id: "/projects/$projectId", - path: "/projects/$projectId", - getParentRoute: () => ShellRoute, -} as any); -const ShellProjectsProjectIdSettingsRoute = ShellProjectsProjectIdSettingsRouteImport.update({ - id: "/projects/$projectId_/settings", - path: "/projects/$projectId/settings", - getParentRoute: () => ShellRoute, -} as any); -const ShellProjectsProjectIdSessionsSessionIdRoute = ShellProjectsProjectIdSessionsSessionIdRouteImport.update({ - id: "/projects/$projectId_/sessions/$sessionId", - path: "/projects/$projectId/sessions/$sessionId", - getParentRoute: () => ShellRoute, -} as any); + id: '/projects/$projectId', + path: '/projects/$projectId', + getParentRoute: () => ShellRoute, +} as any) +const ShellProjectsProjectIdSettingsRoute = + ShellProjectsProjectIdSettingsRouteImport.update({ + id: '/projects/$projectId_/settings', + path: '/projects/$projectId/settings', + getParentRoute: () => ShellRoute, + } as any) +const ShellProjectsProjectIdSessionsSessionIdRoute = + ShellProjectsProjectIdSessionsSessionIdRouteImport.update({ + id: '/projects/$projectId_/sessions/$sessionId', + path: '/projects/$projectId/sessions/$sessionId', + getParentRoute: () => ShellRoute, + } as any) export interface FileRoutesByFullPath { - "/": typeof ShellIndexRoute; - "/prs": typeof ShellPrsRoute; - "/projects/$projectId": typeof ShellProjectsProjectIdRoute; - "/sessions/$sessionId": typeof ShellSessionsSessionIdRoute; - "/projects/$projectId/settings": typeof ShellProjectsProjectIdSettingsRoute; - "/projects/$projectId/sessions/$sessionId": typeof ShellProjectsProjectIdSessionsSessionIdRoute; + '/': typeof ShellIndexRoute + '/prs': typeof ShellPrsRoute + '/projects/$projectId': typeof ShellProjectsProjectIdRoute + '/sessions/$sessionId': typeof ShellSessionsSessionIdRoute + '/projects/$projectId/settings': typeof ShellProjectsProjectIdSettingsRoute + '/projects/$projectId/sessions/$sessionId': typeof ShellProjectsProjectIdSessionsSessionIdRoute } export interface FileRoutesByTo { - "/prs": typeof ShellPrsRoute; - "/": typeof ShellIndexRoute; - "/projects/$projectId": typeof ShellProjectsProjectIdRoute; - "/sessions/$sessionId": typeof ShellSessionsSessionIdRoute; - "/projects/$projectId/settings": typeof ShellProjectsProjectIdSettingsRoute; - "/projects/$projectId/sessions/$sessionId": typeof ShellProjectsProjectIdSessionsSessionIdRoute; + '/prs': typeof ShellPrsRoute + '/': typeof ShellIndexRoute + '/projects/$projectId': typeof ShellProjectsProjectIdRoute + '/sessions/$sessionId': typeof ShellSessionsSessionIdRoute + '/projects/$projectId/settings': typeof ShellProjectsProjectIdSettingsRoute + '/projects/$projectId/sessions/$sessionId': typeof ShellProjectsProjectIdSessionsSessionIdRoute } export interface FileRoutesById { - __root__: typeof rootRouteImport; - "/_shell": typeof ShellRouteWithChildren; - "/_shell/prs": typeof ShellPrsRoute; - "/_shell/": typeof ShellIndexRoute; - "/_shell/projects/$projectId": typeof ShellProjectsProjectIdRoute; - "/_shell/sessions/$sessionId": typeof ShellSessionsSessionIdRoute; - "/_shell/projects/$projectId_/settings": typeof ShellProjectsProjectIdSettingsRoute; - "/_shell/projects/$projectId_/sessions/$sessionId": typeof ShellProjectsProjectIdSessionsSessionIdRoute; + __root__: typeof rootRouteImport + '/_shell': typeof ShellRouteWithChildren + '/_shell/prs': typeof ShellPrsRoute + '/_shell/': typeof ShellIndexRoute + '/_shell/projects/$projectId': typeof ShellProjectsProjectIdRoute + '/_shell/sessions/$sessionId': typeof ShellSessionsSessionIdRoute + '/_shell/projects/$projectId_/settings': typeof ShellProjectsProjectIdSettingsRoute + '/_shell/projects/$projectId_/sessions/$sessionId': typeof ShellProjectsProjectIdSessionsSessionIdRoute } export interface FileRouteTypes { - fileRoutesByFullPath: FileRoutesByFullPath; - fullPaths: - | "/" - | "/prs" - | "/projects/$projectId" - | "/sessions/$sessionId" - | "/projects/$projectId/settings" - | "/projects/$projectId/sessions/$sessionId"; - fileRoutesByTo: FileRoutesByTo; - to: - | "/prs" - | "/" - | "/projects/$projectId" - | "/sessions/$sessionId" - | "/projects/$projectId/settings" - | "/projects/$projectId/sessions/$sessionId"; - id: - | "__root__" - | "/_shell" - | "/_shell/prs" - | "/_shell/" - | "/_shell/projects/$projectId" - | "/_shell/sessions/$sessionId" - | "/_shell/projects/$projectId_/settings" - | "/_shell/projects/$projectId_/sessions/$sessionId"; - fileRoutesById: FileRoutesById; + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/prs' + | '/projects/$projectId' + | '/sessions/$sessionId' + | '/projects/$projectId/settings' + | '/projects/$projectId/sessions/$sessionId' + fileRoutesByTo: FileRoutesByTo + to: + | '/prs' + | '/' + | '/projects/$projectId' + | '/sessions/$sessionId' + | '/projects/$projectId/settings' + | '/projects/$projectId/sessions/$sessionId' + id: + | '__root__' + | '/_shell' + | '/_shell/prs' + | '/_shell/' + | '/_shell/projects/$projectId' + | '/_shell/sessions/$sessionId' + | '/_shell/projects/$projectId_/settings' + | '/_shell/projects/$projectId_/sessions/$sessionId' + fileRoutesById: FileRoutesById } export interface RootRouteChildren { - ShellRoute: typeof ShellRouteWithChildren; + ShellRoute: typeof ShellRouteWithChildren } -declare module "@tanstack/react-router" { - interface FileRoutesByPath { - "/_shell": { - id: "/_shell"; - path: ""; - fullPath: "/"; - preLoaderRoute: typeof ShellRouteImport; - parentRoute: typeof rootRouteImport; - }; - "/_shell/": { - id: "/_shell/"; - path: "/"; - fullPath: "/"; - preLoaderRoute: typeof ShellIndexRouteImport; - parentRoute: typeof ShellRoute; - }; - "/_shell/prs": { - id: "/_shell/prs"; - path: "/prs"; - fullPath: "/prs"; - preLoaderRoute: typeof ShellPrsRouteImport; - parentRoute: typeof ShellRoute; - }; - "/_shell/sessions/$sessionId": { - id: "/_shell/sessions/$sessionId"; - path: "/sessions/$sessionId"; - fullPath: "/sessions/$sessionId"; - preLoaderRoute: typeof ShellSessionsSessionIdRouteImport; - parentRoute: typeof ShellRoute; - }; - "/_shell/projects/$projectId": { - id: "/_shell/projects/$projectId"; - path: "/projects/$projectId"; - fullPath: "/projects/$projectId"; - preLoaderRoute: typeof ShellProjectsProjectIdRouteImport; - parentRoute: typeof ShellRoute; - }; - "/_shell/projects/$projectId_/settings": { - id: "/_shell/projects/$projectId_/settings"; - path: "/projects/$projectId/settings"; - fullPath: "/projects/$projectId/settings"; - preLoaderRoute: typeof ShellProjectsProjectIdSettingsRouteImport; - parentRoute: typeof ShellRoute; - }; - "/_shell/projects/$projectId_/sessions/$sessionId": { - id: "/_shell/projects/$projectId_/sessions/$sessionId"; - path: "/projects/$projectId/sessions/$sessionId"; - fullPath: "/projects/$projectId/sessions/$sessionId"; - preLoaderRoute: typeof ShellProjectsProjectIdSessionsSessionIdRouteImport; - parentRoute: typeof ShellRoute; - }; - } +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/_shell': { + id: '/_shell' + path: '' + fullPath: '/' + preLoaderRoute: typeof ShellRouteImport + parentRoute: typeof rootRouteImport + } + '/_shell/': { + id: '/_shell/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof ShellIndexRouteImport + parentRoute: typeof ShellRoute + } + '/_shell/prs': { + id: '/_shell/prs' + path: '/prs' + fullPath: '/prs' + preLoaderRoute: typeof ShellPrsRouteImport + parentRoute: typeof ShellRoute + } + '/_shell/sessions/$sessionId': { + id: '/_shell/sessions/$sessionId' + path: '/sessions/$sessionId' + fullPath: '/sessions/$sessionId' + preLoaderRoute: typeof ShellSessionsSessionIdRouteImport + parentRoute: typeof ShellRoute + } + '/_shell/projects/$projectId': { + id: '/_shell/projects/$projectId' + path: '/projects/$projectId' + fullPath: '/projects/$projectId' + preLoaderRoute: typeof ShellProjectsProjectIdRouteImport + parentRoute: typeof ShellRoute + } + '/_shell/projects/$projectId_/settings': { + id: '/_shell/projects/$projectId_/settings' + path: '/projects/$projectId/settings' + fullPath: '/projects/$projectId/settings' + preLoaderRoute: typeof ShellProjectsProjectIdSettingsRouteImport + parentRoute: typeof ShellRoute + } + '/_shell/projects/$projectId_/sessions/$sessionId': { + id: '/_shell/projects/$projectId_/sessions/$sessionId' + path: '/projects/$projectId/sessions/$sessionId' + fullPath: '/projects/$projectId/sessions/$sessionId' + preLoaderRoute: typeof ShellProjectsProjectIdSessionsSessionIdRouteImport + parentRoute: typeof ShellRoute + } + } } interface ShellRouteChildren { - ShellPrsRoute: typeof ShellPrsRoute; - ShellIndexRoute: typeof ShellIndexRoute; - ShellProjectsProjectIdRoute: typeof ShellProjectsProjectIdRoute; - ShellSessionsSessionIdRoute: typeof ShellSessionsSessionIdRoute; - ShellProjectsProjectIdSettingsRoute: typeof ShellProjectsProjectIdSettingsRoute; - ShellProjectsProjectIdSessionsSessionIdRoute: typeof ShellProjectsProjectIdSessionsSessionIdRoute; + ShellPrsRoute: typeof ShellPrsRoute + ShellIndexRoute: typeof ShellIndexRoute + ShellProjectsProjectIdRoute: typeof ShellProjectsProjectIdRoute + ShellSessionsSessionIdRoute: typeof ShellSessionsSessionIdRoute + ShellProjectsProjectIdSettingsRoute: typeof ShellProjectsProjectIdSettingsRoute + ShellProjectsProjectIdSessionsSessionIdRoute: typeof ShellProjectsProjectIdSessionsSessionIdRoute } const ShellRouteChildren: ShellRouteChildren = { - ShellPrsRoute: ShellPrsRoute, - ShellIndexRoute: ShellIndexRoute, - ShellProjectsProjectIdRoute: ShellProjectsProjectIdRoute, - ShellSessionsSessionIdRoute: ShellSessionsSessionIdRoute, - ShellProjectsProjectIdSettingsRoute: ShellProjectsProjectIdSettingsRoute, - ShellProjectsProjectIdSessionsSessionIdRoute: ShellProjectsProjectIdSessionsSessionIdRoute, -}; + ShellPrsRoute: ShellPrsRoute, + ShellIndexRoute: ShellIndexRoute, + ShellProjectsProjectIdRoute: ShellProjectsProjectIdRoute, + ShellSessionsSessionIdRoute: ShellSessionsSessionIdRoute, + ShellProjectsProjectIdSettingsRoute: ShellProjectsProjectIdSettingsRoute, + ShellProjectsProjectIdSessionsSessionIdRoute: + ShellProjectsProjectIdSessionsSessionIdRoute, +} -const ShellRouteWithChildren = ShellRoute._addFileChildren(ShellRouteChildren); +const ShellRouteWithChildren = ShellRoute._addFileChildren(ShellRouteChildren) const rootRouteChildren: RootRouteChildren = { - ShellRoute: ShellRouteWithChildren, -}; -export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)._addFileTypes(); + ShellRoute: ShellRouteWithChildren, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() diff --git a/frontend/src/shared/daemon-takeover.test.ts b/frontend/src/shared/daemon-takeover.test.ts new file mode 100644 index 00000000..7621501b --- /dev/null +++ b/frontend/src/shared/daemon-takeover.test.ts @@ -0,0 +1,27 @@ +// Unit tests for shouldReplacePortHolder. Run with: +// cd frontend && npx vitest run src/shared/daemon-takeover.test.ts +import { describe, expect, it } from "vitest"; +import { DAEMON_SERVICE_NAME, type DaemonProbe } from "./daemon-attach"; +import { shouldReplacePortHolder } from "./daemon-takeover"; + +// A minimal valid DaemonProbe (non-null means the AO daemon answered /healthz). +const healthyProbe: DaemonProbe = { + status: "ok", + service: DAEMON_SERVICE_NAME, + pid: 1234, +}; + +describe("shouldReplacePortHolder", () => { + it("returns true when a probe answered (rejected responder, any holderPidAlive)", () => { + expect(shouldReplacePortHolder(healthyProbe, false)).toBe(true); + expect(shouldReplacePortHolder(healthyProbe, true)).toBe(true); + }); + + it("returns true when probe is null but the run-file PID is still alive (hung holder)", () => { + expect(shouldReplacePortHolder(null, true)).toBe(true); + }); + + it("returns false when probe is null and no live holder PID (nothing to kill)", () => { + expect(shouldReplacePortHolder(null, false)).toBe(false); + }); +}); diff --git a/frontend/src/shared/daemon-takeover.ts b/frontend/src/shared/daemon-takeover.ts new file mode 100644 index 00000000..98a0f58b --- /dev/null +++ b/frontend/src/shared/daemon-takeover.ts @@ -0,0 +1,38 @@ +// Pure decision helper for the wedged-orphan kill+replace path. +// +// Context: on app launch, after both attach attempts fail (inspectExistingDaemon +// and resolveDaemonFromPort both returned null/non-ready), a process may still +// be holding the daemon port. Spawning a new daemon then makes the Go child +// collide on the port and exit 1. This helper encodes the decision: kill the +// holder when the run-file names a PID that is still alive (a hung/wedged holder +// that bound the port but is not answering /healthz). The probe disjunct +// (probe !== null) is kept as a defensive guard only: by the time this helper +// is called, resolveDaemonFromPort already returned null, so a holder that +// answers /healthz at this point is unexpected. If it does happen (e.g. a race), +// we still replace it rather than colliding on spawn. +// +// Kept side-effect free and dependency-injected (no node:* or electron imports) +// so it can be exercised in vitest without the Electron polyfill layer. + +import type { DaemonProbe } from "./daemon-attach"; + +/** + * Reports whether something is holding the daemon port that we must kill before + * spawning. By the time it is called, both healthy-reuse attach paths have already + * returned null. The primary trigger is holderPidAlive: the run-file names a PID + * that is still alive but is not answering /healthz (a hung/wedged holder). The + * probe disjunct is a defensive guard: a holder that answers at this point is + * unexpected (resolveDaemonFromPort already returned null), but if it does appear, + * we replace it rather than colliding on spawn. + * + * Returns true when the caller should kill the holder, wait for the port to + * free, clear the stale run-file, then spawn a fresh daemon. + * + * Returns false when there is no detectable holder; spawn immediately. + * + * ponytail: two-condition OR covers the entire decision surface; the probe's + * content (pid, executablePath) is for the caller's kill logic, not ours. + */ +export function shouldReplacePortHolder(probe: DaemonProbe | null, holderPidAlive: boolean): boolean { + return probe !== null || holderPidAlive; +} diff --git a/frontend/src/shared/shell-env.test.ts b/frontend/src/shared/shell-env.test.ts index f2dcb018..f5a1c40a 100644 --- a/frontend/src/shared/shell-env.test.ts +++ b/frontend/src/shared/shell-env.test.ts @@ -82,6 +82,16 @@ describe("buildDaemonEnv", () => { expect(env.PATH?.split(":")).toContain(dir); } }); + + it("defaults TERM when neither shell nor process env sets it (Finder launch)", () => { + const env = buildDaemonEnv(minimalProcessEnv, null, {}); + expect(env.TERM).toBe("xterm-256color"); + }); + + it("lets a real TERM from the process env win over the default", () => { + const env = buildDaemonEnv({ ...minimalProcessEnv, TERM: "screen-256color" }, null, {}); + expect(env.TERM).toBe("screen-256color"); + }); }); describe("resolveShellPath", () => { diff --git a/frontend/src/shared/shell-env.ts b/frontend/src/shared/shell-env.ts index 768c670a..d2da45c6 100644 --- a/frontend/src/shared/shell-env.ts +++ b/frontend/src/shared/shell-env.ts @@ -65,12 +65,18 @@ export function withFallbackPath(currentPath: string | undefined): string { // Base = shell env, overlaid by processEnv so Electron/AO runtime vars win, then // PATH forced to the shell's PATH (with floor), then explicit overrides. +// +// TERM defaults to xterm-256color (what the renderer's xterm.js emulates): a +// Finder/Dock launch starts under launchd with no controlling tty, so TERM is +// unset, and the daemon's tmux attach client inherits that and dies with +// "open terminal failed: terminal does not support clear". Seeded as the base +// so a real TERM from the shell/process env still wins. export function buildDaemonEnv( processEnv: NodeJS.ProcessEnv, shellEnv: Record | null, overrides: Record, ): NodeJS.ProcessEnv { - const merged: NodeJS.ProcessEnv = { ...(shellEnv ?? {}), ...processEnv }; + const merged: NodeJS.ProcessEnv = { TERM: "xterm-256color", ...(shellEnv ?? {}), ...processEnv }; merged.PATH = withFallbackPath(shellEnv?.PATH ?? processEnv.PATH); return { ...merged, ...overrides }; }