Merge remote-tracking branch 'origin/main' into staging

This commit is contained in:
Andrey Antukh 2026-05-27 11:03:34 +02:00
commit f6c76711f4
14 changed files with 326 additions and 190 deletions

1
.gitignore vendored
View File

@ -89,3 +89,4 @@
/.idea
/.claude
/.playwright-mcp
/tools/__pycache__

View File

@ -134,7 +134,11 @@
- Fix incorrect error message when applying tokens while editing text [#9620](https://github.com/penpot/penpot/issues/9620) (PR: [#9708](https://github.com/penpot/penpot/pull/9708))
## 2.15.4 (Unreleased)
## 2.15.4
### :sparkles: New features & Enhancements
- Add rate limiting and concurrency safety for file snapshot operations [#9723](https://github.com/penpot/penpot/issues/9723) (PR: [#9722](https://github.com/penpot/penpot/pull/9722))
### :bug: Bugs fixed
@ -143,6 +147,7 @@
- Fix API doc endpoint returning HTML as text/plain [#9680](https://github.com/penpot/penpot/issues/9680) (PR: [#9681](https://github.com/penpot/penpot/pull/9681))
- Fix unexpected error when opening the export dialog [#9721](https://github.com/penpot/penpot/issues/9721) (PR: [#9704](https://github.com/penpot/penpot/pull/9704))
## 2.15.3
### :bug: Bugs fixed

View File

@ -19,7 +19,7 @@
{:permits 40}
:root/by-profile
{:permits 10}
{:permits 10 :queue 30 :timeout 30000}
:file-thumbnail-ops/global
{:permits 20}
@ -27,4 +27,16 @@
{:permits 2}
:submit-audit-events/by-profile
{:permits 1 :queue 3}}
{:permits 1 :queue 3}
:restore-file-snapshot/global
{:permits 3}
:restore-file-snapshot/by-profile
{:permits 1 :queue 2 :timeout 60000}
:create-file-snapshot/global
{:permits 3}
:create-file-snapshot/by-profile
{:permits 1 :queue 2 :timeout 60000}}

View File

@ -27,7 +27,9 @@
[next.jdbc.transaction])
(:import
com.zaxxer.hikari.HikariConfig
com.zaxxer.hikari.HikariConfigMXBean
com.zaxxer.hikari.HikariDataSource
com.zaxxer.hikari.HikariPoolMXBean
com.zaxxer.hikari.metrics.prometheus.PrometheusMetricsTrackerFactory
io.whitfin.siphash.SipHasher
io.whitfin.siphash.SipHasherContainer
@ -67,9 +69,8 @@
(def defaults
{::name :main
::min-size 0
::max-size 60
::connection-timeout 10000
::connection-timeout 30000
::validation-timeout 10000
::idle-timeout 120000 ; 2min
::max-lifetime 1800000 ; 30m
@ -82,7 +83,7 @@
(defmethod ig/init-key ::pool
[_ cfg]
(let [{:keys [::uri ::read-only] :as cfg}
(merge defaults cfg)]
(merge defaults (d/without-nils cfg))]
(when uri
(l/info :hint "initialize connection pool"
:name (d/name (::name cfg))
@ -90,7 +91,8 @@
:read-only read-only
:credentials (and (contains? cfg ::username)
(contains? cfg ::password))
:min-size (::min-size cfg)
:min-size (or (::min-size cfg)
(::max-size cfg))
:max-size (::max-size cfg))
(create-pool cfg))))
@ -111,7 +113,9 @@
[{:keys [::uri] :as cfg}]
;; (app.common.pprint/pprint cfg)
(let [config (HikariConfig.)]
(let [config (HikariConfig.)
max-size (::max-size cfg)
min-size (or (::min-size cfg) max-size)]
(doto config
(.setJdbcUrl (str "jdbc:" uri))
(.setPoolName (d/name (::name cfg)))
@ -121,8 +125,8 @@
(.setValidationTimeout (::validation-timeout cfg))
(.setIdleTimeout (::idle-timeout cfg))
(.setMaxLifetime (::max-lifetime cfg))
(.setMinimumIdle (::min-size cfg))
(.setMaximumPoolSize (::max-size cfg))
(.setMinimumIdle min-size)
(.setMaximumPoolSize max-size)
(.setConnectionInitSql initsql)
(.setInitializationFailTimeout -1))
@ -180,6 +184,20 @@
:code :invalid-connection
:hint "invalid connection provided")))
(defn pool-stats
"Given a HikariDataSource instance, returns a map with current pool
statistics: active/idle connections, threads awaiting connection,
total connections, maximum pool size, and minimum idle connections."
[^HikariDataSource pool]
(let [^HikariPoolMXBean pool-mxbean (.getHikariPoolMXBean pool)
^HikariConfigMXBean cfg-mxbean (.getHikariConfigMXBean pool)]
{:active-connections (.getActiveConnections pool-mxbean)
:idle-connections (.getIdleConnections pool-mxbean)
:threads-awaiting-connection (.getThreadsAwaitingConnection pool-mxbean)
:total-connections (.getTotalConnections pool-mxbean)
:maximum-pool-size (.getMaximumPoolSize cfg-mxbean)
:minimum-idle (.getMinimumIdle cfg-mxbean)}))
(defn create-pool
[cfg]
(let [dsc (create-datasource-config cfg)]

View File

@ -66,11 +66,6 @@
LEFT JOIN file_data AS fd ON (fd.file_id = f.id AND fd.id = f.id)
WHERE f.id = ?")
(defn- get-minimal-file
[cfg id & {:as opts}]
(-> (db/get-with-sql cfg [sql:get-minimal-file id] opts)
(d/update-when :metadata fdata/decode-metadata)))
(def ^:private sql:get-snapshot-without-data
(str "WITH snapshots AS (" sql:snapshots ")"
"SELECT c.id,
@ -112,7 +107,7 @@
THEN (c.deleted_at IS NULL OR c.deleted_at >= ?::timestamptz)
END"))
(defn get-snapshot-data
(defn get-snapshot
"Get a fully decoded snapshot for read-only preview or restoration.
Returns the snapshot map with decoded :data field."
[cfg file-id snapshot-id]
@ -320,79 +315,87 @@
(defn restore!
[{:keys [::db/conn] :as cfg} file-id snapshot-id]
(let [file (get-minimal-file conn file-id {::db/for-update true})
vern (rand-int Integer/MAX_VALUE)
(let [lock-sql (str sql:get-minimal-file " FOR UPDATE OF f SKIP LOCKED")
row (db/exec-one! conn [lock-sql file-id])]
storage
(sto/resolve cfg {::db/reuse-conn true})
(when-not row
(ex/raise :type :conflict
:code :file-locked
:hint "the file is currently locked by another operation, retry later"))
snapshot
(get-snapshot-data cfg file-id snapshot-id)]
(let [file (d/update-when row :metadata fdata/decode-metadata)
vern (rand-int Integer/MAX_VALUE)
(when-not snapshot
(ex/raise :type :not-found
:code :snapshot-not-found
:hint "unable to find snapshot with the provided label"
:snapshot-id snapshot-id
:file-id file-id))
storage
(sto/resolve cfg {::db/reuse-conn true})
(when-not (:data snapshot)
(ex/raise :type :internal
:code :snapshot-without-data
:hint "snapshot has no data"
:label (:label snapshot)
:file-id file-id))
snapshot
(get-snapshot cfg file-id snapshot-id)]
(let [;; If the snapshot has applied migrations stored, we reuse
;; them, if not, we take a safest set of migrations as
;; starting point. This is because, at the time of
;; implementing snapshots, migrations were not taken into
;; account so we need to make this backward compatible in
;; some way.
migrations
(or (:migrations snapshot)
(fmg/generate-migrations-from-version 67))
(when-not snapshot
(ex/raise :type :not-found
:code :snapshot-not-found
:hint "unable to find snapshot with the provided label"
:snapshot-id snapshot-id
:file-id file-id))
file
(-> file
(update :revn inc)
(assoc :migrations migrations)
(assoc :data (:data snapshot))
(assoc :vern vern)
(assoc :version (:version snapshot))
(assoc :has-media-trimmed false)
(assoc :modified-at (:modified-at snapshot))
(assoc :features (:features snapshot)))]
(when-not (:data snapshot)
(ex/raise :type :internal
:code :snapshot-without-data
:hint "snapshot has no data"
:label (:label snapshot)
:file-id file-id))
(l/dbg :hint "restoring snapshot"
:file-id (str file-id)
:label (:label snapshot)
:snapshot-id (str (:id snapshot)))
(let [;; If the snapshot has applied migrations stored, we reuse
;; them, if not, we take a safest set of migrations as
;; starting point. This is because, at the time of
;; implementing snapshots, migrations were not taken into
;; account so we need to make this backward compatible in
;; some way.
migrations
(or (:migrations snapshot)
(fmg/generate-migrations-from-version 67))
;; In the same way, on reseting the file data, we need to restore
;; the applied migrations on the moment of taking the snapshot
(bfc/update-file! cfg file ::bfc/reset-migrations? true)
file
(-> file
(update :revn inc)
(assoc :migrations migrations)
(assoc :data (:data snapshot))
(assoc :vern vern)
(assoc :version (:version snapshot))
(assoc :has-media-trimmed false)
(assoc :modified-at (:modified-at snapshot))
(assoc :features (:features snapshot)))]
;; FIXME: this should be separated functions, we should not have
;; inline sql here.
(l/dbg :hint "restoring snapshot"
:file-id (str file-id)
:label (:label snapshot)
:snapshot-id (str (:id snapshot)))
;; clean object thumbnails
(let [sql (str "update file_tagged_object_thumbnail "
" set deleted_at = now() "
" where file_id=? returning media_id")
res (db/exec! conn [sql file-id])]
(doseq [media-id (into #{} (keep :media-id) res)]
(sto/touch-object! storage media-id)))
;; In the same way, on reseting the file data, we need to restore
;; the applied migrations on the moment of taking the snapshot
(bfc/update-file! cfg file ::bfc/reset-migrations? true)
;; clean file thumbnails
(let [sql (str "update file_thumbnail "
" set deleted_at = now() "
" where file_id=? returning media_id")
res (db/exec! conn [sql file-id])]
(doseq [media-id (into #{} (keep :media-id) res)]
(sto/touch-object! storage media-id)))
;; FIXME: this should be separated functions, we should not have
;; inline sql here.
vern)))
;; clean object thumbnails
(let [sql (str "update file_tagged_object_thumbnail "
" set deleted_at = now() "
" where file_id=? returning media_id")
res (db/exec! conn [sql file-id])]
(doseq [media-id (into #{} (keep :media-id) res)]
(sto/touch-object! storage media-id)))
;; clean file thumbnails
(let [sql (str "update file_thumbnail "
" set deleted_at = now() "
" where file_id=? returning media_id")
res (db/exec! conn [sql file-id])]
(doseq [media-id (into #{} (keep :media-id) res)]
(sto/touch-object! storage media-id)))
vern))))
(defn delete!
[cfg & {:keys [id file-id deleted-at]}]

View File

@ -154,8 +154,8 @@
::db/username (cf/get :database-username)
::db/password (cf/get :database-password)
::db/read-only (cf/get :database-readonly false)
::db/min-size (cf/get :database-min-pool-size 0)
::db/max-size (cf/get :database-max-pool-size 60)
::db/min-size (cf/get :database-min-pool-size)
::db/max-size (cf/get :database-max-pool-size)
::mtx/metrics (ig/ref ::mtx/metrics)}
;; Default netty IO pool (shared between several services)

View File

@ -18,6 +18,7 @@
[app.main :as-alias main]
[app.msgbus :as mbus]
[app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit]
[app.rpc.commands.files :as files]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
@ -54,7 +55,7 @@
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id id] :as params}]
(let [perms (bfc/get-file-permissions conn profile-id file-id)]
(files/check-read-permissions! perms)
(let [snapshot (fsnap/get-snapshot-data cfg file-id id)]
(let [snapshot (fsnap/get-snapshot cfg file-id id)]
(when-not snapshot
(ex/raise :type :not-found
:code :snapshot-not-found
@ -81,9 +82,10 @@
(sv/defmethod ::create-file-snapshot
{::doc/added "1.20"
::sm/params schema:create-file-snapshot
::db/transaction true}
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id label]}]
(files/check-edition-permissions! conn profile-id file-id)
::climit/id [[:create-file-snapshot/by-profile ::rpc/profile-id]
[:create-file-snapshot/global]]}
[cfg {:keys [::rpc/profile-id file-id label]}]
(files/check-edition-permissions! cfg profile-id file-id)
(let [file (bfc/get-file cfg file-id :realize? true)
project (db/get-by-id cfg :project (:project-id file))]
@ -95,10 +97,10 @@
(quotes/check! {::quotes/id ::quotes/snapshots-per-file}
{::quotes/id ::quotes/snapshots-per-team}))
(fsnap/create! cfg file
{:label label
:profile-id profile-id
:created-by "user"})))
(db/tx-run! cfg fsnap/create! file
{:label label
:profile-id profile-id
:created-by "user"})))
(def ^:private schema:restore-file-snapshot
[:map {:title "restore-file-snapshot"}
@ -108,29 +110,43 @@
(sv/defmethod ::restore-file-snapshot
{::doc/added "1.20"
::sm/params schema:restore-file-snapshot
::db/transaction true}
[{:keys [::db/conn ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id ::rpc/session-id file-id id] :as params}]
(files/check-edition-permissions! conn profile-id file-id)
::climit/id [[:restore-file-snapshot/by-profile ::rpc/profile-id]
[:restore-file-snapshot/global]]}
[{:keys [::db/pool ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id ::rpc/session-id file-id id] :as params}]
;; Check permissions and read current file state (short-lived, outside restore transaction)
(files/check-edition-permissions! pool profile-id file-id)
(let [file (bfc/get-file cfg file-id)
team (teams/get-team conn
team (teams/get-team pool
:profile-id profile-id
:file-id file-id)
delay (ldel/get-deletion-delay team)]
delay (ldel/get-deletion-delay team)
file-revn (:revn file)]
;; Create backup snapshot of the current state (committed immediately
;; independently of the restore outcome)
(fsnap/create! cfg file
{:profile-id profile-id
:deleted-at (ct/in-future delay)
:created-by "system"})
(let [vern (fsnap/restore! cfg file-id id)]
;; Send to the clients a notification to reload the file
(mbus/pub! msgbus
:topic (:id file)
:message {:type :file-restored
:session-id session-id
:file-id (:id file)
:vern vern})
nil)))
;; Restore snapshot inside its own transaction; the revn check
;; ensures no data is lost if the file was edited concurrently
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(let [current (bfc/get-minimal-file conn file-id {::db/for-update true})]
(when (not= (:revn current) file-revn)
(ex/raise :type :conflict
:code :file-modified
:hint "the file was modified during the restore process, please retry")))
(let [vern (fsnap/restore! cfg file-id id)]
(mbus/pub! msgbus
:topic (:id file)
:message {:type :file-restored
:session-id session-id
:file-id (:id file)
:vern vern})
nil)))))
(def ^:private schema:update-file-snapshot
[:map {:title "update-file-snapshot"}

View File

@ -0,0 +1,43 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns backend-tests.db-test
(:require
[app.db :as db]
[backend-tests.helpers :as th]
[clojure.test :as t])
(:import
com.zaxxer.hikari.HikariConfig
com.zaxxer.hikari.HikariDataSource
java.sql.Connection))
(t/use-fixtures :once th/state-init)
(t/deftest pool-stats-returns-expected-keys
(let [stats (db/pool-stats th/*pool*)]
(t/testing "all expected keys are present"
(t/is (contains? stats :active-connections))
(t/is (contains? stats :idle-connections))
(t/is (contains? stats :threads-awaiting-connection))
(t/is (contains? stats :total-connections))
(t/is (contains? stats :maximum-pool-size))
(t/is (contains? stats :minimum-idle)))
(t/testing "values are non-negative integers"
(t/is (>= (:active-connections stats) 0))
(t/is (>= (:idle-connections stats) 0))
(t/is (>= (:threads-awaiting-connection stats) 0))
(t/is (>= (:total-connections stats) 0))
(t/is (>= (:maximum-pool-size stats) 0))
(t/is (>= (:minimum-idle stats) 0)))
(t/testing "total connections equals active + idle"
(t/is (= (:total-connections stats)
(+ (:active-connections stats)
(:idle-connections stats)))))
(t/testing "maximum pool size is reasonable"
(t/is (pos? (:maximum-pool-size stats))))))

View File

@ -89,6 +89,7 @@ services:
depends_on:
- penpot-backend
- penpot-exporter
- penpot-mcp
networks:
- penpot
@ -106,7 +107,8 @@ services:
environment:
<< : [*penpot-flags, *penpot-http-body-size, *penpot-public-uri]
# Set to "true" on hosts where IPv6 is disabled at kernel boot level.
# PENPOT_DISABLE_IPV6_LISTEN: "true"
penpot-backend:
image: "penpotapp/backend:${PENPOT_VERSION:-2.15}"
restart: always

View File

@ -46,7 +46,11 @@ export PENPOT_NITRATE_URI=${PENPOT_NITRATE_URI:-http://penpot-nitrate:3000}
export PENPOT_MCP_URI=${PENPOT_MCP_URI:-http://penpot-mcp:4401}
export PENPOT_MCP_URI_WS=${PENPOT_MCP_URI_WS:-http://penpot-mcp:4402}
export PENPOT_HTTP_SERVER_MAX_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_BODY_SIZE:-367001600} # Default to 350MiB
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_MCP_URI,\$PENPOT_MCP_URI_WS,\$PENPOT_HTTP_SERVER_MAX_BODY_SIZE" \
export PENPOT_IPV6_LISTEN_DIRECTIVE=${PENPOT_IPV6_LISTEN_DIRECTIVE:-"listen [::]:8080 default_server;"}
if [ "${PENPOT_DISABLE_IPV6_LISTEN}" = "true" ]; then
export PENPOT_IPV6_LISTEN_DIRECTIVE=""
fi
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_MCP_URI,\$PENPOT_MCP_URI_WS,\$PENPOT_HTTP_SERVER_MAX_BODY_SIZE,\$PENPOT_IPV6_LISTEN_DIRECTIVE" \
< /tmp/nginx.conf.template > /etc/nginx/nginx.conf
PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)"

View File

@ -73,7 +73,7 @@ http {
server {
listen 8080 default_server;
listen [::]:8080 default_server;
${PENPOT_IPV6_LISTEN_DIRECTIVE}
server_name _;
client_max_body_size $PENPOT_HTTP_SERVER_MAX_BODY_SIZE;

View File

@ -77,7 +77,7 @@
[:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
:total 3}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]
@ -118,7 +118,7 @@
[:> c/navigation-bullets*
{:slide slide
:navigate navigate
:total 4}]
:total 3}]
[:button {:on-click next
:class (stl/css :next-btn)} "Continue"]]]]]]

View File

@ -16,10 +16,10 @@
"fmt": "./scripts/fmt"
},
"devDependencies": {
"@github/copilot": "^1.0.44",
"@github/copilot": "^1.0.54",
"@types/node": "^25.6.2",
"esbuild": "^0.28.0",
"nrepl-client": "^0.3.0",
"opencode-ai": "^1.15.4"
"opencode-ai": "^1.15.11"
}
}

196
pnpm-lock.yaml generated
View File

@ -9,8 +9,8 @@ importers:
.:
devDependencies:
'@github/copilot':
specifier: ^1.0.44
version: 1.0.44
specifier: ^1.0.54
version: 1.0.54
'@types/node':
specifier: ^25.6.2
version: 25.6.2
@ -21,8 +21,8 @@ importers:
specifier: ^0.3.0
version: 0.3.0
opencode-ai:
specifier: ^1.15.4
version: 1.15.4
specifier: ^1.15.11
version: 1.15.11
packages:
@ -182,44 +182,60 @@ packages:
cpu: [x64]
os: [win32]
'@github/copilot-darwin-arm64@1.0.44':
resolution: {integrity: sha512-9NqA5sT2spmNsehxhs51GhXRZIZga5nq+WcMl4LG2QrUPJRDwvHf1bDKqETJUBbYvBY8jONGuTKMRofkMI68YQ==}
'@github/copilot-darwin-arm64@1.0.54':
resolution: {integrity: sha512-ZRiKkxCvDccdGSNB/gmge4UkqMsWWZNIOr0pcim4/x2YUdHbh9cex9RZRjEMXijtUkBTzW5DP/cACuoAqTCyEg==}
cpu: [arm64]
os: [darwin]
hasBin: true
'@github/copilot-darwin-x64@1.0.44':
resolution: {integrity: sha512-QPD8KtXx07SIKILGBl4JDhPyL2Qo0FMmaTYVxR6nkyHkHnFPsUZD6VWGR+T/KMLkcUXFM85Xc1ba9Y27s4nRrQ==}
'@github/copilot-darwin-x64@1.0.54':
resolution: {integrity: sha512-DGqs8x5r4y+SebMco890lNsPrqe6L4v2hCmV1IQ1pvYPvD1o1NMVSZPAQhkdvUeR5bqusOg8+0ugIZOQGTFpFQ==}
cpu: [x64]
os: [darwin]
hasBin: true
'@github/copilot-linux-arm64@1.0.44':
resolution: {integrity: sha512-Z8ScIUP433xS18f68NP9jM9zW320Xzpi2wf7Nig/VyfrwupBy25UTezydQMT0KQHLWTEleHOPcYnASY3HgJXnQ==}
'@github/copilot-linux-arm64@1.0.54':
resolution: {integrity: sha512-waVKu6RuG8YBvCoGrOgtsOxmnfLaUywvbqZXRgvMya1m4akRkMi5r9B2UDr3+egjChp+FIUJVbGIoXN6ZST0rQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
hasBin: true
'@github/copilot-linux-x64@1.0.44':
resolution: {integrity: sha512-KUl6lvJt0HNKaXSx0T0bIWJ3rvrGwgZYMlkDfqMbuMnZatEQJbjPwxmL/IDfp/c0DyKd7K+ajl17wHYcN/hJIQ==}
'@github/copilot-linux-x64@1.0.54':
resolution: {integrity: sha512-u/ltZa+HDIuhMivkIwkkuylRdEMk5Lp0XjE9w/OityW+BPKjZ+VKAmJ1/1Xm/uUx1IUlZaE3TJnka52wVNOD0A==}
cpu: [x64]
os: [linux]
libc: [glibc]
hasBin: true
'@github/copilot-win32-arm64@1.0.44':
resolution: {integrity: sha512-JVJxZJwAc95ZfapgOXjNFwSqrWlvC3heo128L+CDkdZ6lwpD1dTGMHT/6rMMEeo3xjZmMm8tiynfwsHLDgTtvQ==}
'@github/copilot-linuxmusl-arm64@1.0.54':
resolution: {integrity: sha512-21LLjoQnD57Y1fvO56G1FGVbkt/ffZNDpHqVe2NW7C4r78Gn0hOTqwp+xWRUMpdmxrGZyKeFjX8jK6qox2uF5w==}
cpu: [arm64]
os: [linux]
libc: [musl]
hasBin: true
'@github/copilot-linuxmusl-x64@1.0.54':
resolution: {integrity: sha512-sbeATKa9vaIetsY1vhQJO0PN/5FgoK48wkGBWCy4BpO8ER/kGYczT22qv6n96gBYrVmC2IZuTFTM4GFpC3bbBw==}
cpu: [x64]
os: [linux]
libc: [musl]
hasBin: true
'@github/copilot-win32-arm64@1.0.54':
resolution: {integrity: sha512-muOX8qrJSi56BWQejkH0TgXpZYRO8Y9k1qIfMuRojZyLyATn1P4lIKb67ZqDCXJLkcPfVJ5eJYsSAeGwU3Qpww==}
cpu: [arm64]
os: [win32]
hasBin: true
'@github/copilot-win32-x64@1.0.44':
resolution: {integrity: sha512-Yj3KQ/DqwS50PwRtyQITX2mWIVZeJeX+y0faVSMwUUzG1qxmMcme7wimhKOyc4LSV11DpgVB9MSiBw2xys7iww==}
'@github/copilot-win32-x64@1.0.54':
resolution: {integrity: sha512-BheXmqrYFmfRXA0iveKkjKks/2wgK5glrEOARomzy3JCbvVMSPIE8YeK+3YysiOh2SUkWjahwJc09cxaBq4+qQ==}
cpu: [x64]
os: [win32]
hasBin: true
'@github/copilot@1.0.44':
resolution: {integrity: sha512-wr/GmNOUaJK/giJK5abyB1oTpEowgFKLi+NJnlyAymKiK/GKCaRlJqiX23H2RetM8vD2hDYUFUFm9lTCooGy0g==}
'@github/copilot@1.0.54':
resolution: {integrity: sha512-gxiWEQFWxJ3J2Rh67CxKEfER/zayB1z2qaSBUz3RZ0u1iDNJdGPry/1vOQ72X/yHmpGNm+9egucN5VMzyedsIg==}
hasBin: true
'@types/node@25.6.2':
@ -228,6 +244,10 @@ packages:
bencode@2.0.3:
resolution: {integrity: sha512-D/vrAD4dLVX23NalHwb8dSvsUsxeRPO8Y7ToKA015JQYq69MLDOMkC0uGZYA/MPpltLO8rt8eqFC2j8DxjTZ/w==}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
esbuild@0.28.0:
resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==}
engines: {node: '>=18'}
@ -236,69 +256,69 @@ packages:
nrepl-client@0.3.0:
resolution: {integrity: sha512-EcROXUrzlGHKOdu/E/5WB0OESCI0iGHhdXeYk9cULYtd72eFJrM/Q1umvjTBfKWlT62y76cnyLG/3CmSCqT12w==}
opencode-ai@1.15.4:
resolution: {integrity: sha512-Z5PeJwFNUW4sFW+jYHQTnJm6858dECvWOATvnG0S66Nn46zwjaaZJEJMKQEPOW7Yog99j6k7xRKvJPPAjKDXfQ==}
opencode-ai@1.15.11:
resolution: {integrity: sha512-i3DYIATyWT3ukP+5OCyEuXvbCEq7PgBAlVA4yp01+W5BaYeoj/f0bpXdDPP5q9B/Yl8drtyEhWh0YC9UAHF06A==}
cpu: [arm64, x64]
os: [darwin, linux, win32]
hasBin: true
opencode-darwin-arm64@1.15.4:
resolution: {integrity: sha512-d+0sFAAhrDtjQbxRZvYSzy3g/xj/xUDRWUVBWGfkJAx0QEWc/v7cksmnYj3p3l88Gxm/rWeLCh6H32pw1/En8A==}
opencode-darwin-arm64@1.15.11:
resolution: {integrity: sha512-XuiTIkBj0YKpfT8KHNan4JaX686vROCwXQHDzsZ55g/I7rdyQXG2wdt2CsUhRDaPyQTOkhrM+VqC3uYsT+kZzw==}
cpu: [arm64]
os: [darwin]
opencode-darwin-x64-baseline@1.15.4:
resolution: {integrity: sha512-Lj9wsEPFyEOgLO6J3DsCXdSu/IAJnW/RjtD1oojAao6uHvhs5uXyj1mrsmK8GrtAfCT4JUh8W38o3YYXGjItSw==}
opencode-darwin-x64-baseline@1.15.11:
resolution: {integrity: sha512-itb1FRgGyve89/W+sQBqmTVWUoldb0TdH1Qm805c52UbY+nkEW6oE40vveHJEDDPxFHypyzSymYFcJ7wqYBisw==}
cpu: [x64]
os: [darwin]
opencode-darwin-x64@1.15.4:
resolution: {integrity: sha512-H412BUw5O+bmXfzLo6UMCWVc3DOYEM0RCI5Kt+Ynqh+Q9878bXK6mwRR7VXgGVBkkH2U4GtT1uDgY0BzSK185Q==}
opencode-darwin-x64@1.15.11:
resolution: {integrity: sha512-PimsC+uaSmVxszQ3sbrIEjDoba9jUyAwAbydEukY3EoQ7cgLFd8tn9H/8yeA0aY7bh3uiCttDCctoQfMpV+S9Q==}
cpu: [x64]
os: [darwin]
opencode-linux-arm64-musl@1.15.4:
resolution: {integrity: sha512-TO2IVSoYolGKJahf73/hRsJBGxLKOdP/akYPzI0hQQvW4oVrmQkZ3s13jU1+LXIEn4Zbj/TB18QvLzvXrnrEhA==}
opencode-linux-arm64-musl@1.15.11:
resolution: {integrity: sha512-wLaAM12H0mH93XpdXuFz4+oeNA9+CDj8WEvdL8NNrz/Sfgi/zWIo8g3UMH+lp+pfO0s7PTM84RzxGQTOvcejXg==}
cpu: [arm64]
os: [linux]
opencode-linux-arm64@1.15.4:
resolution: {integrity: sha512-V+x/u9JnPOLPEfqbePSCL0OQdin5gs1V35VsVxj19WaZDEwxlMVjOe6HjVKEY64/O6htkPxCCZohmnMU4dVBMQ==}
opencode-linux-arm64@1.15.11:
resolution: {integrity: sha512-SM+xMU8pUd5p6KD2wdIR2d0q3IRw6axKSGUqlcVrit7ZhFlTjdr3Ca0KqVv3JsUny8Bu8N8Z5b3MHvqRf4nTSQ==}
cpu: [arm64]
os: [linux]
opencode-linux-x64-baseline-musl@1.15.4:
resolution: {integrity: sha512-xOJ3aHg2+2GrT9F/KmAF0JLB1D6K3SCY/626n+fLjs/AEFvLdmE3TYhoXPEyGH2I9F4kF+4p2xk0pg2b+LVlZQ==}
opencode-linux-x64-baseline-musl@1.15.11:
resolution: {integrity: sha512-N+geBY9Zv2kfoMKYMnPxJZQ1R9xOrgA55BMa7aMtMHr2x9tkjI5mCT7ese6Igna/cnoicxo216YhkSzyY1+p9A==}
cpu: [x64]
os: [linux]
opencode-linux-x64-baseline@1.15.4:
resolution: {integrity: sha512-dTlV8tAVN8nFdPb7527GR6/BpyIVavAcXJmZ2VbS1daXu4C6k6bpmjiS/ZFKlphRZiKKiEzFrHlimao4BMchVQ==}
opencode-linux-x64-baseline@1.15.11:
resolution: {integrity: sha512-MX+eaLOkFVO5IA8jA0QLUJma3KBwRzUrzZrCFuv6+vq9U/SsD+F9Jz2Bnfw9iE9v3QwnJ+8Yf/vKCsb5LrrWvQ==}
cpu: [x64]
os: [linux]
opencode-linux-x64-musl@1.15.4:
resolution: {integrity: sha512-IbMaM6zrakdtDD55GUhlT/WeXomXmKsVqo3XQuOaGXprBg3W5alsxXh60SZpV3ftbdcMD/eiB/PYtN/ZN8Fa5w==}
opencode-linux-x64-musl@1.15.11:
resolution: {integrity: sha512-/xbhh8aDFR5E8Ggc00ZG3qXXAwWxosEfWaPXiP04/Y7kDbz8T28a1cSIWniNmY426rYdnYLXgwJzOgpD/ZhDDg==}
cpu: [x64]
os: [linux]
opencode-linux-x64@1.15.4:
resolution: {integrity: sha512-2c20aldKLfNkg6N6nABvvK1fuaCwYLo/HNeL8ikellkFMeGalCGDhkL/VQ8R8KPV3ohVZJtZwG0nkFiA8MeHCg==}
opencode-linux-x64@1.15.11:
resolution: {integrity: sha512-RuXo1XFiAqW6ypnP4V+rPDmTrdDKh8FuO+3whMpw2qIYs8eVil72QwX1Rv6W96FDCFQkyUZW9R6MK/MGbGWCUQ==}
cpu: [x64]
os: [linux]
opencode-windows-arm64@1.15.4:
resolution: {integrity: sha512-kr3nIWmYH7NC0Vgrhgjp9EmCuy5MuxjIRrSjzlfRLMaML6U/a0Hsr3AahBwI1KjT+HEhz5u6xpodZeeEDY3nPQ==}
opencode-windows-arm64@1.15.11:
resolution: {integrity: sha512-sdWtLGq1aoaCzbTQY8NR7+g56lzYREBBcT7Na81FKg4H/ZybLQHXV+lwbaw+cK/d0aPpM8EAB+TV6Wbe5nEzGw==}
cpu: [arm64]
os: [win32]
opencode-windows-x64-baseline@1.15.4:
resolution: {integrity: sha512-2/elQ163r4Q97bYJRrY09IG+bpqh0AKpfutDGCaokFdLWIWQN/cFvjzb4C+BKzLFsU9LRfoyvPhe4nXMm1+S4A==}
opencode-windows-x64-baseline@1.15.11:
resolution: {integrity: sha512-Lj9TkJIeUD++idJWIKK5z+k5hvNubAEXItdxzBLM8LlWArSd2tXCGbxbBktsM5URlhFBdAN05ghyiUtAVOcMPg==}
cpu: [x64]
os: [win32]
opencode-windows-x64@1.15.4:
resolution: {integrity: sha512-f6p40u3yLEbiq4pzBOXAwtW/NP/dL8uTurHfraPcfezA4ua5DEm4vSoSePUY0CHtubUPuDe0wRUA1s53sysjPQ==}
opencode-windows-x64@1.15.11:
resolution: {integrity: sha512-d5jJqLA+d2DmbEzVrriePxoOjLfqjKZar0OYkNgvmUcWHCHeyn1NcL5JH6T7L19s/X3qY1tEDvZZ3uWFqNMdGQ==}
cpu: [x64]
os: [win32]
@ -389,32 +409,42 @@ snapshots:
'@esbuild/win32-x64@0.28.0':
optional: true
'@github/copilot-darwin-arm64@1.0.44':
'@github/copilot-darwin-arm64@1.0.54':
optional: true
'@github/copilot-darwin-x64@1.0.44':
'@github/copilot-darwin-x64@1.0.54':
optional: true
'@github/copilot-linux-arm64@1.0.44':
'@github/copilot-linux-arm64@1.0.54':
optional: true
'@github/copilot-linux-x64@1.0.44':
'@github/copilot-linux-x64@1.0.54':
optional: true
'@github/copilot-win32-arm64@1.0.44':
'@github/copilot-linuxmusl-arm64@1.0.54':
optional: true
'@github/copilot-win32-x64@1.0.44':
'@github/copilot-linuxmusl-x64@1.0.54':
optional: true
'@github/copilot@1.0.44':
'@github/copilot-win32-arm64@1.0.54':
optional: true
'@github/copilot-win32-x64@1.0.54':
optional: true
'@github/copilot@1.0.54':
dependencies:
detect-libc: 2.1.2
optionalDependencies:
'@github/copilot-darwin-arm64': 1.0.44
'@github/copilot-darwin-x64': 1.0.44
'@github/copilot-linux-arm64': 1.0.44
'@github/copilot-linux-x64': 1.0.44
'@github/copilot-win32-arm64': 1.0.44
'@github/copilot-win32-x64': 1.0.44
'@github/copilot-darwin-arm64': 1.0.54
'@github/copilot-darwin-x64': 1.0.54
'@github/copilot-linux-arm64': 1.0.54
'@github/copilot-linux-x64': 1.0.54
'@github/copilot-linuxmusl-arm64': 1.0.54
'@github/copilot-linuxmusl-x64': 1.0.54
'@github/copilot-win32-arm64': 1.0.54
'@github/copilot-win32-x64': 1.0.54
'@types/node@25.6.2':
dependencies:
@ -422,6 +452,8 @@ snapshots:
bencode@2.0.3: {}
detect-libc@2.1.2: {}
esbuild@0.28.0:
optionalDependencies:
'@esbuild/aix-ppc64': 0.28.0
@ -456,55 +488,55 @@ snapshots:
bencode: 2.0.3
tree-kill: 1.2.2
opencode-ai@1.15.4:
opencode-ai@1.15.11:
optionalDependencies:
opencode-darwin-arm64: 1.15.4
opencode-darwin-x64: 1.15.4
opencode-darwin-x64-baseline: 1.15.4
opencode-linux-arm64: 1.15.4
opencode-linux-arm64-musl: 1.15.4
opencode-linux-x64: 1.15.4
opencode-linux-x64-baseline: 1.15.4
opencode-linux-x64-baseline-musl: 1.15.4
opencode-linux-x64-musl: 1.15.4
opencode-windows-arm64: 1.15.4
opencode-windows-x64: 1.15.4
opencode-windows-x64-baseline: 1.15.4
opencode-darwin-arm64: 1.15.11
opencode-darwin-x64: 1.15.11
opencode-darwin-x64-baseline: 1.15.11
opencode-linux-arm64: 1.15.11
opencode-linux-arm64-musl: 1.15.11
opencode-linux-x64: 1.15.11
opencode-linux-x64-baseline: 1.15.11
opencode-linux-x64-baseline-musl: 1.15.11
opencode-linux-x64-musl: 1.15.11
opencode-windows-arm64: 1.15.11
opencode-windows-x64: 1.15.11
opencode-windows-x64-baseline: 1.15.11
opencode-darwin-arm64@1.15.4:
opencode-darwin-arm64@1.15.11:
optional: true
opencode-darwin-x64-baseline@1.15.4:
opencode-darwin-x64-baseline@1.15.11:
optional: true
opencode-darwin-x64@1.15.4:
opencode-darwin-x64@1.15.11:
optional: true
opencode-linux-arm64-musl@1.15.4:
opencode-linux-arm64-musl@1.15.11:
optional: true
opencode-linux-arm64@1.15.4:
opencode-linux-arm64@1.15.11:
optional: true
opencode-linux-x64-baseline-musl@1.15.4:
opencode-linux-x64-baseline-musl@1.15.11:
optional: true
opencode-linux-x64-baseline@1.15.4:
opencode-linux-x64-baseline@1.15.11:
optional: true
opencode-linux-x64-musl@1.15.4:
opencode-linux-x64-musl@1.15.11:
optional: true
opencode-linux-x64@1.15.4:
opencode-linux-x64@1.15.11:
optional: true
opencode-windows-arm64@1.15.4:
opencode-windows-arm64@1.15.11:
optional: true
opencode-windows-x64-baseline@1.15.4:
opencode-windows-x64-baseline@1.15.11:
optional: true
opencode-windows-x64@1.15.4:
opencode-windows-x64@1.15.11:
optional: true
tree-kill@1.2.2: {}