Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 112 additions & 18 deletions cmd/lk/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
Expand All @@ -26,6 +27,9 @@ import (
"time"

"github.com/charmbracelet/huh"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/crane"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/twitchtv/twirp"
"github.com/urfave/cli/v3"

Expand Down Expand Up @@ -97,6 +101,16 @@ var (
Required: false,
}

agentPrebuiltImageFlag = &cli.StringFlag{
Name: "image",
Usage: "Pre-built image from the local Docker daemon (e.g. myimage:latest). Requires Docker.",
}

agentPrebuiltImageTarFlag = &cli.StringFlag{
Name: "image-tar",
Usage: "Pre-built image from an OCI tar file (e.g. ./image.tar). No Docker daemon required.",
}

skipSDKCheckFlag = &cli.BoolFlag{
Name: "skip-sdk-check",
Required: false,
Expand Down Expand Up @@ -169,6 +183,8 @@ var (
silentFlag,
regionFlag,
skipSDKCheckFlag,
agentPrebuiltImageFlag,
agentPrebuiltImageTarFlag,
},
// NOTE: since secrets may contain commas, or indeed any special character we might want to treat as a flag separator,
// we disable it entirely here and require multiple --secrets flags to be used.
Expand Down Expand Up @@ -214,6 +230,8 @@ var (
regionFlag,
ignoreEmptySecretsFlag,
skipSDKCheckFlag,
agentPrebuiltImageFlag,
agentPrebuiltImageTarFlag,
},
// NOTE: since secrets may contain commas, or indeed any special character we might want to treat as a flag separator,
// we disable it entirely here and require multiple --secrets flags to be used.
Expand Down Expand Up @@ -585,6 +603,26 @@ func createAgent(ctx context.Context, cmd *cli.Command) error {
buildContext, cancel := context.WithTimeout(ctx, buildTimeout)
defer cancel()
regions := []string{region}

// --image or --image-tar: register the agent record then push the prebuilt image
imageRef := cmd.String("image")
imageTar := cmd.String("image-tar")
if imageRef != "" || imageTar != "" {
agentID, err := agentsClient.RegisterAgent(buildContext, secrets, regions)
if err != nil {
if twerr, ok := err.(twirp.Error); ok {
return fmt.Errorf("unable to create agent: %s", twerr.Msg())
}
return fmt.Errorf("unable to create agent: %w", err)
}
lkConfig.Agent.ID = agentID
if err := lkConfig.SaveTOMLFile(workingDir, tomlFilename); err != nil {
return err
}
fmt.Printf("Created agent with ID [%s]\n", util.Accented(agentID))
return deployPrebuiltImage(buildContext, agentID, imageRef, imageTar)
}

excludeFiles := []string{fmt.Sprintf("**/%s", config.LiveKitTOMLFile)}
resp, err := agentsClient.CreateAgent(buildContext, os.DirFS(workingDir), secrets, regions, excludeFiles, os.Stderr)
if err != nil {
Expand Down Expand Up @@ -702,22 +740,25 @@ func createAgentConfig(ctx context.Context, cmd *cli.Command) error {
}

func deployAgent(ctx context.Context, cmd *cli.Command) error {
// If no agent exists yet (no --id and no config with agent), do first-time create (which deploys).
if cmd.String("id") == "" {
configExists, err := requireConfig(workingDir, tomlFilename)
if err != nil && configExists {
return err
}
if !configExists || lkConfig == nil || !lkConfig.HasAgent() {
return createAgent(ctx, cmd)
}
}

agentId, err := getAgentID(ctx, cmd, workingDir, tomlFilename, false)
if err != nil {
return err
}

buildContext, cancel := context.WithTimeout(ctx, buildTimeout)
defer cancel()

// --image or --image-tar: skip source build and push a prebuilt image via the OCI proxy.
imageRef := cmd.String("image")
imageTar := cmd.String("image-tar")
if imageRef != "" || imageTar != "" {
if err := deployPrebuiltImage(buildContext, agentId, imageRef, imageTar); err != nil {
return fmt.Errorf("unable to deploy prebuilt image: %w", err)
}
fmt.Println("Deployed agent")
return nil
}

secrets, err := requireSecrets(ctx, cmd, false, true)
if err != nil {
return err
Expand All @@ -742,8 +783,6 @@ func deployAgent(ctx context.Context, cmd *cli.Command) error {
}
}

buildContext, cancel := context.WithTimeout(ctx, buildTimeout)
defer cancel()
excludeFiles := []string{fmt.Sprintf("**/%s", config.LiveKitTOMLFile)}
if err := agentsClient.DeployAgent(buildContext, agentId, os.DirFS(workingDir), secrets, excludeFiles, os.Stderr); err != nil {
if twerr, ok := err.(twirp.Error); ok {
Expand All @@ -756,6 +795,45 @@ func deployAgent(ctx context.Context, cmd *cli.Command) error {
return nil
}

// deployPrebuiltImage pushes a locally-built image through the cloud-agents OCI proxy.
// Exactly one of imageRef (Docker daemon via the Docker API) or imageTar must be non-empty.
func deployPrebuiltImage(ctx context.Context, agentID, imageRef, imageTar string) error {
target, err := agentsClient.GetPushTarget(ctx, agentID)
if err != nil {
return fmt.Errorf("failed to get push target: %w", err)
}

var img v1.Image
if imageRef != "" {
imageRef = strings.TrimSpace(imageRef)
fmt.Printf("Loading image from Docker daemon [%s]\n", util.Accented(imageRef))
var dockerCloser io.Closer
img, dockerCloser, err = agentfs.LoadDockerDaemonImage(ctx, imageRef)
if err != nil {
return err
}
defer dockerCloser.Close()
} else {
fmt.Printf("Loading image from [%s]\n", util.Accented(imageTar))
img, err = crane.Load(imageTar)
if err != nil {
return fmt.Errorf("failed to load image: %w", err)
}
}

proxyRef := fmt.Sprintf("%s/%s:%s", target.ProxyHost, target.Name, target.Tag)
fmt.Printf("Pushing image [%s]\n", util.Accented(proxyRef))

rt := agentsClient.NewRegistryTransport()
if err := crane.Push(img, proxyRef,
crane.WithTransport(rt),
crane.WithAuth(authn.Anonymous),
); err != nil {
return fmt.Errorf("failed to push image: %w", err)
}
return nil
}

func getAgentStatus(ctx context.Context, cmd *cli.Command) error {
agentID, err := getAgentID(ctx, cmd, workingDir, tomlFilename, false)
if err != nil {
Expand Down Expand Up @@ -1005,21 +1083,37 @@ func listAgentVersions(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("unable to list agent versions: %w", err)
}

table := util.CreateTable().
Headers("Version", "Current", "Status", "Created At", "Deployed At")

// Sort versions by created date descending
slices.SortFunc(versions.Versions, func(a, b *lkproto.AgentVersion) int {
return b.CreatedAt.AsTime().Compare(a.CreatedAt.AsTime())
})

showDigest := false
for _, v := range versions.Versions {
if v.Attributes["image_digest"] != "" {
showDigest = true
break
}
}

headers := []string{"Version", "Current", "Status", "Created At", "Deployed At"}
if showDigest {
headers = append(headers, "Digest")
}
table := util.CreateTable().Headers(headers...)

for _, version := range versions.Versions {
table.Row(
row := []string{
version.Version,
fmt.Sprintf("%t", version.Current),
version.Status,
version.CreatedAt.AsTime().Format(time.RFC3339),
version.DeployedAt.AsTime().Format(time.RFC3339),
)
}
if showDigest {
row = append(row, version.Attributes["image_digest"])
}
table.Row(row...)
}

fmt.Println(table)
Expand Down
10 changes: 10 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ require (
github.com/charmbracelet/huh v1.0.0
github.com/charmbracelet/huh/spinner v0.0.0-20260223110133-9dc45e34a40b
github.com/charmbracelet/lipgloss v1.1.0
github.com/docker/docker v28.2.2+incompatible
github.com/frostbyte73/core v0.1.1
github.com/go-logr/logr v1.4.3
github.com/go-task/task/v3 v3.49.1
github.com/google/go-containerregistry v0.20.6
github.com/google/go-querystring v1.2.0
github.com/joho/godotenv v1.5.1
github.com/livekit/protocol v1.45.4-0.20260417173102-5d4f88f73b7c
Expand Down Expand Up @@ -107,13 +109,19 @@ require (
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v1.0.0-rc.2 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.17.0 // indirect
github.com/containerd/ttrpc v1.2.7 // indirect
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dennwc/iters v1.2.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/docker/cli v29.0.0+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dominikbraun/graph v0.23.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
Expand Down Expand Up @@ -165,6 +173,7 @@ require (
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/moby/buildkit v0.26.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/sys/signal v0.7.1 // indirect
github.com/morikuni/aec v1.0.0 // indirect
Expand Down Expand Up @@ -217,6 +226,7 @@ require (
github.com/u-root/u-root v0.16.0 // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/vbatts/tar-split v0.12.2 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
Expand Down
19 changes: 14 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ=
Expand Down Expand Up @@ -204,7 +206,6 @@ github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6a
github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=
github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y=
github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8=
github.com/containerd/stargz-snapshotter v0.17.0 h1:djNS4KU8ztFhLdEDZ1bsfzOiYuVHT6TgSU5qwRk+cNc=
github.com/containerd/stargz-snapshotter/estargz v0.17.0 h1:+TyQIsR/zSFI1Rm31EQBwpAA1ovYgIKHy7kctL3sLcE=
github.com/containerd/stargz-snapshotter/estargz v0.17.0/go.mod h1:s06tWAiJcXQo9/8AReBCIo/QxcXFZ2n4qfsRnpl71SM=
github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ=
Expand All @@ -229,6 +230,10 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli v29.0.0+incompatible h1:KgsN2RUFMNM8wChxryicn4p46BdQWpXOA1XLGBGPGAw=
github.com/docker/cli v29.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
Expand Down Expand Up @@ -298,6 +303,8 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU=
github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
Expand Down Expand Up @@ -359,10 +366,6 @@ github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 h1:9x+U2HGLrSw5AT
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ=
github.com/livekit/mediatransportutil v0.0.0-20260309115634-0e2e24b36ee8 h1:coWig9fKxdb/nwOaIoGUUAogso12GblAJh/9SA9hcxk=
github.com/livekit/mediatransportutil v0.0.0-20260309115634-0e2e24b36ee8/go.mod h1:RCd46PT+6sEztld6XpkCrG1xskb0u3SqxIjy4G897Ss=
github.com/livekit/protocol v1.45.4-0.20260414175434-28604d1045cd h1:T6z+yCaQGs8C+fSWXo8ZfM2qUyEpoC6Fbaa/dR3JodU=
github.com/livekit/protocol v1.45.4-0.20260414175434-28604d1045cd/go.mod h1:e6QdWDkfot+M2nRh0eitJUS0ZLuwvKCsfiz2pWWSG3s=
github.com/livekit/protocol v1.45.4-0.20260417165950-df0615eb700a h1:w4uHb432qHLCbyLoCHzaVwiVXvh6sCeHTkF7hIM8S9I=
github.com/livekit/protocol v1.45.4-0.20260417165950-df0615eb700a/go.mod h1:e6QdWDkfot+M2nRh0eitJUS0ZLuwvKCsfiz2pWWSG3s=
github.com/livekit/protocol v1.45.4-0.20260417173102-5d4f88f73b7c h1:TusqCkqnqw/9B6BVJhFjFI8D/1Zy6z4oBsk1ehbpZIc=
github.com/livekit/protocol v1.45.4-0.20260417173102-5d4f88f73b7c/go.mod h1:e6QdWDkfot+M2nRh0eitJUS0ZLuwvKCsfiz2pWWSG3s=
github.com/livekit/psrpc v0.7.1 h1:ms37az0QTD3UXIWuUC5D/SkmKOlRMVRsI261eBWu/Vw=
Expand Down Expand Up @@ -397,6 +400,8 @@ github.com/moby/moby/client v0.1.0 h1:nt+hn6O9cyJQqq5UWnFGqsZRTS/JirUqzPjEl0Bdc/
github.com/moby/moby/client v0.1.0/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
Expand Down Expand Up @@ -595,6 +600,8 @@ go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 h1:lSZHgNHfbmQTPfuTmWVkEu8J8qXaQwuV30pjCcAUvP8=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0/go.mod h1:so9ounLcuoRDu033MW/E0AD4hhUjVqswrMF5FoZlBcw=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
Expand Down Expand Up @@ -724,6 +731,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
mvdan.cc/sh/moreinterp v0.0.0-20260120230322-19def062a997 h1:3bbJwtPFh98dJ6lxRdR3eLHTH1CmR3BcU6TriIMiXjE=
Expand Down
Loading
Loading