diff --git a/assets/components.d.ts b/assets/components.d.ts index cfd5f52a..2ac014cc 100644 --- a/assets/components.d.ts +++ b/assets/components.d.ts @@ -62,6 +62,7 @@ declare module 'vue' { LogMessageActions: typeof import('./components/LogViewer/LogMessageActions.vue')['default'] LogStd: typeof import('./components/LogViewer/LogStd.vue')['default'] LogViewer: typeof import('./components/LogViewer/LogViewer.vue')['default'] + 'MaterialSymbols:terminal': typeof import('~icons/material-symbols/terminal')['default'] 'Mdi:account': typeof import('~icons/mdi/account')['default'] 'Mdi:announcement': typeof import('~icons/mdi/announcement')['default'] 'Mdi:arrowUp': typeof import('~icons/mdi/arrow-up')['default'] @@ -103,6 +104,7 @@ declare module 'vue' { 'Ph:stackSimple': typeof import('~icons/ph/stack-simple')['default'] Popup: typeof import('./components/Popup.vue')['default'] RandomColorTag: typeof import('./components/LogViewer/RandomColorTag.vue')['default'] + 'Ri:terminalWindowFill': typeof import('~icons/ri/terminal-window-fill')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] ScrollableView: typeof import('./components/ScrollableView.vue')['default'] @@ -121,6 +123,7 @@ declare module 'vue' { StatSparkline: typeof import('./components/LogViewer/StatSparkline.vue')['default'] SwarmMenu: typeof import('./components/SwarmMenu.vue')['default'] Tag: typeof import('./components/common/Tag.vue')['default'] + Terminal: typeof import('./components/Terminal.vue')['default'] TimedButton: typeof import('./components/common/TimedButton.vue')['default'] ToastModal: typeof import('./components/common/ToastModal.vue')['default'] Toggle: typeof import('./components/common/Toggle.vue')['default'] diff --git a/assets/components/ContainerViewer/ContainerActionsToolbar.vue b/assets/components/ContainerViewer/ContainerActionsToolbar.vue index df136bb4..c00e8910 100644 --- a/assets/components/ContainerViewer/ContainerActionsToolbar.vue +++ b/assets/components/ContainerViewer/ContainerActionsToolbar.vue @@ -134,6 +134,20 @@ + +
  • +
  • + + Attach + + +
  • +
  • + + Shell + + +
  • @@ -142,6 +156,7 @@ import { Container } from "@/models/Container"; import { allLevels } from "@/composable/logContext"; import LogAnalytics from "../LogViewer/LogAnalytics.vue"; +import Terminal from "@/components/Terminal.vue"; const { showSearch } = useSearchFilter(); const { enableActions } = config; @@ -161,6 +176,20 @@ onKeyStroke("f", (e) => { } }); +onKeyStroke("a", (e) => { + if ((e.ctrlKey || e.metaKey) && e.shiftKey) { + showDrawer(Terminal, { container, action: "attach" }, "lg"); + e.preventDefault(); + } +}); + +onKeyStroke("e", (e) => { + if ((e.ctrlKey || e.metaKey) && e.shiftKey) { + showDrawer(Terminal, { container, action: "exec" }, "lg"); + e.preventDefault(); + } +}); + const downloadParams = computed(() => Object.entries(toValue(streamConfig)) .filter(([, value]) => value) diff --git a/assets/components/Terminal.vue b/assets/components/Terminal.vue new file mode 100644 index 00000000..6b81fd8d --- /dev/null +++ b/assets/components/Terminal.vue @@ -0,0 +1,79 @@ + + + + diff --git a/go.mod b/go.mod index 2842c1ec..fe11be86 100644 --- a/go.mod +++ b/go.mod @@ -66,6 +66,7 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect diff --git a/go.sum b/go.sum index 88d94722..46de12e0 100644 --- a/go.sum +++ b/go.sum @@ -90,6 +90,8 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgY github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= diff --git a/internal/agent/client.go b/internal/agent/client.go index 730b93ed..8ec7188e 100644 --- a/internal/agent/client.go +++ b/internal/agent/client.go @@ -395,6 +395,14 @@ func (c *Client) ContainerAction(ctx context.Context, containerId string, action return err } +func (c *Client) ContainerAttach(ctx context.Context, containerId string) (io.WriteCloser, io.Reader, error) { + panic("not implemented") +} + +func (c *Client) ContainerExec(ctx context.Context, containerId string, cmd []string) (io.WriteCloser, io.Reader, error) { + panic("not implemented") +} + func (c *Client) Close() error { return c.conn.Close() } diff --git a/internal/container/client.go b/internal/container/client.go index ab56b8e4..8b43e8d2 100644 --- a/internal/container/client.go +++ b/internal/container/client.go @@ -38,4 +38,6 @@ type Client interface { Ping(context.Context) error Host() Host ContainerActions(ctx context.Context, action ContainerAction, containerID string) error + ContainerAttach(ctx context.Context, id string) (io.WriteCloser, io.Reader, error) + ContainerExec(ctx context.Context, id string, cmd []string) (io.WriteCloser, io.Reader, error) } diff --git a/internal/docker/client.go b/internal/docker/client.go index 22583803..2dac4b4a 100644 --- a/internal/docker/client.go +++ b/internal/docker/client.go @@ -33,6 +33,10 @@ type DockerCLI interface { ContainerStart(ctx context.Context, containerID string, options docker.StartOptions) error ContainerStop(ctx context.Context, containerID string, options docker.StopOptions) error ContainerRestart(ctx context.Context, containerID string, options docker.StopOptions) error + ContainerAttach(ctx context.Context, containerID string, options docker.AttachOptions) (types.HijackedResponse, error) + ContainerExecCreate(ctx context.Context, containerID string, options docker.ExecOptions) (docker.ExecCreateResponse, error) + ContainerExecAttach(ctx context.Context, execID string, config docker.ExecAttachOptions) (types.HijackedResponse, error) + ContainerExecResize(ctx context.Context, execID string, options docker.ResizeOptions) error Info(ctx context.Context) (system.Info, error) } @@ -297,6 +301,54 @@ func (d *DockerClient) Host() container.Host { return d.host } +func (d *DockerClient) ContainerAttach(ctx context.Context, id string) (io.WriteCloser, io.Reader, error) { + log.Debug().Str("id", id).Str("host", d.host.Name).Msg("Attaching to container") + options := docker.AttachOptions{ + Stream: true, + Stdin: true, + Stdout: true, + Stderr: true, + } + + waiter, err := d.cli.ContainerAttach(ctx, id, options) + + if err != nil { + return nil, nil, err + } + + return waiter.Conn, waiter.Reader, nil +} + +func (d *DockerClient) ContainerExec(ctx context.Context, id string, cmd []string) (io.WriteCloser, io.Reader, error) { + log.Debug().Str("id", id).Str("host", d.host.Name).Msg("Executing command in container") + options := docker.ExecOptions{ + AttachStdout: true, + AttachStderr: true, + AttachStdin: true, + Cmd: cmd, + Tty: true, + } + + execID, err := d.cli.ContainerExecCreate(ctx, id, options) + if err != nil { + return nil, nil, err + } + + waiter, err := d.cli.ContainerExecAttach(ctx, execID.ID, docker.ExecAttachOptions{}) + if err != nil { + return nil, nil, err + } + + if err = d.cli.ContainerExecResize(ctx, execID.ID, docker.ResizeOptions{ + Width: 100, + Height: 40, + }); err != nil { + return nil, nil, err + } + + return waiter.Conn, waiter.Reader, nil +} + func newContainer(c docker.Summary, host string) container.Container { name := "no name" if c.Labels["dev.dozzle.name"] != "" { diff --git a/internal/k8s/client.go b/internal/k8s/client.go index 974f59f6..ddbaa8b9 100644 --- a/internal/k8s/client.go +++ b/internal/k8s/client.go @@ -241,8 +241,15 @@ func (k *K8sClient) Host() container.Host { } func (k *K8sClient) ContainerActions(ctx context.Context, action container.ContainerAction, containerID string) error { - // Implementation for container actions (start, stop, restart, etc.) - return nil + panic("not implemented") +} + +func (k *K8sClient) ContainerAttach(ctx context.Context, id string) (io.WriteCloser, io.Reader, error) { + panic("not implemented") +} + +func (k *K8sClient) ContainerExec(ctx context.Context, id string, cmd []string) (io.WriteCloser, io.Reader, error) { + panic("not implemented") } // Helper function to parse pod and container names from container ID diff --git a/internal/support/container/agent_service.go b/internal/support/container/agent_service.go index a7a0c8b2..57b9b875 100644 --- a/internal/support/container/agent_service.go +++ b/internal/support/container/agent_service.go @@ -70,3 +70,11 @@ func (d *agentService) SubscribeContainersStarted(ctx context.Context, container func (a *agentService) ContainerAction(ctx context.Context, container container.Container, action container.ContainerAction) error { return a.client.ContainerAction(ctx, container.ID, action) } + +func (a *agentService) Attach(ctx context.Context, container container.Container, stdin io.Reader, stdout io.Writer) error { + panic("not implemented") +} + +func (a *agentService) Exec(ctx context.Context, container container.Container, cmd []string, stdin io.Reader, stdout io.Writer) error { + panic("not implemented") +} diff --git a/internal/support/container/client_service.go b/internal/support/container/client_service.go index ae1bcc94..77e244b5 100644 --- a/internal/support/container/client_service.go +++ b/internal/support/container/client_service.go @@ -16,13 +16,17 @@ type ClientService interface { Host(ctx context.Context) (container.Host, error) ContainerAction(ctx context.Context, container container.Container, action container.ContainerAction) error LogsBetweenDates(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (<-chan *container.LogEvent, error) - RawLogs(ctx context.Context, container container.Container, from time.Time, to time.Time, stdTypes container.StdType) (io.ReadCloser, error) + RawLogs(context.Context, container.Container, time.Time, time.Time, container.StdType) (io.ReadCloser, error) // Subscriptions - SubscribeStats(ctx context.Context, stats chan<- container.ContainerStat) - SubscribeEvents(ctx context.Context, events chan<- container.ContainerEvent) - SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container) + SubscribeStats(context.Context, chan<- container.ContainerStat) + SubscribeEvents(context.Context, chan<- container.ContainerEvent) + SubscribeContainersStarted(context.Context, chan<- container.Container) // Blocking streaming functions that should be used in a goroutine - StreamLogs(ctx context.Context, container container.Container, from time.Time, stdTypes container.StdType, events chan<- *container.LogEvent) error + StreamLogs(context.Context, container.Container, time.Time, container.StdType, chan<- *container.LogEvent) error + + // Terminal + Attach(context.Context, container.Container, io.Reader, io.Writer) error + Exec(context.Context, container.Container, []string, io.Reader, io.Writer) error } diff --git a/internal/support/container/container_service.go b/internal/support/container/container_service.go index 4e97e06e..07a97760 100644 --- a/internal/support/container/container_service.go +++ b/internal/support/container/container_service.go @@ -35,3 +35,11 @@ func (c *ContainerService) StreamLogs(ctx context.Context, from time.Time, stdTy func (c *ContainerService) Action(ctx context.Context, action container.ContainerAction) error { return c.clientService.ContainerAction(ctx, c.Container, action) } + +func (c *ContainerService) Attach(ctx context.Context, stdin io.Reader, stdout io.Writer) error { + return c.clientService.Attach(ctx, c.Container, stdin, stdout) +} + +func (c *ContainerService) Exec(ctx context.Context, cmd []string, stdin io.Reader, stdout io.Writer) error { + return c.clientService.Exec(ctx, c.Container, cmd, stdin, stdout) +} diff --git a/internal/support/docker/docker_service.go b/internal/support/docker/docker_service.go index 9952a13a..970f862a 100644 --- a/internal/support/docker/docker_service.go +++ b/internal/support/docker/docker_service.go @@ -3,6 +3,7 @@ package docker_support import ( "context" "io" + "sync" "time" "github.com/amir20/dozzle/internal/container" @@ -109,3 +110,75 @@ func (d *DockerClientService) SubscribeEvents(ctx context.Context, events chan<- func (d *DockerClientService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container) { d.store.SubscribeNewContainers(ctx, containers) } + +func (d *DockerClientService) Attach(ctx context.Context, container container.Container, stdin io.Reader, stdout io.Writer) error { + cancelCtx, cancel := context.WithCancel(ctx) + containerWriter, containerReader, err := d.client.ContainerAttach(cancelCtx, container.ID) + if err != nil { + cancel() + return err + } + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + if _, err := io.Copy(containerWriter, stdin); err != nil { + log.Error().Err(err).Msg("error while reading from ws") + } + cancel() + containerWriter.Close() + }() + + go func() { + defer wg.Done() + if container.Tty { + if _, err := io.Copy(stdout, containerReader); err != nil { + log.Error().Err(err).Msg("error while writing to ws") + } + } else { + if _, err := stdcopy.StdCopy(stdout, stdout, containerReader); err != nil { + log.Error().Err(err).Msg("error while writing to ws") + } + } + cancel() + }() + + wg.Wait() + + return nil +} + +func (d *DockerClientService) Exec(ctx context.Context, container container.Container, cmd []string, stdin io.Reader, stdout io.Writer) error { + cancelCtx, cancel := context.WithCancel(ctx) + containerWriter, containerReader, err := d.client.ContainerExec(cancelCtx, container.ID, cmd) + if err != nil { + cancel() + return err + } + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + if _, err := io.Copy(containerWriter, stdin); err != nil { + log.Error().Err(err).Msg("error while reading from ws") + } + cancel() + containerWriter.Close() + }() + + go func() { + defer wg.Done() + if _, err := stdcopy.StdCopy(stdout, stdout, containerReader); err != nil { + log.Error().Err(err).Msg("error while writing to ws") + } + cancel() + }() + + wg.Wait() + + return nil +} diff --git a/internal/support/k8s/k8s_service.go b/internal/support/k8s/k8s_service.go index 5cc54266..3efcebb6 100644 --- a/internal/support/k8s/k8s_service.go +++ b/internal/support/k8s/k8s_service.go @@ -90,3 +90,11 @@ func (k *K8sClientService) SubscribeEvents(ctx context.Context, events chan<- co func (k *K8sClientService) SubscribeContainersStarted(ctx context.Context, containers chan<- container.Container) { k.store.SubscribeNewContainers(ctx, containers) } + +func (k *K8sClientService) Attach(ctx context.Context, container container.Container, stdin io.Reader, stdout io.Writer) error { + panic("not implemented") +} + +func (k *K8sClientService) Exec(ctx context.Context, container container.Container, cmd []string, stdin io.Reader, stdout io.Writer) error { + panic("not implemented") +} diff --git a/internal/web/routes.go b/internal/web/routes.go index 70ccc669..8ce7b83c 100644 --- a/internal/web/routes.go +++ b/internal/web/routes.go @@ -107,6 +107,8 @@ func createRouter(h *handler) *chi.Mux { r.Get("/hosts/{host}/containers/{id}/logs/stream", h.streamContainerLogs) r.Get("/hosts/{host}/logs/stream", h.streamHostLogs) r.Get("/hosts/{host}/containers/{id}/logs", h.fetchLogsBetweenDates) + r.Get("/hosts/{host}/containers/{id}/attach", h.attach) + r.Get("/hosts/{host}/containers/{id}/exec", h.exec) r.Get("/hosts/{host}/logs/mergedStream/{ids}", h.streamLogsMerged) r.Get("/containers/{hostIds}/download", h.downloadLogs) // formatted as host:container,host:container r.Get("/stacks/{stack}/logs/stream", h.streamStackLogs) diff --git a/internal/web/terminal.go b/internal/web/terminal.go new file mode 100644 index 00000000..67c6232c --- /dev/null +++ b/internal/web/terminal.go @@ -0,0 +1,120 @@ +package web + +import ( + "net/http" + + "github.com/amir20/dozzle/internal/auth" + "github.com/go-chi/chi/v5" + "github.com/gorilla/websocket" + "github.com/rs/zerolog/log" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +func (h *handler) attach(w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Error().Err(err).Msg("error while trying to upgrade connection") + return + } + defer conn.Close() + + id := chi.URLParam(r, "id") + userLabels := h.config.Labels + if h.config.Authorization.Provider != NONE { + user := auth.UserFromContext(r.Context()) + if user.ContainerLabels.Exists() { + userLabels = user.ContainerLabels + } + } + + containerService, err := h.hostService.FindContainer(hostKey(r), id, userLabels) + + if err != nil { + log.Error().Err(err).Msg("error while trying to find container") + return + } + + wsReader := &webSocketReader{conn: conn} + wsWriter := &webSocketWriter{conn: conn} + if err = containerService.Attach(r.Context(), wsReader, wsWriter); err != nil { + log.Error().Err(err).Msg("error while trying to attach to container") + conn.WriteMessage(websocket.TextMessage, []byte("🚨 Error while trying to attach to container\r\n")) + return + } +} + +func (h *handler) exec(w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Error().Err(err).Msg("error while trying to upgrade connection") + return + } + defer conn.Close() + + id := chi.URLParam(r, "id") + userLabels := h.config.Labels + if h.config.Authorization.Provider != NONE { + user := auth.UserFromContext(r.Context()) + if user.ContainerLabels.Exists() { + userLabels = user.ContainerLabels + } + } + + containerService, err := h.hostService.FindContainer(hostKey(r), id, userLabels) + if err != nil { + log.Error().Err(err).Msg("error while trying to find container") + return + } + + wsReader := &webSocketReader{conn: conn} + wsWriter := &webSocketWriter{conn: conn} + if err = containerService.Exec(r.Context(), []string{"sh", "-c", "command -v bash >/dev/null 2>&1 && exec bash || exec sh"}, wsReader, wsWriter); err != nil { + log.Error().Err(err).Msg("error while trying to attach to container") + conn.WriteMessage(websocket.TextMessage, []byte("🚨 Error while trying to attach to container\r\n")) + return + } +} + +type webSocketWriter struct { + conn *websocket.Conn +} + +func (w *webSocketWriter) Write(p []byte) (int, error) { + err := w.conn.WriteMessage(websocket.TextMessage, p) + return len(p), err +} + +type webSocketReader struct { + conn *websocket.Conn + buffer []byte +} + +func (r *webSocketReader) Read(p []byte) (n int, err error) { + if len(r.buffer) > 0 { + n = copy(p, r.buffer) + r.buffer = r.buffer[n:] + return n, nil + } + + // Otherwise, read a new message + _, message, err := r.conn.ReadMessage() + if err != nil { + return 0, err + } + + n = copy(p, message) + + // If we couldn't copy the entire message, store the rest in our buffer + if n < len(message) { + r.buffer = message[n:] + } + + return n, nil +} diff --git a/package.json b/package.json index 412f4c38..06654283 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,8 @@ "@vueuse/core": "^13.0.0", "@vueuse/integrations": "^13.0.0", "@vueuse/router": "^13.0.0", + "@xterm/addon-web-links": "^0.11.0", + "@xterm/xterm": "^5.5.0", "ansi-to-html": "^0.7.2", "d3-array": "^3.2.4", "d3-ease": "^3.0.1", @@ -79,6 +81,8 @@ }, "devDependencies": { "@apache-arrow/esnext-esm": "^19.0.1", + "@iconify-json/material-symbols-light": "^1.2.17", + "@iconify-json/ri": "^1.2.5", "@pinia/testing": "^1.0.0", "@playwright/test": "^1.51.1", "@types/d3-array": "^3.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1088985..ff754727 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,12 @@ importers: '@vueuse/router': specifier: ^13.0.0 version: 13.0.0(vue-router@4.5.0(vue@3.5.13(typescript@5.8.2)))(vue@3.5.13(typescript@5.8.2)) + '@xterm/addon-web-links': + specifier: ^0.11.0 + version: 0.11.0(@xterm/xterm@5.5.0) + '@xterm/xterm': + specifier: ^5.5.0 + version: 5.5.0 ansi-to-html: specifier: ^0.7.2 version: 0.7.2 @@ -153,6 +159,12 @@ importers: '@apache-arrow/esnext-esm': specifier: ^19.0.1 version: 19.0.1 + '@iconify-json/material-symbols-light': + specifier: ^1.2.17 + version: 1.2.17 + '@iconify-json/ri': + specifier: ^1.2.5 + version: 1.2.5 '@pinia/testing': specifier: ^1.0.0 version: 1.0.0(pinia@3.0.1(typescript@5.8.2)(vue@3.5.13(typescript@5.8.2))) @@ -939,6 +951,9 @@ packages: '@iconify-json/ic@1.2.2': resolution: {integrity: sha512-QmjwS3lYiOmVWgTCEOTFyGODaR/+689+ajep/VsrCcsUN0Gdle5PmIcibDsdmRyrOsW/E77G41UUijdbjQUofw==} + '@iconify-json/material-symbols-light@1.2.17': + resolution: {integrity: sha512-sq8pSITGy15SjJ6FuOr/oH/8emJPcukdSJJeuL9YHZ44KKQD0ITL7ZO44TAQxdOsef0ptZbhPWxSwCqK7ytrxQ==} + '@iconify-json/material-symbols@1.2.17': resolution: {integrity: sha512-hKb+Ii5cqLXXefYMxUB2jIc8BNqxixQogud4KU/fn0F4puM1iCdCF2lFV+0U8wnJ6dZIx6E+w8Ree4bIT7To+A==} @@ -954,6 +969,9 @@ packages: '@iconify-json/ph@1.2.2': resolution: {integrity: sha512-PgkEZNtqa8hBGjHXQa4pMwZa93hmfu8FUSjs/nv4oUU6yLsgv+gh9nu28Kqi8Fz9CCVu4hj1MZs9/60J57IzFw==} + '@iconify-json/ri@1.2.5': + resolution: {integrity: sha512-kWGimOXMZrlYusjBKKXYOWcKhbOHusFsmrmRGmjS7rH0BpML5A9/fy8KHZqFOwZfC4M6amObQYbh8BqO5cMC3w==} + '@iconify-json/simple-icons@1.2.23': resolution: {integrity: sha512-ySyZ0ZXdNveWnR71t7XGV7jhknxSlTtpM2TyIR1cUHTUzZLP36hYHTNqb2pYYsCzH5ed85KTTKz7vYT33FyNIQ==} @@ -1882,6 +1900,14 @@ packages: peerDependencies: vue: ^3.5.0 + '@xterm/addon-web-links@0.11.0': + resolution: {integrity: sha512-nIHQ38pQI+a5kXnRaTgwqSHnX7KE6+4SVoceompgHL26unAxdfP6IPqUTSYPQgSwM56hsElfoNrrW5V7BUED/Q==} + peerDependencies: + '@xterm/xterm': ^5.0.0 + + '@xterm/xterm@5.5.0': + resolution: {integrity: sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==} + abbrev@2.0.0: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -4596,6 +4622,10 @@ snapshots: dependencies: '@iconify/types': 2.0.0 + '@iconify-json/material-symbols-light@1.2.17': + dependencies: + '@iconify/types': 2.0.0 + '@iconify-json/material-symbols@1.2.17': dependencies: '@iconify/types': 2.0.0 @@ -4616,6 +4646,10 @@ snapshots: dependencies: '@iconify/types': 2.0.0 + '@iconify-json/ri@1.2.5': + dependencies: + '@iconify/types': 2.0.0 + '@iconify-json/simple-icons@1.2.23': dependencies: '@iconify/types': 2.0.0 @@ -5609,6 +5643,12 @@ snapshots: dependencies: vue: 3.5.13(typescript@5.8.2) + '@xterm/addon-web-links@0.11.0(@xterm/xterm@5.5.0)': + dependencies: + '@xterm/xterm': 5.5.0 + + '@xterm/xterm@5.5.0': {} + abbrev@2.0.0: {} acorn-jsx@5.3.2(acorn@8.14.1):