Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fec5db744c | ||
|
|
cb75011244 | ||
|
|
18b23886ab | ||
|
|
beb50f4b49 | ||
|
|
623a414b9b | ||
|
|
91c7e44031 | ||
|
|
850fc9c78e | ||
|
|
f5ec0882d5 | ||
|
|
b2e751c5fc | ||
|
|
4c8032bb97 | ||
|
|
4c566268a9 | ||
|
|
2c435a3d7b | ||
|
|
1f990cf66d | ||
|
|
e54fc1939a | ||
|
|
8b87c31a99 | ||
|
|
f7bc8b6349 | ||
|
|
5b8f09167a | ||
|
|
17fbd13bc9 | ||
|
|
aab7dc9ba4 | ||
|
|
dde8796790 | ||
|
|
acc6d292de | ||
|
|
2236ed7be1 | ||
|
|
f6fb7ccfc0 | ||
|
|
b73758c7c8 | ||
|
|
fbc07c5617 | ||
|
|
80e5aec577 | ||
|
|
f869b31ad6 | ||
|
|
9a853f2fcc | ||
|
|
b4a7540157 | ||
|
|
0ea1127a2b | ||
|
|
aeab23a6d6 |
@@ -6,6 +6,10 @@
|
||||
|
||||
- Search: add CJK tokenization support (Chinese/Japanese/Korean) with Intl.Segmenter plus fallback behavior to improve skill query matching (#1596) (thanks @pq-dong).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Stats maintenance: keep skill stat migration fields synchronized by treating top-level stat fields as canonical during backfill/reconcile fallback reads (#1704) (thanks @momothemage).
|
||||
|
||||
## 0.10.0 - 2026-04-05
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "clawhub",
|
||||
@@ -124,9 +123,9 @@
|
||||
|
||||
"@ark/util": ["@ark/util@0.56.0", "", {}, "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA=="],
|
||||
|
||||
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.0.1", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.2.6" } }, "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw=="],
|
||||
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.10", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-02OhhkKtgNRuicQ/nF3TRnGsxL9wp0r3Y7VlKWyOHHGmGyvXv03y+PnymU8FKFJMTjIr1Bk8U2g1HWSLrpAHww=="],
|
||||
|
||||
"@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.0.4", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.7" } }, "sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w=="],
|
||||
"@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.0.9", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-r3ElRr7y8ucyN2KdICwGsmj19RoN13CLCa/pvGydghWK6ZzeKQ+TcDjVdtEZz2ElpndM5jXw//B9CEee0mWnVg=="],
|
||||
|
||||
"@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
|
||||
|
||||
@@ -176,33 +175,33 @@
|
||||
|
||||
"@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="],
|
||||
|
||||
"@clack/core": ["@clack/core@1.1.0", "", { "dependencies": { "sisteransi": "^1.0.5" } }, "sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA=="],
|
||||
"@clack/core": ["@clack/core@1.2.0", "", { "dependencies": { "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-qfxof/3T3t9DPU/Rj3OmcFyZInceqj/NVtO9rwIuJqCUgh32gwPjpFQQp/ben07qKlhpwq7GzfWpST4qdJ5Drg=="],
|
||||
|
||||
"@clack/prompts": ["@clack/prompts@1.1.0", "", { "dependencies": { "@clack/core": "1.1.0", "sisteransi": "^1.0.5" } }, "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g=="],
|
||||
"@clack/prompts": ["@clack/prompts@1.2.0", "", { "dependencies": { "@clack/core": "1.2.0", "fast-string-width": "^1.1.0", "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w=="],
|
||||
|
||||
"@convex-dev/auth": ["@convex-dev/auth@0.0.91", "", { "dependencies": { "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0", "cookie": "^1.0.1", "is-network-error": "^1.1.0", "jose": "^5.2.2", "jwt-decode": "^4.0.0", "lucia": "^3.2.0", "oauth4webapi": "^3.1.2", "path-to-regexp": "^6.3.0", "server-only": "^0.0.1" }, "peerDependencies": { "@auth/core": "^0.37.0", "convex": "^1.17.0", "react": "^18.2.0 || ^19.0.0-0" }, "optionalPeers": ["react"], "bin": { "auth": "dist/bin.cjs" } }, "sha512-wLD4hszo3IhhMkwPs6ozWf0cUauwmhOvjUVn0g//kC338n/jApOjeDYWKCrn/qYUkveyDsbag5zrY8mVzA09Qg=="],
|
||||
|
||||
"@create-markdown/core": ["@create-markdown/core@2.0.2", "", {}, "sha512-maA3zw9HkdOZORpKyvmxcRFTTOCpClLW01oAuVtzW7LvafHippRz67VHngIBEsIPKEO5j4COItwXKFnzo9/dfA=="],
|
||||
"@create-markdown/core": ["@create-markdown/core@2.0.3", "", {}, "sha512-qAYukvE603z42OGZF1LzwxxkOVDksB76wXu+fnlKBzGizhR7uN3xHQO8PFFZDqjkZpaTrmtDd768qzl+Ir+3pQ=="],
|
||||
|
||||
"@create-markdown/preview": ["@create-markdown/preview@2.0.2", "", { "peerDependencies": { "@create-markdown/core": ">=2.0.2", "mermaid": ">=10.0.0", "shiki": ">=1.0.0" }, "optionalPeers": ["@create-markdown/core", "mermaid", "shiki"] }, "sha512-ty1mp7qXVI0Bap8M0jiDiJsAqZkP3oaYNp0JX0wiY4K+KfWgK4IqeB8R2W+9vLpRxzxhX6Rggf5Qj2Sv5p75Eg=="],
|
||||
"@create-markdown/preview": ["@create-markdown/preview@2.0.3", "", { "peerDependencies": { "@create-markdown/core": ">=2.0.3", "shiki": ">=1.0.0" }, "optionalPeers": ["@create-markdown/core", "shiki"] }, "sha512-Vrp8DyuiouryZ3E4NQ7tBgoYQdoekd0+DzN64mZ48QYCw3V+MCb/H2q10SW8KC8XPr931XOMDvKX4I83qpQh3g=="],
|
||||
|
||||
"@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="],
|
||||
|
||||
"@csstools/css-calc": ["@csstools/css-calc@3.1.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ=="],
|
||||
"@csstools/css-calc": ["@csstools/css-calc@3.2.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w=="],
|
||||
|
||||
"@csstools/css-color-parser": ["@csstools/css-color-parser@4.0.2", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.1.1" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw=="],
|
||||
"@csstools/css-color-parser": ["@csstools/css-color-parser@4.1.0", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.2.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ=="],
|
||||
|
||||
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="],
|
||||
|
||||
"@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.1", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w=="],
|
||||
"@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.3", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg=="],
|
||||
|
||||
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
|
||||
"@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="],
|
||||
|
||||
@@ -336,7 +335,7 @@
|
||||
|
||||
"@monaco-editor/react": ["@monaco-editor/react@4.7.0", "", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "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" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="],
|
||||
|
||||
"@next/env": ["@next/env@16.2.3", "", {}, "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA=="],
|
||||
|
||||
@@ -424,43 +423,43 @@
|
||||
|
||||
"@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@0.17.4", "", { "os": "win32", "cpu": "x64" }, "sha512-JxT81aEUBNA/s01Ql2OQ2DLAsuM0M+mK9iLHunukOdPMhjA6NvFE/GtTablBYJKScK21d/xTvnoSLgQU3l22Cw=="],
|
||||
|
||||
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.58.0", "", { "os": "android", "cpu": "arm" }, "sha512-1T7UN3SsWWxpWyWGn1cT3ASNJOo+pI3eUkmEl7HgtowapcV8kslYpFQcYn431VuxghXakPNlbjRwhqmR37PFOg=="],
|
||||
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.60.0", "", { "os": "android", "cpu": "arm" }, "sha512-YdeJKaZckDQL1qa62a1aKq/goyq48aX3yOxaaWqWb4sau4Ee4IiLbamftNLU3zbePky6QsDj6thnSSzHRBjDfA=="],
|
||||
|
||||
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.58.0", "", { "os": "android", "cpu": "arm64" }, "sha512-GryzujxuiRv2YFF7bRy8mKcxlbuAN+euVUtGJt9KKbLT8JBUIosamVhcthLh+VEr6KE6cjeVMAQxKAzJcoN7dg=="],
|
||||
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.60.0", "", { "os": "android", "cpu": "arm64" }, "sha512-7ANS7PpXCfq84xZQ8E5WPs14gwcuPcl+/8TFNXfpSu0CQBXz3cUo2fDpHT8v8HJN+Ut02eacvMAzTnc9s6X4tw=="],
|
||||
|
||||
"@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.58.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7/bRSJIwl4GxeZL9rPZ11anNTyUO9epZrfEJH/ZMla3+/gbQ6xZixh9nOhsZ0QwsTW7/5J2A/fHbD1udC5DQQA=="],
|
||||
"@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.60.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pJsgd9AfplLGBm1fIr25V6V14vMrayhx4uIQvlfH7jWs2SZwSrvi3TfgfJySB8T+hvyEH8K2zXljQiUnkgUnfQ=="],
|
||||
|
||||
"@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.58.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-EqdtJSiHweS2vfILNrpyJ6HUwpEq2g7+4Zx1FPi4hu3Hu7tC3znF6ufbXO8Ub2LD4mGgznjI7kSdku9NDD1Mkg=="],
|
||||
"@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.60.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ue1aXHX49ivwflKqGJc7zcd/LeLgbhaTcDCQStgx5x06AXgjEAZmvrlMuIkWd4AL4FHQe6QJ9f33z04Cg448VQ=="],
|
||||
|
||||
"@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.58.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-VQt5TH4M42mY20F545G637RKxV/yjwVtKk2vfXuazfReSIiuvWBnv+FVSvIV5fKVTJNjt3GSJibh6JecbhGdBw=="],
|
||||
"@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.60.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YCyQzsQtusQw+gNRW9rRTifSO+Dt/+dtCl2NHoDMZqJlRTEZ/Oht9YnuporI9yiTx7+cB+eqzX3MtHHVHGIWhg=="],
|
||||
|
||||
"@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.58.0", "", { "os": "linux", "cpu": "arm" }, "sha512-fBYcj4ucwpAtjJT3oeBdFBYKvNyjRSK+cyuvBOTQjh0jvKp4yeA4S/D0IsCHus/VPaNG5L48qQkh+Vjy3HL2/Q=="],
|
||||
"@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-c7dxM2Zksa45Qw16i2iGY3Fti2NirJ38FrsBsKw+qcJ0OtqTsBgKJLF0xV+yLG56UH01Z8WRPgsw31e0MoRoGQ=="],
|
||||
|
||||
"@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.58.0", "", { "os": "linux", "cpu": "arm" }, "sha512-0BeuFfwlUHlJ1xpEdSD1YO3vByEFGPg36uLjK1JgFaxFb4W6w17F8ET8sz5cheZ4+x5f2xzdnRrrWv83E3Yd8g=="],
|
||||
"@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.60.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ZWALoA42UYqBEP1Tbw9OWURgFGS1nWj2AAvLdY6ZcGx/Gj93qVCBKjcvwXMupZibYwFbi9s/rzqkZseb/6gVtQ=="],
|
||||
|
||||
"@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.58.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-TXlZgnPTlxrQzxG9ZXU7BNwx1Ilrr17P3GwZY0If2EzrinqRH3zXPc3HrRcBJgcsoZNMuNL5YivtkJYgp467UQ=="],
|
||||
"@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-tpy+1w4p9hN5CicMCxqNy6ymfRtV5ayE573vFNjp1k1TN/qhLFgflveZoE/0++RlkHikBz2vY545NWm/hp7big=="],
|
||||
|
||||
"@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.58.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-zSoYRo5dxHLcUx93Stl2hW3hSNjPt99O70eRVWt5A1zwJ+FPjeCCANCD2a9R4JbHsdcl11TIQOjyigcRVOH2mw=="],
|
||||
"@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.60.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-eDYDXZGhQAXyn6GwtwiX/qcLS0HlOLPJ/+iiIY8RYr+3P8oKBmgKxADLlniL6FtWfE7pPk7IGN9/xvDEvDvFeg=="],
|
||||
|
||||
"@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.58.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NQ0U/lqxH2/VxBYeAIvMNUK1y0a1bJ3ZicqkF2c6wfakbEciP9jvIE4yNzCFpZaqeIeRYaV7AVGqEO1yrfVPjA=="],
|
||||
"@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.60.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nxehly5XYBHUWI9VJX1bqCf9j/B43DaK/aS/T1fcxCpX3PA4Rm9BB54nPD1CKayT8xg6REN1ao+01hSRNgy8OA=="],
|
||||
|
||||
"@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-X9J+kr3gIC9FT8GuZt0ekzpNUtkBVzMVU4KiKDSlocyQuEgi3gBbXYN8UkQiV77FTusLDPsovjo95YedHr+3yg=="],
|
||||
"@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-j1qf/NaUfOWQutjeoooNG1Q0zsK0XGmSu1uDLq3cctquRF3j7t9Hxqf/76ehCc5GEUAanth2W4Fa+XT1RFg/nw=="],
|
||||
|
||||
"@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.58.0", "", { "os": "linux", "cpu": "none" }, "sha512-CDze3pi1OO3Wvb/QsXjmLEY4XPKGM6kIo82ssNOgmcl1IdndF9VSGAE38YLhADWmOac7fjqhBw82LozuUVxD0Q=="],
|
||||
"@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.60.0", "", { "os": "linux", "cpu": "none" }, "sha512-YELKPRefQ/q/h3RUmeRfPCUhh2wBvgV1RyZ/F9M9u8cDyXsQW2ojv1DeWQTt466yczDITjZnIOg/s05pk7Ve2A=="],
|
||||
|
||||
"@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.58.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-b/89glbxFaEAcA6Uf1FvCNecBJEgcUTsV1quzrqXM/o4R1M4u+2KCVuyGCayN2UpsRWtGGLb+Ver0tBBpxaPog=="],
|
||||
"@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.60.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-JkO3C6Gki7Y6h/MiIkFKvHFOz98/YWvQ4WYbK9DLXACMP2rjULzkeGyAzorJE5S1dzLQGFgeqvN779kSFwoV1g=="],
|
||||
|
||||
"@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.58.0", "", { "os": "linux", "cpu": "x64" }, "sha512-0/yYpkq9VJFCEcuRlrViGj8pJUFFvNS4EkEREaN7CB1EcLXJIaVSSa5eCihwBGXtOZxhnblWgxks9juRdNQI7w=="],
|
||||
"@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XjKHdFVCpZZZSWBCKyyqCq65s2AKXykMXkjLoKYODrD+f5toLhlwsMESscu8FbgnJQ4Y/dpR/zdazsahmgBJIA=="],
|
||||
|
||||
"@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.58.0", "", { "os": "linux", "cpu": "x64" }, "sha512-hr6FNvmcAXiH+JxSvaJ4SJ1HofkdqEElXICW9sm3/Rd5eC3t7kzvmLyRAB3NngKO2wzXRCAm4Z/mGWfrsS4X8w=="],
|
||||
"@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.60.0", "", { "os": "linux", "cpu": "x64" }, "sha512-js29ZWIuPhNWzY8NC7KoffEMEeWG105vbmm+8EOJsC+T/jHBiKIJEUF78+F/IrgEWMMP9N0kRND4Pp75+xAhKg=="],
|
||||
|
||||
"@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.58.0", "", { "os": "none", "cpu": "arm64" }, "sha512-R+O368VXgRql1K6Xar+FEo7NEwfo13EibPMoTv3sesYQedRXd6m30Dh/7lZMxnrQVFfeo4EOfYIP4FpcgWQNHg=="],
|
||||
"@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.60.0", "", { "os": "none", "cpu": "arm64" }, "sha512-H+PUITKHk04stFpWj3x3Kg08Afp/bcXSBi0EhasR5a0Vw7StXHTzdl655PUI0fB4qdh2Wsu6Dsi+3ACxPoyQnA=="],
|
||||
|
||||
"@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.58.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q0FZiAY/3c4YRj4z3h9K1PgaByrifrfbBoODSeX7gy97UtB7pySPUQfC2B/GbxWU6k7CzQrRy5gME10PltLAFQ=="],
|
||||
"@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.60.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-WA/yc7f7ZfCefBXVzNHn1Ztulb1EFwNBb4jMZ6pjML0zz6pHujlF3Q3jySluz3XHl/GNeMTntG1seUBWVMlMag=="],
|
||||
|
||||
"@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.58.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-Y8FKBABrSPp9H0QkRLHDHOSUgM/309a3IvOVgPcVxYcX70wxJrk608CuTg7w+C6vEd724X5wJoNkBcGYfH7nNQ=="],
|
||||
"@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.60.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-33YxL1sqwYNZXtn3MD/4dno6s0xeedXOJlT1WohkVD565WvohClZUr7vwKdAk954n4xiEWJkewiCr+zLeq7AeA=="],
|
||||
|
||||
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.58.0", "", { "os": "win32", "cpu": "x64" }, "sha512-bCn5rbiz5My+Bj7M09sDcnqW0QJyINRVxdZ65x1/Y2tGrMwherwK/lpk+HRQCKvXa8pcaQdF5KY5j54VGZLwNg=="],
|
||||
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.60.0", "", { "os": "win32", "cpu": "x64" }, "sha512-JOro4ZcfBLamJCyfURQmOQByoorgOdx3ZjAkSqnb/CyG/i+lN3KoV5LAgk5ZAW6DPq7/Cx7n23f8DuTWXTWgyQ=="],
|
||||
|
||||
"@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="],
|
||||
|
||||
@@ -670,7 +669,7 @@
|
||||
|
||||
"@tanstack/react-start-server": ["@tanstack/react-start-server@1.166.16", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-router": "1.168.1", "@tanstack/router-core": "1.168.1", "@tanstack/start-client-core": "1.167.1", "@tanstack/start-server-core": "1.167.1" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-YEuM5XSxNQhLr30e6uyep7m5yZHtZwCeEeQVyo7CSWKmUpkBtN60+bg4T2/nLY0MXrwo6DTK1Crsu80ZZLkPAA=="],
|
||||
|
||||
"@tanstack/react-store": ["@tanstack/react-store@0.9.2", "", { "dependencies": { "@tanstack/store": "0.9.2", "use-sync-external-store": "^1.6.0" }, "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" } }, "sha512-Vt5usJE5sHG/cMechQfmwvwne6ktGCELe89Lmvoxe3LKRoFrhPa8OCKWs0NliG8HTJElEIj7PLtaBQIcux5pAQ=="],
|
||||
"@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "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" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="],
|
||||
|
||||
"@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="],
|
||||
|
||||
@@ -694,7 +693,7 @@
|
||||
|
||||
"@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.166.15", "", { "dependencies": { "@tanstack/router-core": "1.168.1" } }, "sha512-mGDNfJo/eFtwgFFBrJ85rNdIBNTroE3zy5zbwHZ/FV0HPYOawnev7KscDjKBuVxBGY2jl0fQLrRNUO/Sjqy3cg=="],
|
||||
|
||||
"@tanstack/store": ["@tanstack/store@0.9.2", "", {}, "sha512-K013lUJEFJK2ofFQ/hZKJUmCnpcV00ebLyOyFOWQvyQHUOZp/iYO84BM6aOGiV81JzwbX0APTVmW8YI7yiG5oA=="],
|
||||
"@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="],
|
||||
|
||||
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
|
||||
|
||||
@@ -724,7 +723,7 @@
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="],
|
||||
"@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||
|
||||
@@ -742,21 +741,21 @@
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
|
||||
|
||||
"@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.2", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.2", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.2", "vitest": "4.1.2" }, "optionalPeers": ["@vitest/browser"] }, "sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg=="],
|
||||
"@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.4", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.4", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.4", "vitest": "4.1.4" }, "optionalPeers": ["@vitest/browser"] }, "sha512-x7FptB5oDruxNPDNY2+S8tCh0pcq7ymCe1gTHcsp733jYjrJl8V1gMUlVysuCD9Kz46Xz9t1akkv08dPcYDs1w=="],
|
||||
|
||||
"@vitest/expect": ["@vitest/expect@4.1.2", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.2", "@vitest/utils": "4.1.2", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ=="],
|
||||
"@vitest/expect": ["@vitest/expect@4.1.4", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.4", "@vitest/utils": "4.1.4", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww=="],
|
||||
|
||||
"@vitest/mocker": ["@vitest/mocker@4.1.2", "", { "dependencies": { "@vitest/spy": "4.1.2", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q=="],
|
||||
"@vitest/mocker": ["@vitest/mocker@4.1.4", "", { "dependencies": { "@vitest/spy": "4.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg=="],
|
||||
|
||||
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.2", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA=="],
|
||||
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.4", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A=="],
|
||||
|
||||
"@vitest/runner": ["@vitest/runner@4.1.2", "", { "dependencies": { "@vitest/utils": "4.1.2", "pathe": "^2.0.3" } }, "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ=="],
|
||||
"@vitest/runner": ["@vitest/runner@4.1.4", "", { "dependencies": { "@vitest/utils": "4.1.4", "pathe": "^2.0.3" } }, "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ=="],
|
||||
|
||||
"@vitest/snapshot": ["@vitest/snapshot@4.1.2", "", { "dependencies": { "@vitest/pretty-format": "4.1.2", "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A=="],
|
||||
"@vitest/snapshot": ["@vitest/snapshot@4.1.4", "", { "dependencies": { "@vitest/pretty-format": "4.1.4", "@vitest/utils": "4.1.4", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw=="],
|
||||
|
||||
"@vitest/spy": ["@vitest/spy@4.1.2", "", {}, "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA=="],
|
||||
"@vitest/spy": ["@vitest/spy@4.1.4", "", {}, "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ=="],
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@4.1.2", "", { "dependencies": { "@vitest/pretty-format": "4.1.2", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ=="],
|
||||
"@vitest/utils": ["@vitest/utils@4.1.4", "", { "dependencies": { "@vitest/pretty-format": "4.1.4", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
@@ -788,7 +787,7 @@
|
||||
|
||||
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.10", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ=="],
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.18", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A=="],
|
||||
|
||||
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
|
||||
|
||||
@@ -798,9 +797,9 @@
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001780", "", {}, "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ=="],
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001787", "", {}, "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg=="],
|
||||
|
||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
@@ -844,15 +843,15 @@
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"convex": ["convex@1.34.1", "", { "dependencies": { "esbuild": "0.27.0", "prettier": "^3.0.0", "ws": "8.18.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-ooyFnZVVq0u6b5zt0Ptq8QB2ixhf/2vXe+PIcUtdtrs0lq/TwpkmmruHdqkFmWgMd6N+Tmfy8AGkz6QnZUYZBA=="],
|
||||
"convex": ["convex@1.35.1", "", { "dependencies": { "esbuild": "0.27.0", "prettier": "^3.0.0", "ws": "8.18.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "@clerk/react": "^6.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "@clerk/react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-g23KrTjBiXqRHzWIN0PVFagKjrmFxWUaOSiBsAWPTpXX2rXl0L1F4PR0YpAcMJEzMgfZR9AGymJvLTM+KA6lsQ=="],
|
||||
|
||||
"convex-helpers": ["convex-helpers@0.1.114", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "convex": "^1.32.0", "hono": "^4.0.5", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "typescript": "^5.5", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@standard-schema/spec", "hono", "react", "typescript", "zod"], "bin": { "convex-helpers": "bin.cjs" } }, "sha512-elEdh+gG6BDv2dWIWVvBeJPbHnDQS5+WexUuwlGVJXz1EbMkXz/UIQwFIfLMZIXUwW6ot4JYf/1JJKNStrE6lg=="],
|
||||
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="],
|
||||
"cookie-es": ["cookie-es@2.0.1", "", {}, "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA=="],
|
||||
|
||||
"crossws": ["crossws@0.4.4", "", { "peerDependencies": { "srvx": ">=0.7.1" }, "optionalPeers": ["srvx"] }, "sha512-w6c4OdpRNnudVmcgr7brb/+/HmYjMQvYToO/oTrprTwxRUiom3LYWU1PMWuD006okbUWpII1Ea9/+kwpUfmyRg=="],
|
||||
"crossws": ["crossws@0.4.5", "", { "peerDependencies": { "srvx": ">=0.11.5" }, "optionalPeers": ["srvx"] }, "sha512-wUR89x/Rw7/8t+vn0CmGDYM9TD6VtARGb0LD5jq2wjtMy1vCP4M+sm6N6TigWeTYvnA8MoW29NqqXD0ep0rfBA=="],
|
||||
|
||||
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
|
||||
|
||||
@@ -882,7 +881,7 @@
|
||||
|
||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
|
||||
"diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
|
||||
"diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
|
||||
|
||||
"dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
|
||||
|
||||
@@ -896,7 +895,7 @@
|
||||
|
||||
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.321", "", {}, "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ=="],
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.336", "", {}, "sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ=="],
|
||||
|
||||
"encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="],
|
||||
|
||||
@@ -904,7 +903,7 @@
|
||||
|
||||
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||
|
||||
"env-runner": ["env-runner@0.1.6", "", { "dependencies": { "crossws": "^0.4.4", "httpxy": "^0.3.1", "srvx": "^0.11.9" }, "peerDependencies": { "miniflare": "^4.0.0" }, "optionalPeers": ["miniflare"], "bin": { "env-runner": "dist/cli.mjs" } }, "sha512-fSb7X1zdda8k6611a6/SdSQpDe7a/bqMz2UWdbHjk9YWzpUR4/fn9YtE/hqgGQ2nhvVN0zUtcL1SRMKwIsDbAA=="],
|
||||
"env-runner": ["env-runner@0.1.7", "", { "dependencies": { "crossws": "^0.4.4", "exsolve": "^1.0.8", "httpxy": "^0.5.0", "srvx": "^0.11.13" }, "peerDependencies": { "@netlify/runtime": "^4", "miniflare": "^4.20260317.3" }, "optionalPeers": ["@netlify/runtime", "miniflare"], "bin": { "env-runner": "dist/cli.mjs" } }, "sha512-i7h96jxETJYhXy5grgHNJ9xNzCzWIn9Ck/VkkYgOlE4gOqknsLX3CmlVb5LmwNex8sOoLFVZLz+TIw/+b5rktA=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],
|
||||
|
||||
@@ -926,6 +925,12 @@
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"fast-string-truncated-width": ["fast-string-truncated-width@1.2.1", "", {}, "sha512-Q9acT/+Uu3GwGj+5w/zsGuQjh9O1TyywhIwAxHudtWrgF09nHOPrvTLhQevPbttcxjr/SNN7mJmfOw/B1bXgow=="],
|
||||
|
||||
"fast-string-width": ["fast-string-width@1.1.0", "", { "dependencies": { "fast-string-truncated-width": "^1.2.0" } }, "sha512-O3fwIVIH5gKB38QNbdg+3760ZmGz0SZMgvwJbA1b2TGXceKE6A2cOlfogh1iw8lr049zPyd7YADHy+B7U4W9bQ=="],
|
||||
|
||||
"fast-wrap-ansi": ["fast-wrap-ansi@0.1.6", "", { "dependencies": { "fast-string-width": "^1.1.0" } }, "sha512-HlUwET7a5gqjURj70D5jl7aC3Zmy4weA1SHUfM0JFI0Ptq987NH2TwbBFLoERhfwk+E+eaq4EK3jXoT+R3yp3w=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
|
||||
@@ -974,7 +979,7 @@
|
||||
|
||||
"htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="],
|
||||
|
||||
"httpxy": ["httpxy@0.3.1", "", {}, "sha512-XjG/CEoofEisMrnFr0D6U6xOZ4mRfnwcYQ9qvvnT4lvnX8BoeA3x3WofB75D+vZwpaobFVkBIHrZzoK40w8XSw=="],
|
||||
"httpxy": ["httpxy@0.5.0", "", {}, "sha512-qwX7QX/rK2visT10/b7bSeZWQOMlSm3svTD0pZpU+vJjNUP0YHtNv4c3z+MO+MSnGuRFWJFdCZiV+7F7dXIOzg=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
@@ -1008,7 +1013,7 @@
|
||||
|
||||
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||
|
||||
"isbot": ["isbot@5.1.36", "", {}, "sha512-C/ZtXyJqDPZ7G7JPr06ApWyYoHjYexQbS6hPYD4WYCzpv2Qes6Z+CCEfTX4Owzf+1EJ933PoI2p+B9v7wpGZBQ=="],
|
||||
"isbot": ["isbot@5.1.38", "", {}, "sha512-Cus2702JamTNMEY4zTP+TShgq/3qzjvGcBC4XMOV45BLaxD4iUFENkqu7ZhFeSzwNsCSZLjnGlihDQznnpnEEA=="],
|
||||
|
||||
"istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="],
|
||||
|
||||
@@ -1024,7 +1029,7 @@
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||
|
||||
"jsdom": ["jsdom@29.0.1", "", { "dependencies": { "@asamuzakjp/css-color": "^5.0.1", "@asamuzakjp/dom-selector": "^7.0.3", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.1", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.7", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.24.5", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg=="],
|
||||
"jsdom": ["jsdom@29.0.2", "", { "dependencies": { "@asamuzakjp/css-color": "^5.1.5", "@asamuzakjp/dom-selector": "^7.0.6", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.1", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.7", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.24.5", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
@@ -1062,7 +1067,7 @@
|
||||
|
||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="],
|
||||
"lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="],
|
||||
|
||||
"lucia": ["lucia@3.2.2", "", { "dependencies": { "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0" } }, "sha512-P1FlFBGCMPMXu+EGdVD9W4Mjm0DqsusmKgO7Xc33mI5X1bklmsQb0hfzPhXomQr9waWIBDsiOjvr1e6BTaUqpA=="],
|
||||
|
||||
@@ -1182,11 +1187,11 @@
|
||||
|
||||
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
||||
|
||||
"nf3": ["nf3@0.3.13", "", {}, "sha512-drDt0yl4d/yUhlpD0GzzqahSpA5eUNeIfFq0/aoZb0UlPY0ZwP4u1EfREVvZrYdEnJ3OU9Le9TrzbvWgEkkeKw=="],
|
||||
"nf3": ["nf3@0.3.16", "", {}, "sha512-Gs0xRPpUm2nDkqbi40NJ9g7qDIcjcJzgExiydnq6LAyqhI2jfno8wG3NKTL+IiJsx799UHOb1CnSd4Wg4SG4Pw=="],
|
||||
|
||||
"nitro": ["nitro@3.0.260311-beta", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.4", "db0": "^0.3.4", "env-runner": "^0.1.6", "h3": "^2.0.1-rc.16", "hookable": "^6.0.1", "nf3": "^0.3.11", "ocache": "^0.1.2", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "rolldown": "^1.0.0-rc.8", "srvx": "^0.11.9", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.6" }, "peerDependencies": { "dotenv": "*", "giget": "*", "jiti": "^2.6.1", "rollup": "^4.59.0", "vite": "^7 || ^8 || >=8.0.0-0", "xml2js": "^0.6.2", "zephyr-agent": "^0.1.15" }, "optionalPeers": ["dotenv", "giget", "jiti", "rollup", "vite", "xml2js", "zephyr-agent"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-0o0fJ9LUh4WKUqJNX012jyieUOtMCnadkNDWr0mHzdraoHpJP/1CGNefjRyZyMXSpoJfwoWdNEZu2iGf35TUvQ=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
|
||||
"node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
@@ -1214,7 +1219,7 @@
|
||||
|
||||
"oxfmt": ["oxfmt@0.41.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.41.0", "@oxfmt/binding-android-arm64": "0.41.0", "@oxfmt/binding-darwin-arm64": "0.41.0", "@oxfmt/binding-darwin-x64": "0.41.0", "@oxfmt/binding-freebsd-x64": "0.41.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.41.0", "@oxfmt/binding-linux-arm-musleabihf": "0.41.0", "@oxfmt/binding-linux-arm64-gnu": "0.41.0", "@oxfmt/binding-linux-arm64-musl": "0.41.0", "@oxfmt/binding-linux-ppc64-gnu": "0.41.0", "@oxfmt/binding-linux-riscv64-gnu": "0.41.0", "@oxfmt/binding-linux-riscv64-musl": "0.41.0", "@oxfmt/binding-linux-s390x-gnu": "0.41.0", "@oxfmt/binding-linux-x64-gnu": "0.41.0", "@oxfmt/binding-linux-x64-musl": "0.41.0", "@oxfmt/binding-openharmony-arm64": "0.41.0", "@oxfmt/binding-win32-arm64-msvc": "0.41.0", "@oxfmt/binding-win32-ia32-msvc": "0.41.0", "@oxfmt/binding-win32-x64-msvc": "0.41.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-sKLdJZdQ3bw6x9qKiT7+eID4MNEXlDHf5ZacfIircrq6Qwjk0L6t2/JQlZZrVHTXJawK3KaMuBoJnEJPcqCEdg=="],
|
||||
|
||||
"oxlint": ["oxlint@1.58.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.58.0", "@oxlint/binding-android-arm64": "1.58.0", "@oxlint/binding-darwin-arm64": "1.58.0", "@oxlint/binding-darwin-x64": "1.58.0", "@oxlint/binding-freebsd-x64": "1.58.0", "@oxlint/binding-linux-arm-gnueabihf": "1.58.0", "@oxlint/binding-linux-arm-musleabihf": "1.58.0", "@oxlint/binding-linux-arm64-gnu": "1.58.0", "@oxlint/binding-linux-arm64-musl": "1.58.0", "@oxlint/binding-linux-ppc64-gnu": "1.58.0", "@oxlint/binding-linux-riscv64-gnu": "1.58.0", "@oxlint/binding-linux-riscv64-musl": "1.58.0", "@oxlint/binding-linux-s390x-gnu": "1.58.0", "@oxlint/binding-linux-x64-gnu": "1.58.0", "@oxlint/binding-linux-x64-musl": "1.58.0", "@oxlint/binding-openharmony-arm64": "1.58.0", "@oxlint/binding-win32-arm64-msvc": "1.58.0", "@oxlint/binding-win32-ia32-msvc": "1.58.0", "@oxlint/binding-win32-x64-msvc": "1.58.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.18.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-t4s9leczDMqlvOSjnbCQe7gtoLkWgBGZ7sBdCJ9EOj5IXFSG/X7OAzK4yuH4iW+4cAYe8kLFbC8tuYMwWZm+Cg=="],
|
||||
"oxlint": ["oxlint@1.60.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.60.0", "@oxlint/binding-android-arm64": "1.60.0", "@oxlint/binding-darwin-arm64": "1.60.0", "@oxlint/binding-darwin-x64": "1.60.0", "@oxlint/binding-freebsd-x64": "1.60.0", "@oxlint/binding-linux-arm-gnueabihf": "1.60.0", "@oxlint/binding-linux-arm-musleabihf": "1.60.0", "@oxlint/binding-linux-arm64-gnu": "1.60.0", "@oxlint/binding-linux-arm64-musl": "1.60.0", "@oxlint/binding-linux-ppc64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-gnu": "1.60.0", "@oxlint/binding-linux-riscv64-musl": "1.60.0", "@oxlint/binding-linux-s390x-gnu": "1.60.0", "@oxlint/binding-linux-x64-gnu": "1.60.0", "@oxlint/binding-linux-x64-musl": "1.60.0", "@oxlint/binding-openharmony-arm64": "1.60.0", "@oxlint/binding-win32-arm64-msvc": "1.60.0", "@oxlint/binding-win32-ia32-msvc": "1.60.0", "@oxlint/binding-win32-x64-msvc": "1.60.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.18.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-tnRzTWiWJ9pg3ftRWnD0+Oqh78L6ZSwcEudvCZaER0PIqiAnNyXj5N1dPwjmNpDalkKS9m/WMLN1CTPUBPmsgw=="],
|
||||
|
||||
"oxlint-tsgolint": ["oxlint-tsgolint@0.17.4", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.17.4", "@oxlint-tsgolint/darwin-x64": "0.17.4", "@oxlint-tsgolint/linux-arm64": "0.17.4", "@oxlint-tsgolint/linux-x64": "0.17.4", "@oxlint-tsgolint/win32-arm64": "0.17.4", "@oxlint-tsgolint/win32-x64": "0.17.4" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-4F/NXJiK2KnK4LQiULUPXRzVq0LOfextGvwCVRW1VKQbF5epI3MDMEGVAl5XjAGL6IFc7xBc/eVA95wczPeEQg=="],
|
||||
|
||||
@@ -1234,19 +1239,19 @@
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
|
||||
"playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="],
|
||||
|
||||
"playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="],
|
||||
|
||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||
"postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="],
|
||||
|
||||
"preact": ["preact@10.24.3", "", {}, "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA=="],
|
||||
|
||||
"preact-render-to-string": ["preact-render-to-string@6.5.11", "", { "peerDependencies": { "preact": ">=10" } }, "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw=="],
|
||||
|
||||
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
||||
"prettier": ["prettier@3.8.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q=="],
|
||||
|
||||
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
||||
|
||||
@@ -1254,9 +1259,9 @@
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
||||
"react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||
"react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="],
|
||||
|
||||
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
||||
|
||||
@@ -1304,9 +1309,9 @@
|
||||
|
||||
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"seroval": ["seroval@1.5.1", "", {}, "sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA=="],
|
||||
"seroval": ["seroval@1.5.2", "", {}, "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q=="],
|
||||
|
||||
"seroval-plugins": ["seroval-plugins@1.5.1", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw=="],
|
||||
"seroval-plugins": ["seroval-plugins@1.5.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg=="],
|
||||
|
||||
"server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="],
|
||||
|
||||
@@ -1322,7 +1327,7 @@
|
||||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
||||
"solid-js": ["solid-js@1.9.11", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q=="],
|
||||
"solid-js": ["solid-js@1.9.12", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw=="],
|
||||
|
||||
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
|
||||
|
||||
@@ -1332,7 +1337,7 @@
|
||||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"srvx": ["srvx@0.11.12", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-AQfrGqntqVPXgP03pvBDN1KyevHC+KmYVqb8vVf4N+aomQqdhaZxjvoVp+AOm4u6x+GgNQY3MVzAUIn+TqwkOA=="],
|
||||
"srvx": ["srvx@0.11.15", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg=="],
|
||||
|
||||
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||
|
||||
@@ -1362,7 +1367,7 @@
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
|
||||
|
||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||
"tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="],
|
||||
|
||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||
|
||||
@@ -1370,17 +1375,17 @@
|
||||
|
||||
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||
|
||||
"tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
|
||||
"tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
|
||||
|
||||
"tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="],
|
||||
|
||||
"tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
|
||||
|
||||
"tldts": ["tldts@7.0.27", "", { "dependencies": { "tldts-core": "^7.0.27" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg=="],
|
||||
"tldts": ["tldts@7.0.28", "", { "dependencies": { "tldts-core": "^7.0.28" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw=="],
|
||||
|
||||
"tldts-core": ["tldts-core@7.0.27", "", {}, "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg=="],
|
||||
"tldts-core": ["tldts-core@7.0.28", "", {}, "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
@@ -1404,9 +1409,9 @@
|
||||
|
||||
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
||||
|
||||
"undici": ["undici@7.24.7", "", {}, "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ=="],
|
||||
"undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
"undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
|
||||
|
||||
"unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="],
|
||||
|
||||
@@ -1442,9 +1447,9 @@
|
||||
|
||||
"vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="],
|
||||
|
||||
"vitefu": ["vitefu@1.1.2", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw=="],
|
||||
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
|
||||
|
||||
"vitest": ["vitest@4.1.2", "", { "dependencies": { "@vitest/expect": "4.1.2", "@vitest/mocker": "4.1.2", "@vitest/pretty-format": "4.1.2", "@vitest/runner": "4.1.2", "@vitest/snapshot": "4.1.2", "@vitest/spy": "4.1.2", "@vitest/utils": "4.1.2", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "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.2", "@vitest/browser-preview": "4.1.2", "@vitest/browser-webdriverio": "4.1.2", "@vitest/ui": "4.1.2", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg=="],
|
||||
"vitest": ["vitest@4.1.4", "", { "dependencies": { "@vitest/expect": "4.1.4", "@vitest/mocker": "4.1.4", "@vitest/pretty-format": "4.1.4", "@vitest/runner": "4.1.4", "@vitest/snapshot": "4.1.4", "@vitest/spy": "4.1.4", "@vitest/utils": "4.1.4", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "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.4", "@vitest/browser-preview": "4.1.4", "@vitest/browser-webdriverio": "4.1.4", "@vitest/coverage-istanbul": "4.1.4", "@vitest/coverage-v8": "4.1.4", "@vitest/ui": "4.1.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg=="],
|
||||
|
||||
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
|
||||
|
||||
@@ -1556,13 +1561,13 @@
|
||||
|
||||
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
@@ -1580,27 +1585,19 @@
|
||||
|
||||
"@tanstack/start-plugin-core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
"anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"ast-v8-to-istanbul/js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="],
|
||||
|
||||
"cheerio/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
|
||||
"cheerio/undici": ["undici@7.24.5", "", {}, "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q=="],
|
||||
|
||||
"cheerio/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
|
||||
|
||||
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"jsdom/undici": ["undici@7.24.5", "", {}, "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q=="],
|
||||
|
||||
<<<<<<< staging
|
||||
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
=======
|
||||
"nitro/rolldown": ["rolldown@1.0.0-rc.10", "", { "dependencies": { "@oxc-project/types": "=0.120.0", "@rolldown/pluginutils": "1.0.0-rc.10" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-x64": "1.0.0-rc.10", "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA=="],
|
||||
>>>>>>> main
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
@@ -1610,7 +1607,7 @@
|
||||
|
||||
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||
|
||||
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
"readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
@@ -1618,7 +1615,6 @@
|
||||
|
||||
"strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
<<<<<<< staging
|
||||
"@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
@@ -1644,80 +1640,5 @@
|
||||
"@radix-ui/react-toggle/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
=======
|
||||
"vite/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
|
||||
"vitest/vite": ["vite@8.0.1", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.10", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw=="],
|
||||
|
||||
"nitro/rolldown/@oxc-project/types": ["@oxc-project/types@0.120.0", "", {}, "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.10", "", { "os": "android", "cpu": "arm64" }, "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm" }, "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "x64" }, "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.10", "", { "os": "linux", "cpu": "x64" }, "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.10", "", { "os": "none", "cpu": "arm64" }, "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.10", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.10", "", { "os": "win32", "cpu": "x64" }, "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w=="],
|
||||
|
||||
"nitro/rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.10", "", {}, "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg=="],
|
||||
|
||||
"vitest/vite/rolldown": ["rolldown@1.0.0-rc.10", "", { "dependencies": { "@oxc-project/types": "=0.120.0", "@rolldown/pluginutils": "1.0.0-rc.10" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-x64": "1.0.0-rc.10", "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA=="],
|
||||
|
||||
"vitest/vite/rolldown/@oxc-project/types": ["@oxc-project/types@0.120.0", "", {}, "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg=="],
|
||||
|
||||
"vitest/vite/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.10", "", { "os": "android", "cpu": "arm64" }, "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg=="],
|
||||
|
||||
"vitest/vite/rolldown/@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w=="],
|
||||
|
||||
"vitest/vite/rolldown/@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A=="],
|
||||
|
||||
"vitest/vite/rolldown/@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w=="],
|
||||
|
||||
"vitest/vite/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm" }, "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA=="],
|
||||
|
||||
"vitest/vite/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg=="],
|
||||
|
||||
"vitest/vite/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g=="],
|
||||
|
||||
"vitest/vite/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w=="],
|
||||
|
||||
"vitest/vite/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg=="],
|
||||
|
||||
"vitest/vite/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.10", "", { "os": "linux", "cpu": "x64" }, "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw=="],
|
||||
|
||||
"vitest/vite/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.10", "", { "os": "linux", "cpu": "x64" }, "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA=="],
|
||||
|
||||
"vitest/vite/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.10", "", { "os": "none", "cpu": "arm64" }, "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q=="],
|
||||
|
||||
"vitest/vite/rolldown/@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.10", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA=="],
|
||||
|
||||
"vitest/vite/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ=="],
|
||||
|
||||
"vitest/vite/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.10", "", { "os": "win32", "cpu": "x64" }, "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w=="],
|
||||
|
||||
"vitest/vite/rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.10", "", {}, "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg=="],
|
||||
>>>>>>> main
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1197,6 +1197,154 @@ describe("httpApiV1 handlers", () => {
|
||||
expect(json.version.security.virustotalUrl).toContain("virustotal.com/gui/file/");
|
||||
});
|
||||
|
||||
it("surfaces static-scan suspicious status in version security snapshot", async () => {
|
||||
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
|
||||
if ("slug" in args) {
|
||||
return {
|
||||
skill: { _id: "skills:1", slug: "demo", displayName: "Demo" },
|
||||
latestVersion: null,
|
||||
owner: { handle: "owner", displayName: "Owner", image: null },
|
||||
};
|
||||
}
|
||||
if ("skillId" in args && "version" in args) {
|
||||
return {
|
||||
version: "1.0.0",
|
||||
createdAt: 1,
|
||||
changelog: "c",
|
||||
changelogSource: "auto",
|
||||
sha256hash: "a".repeat(64),
|
||||
staticScan: {
|
||||
status: "suspicious",
|
||||
reasonCodes: ["suspicious.dangerous_exec"],
|
||||
summary: "Detected: suspicious.dangerous_exec",
|
||||
engineVersion: "v2.4.0",
|
||||
checkedAt: 555,
|
||||
},
|
||||
vtAnalysis: {
|
||||
status: "clean",
|
||||
verdict: "benign",
|
||||
checkedAt: 111,
|
||||
},
|
||||
llmAnalysis: {
|
||||
status: "completed",
|
||||
verdict: "benign",
|
||||
checkedAt: 222,
|
||||
},
|
||||
files: [],
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const runMutation = vi.fn().mockResolvedValue(okRate());
|
||||
const response = await __handlers.skillsGetRouterV1Handler(
|
||||
makeCtx({ runQuery, runMutation }),
|
||||
new Request("https://example.com/api/v1/skills/demo/versions/1.0.0"),
|
||||
);
|
||||
expect(response.status).toBe(200);
|
||||
const json = await response.json();
|
||||
expect(json.version.security.status).toBe("suspicious");
|
||||
expect(json.version.security.hasWarnings).toBe(true);
|
||||
expect(json.version.security.hasScanResult).toBe(true);
|
||||
expect(json.version.security.scanners.static.normalizedStatus).toBe("suspicious");
|
||||
expect(json.version.security.scanners.vt.normalizedStatus).toBe("clean");
|
||||
expect(json.version.security.scanners.llm.normalizedStatus).toBe("clean");
|
||||
});
|
||||
|
||||
it("lets static-scan malicious status dominate benign vt and llm results", async () => {
|
||||
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
|
||||
if ("slug" in args) {
|
||||
return {
|
||||
skill: { _id: "skills:1", slug: "demo", displayName: "Demo" },
|
||||
latestVersion: null,
|
||||
owner: { handle: "owner", displayName: "Owner", image: null },
|
||||
};
|
||||
}
|
||||
if ("skillId" in args && "version" in args) {
|
||||
return {
|
||||
version: "1.0.0",
|
||||
createdAt: 1,
|
||||
changelog: "c",
|
||||
changelogSource: "auto",
|
||||
sha256hash: "a".repeat(64),
|
||||
staticScan: {
|
||||
status: "malicious",
|
||||
reasonCodes: ["malicious.credential_harvest"],
|
||||
summary: "Detected: malicious.credential_harvest",
|
||||
engineVersion: "v2.4.0",
|
||||
checkedAt: 555,
|
||||
},
|
||||
vtAnalysis: {
|
||||
status: "clean",
|
||||
verdict: "benign",
|
||||
checkedAt: 111,
|
||||
},
|
||||
llmAnalysis: {
|
||||
status: "completed",
|
||||
verdict: "benign",
|
||||
checkedAt: 222,
|
||||
},
|
||||
files: [],
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const runMutation = vi.fn().mockResolvedValue(okRate());
|
||||
const response = await __handlers.skillsGetRouterV1Handler(
|
||||
makeCtx({ runQuery, runMutation }),
|
||||
new Request("https://example.com/api/v1/skills/demo/versions/1.0.0"),
|
||||
);
|
||||
expect(response.status).toBe(200);
|
||||
const json = await response.json();
|
||||
expect(json.version.security.status).toBe("malicious");
|
||||
expect(json.version.security.hasWarnings).toBe(true);
|
||||
expect(json.version.security.hasScanResult).toBe(true);
|
||||
expect(json.version.security.checkedAt).toBe(555);
|
||||
expect(json.version.security.scanners.static.normalizedStatus).toBe("malicious");
|
||||
});
|
||||
|
||||
it("treats a static scan by itself as a definitive scan result", async () => {
|
||||
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
|
||||
if ("slug" in args) {
|
||||
return {
|
||||
skill: { _id: "skills:1", slug: "demo", displayName: "Demo" },
|
||||
latestVersion: null,
|
||||
owner: { handle: "owner", displayName: "Owner", image: null },
|
||||
};
|
||||
}
|
||||
if ("skillId" in args && "version" in args) {
|
||||
return {
|
||||
version: "1.0.0",
|
||||
createdAt: 1,
|
||||
changelog: "c",
|
||||
changelogSource: "auto",
|
||||
staticScan: {
|
||||
status: "clean",
|
||||
reasonCodes: [],
|
||||
summary: "No issues found",
|
||||
engineVersion: "v2.4.0",
|
||||
checkedAt: 555,
|
||||
},
|
||||
files: [],
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const runMutation = vi.fn().mockResolvedValue(okRate());
|
||||
const response = await __handlers.skillsGetRouterV1Handler(
|
||||
makeCtx({ runQuery, runMutation }),
|
||||
new Request("https://example.com/api/v1/skills/demo/versions/1.0.0"),
|
||||
);
|
||||
expect(response.status).toBe(200);
|
||||
const json = await response.json();
|
||||
expect(json.version.security.status).toBe("clean");
|
||||
expect(json.version.security.hasWarnings).toBe(false);
|
||||
expect(json.version.security.hasScanResult).toBe(true);
|
||||
expect(json.version.security.virustotalUrl).toBeNull();
|
||||
expect(json.version.security.scanners.static.normalizedStatus).toBe("clean");
|
||||
expect(json.version.security.scanners.vt).toBeNull();
|
||||
expect(json.version.security.scanners.llm).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps hasWarnings true when llm dimensions include non-ok ratings", async () => {
|
||||
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
|
||||
if ("slug" in args) {
|
||||
|
||||
@@ -71,6 +71,11 @@ type PublicSkillVersionParsed = {
|
||||
clawdis?: { os?: string[]; nix?: { plugin?: boolean; systems?: string[] } };
|
||||
};
|
||||
|
||||
type PublicSkillVersionStaticScan = Pick<
|
||||
NonNullable<Doc<"skillVersions">["staticScan"]>,
|
||||
"status" | "reasonCodes" | "summary" | "engineVersion" | "checkedAt"
|
||||
>;
|
||||
|
||||
type PublicSkillVersionResponse = {
|
||||
_id: Id<"skillVersions">;
|
||||
version: string;
|
||||
@@ -83,6 +88,7 @@ type PublicSkillVersionResponse = {
|
||||
sha256hash?: string;
|
||||
vtAnalysis?: Doc<"skillVersions">["vtAnalysis"];
|
||||
llmAnalysis?: Doc<"skillVersions">["llmAnalysis"];
|
||||
staticScan?: PublicSkillVersionStaticScan;
|
||||
capabilityTags?: string[];
|
||||
};
|
||||
|
||||
@@ -194,6 +200,14 @@ type SkillSecuritySnapshot = {
|
||||
virustotalUrl: string | null;
|
||||
capabilityTags: string[];
|
||||
scanners: {
|
||||
static: {
|
||||
status: string;
|
||||
normalizedStatus: NormalizedSecurityStatus;
|
||||
reasonCodes: string[];
|
||||
summary: string | null;
|
||||
engineVersion: string | null;
|
||||
checkedAt: number | null;
|
||||
} | null;
|
||||
vt: {
|
||||
status: string;
|
||||
verdict: string | null;
|
||||
@@ -277,30 +291,35 @@ function hasLlmDimensionWarnings(
|
||||
function buildSkillSecuritySnapshot(
|
||||
version: Pick<
|
||||
PublicSkillVersionResponse,
|
||||
"sha256hash" | "vtAnalysis" | "llmAnalysis" | "capabilityTags"
|
||||
"sha256hash" | "vtAnalysis" | "llmAnalysis" | "staticScan" | "capabilityTags"
|
||||
>,
|
||||
): SkillSecuritySnapshot | null {
|
||||
const capabilityTags = version.capabilityTags ?? [];
|
||||
const sha256hash = version.sha256hash ?? null;
|
||||
const vt = version.vtAnalysis;
|
||||
const llm = version.llmAnalysis;
|
||||
const staticScan = version.staticScan;
|
||||
|
||||
if (!sha256hash && !vt && !llm && capabilityTags.length === 0) return null;
|
||||
if (!sha256hash && !vt && !llm && !staticScan && capabilityTags.length === 0) return null;
|
||||
|
||||
const staticStatus = staticScan ? normalizeSecurityStatus(staticScan.status) : null;
|
||||
const vtStatus = vt ? normalizeSecurityStatus(vt.verdict ?? vt.status) : null;
|
||||
const llmStatus = llm ? normalizeSecurityStatus(llm.verdict ?? llm.status) : null;
|
||||
|
||||
const statuses: NormalizedSecurityStatus[] = [];
|
||||
if (staticStatus) statuses.push(staticStatus);
|
||||
if (vtStatus) statuses.push(vtStatus);
|
||||
if (llmStatus) statuses.push(llmStatus);
|
||||
if (statuses.length === 0 && sha256hash) statuses.push("pending");
|
||||
const status = mergeSecurityStatuses(statuses);
|
||||
const hasScanResult =
|
||||
isDefinitiveSecurityStatus(vtStatus) || isDefinitiveSecurityStatus(llmStatus);
|
||||
isDefinitiveSecurityStatus(staticStatus) ||
|
||||
isDefinitiveSecurityStatus(vtStatus) ||
|
||||
isDefinitiveSecurityStatus(llmStatus);
|
||||
const hasWarnings =
|
||||
status === "suspicious" || status === "malicious" || hasLlmDimensionWarnings(llm?.dimensions);
|
||||
|
||||
const checkedAtCandidates = [vt?.checkedAt, llm?.checkedAt].filter(
|
||||
const checkedAtCandidates = [staticScan?.checkedAt, vt?.checkedAt, llm?.checkedAt].filter(
|
||||
(value): value is number => typeof value === "number",
|
||||
);
|
||||
const checkedAt = checkedAtCandidates.length > 0 ? Math.max(...checkedAtCandidates) : null;
|
||||
@@ -315,6 +334,16 @@ function buildSkillSecuritySnapshot(
|
||||
virustotalUrl: sha256hash ? `https://www.virustotal.com/gui/file/${sha256hash}` : null,
|
||||
capabilityTags,
|
||||
scanners: {
|
||||
static: staticScan
|
||||
? {
|
||||
status: staticScan.status,
|
||||
normalizedStatus: staticStatus ?? "pending",
|
||||
reasonCodes: staticScan.reasonCodes ?? [],
|
||||
summary: staticScan.summary ?? null,
|
||||
engineVersion: staticScan.engineVersion ?? null,
|
||||
checkedAt: staticScan.checkedAt ?? null,
|
||||
}
|
||||
: null,
|
||||
vt: vt
|
||||
? {
|
||||
status: vt.status,
|
||||
|
||||
@@ -26,6 +26,7 @@ describe("deriveSkillCapabilityTags", () => {
|
||||
"requires-wallet",
|
||||
"can-make-purchases",
|
||||
"can-sign-transactions",
|
||||
"requires-sensitive-credentials",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -39,7 +40,24 @@ describe("deriveSkillCapabilityTags", () => {
|
||||
fileContents: [],
|
||||
});
|
||||
|
||||
expect(tags).toEqual(["requires-oauth-token", "posts-externally"]);
|
||||
expect(tags).toEqual([
|
||||
"requires-oauth-token",
|
||||
"requires-sensitive-credentials",
|
||||
"posts-externally",
|
||||
]);
|
||||
});
|
||||
|
||||
it("detects non-oauth API key skills that still need sensitive credentials", () => {
|
||||
const tags = deriveSkillCapabilityTags({
|
||||
slug: "minimax-usage",
|
||||
displayName: "Minimax Usage",
|
||||
frontmatter: {},
|
||||
readmeText:
|
||||
"Create a .env file with MINIMAX_CODING_API_KEY and MINIMAX_GROUP_ID, then send an authorization: Bearer header to the MiniMax endpoint.",
|
||||
fileContents: [],
|
||||
});
|
||||
|
||||
expect(tags).toEqual(["requires-sensitive-credentials"]);
|
||||
});
|
||||
|
||||
it("does not treat generic broadcast wording as a crypto transaction signal", () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ export const SKILL_CAPABILITY_TAGS = [
|
||||
"can-make-purchases",
|
||||
"can-sign-transactions",
|
||||
"requires-oauth-token",
|
||||
"requires-sensitive-credentials",
|
||||
"posts-externally",
|
||||
] as const;
|
||||
|
||||
@@ -96,6 +97,19 @@ const OAUTH_PATTERNS = [
|
||||
/\btweet\.write\b/,
|
||||
] satisfies RegExp[];
|
||||
|
||||
const SENSITIVE_CREDENTIAL_PATTERNS = [
|
||||
/api[_ -]?key\b/,
|
||||
/\baccess token\b/,
|
||||
/\brefresh token\b/,
|
||||
/\bbearer token\b/,
|
||||
/\bsession (?:cookie|cookies)\b/,
|
||||
/\bauth(?:entication)? (?:cookie|cookies)\b/,
|
||||
/\bprivate[_ -]?key\b/,
|
||||
/\bmnemonic\b/,
|
||||
/\bseed phrase\b/,
|
||||
/\bsigner\b/,
|
||||
] satisfies RegExp[];
|
||||
|
||||
const EXTERNAL_POST_PATTERNS = [
|
||||
/\bpost(?: a| this)? tweet\b/,
|
||||
/\breply to (?:this )?tweet\b/,
|
||||
@@ -129,6 +143,7 @@ export function deriveSkillCapabilityTags(params: {
|
||||
const canMakePurchases = matches(text, PURCHASE_PATTERNS);
|
||||
const canSignTransactions = matches(text, TRANSACTION_PATTERNS);
|
||||
const requiresOauthToken = matches(text, OAUTH_PATTERNS);
|
||||
const requiresSensitiveCredentials = matches(text, SENSITIVE_CREDENTIAL_PATTERNS);
|
||||
const postsExternally = matches(text, EXTERNAL_POST_PATTERNS);
|
||||
|
||||
if (isCrypto) tags.add("crypto");
|
||||
@@ -136,6 +151,7 @@ export function deriveSkillCapabilityTags(params: {
|
||||
if (canMakePurchases) tags.add("can-make-purchases");
|
||||
if (canSignTransactions) tags.add("can-sign-transactions");
|
||||
if (requiresOauthToken) tags.add("requires-oauth-token");
|
||||
if (requiresSensitiveCredentials) tags.add("requires-sensitive-credentials");
|
||||
if (postsExternally) tags.add("posts-externally");
|
||||
|
||||
if (canSignTransactions || canMakePurchases) {
|
||||
@@ -144,6 +160,9 @@ export function deriveSkillCapabilityTags(params: {
|
||||
if (canSignTransactions) {
|
||||
tags.add("requires-wallet");
|
||||
}
|
||||
if (requiresWallet || canSignTransactions || requiresOauthToken) {
|
||||
tags.add("requires-sensitive-credentials");
|
||||
}
|
||||
|
||||
return SKILL_CAPABILITY_TAGS.filter((tag) => tags.has(tag));
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ vi.mock("./_generated/api", () => ({
|
||||
getSkillBackfillPageInternal: Symbol("getSkillBackfillPageInternal"),
|
||||
applySkillBackfillPatchInternal: Symbol("applySkillBackfillPatchInternal"),
|
||||
backfillSkillSummariesInternal: Symbol("backfillSkillSummariesInternal"),
|
||||
getUserStatsBackfillPageInternal: Symbol("getUserStatsBackfillPageInternal"),
|
||||
getUserOwnedSkillsBackfillPageInternal: Symbol("getUserOwnedSkillsBackfillPageInternal"),
|
||||
applyUserStatsBackfillPatchInternal: Symbol("applyUserStatsBackfillPatchInternal"),
|
||||
backfillUserStatsInternal: Symbol("backfillUserStatsInternal"),
|
||||
getSkillFingerprintBackfillPageInternal: Symbol("getSkillFingerprintBackfillPageInternal"),
|
||||
applySkillFingerprintBackfillPatchInternal: Symbol(
|
||||
"applySkillFingerprintBackfillPatchInternal",
|
||||
@@ -36,6 +40,7 @@ const {
|
||||
backfillLatestVersionSummaryInternal,
|
||||
backfillSkillFingerprintsInternalHandler,
|
||||
backfillSkillSummariesInternalHandler,
|
||||
backfillUserStatsInternalHandler,
|
||||
cleanupEmptySkillsInternalHandler,
|
||||
nominateEmptySkillSpammersInternalHandler,
|
||||
upsertSkillBadgeRecordInternal,
|
||||
@@ -259,6 +264,63 @@ describe("maintenance backfill", () => {
|
||||
});
|
||||
expect(runAfter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("backfills denormalized user hover stats from indexed owner pages", async () => {
|
||||
const runQuery = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
items: [{ _id: "users:1" }],
|
||||
cursor: null,
|
||||
isDone: true,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
items: [
|
||||
{ stats: { stars: 4, downloads: 30 }, softDeletedAt: undefined },
|
||||
{ stats: { stars: 2, downloads: 10 }, softDeletedAt: 123 },
|
||||
{ stats: { stars: 1, downloads: 5 }, softDeletedAt: undefined },
|
||||
],
|
||||
cursor: null,
|
||||
isDone: true,
|
||||
});
|
||||
const runMutation = vi.fn().mockResolvedValue({ ok: true });
|
||||
|
||||
const result = await backfillUserStatsInternalHandler(
|
||||
{ runQuery, runMutation } as never,
|
||||
{ batchSize: 10, skillBatchSize: 50, maxBatches: 1 },
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
stats: {
|
||||
usersScanned: 1,
|
||||
usersPatched: 1,
|
||||
},
|
||||
isDone: true,
|
||||
cursor: null,
|
||||
});
|
||||
expect(runQuery).toHaveBeenNthCalledWith(1, internal.maintenance.getUserStatsBackfillPageInternal, {
|
||||
cursor: undefined,
|
||||
batchSize: 10,
|
||||
});
|
||||
expect(runQuery).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
internal.maintenance.getUserOwnedSkillsBackfillPageInternal,
|
||||
{
|
||||
ownerUserId: "users:1",
|
||||
cursor: undefined,
|
||||
batchSize: 50,
|
||||
},
|
||||
);
|
||||
expect(runMutation).toHaveBeenCalledWith(
|
||||
internal.maintenance.applyUserStatsBackfillPatchInternal,
|
||||
{
|
||||
userId: "users:1",
|
||||
publishedSkills: 2,
|
||||
totalStars: 5,
|
||||
totalDownloads: 35,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("maintenance badge denormalization", () => {
|
||||
|
||||
@@ -36,6 +36,11 @@ type BackfillStats = {
|
||||
missingStorageBlob: number;
|
||||
};
|
||||
|
||||
type UserStatsBackfillStats = {
|
||||
usersScanned: number;
|
||||
usersPatched: number;
|
||||
};
|
||||
|
||||
type BackfillPageItem =
|
||||
| {
|
||||
kind: "ok";
|
||||
@@ -57,6 +62,18 @@ type BackfillPageResult = {
|
||||
isDone: boolean;
|
||||
};
|
||||
|
||||
type UserStatsBackfillPageResult = {
|
||||
items: Array<Pick<Doc<"users">, "_id">>;
|
||||
cursor: string | null;
|
||||
isDone: boolean;
|
||||
};
|
||||
|
||||
type UserOwnedSkillsBackfillPageResult = {
|
||||
items: Array<Pick<Doc<"skills">, "stats" | "softDeletedAt">>;
|
||||
cursor: string | null;
|
||||
isDone: boolean;
|
||||
};
|
||||
|
||||
export const getSkillBackfillPageInternal = internalQuery({
|
||||
args: {
|
||||
cursor: v.optional(v.string()),
|
||||
@@ -136,6 +153,65 @@ export const applySkillBackfillPatchInternal = internalMutation({
|
||||
},
|
||||
});
|
||||
|
||||
export const getUserStatsBackfillPageInternal = internalQuery({
|
||||
args: {
|
||||
cursor: v.optional(v.string()),
|
||||
batchSize: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args): Promise<UserStatsBackfillPageResult> => {
|
||||
const batchSize = clampInt(args.batchSize ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE);
|
||||
const { page, isDone, continueCursor } = await ctx.db
|
||||
.query("users")
|
||||
.order("asc")
|
||||
.paginate({ cursor: args.cursor ?? null, numItems: batchSize });
|
||||
return {
|
||||
items: page.map((user) => ({ _id: user._id })),
|
||||
cursor: continueCursor,
|
||||
isDone,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const getUserOwnedSkillsBackfillPageInternal = internalQuery({
|
||||
args: {
|
||||
ownerUserId: v.id("users"),
|
||||
cursor: v.optional(v.string()),
|
||||
batchSize: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args): Promise<UserOwnedSkillsBackfillPageResult> => {
|
||||
const batchSize = clampInt(args.batchSize ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE);
|
||||
const { page, isDone, continueCursor } = await ctx.db
|
||||
.query("skills")
|
||||
.withIndex("by_owner", (q) => q.eq("ownerUserId", args.ownerUserId))
|
||||
.paginate({ cursor: args.cursor ?? null, numItems: batchSize });
|
||||
return {
|
||||
items: page.map((skill) => ({
|
||||
stats: skill.stats,
|
||||
softDeletedAt: skill.softDeletedAt,
|
||||
})),
|
||||
cursor: continueCursor,
|
||||
isDone,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const applyUserStatsBackfillPatchInternal = internalMutation({
|
||||
args: {
|
||||
userId: v.id("users"),
|
||||
publishedSkills: v.number(),
|
||||
totalStars: v.number(),
|
||||
totalDownloads: v.number(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.userId, {
|
||||
publishedSkills: args.publishedSkills,
|
||||
totalStars: args.totalStars,
|
||||
totalDownloads: args.totalDownloads,
|
||||
});
|
||||
return { ok: true as const };
|
||||
},
|
||||
});
|
||||
|
||||
export type BackfillActionArgs = {
|
||||
dryRun?: boolean;
|
||||
batchSize?: number;
|
||||
@@ -151,6 +227,20 @@ export type BackfillActionResult = {
|
||||
cursor: string | null;
|
||||
};
|
||||
|
||||
export type UserStatsBackfillActionArgs = {
|
||||
batchSize?: number;
|
||||
skillBatchSize?: number;
|
||||
maxBatches?: number;
|
||||
cursor?: string;
|
||||
};
|
||||
|
||||
export type UserStatsBackfillActionResult = {
|
||||
ok: true;
|
||||
stats: UserStatsBackfillStats;
|
||||
isDone: boolean;
|
||||
cursor: string | null;
|
||||
};
|
||||
|
||||
export async function backfillSkillSummariesInternalHandler(
|
||||
ctx: ActionCtx,
|
||||
args: BackfillActionArgs,
|
||||
@@ -246,6 +336,73 @@ export async function backfillSkillSummariesInternalHandler(
|
||||
return { ok: true as const, stats: totals, isDone, cursor };
|
||||
}
|
||||
|
||||
export async function backfillUserStatsInternalHandler(
|
||||
ctx: ActionCtx,
|
||||
args: UserStatsBackfillActionArgs,
|
||||
): Promise<UserStatsBackfillActionResult> {
|
||||
const batchSize = clampInt(args.batchSize ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE);
|
||||
const skillBatchSize = clampInt(args.skillBatchSize ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE);
|
||||
const maxBatches = clampInt(args.maxBatches ?? DEFAULT_MAX_BATCHES, 1, MAX_MAX_BATCHES);
|
||||
const totals: UserStatsBackfillStats = {
|
||||
usersScanned: 0,
|
||||
usersPatched: 0,
|
||||
};
|
||||
|
||||
let cursor: string | null = args.cursor ?? null;
|
||||
let isDone = false;
|
||||
|
||||
for (let i = 0; i < maxBatches; i++) {
|
||||
const page = (await ctx.runQuery(internal.maintenance.getUserStatsBackfillPageInternal, {
|
||||
cursor: cursor ?? undefined,
|
||||
batchSize,
|
||||
})) as UserStatsBackfillPageResult;
|
||||
|
||||
cursor = page.cursor;
|
||||
isDone = page.isDone;
|
||||
|
||||
for (const user of page.items) {
|
||||
totals.usersScanned++;
|
||||
let ownedSkillsCursor: string | null = null;
|
||||
let userPublishedSkills = 0;
|
||||
let userTotalStars = 0;
|
||||
let userTotalDownloads = 0;
|
||||
|
||||
while (true) {
|
||||
const skillPage = (await ctx.runQuery(
|
||||
internal.maintenance.getUserOwnedSkillsBackfillPageInternal,
|
||||
{
|
||||
ownerUserId: user._id,
|
||||
cursor: ownedSkillsCursor ?? undefined,
|
||||
batchSize: skillBatchSize,
|
||||
},
|
||||
)) as UserOwnedSkillsBackfillPageResult;
|
||||
|
||||
for (const skill of skillPage.items) {
|
||||
if (skill.softDeletedAt) continue;
|
||||
userPublishedSkills += 1;
|
||||
userTotalStars += skill.stats?.stars ?? 0;
|
||||
userTotalDownloads += skill.stats?.downloads ?? 0;
|
||||
}
|
||||
|
||||
if (skillPage.isDone) break;
|
||||
ownedSkillsCursor = skillPage.cursor;
|
||||
}
|
||||
|
||||
await ctx.runMutation(internal.maintenance.applyUserStatsBackfillPatchInternal, {
|
||||
userId: user._id,
|
||||
publishedSkills: userPublishedSkills,
|
||||
totalStars: userTotalStars,
|
||||
totalDownloads: userTotalDownloads,
|
||||
});
|
||||
totals.usersPatched++;
|
||||
}
|
||||
|
||||
if (isDone) break;
|
||||
}
|
||||
|
||||
return { ok: true as const, stats: totals, isDone, cursor };
|
||||
}
|
||||
|
||||
export const backfillSkillSummariesInternal = internalAction({
|
||||
args: {
|
||||
dryRun: v.optional(v.boolean()),
|
||||
@@ -257,6 +414,16 @@ export const backfillSkillSummariesInternal = internalAction({
|
||||
handler: backfillSkillSummariesInternalHandler,
|
||||
});
|
||||
|
||||
export const backfillUserStatsInternal = internalAction({
|
||||
args: {
|
||||
batchSize: v.optional(v.number()),
|
||||
skillBatchSize: v.optional(v.number()),
|
||||
maxBatches: v.optional(v.number()),
|
||||
cursor: v.optional(v.string()),
|
||||
},
|
||||
handler: backfillUserStatsInternalHandler,
|
||||
});
|
||||
|
||||
export const backfillSkillSummaries: ReturnType<typeof action> = action({
|
||||
args: {
|
||||
dryRun: v.optional(v.boolean()),
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
listMine,
|
||||
migrateLegacyPublisherHandleToOrgInternal,
|
||||
removeMember,
|
||||
updateProfile,
|
||||
} from "./publishers";
|
||||
|
||||
vi.mock("@convex-dev/auth/server", () => ({
|
||||
@@ -52,6 +53,15 @@ const listMineHandler = (
|
||||
listMine as unknown as WrappedHandler<Record<string, never>, Array<unknown>>
|
||||
)._handler;
|
||||
|
||||
const updateProfileHandler = (
|
||||
updateProfile as unknown as WrappedHandler<{
|
||||
publisherId: string;
|
||||
displayName: string;
|
||||
bio?: string;
|
||||
image?: string;
|
||||
}>
|
||||
)._handler;
|
||||
|
||||
describe("publishers membership controls", () => {
|
||||
it("prevents admins from promoting members to owner", async () => {
|
||||
vi.mocked(getAuthUserId).mockResolvedValue("users:admin" as never);
|
||||
@@ -371,6 +381,135 @@ describe("publishers membership controls", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("lets org admins update org profile fields", async () => {
|
||||
vi.mocked(getAuthUserId).mockResolvedValue("users:admin" as never);
|
||||
const patch = vi.fn(async () => {});
|
||||
const insert = vi.fn(async () => "auditLogs:1");
|
||||
const ctx = {
|
||||
db: {
|
||||
get: vi.fn(async (id: string) => {
|
||||
if (id === "users:admin") return { _id: id };
|
||||
if (id === "publishers:org") {
|
||||
return {
|
||||
_id: id,
|
||||
kind: "org",
|
||||
handle: "shopify",
|
||||
displayName: "Shopify",
|
||||
image: undefined,
|
||||
bio: undefined,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
query: vi.fn((table: string) => {
|
||||
if (table === "publisherMembers") {
|
||||
return {
|
||||
withIndex: vi.fn(() => ({
|
||||
unique: vi.fn().mockResolvedValue({
|
||||
_id: "publisherMembers:admin",
|
||||
publisherId: "publishers:org",
|
||||
userId: "users:admin",
|
||||
role: "admin",
|
||||
}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected table ${table}`);
|
||||
}),
|
||||
patch,
|
||||
insert,
|
||||
delete: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
normalizeId: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
updateProfileHandler(
|
||||
ctx as never,
|
||||
{
|
||||
publisherId: "publishers:org",
|
||||
displayName: "Shopify",
|
||||
bio: "Commerce platform",
|
||||
image: "https://cdn.example.com/shopify.png",
|
||||
} as never,
|
||||
),
|
||||
).resolves.toEqual({
|
||||
ok: true,
|
||||
publisher: expect.objectContaining({
|
||||
_id: "publishers:org",
|
||||
displayName: "Shopify",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(patch).toHaveBeenCalledWith(
|
||||
"publishers:org",
|
||||
expect.objectContaining({
|
||||
displayName: "Shopify",
|
||||
bio: "Commerce platform",
|
||||
image: "https://cdn.example.com/shopify.png",
|
||||
}),
|
||||
);
|
||||
expect(insert).toHaveBeenCalledWith(
|
||||
"auditLogs",
|
||||
expect.objectContaining({
|
||||
action: "publisher.profile.update",
|
||||
targetId: "publishers:org",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects invalid org profile image URLs", async () => {
|
||||
vi.mocked(getAuthUserId).mockResolvedValue("users:admin" as never);
|
||||
const ctx = {
|
||||
db: {
|
||||
get: vi.fn(async (id: string) => {
|
||||
if (id === "users:admin") return { _id: id };
|
||||
if (id === "publishers:org") {
|
||||
return {
|
||||
_id: id,
|
||||
kind: "org",
|
||||
handle: "shopify",
|
||||
displayName: "Shopify",
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
query: vi.fn((table: string) => {
|
||||
if (table === "publisherMembers") {
|
||||
return {
|
||||
withIndex: vi.fn(() => ({
|
||||
unique: vi.fn().mockResolvedValue({
|
||||
_id: "publisherMembers:admin",
|
||||
publisherId: "publishers:org",
|
||||
userId: "users:admin",
|
||||
role: "admin",
|
||||
}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected table ${table}`);
|
||||
}),
|
||||
patch: vi.fn(),
|
||||
insert: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
normalizeId: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
updateProfileHandler(
|
||||
ctx as never,
|
||||
{
|
||||
publisherId: "publishers:org",
|
||||
displayName: "Shopify",
|
||||
image: "not-a-url",
|
||||
} as never,
|
||||
),
|
||||
).rejects.toThrow("Image must be a valid URL");
|
||||
});
|
||||
});
|
||||
|
||||
describe("publisher bootstrap", () => {
|
||||
|
||||
@@ -539,6 +539,70 @@ export const createOrg = mutation({
|
||||
},
|
||||
});
|
||||
|
||||
export const updateProfile = mutation({
|
||||
args: {
|
||||
publisherId: v.id("publishers"),
|
||||
displayName: v.string(),
|
||||
bio: v.optional(v.string()),
|
||||
image: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const { userId } = await requireUser(ctx);
|
||||
const publisher = await ctx.db.get(args.publisherId);
|
||||
if (!publisher || publisher.deletedAt || publisher.deactivatedAt) {
|
||||
throw new ConvexError("Publisher not found");
|
||||
}
|
||||
if (publisher.kind !== "org") {
|
||||
throw new ConvexError("Only org publishers can be updated here");
|
||||
}
|
||||
|
||||
const membership = await getPublisherMembership(ctx, publisher._id, userId);
|
||||
if (!membership || !isPublisherRoleAllowed(membership.role, ["admin"])) {
|
||||
throw new ConvexError("Forbidden");
|
||||
}
|
||||
|
||||
const displayName = args.displayName.trim() || publisher.handle;
|
||||
const bio = args.bio?.trim() || undefined;
|
||||
const image = args.image?.trim() || undefined;
|
||||
if (image) {
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(image);
|
||||
} catch {
|
||||
throw new ConvexError("Image must be a valid URL");
|
||||
}
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
throw new ConvexError("Image must use http or https");
|
||||
}
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
await ctx.db.patch(publisher._id, {
|
||||
displayName,
|
||||
bio,
|
||||
image,
|
||||
updatedAt: now,
|
||||
});
|
||||
await ctx.db.insert("auditLogs", {
|
||||
actorUserId: userId,
|
||||
action: "publisher.profile.update",
|
||||
targetType: "publisher",
|
||||
targetId: publisher._id,
|
||||
metadata: {
|
||||
displayName,
|
||||
bio,
|
||||
image,
|
||||
},
|
||||
createdAt: now,
|
||||
});
|
||||
|
||||
return {
|
||||
ok: true as const,
|
||||
publisher: toPublicPublisher(await ctx.db.get(publisher._id)),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const migrateLegacyPublisherHandleToOrg = mutation({
|
||||
args: {
|
||||
handle: v.string(),
|
||||
|
||||
@@ -1,419 +0,0 @@
|
||||
import type { Id } from "./_generated/dataModel";
|
||||
import { internalMutation } from "./functions";
|
||||
|
||||
const DEMO_SKILLS = [
|
||||
{
|
||||
slug: "mcp-github",
|
||||
displayName: "MCP GitHub",
|
||||
summary: "Full GitHub API integration via MCP — issues, PRs, repos, code search, and actions.",
|
||||
downloads: 14200,
|
||||
stars: 342,
|
||||
installs: 8100,
|
||||
},
|
||||
{
|
||||
slug: "claude-memory",
|
||||
displayName: "Claude Memory",
|
||||
summary: "Persistent memory layer for Claude — stores context across conversations with vector recall.",
|
||||
downloads: 11800,
|
||||
stars: 287,
|
||||
installs: 6400,
|
||||
},
|
||||
{
|
||||
slug: "web-scraper-pro",
|
||||
displayName: "Web Scraper Pro",
|
||||
summary: "Intelligent web scraping with automatic pagination, JS rendering, and structured data extraction.",
|
||||
downloads: 9400,
|
||||
stars: 198,
|
||||
installs: 5200,
|
||||
},
|
||||
{
|
||||
slug: "sql-analyst",
|
||||
displayName: "SQL Analyst",
|
||||
summary: "Natural language to SQL with schema introspection, query optimization, and result visualization.",
|
||||
downloads: 8700,
|
||||
stars: 221,
|
||||
installs: 4800,
|
||||
},
|
||||
{
|
||||
slug: "pytest-agent",
|
||||
displayName: "Pytest Agent",
|
||||
summary: "Automated test generation and execution for Python — coverage analysis, mutation testing, fixtures.",
|
||||
downloads: 7200,
|
||||
stars: 156,
|
||||
installs: 3900,
|
||||
},
|
||||
{
|
||||
slug: "docker-compose-helper",
|
||||
displayName: "Docker Compose Helper",
|
||||
summary: "Generate, validate, and debug Docker Compose configurations with multi-service orchestration.",
|
||||
downloads: 6800,
|
||||
stars: 134,
|
||||
installs: 3600,
|
||||
},
|
||||
{
|
||||
slug: "api-docs-generator",
|
||||
displayName: "API Docs Generator",
|
||||
summary: "Auto-generate OpenAPI specs and beautiful documentation from any codebase or endpoint.",
|
||||
downloads: 5900,
|
||||
stars: 178,
|
||||
installs: 3100,
|
||||
},
|
||||
{
|
||||
slug: "slack-bot-builder",
|
||||
displayName: "Slack Bot Builder",
|
||||
summary: "Build and deploy Slack bots with natural language — slash commands, modals, and event handlers.",
|
||||
downloads: 5400,
|
||||
stars: 112,
|
||||
installs: 2800,
|
||||
},
|
||||
{
|
||||
slug: "terraform-assistant",
|
||||
displayName: "Terraform Assistant",
|
||||
summary: "Infrastructure as code helper — plan reviews, drift detection, module generation for AWS/GCP/Azure.",
|
||||
downloads: 4800,
|
||||
stars: 145,
|
||||
installs: 2400,
|
||||
},
|
||||
{
|
||||
slug: "regex-wizard",
|
||||
displayName: "Regex Wizard",
|
||||
summary: "Natural language to regex with live testing, explanation, and edge case generation.",
|
||||
downloads: 4200,
|
||||
stars: 89,
|
||||
installs: 2100,
|
||||
},
|
||||
{
|
||||
slug: "git-history-explorer",
|
||||
displayName: "Git History Explorer",
|
||||
summary: "Semantic search through git history — find commits by intent, trace code evolution, blame analysis.",
|
||||
downloads: 3900,
|
||||
stars: 102,
|
||||
installs: 1900,
|
||||
},
|
||||
{
|
||||
slug: "cron-scheduler",
|
||||
displayName: "Cron Scheduler",
|
||||
summary: "Natural language to cron expressions with timezone handling, overlap protection, and monitoring.",
|
||||
downloads: 3400,
|
||||
stars: 67,
|
||||
installs: 1600,
|
||||
},
|
||||
{
|
||||
slug: "jwt-debugger",
|
||||
displayName: "JWT Debugger",
|
||||
summary: "Decode, verify, and generate JWTs with visual payload inspection and expiry tracking.",
|
||||
downloads: 3100,
|
||||
stars: 78,
|
||||
installs: 1400,
|
||||
},
|
||||
{
|
||||
slug: "graphql-builder",
|
||||
displayName: "GraphQL Builder",
|
||||
summary: "Schema-first GraphQL development — type generation, resolver scaffolding, and playground integration.",
|
||||
downloads: 2800,
|
||||
stars: 94,
|
||||
installs: 1200,
|
||||
},
|
||||
{
|
||||
slug: "security-scanner",
|
||||
displayName: "Security Scanner",
|
||||
summary: "OWASP-aware security scanning for codebases — dependency audit, secret detection, SAST patterns.",
|
||||
downloads: 2500,
|
||||
stars: 156,
|
||||
installs: 1100,
|
||||
},
|
||||
{
|
||||
slug: "markdown-slides",
|
||||
displayName: "Markdown Slides",
|
||||
summary: "Turn markdown into presentation decks with themes, speaker notes, and PDF export.",
|
||||
downloads: 2200,
|
||||
stars: 45,
|
||||
installs: 900,
|
||||
},
|
||||
{
|
||||
slug: "env-manager",
|
||||
displayName: "Env Manager",
|
||||
summary: "Environment variable management across projects — sync .env files, validate schemas, rotate secrets.",
|
||||
downloads: 1800,
|
||||
stars: 56,
|
||||
installs: 800,
|
||||
},
|
||||
{
|
||||
slug: "csv-transform",
|
||||
displayName: "CSV Transform",
|
||||
summary: "Powerful CSV/TSV manipulation — column transforms, joins, pivots, and format conversion.",
|
||||
downloads: 1500,
|
||||
stars: 34,
|
||||
installs: 600,
|
||||
},
|
||||
{
|
||||
slug: "ssh-config-manager",
|
||||
displayName: "SSH Config Manager",
|
||||
summary: "Manage SSH configs, keys, and tunnels with natural language — jump hosts, port forwarding, agent setup.",
|
||||
downloads: 1200,
|
||||
stars: 42,
|
||||
installs: 500,
|
||||
},
|
||||
{
|
||||
slug: "changelog-writer",
|
||||
displayName: "Changelog Writer",
|
||||
summary: "Generate changelogs from git history with conventional commit parsing and release note formatting.",
|
||||
downloads: 980,
|
||||
stars: 28,
|
||||
installs: 400,
|
||||
},
|
||||
];
|
||||
|
||||
const DEMO_OWNERS = [
|
||||
{ handle: "anthropic", displayName: "Anthropic", highlighted: true },
|
||||
{ handle: "openai-labs", displayName: "OpenAI Labs", highlighted: false },
|
||||
{ handle: "devtools-co", displayName: "DevTools Co", highlighted: false },
|
||||
{ handle: "securityfirst", displayName: "SecurityFirst", highlighted: true },
|
||||
{ handle: "dataflow", displayName: "DataFlow", highlighted: false },
|
||||
];
|
||||
|
||||
export const seedDemoSkills = internalMutation({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
// Check if we already seeded
|
||||
const existingSkill = await ctx.db
|
||||
.query("skills")
|
||||
.withIndex("by_slug", (q) => q.eq("slug", "mcp-github"))
|
||||
.first();
|
||||
if (existingSkill) {
|
||||
return { seeded: false, reason: "already seeded" };
|
||||
}
|
||||
|
||||
// Create a seed user
|
||||
const seedUserId = await ctx.db.insert("users", {
|
||||
name: "ClawHub Demo",
|
||||
displayName: "ClawHub Demo",
|
||||
handle: "clawhub-demo",
|
||||
image: undefined,
|
||||
role: "admin",
|
||||
});
|
||||
|
||||
// Create publisher accounts
|
||||
const publisherIds: string[] = [];
|
||||
for (const owner of DEMO_OWNERS) {
|
||||
const pubId = await ctx.db.insert("publishers", {
|
||||
kind: "org",
|
||||
handle: owner.handle,
|
||||
displayName: owner.displayName,
|
||||
linkedUserId: seedUserId,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
publisherIds.push(pubId);
|
||||
|
||||
// Add membership
|
||||
await ctx.db.insert("publisherMembers", {
|
||||
publisherId: pubId as Id<"publishers">,
|
||||
userId: seedUserId,
|
||||
role: "owner",
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const DAY = 86400000;
|
||||
let totalPublishedSkills = 0;
|
||||
let totalStars = 0;
|
||||
let totalDownloads = 0;
|
||||
|
||||
for (let i = 0; i < DEMO_SKILLS.length; i++) {
|
||||
const s = DEMO_SKILLS[i];
|
||||
const ownerIdx = i % publisherIds.length;
|
||||
const createdDaysAgo = Math.floor(Math.random() * 90) + 7;
|
||||
const updatedDaysAgo = Math.floor(Math.random() * createdDaysAgo);
|
||||
const createdAt = now - createdDaysAgo * DAY;
|
||||
const updatedAt = now - updatedDaysAgo * DAY;
|
||||
const version = `${Math.floor(Math.random() * 3) + 1}.${Math.floor(Math.random() * 10)}.${Math.floor(Math.random() * 20)}`;
|
||||
|
||||
const isHighlighted = i < 6;
|
||||
const badges = isHighlighted
|
||||
? { highlighted: { byUserId: seedUserId, at: now } }
|
||||
: undefined;
|
||||
|
||||
const numVersions = Math.floor(Math.random() * 8) + 1;
|
||||
const numComments = Math.floor(Math.random() * 15);
|
||||
|
||||
// Create skill first (without latestVersionId)
|
||||
const skillId = await ctx.db.insert("skills", {
|
||||
slug: s.slug,
|
||||
displayName: s.displayName,
|
||||
summary: s.summary,
|
||||
ownerUserId: seedUserId,
|
||||
ownerPublisherId: publisherIds[ownerIdx] as Id<"publishers">,
|
||||
tags: {},
|
||||
badges,
|
||||
moderationStatus: "active",
|
||||
moderationVerdict: "clean",
|
||||
stats: {
|
||||
downloads: s.downloads,
|
||||
installsCurrent: Math.floor(s.installs * 0.3),
|
||||
installsAllTime: s.installs,
|
||||
stars: s.stars,
|
||||
versions: numVersions,
|
||||
comments: numComments,
|
||||
},
|
||||
statsDownloads: s.downloads,
|
||||
statsStars: s.stars,
|
||||
statsInstallsCurrent: Math.floor(s.installs * 0.3),
|
||||
statsInstallsAllTime: s.installs,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
});
|
||||
|
||||
// Create skillBadges entry for highlighted skills
|
||||
if (isHighlighted) {
|
||||
await ctx.db.insert("skillBadges", {
|
||||
skillId,
|
||||
kind: "highlighted",
|
||||
byUserId: seedUserId,
|
||||
at: now,
|
||||
});
|
||||
}
|
||||
|
||||
// Now create version with real skillId
|
||||
const versionId = await ctx.db.insert("skillVersions", {
|
||||
skillId,
|
||||
version,
|
||||
changelog: `Release ${version} — improvements and bug fixes.`,
|
||||
files: [],
|
||||
parsed: { frontmatter: {} },
|
||||
createdBy: seedUserId,
|
||||
createdAt: updatedAt,
|
||||
});
|
||||
|
||||
// Patch skill with version info
|
||||
await ctx.db.patch(skillId, {
|
||||
latestVersionId: versionId,
|
||||
latestVersionSummary: {
|
||||
version,
|
||||
createdAt: updatedAt,
|
||||
changelog: `Release ${version}`,
|
||||
},
|
||||
tags: { latest: versionId },
|
||||
});
|
||||
|
||||
totalPublishedSkills += 1;
|
||||
totalStars += s.stars;
|
||||
totalDownloads += s.downloads;
|
||||
|
||||
// Create digest for search
|
||||
await ctx.db.insert("skillSearchDigest", {
|
||||
skillId,
|
||||
slug: s.slug,
|
||||
displayName: s.displayName,
|
||||
summary: s.summary,
|
||||
ownerUserId: seedUserId,
|
||||
ownerPublisherId: publisherIds[ownerIdx] as Id<"publishers">,
|
||||
ownerHandle: DEMO_OWNERS[ownerIdx].handle,
|
||||
ownerName: DEMO_OWNERS[ownerIdx].displayName,
|
||||
ownerDisplayName: DEMO_OWNERS[ownerIdx].displayName,
|
||||
ownerImage: undefined,
|
||||
latestVersionId: versionId,
|
||||
latestVersionSummary: {
|
||||
version,
|
||||
createdAt: updatedAt,
|
||||
changelog: `Release ${version}`,
|
||||
},
|
||||
tags: { latest: versionId },
|
||||
badges,
|
||||
stats: {
|
||||
downloads: s.downloads,
|
||||
installsCurrent: Math.floor(s.installs * 0.3),
|
||||
installsAllTime: s.installs,
|
||||
stars: s.stars,
|
||||
versions: numVersions,
|
||||
comments: numComments,
|
||||
},
|
||||
versions: numVersions,
|
||||
comments: numComments,
|
||||
moderationReason: undefined,
|
||||
isSuspicious: false,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db.patch(seedUserId, {
|
||||
publishedSkills: totalPublishedSkills,
|
||||
totalStars,
|
||||
totalDownloads,
|
||||
});
|
||||
|
||||
return { seeded: true, count: DEMO_SKILLS.length };
|
||||
},
|
||||
});
|
||||
|
||||
// Repair globalStats count to match actual seeded data
|
||||
export const repairGlobalStats = internalMutation({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
// Count active digests — push filter server-side
|
||||
const digests = await ctx.db
|
||||
.query("skillSearchDigest")
|
||||
.withIndex("by_active_updated", (q) => q.eq("softDeletedAt", undefined))
|
||||
.filter((q) => q.eq(q.field("moderationStatus"), "active"))
|
||||
.collect();
|
||||
|
||||
const count = digests.length;
|
||||
|
||||
// Update globalStats
|
||||
const stats = await ctx.db
|
||||
.query("globalStats")
|
||||
.filter((q) => q.eq(q.field("key"), "default"))
|
||||
.first();
|
||||
|
||||
if (stats) {
|
||||
await ctx.db.patch(stats._id, { activeSkillsCount: count, updatedAt: Date.now() });
|
||||
} else {
|
||||
await ctx.db.insert("globalStats", {
|
||||
key: "default",
|
||||
activeSkillsCount: count,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
return { count };
|
||||
},
|
||||
});
|
||||
|
||||
// Repair function to add missing skillBadges for already-seeded data
|
||||
export const repairHighlightedBadges = internalMutation({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const highlightedSlugs = DEMO_SKILLS.slice(0, 6).map((s) => s.slug);
|
||||
let fixed = 0;
|
||||
|
||||
for (const slug of highlightedSlugs) {
|
||||
const skill = await ctx.db
|
||||
.query("skills")
|
||||
.withIndex("by_slug", (q) => q.eq("slug", slug))
|
||||
.first();
|
||||
if (!skill) continue;
|
||||
|
||||
// Check if badge already exists
|
||||
const existing = await ctx.db
|
||||
.query("skillBadges")
|
||||
.withIndex("by_skill_kind", (q) =>
|
||||
q.eq("skillId", skill._id).eq("kind", "highlighted"),
|
||||
)
|
||||
.first();
|
||||
if (existing) continue;
|
||||
|
||||
await ctx.db.insert("skillBadges", {
|
||||
skillId: skill._id,
|
||||
kind: "highlighted",
|
||||
byUserId: skill.ownerUserId,
|
||||
at: Date.now(),
|
||||
});
|
||||
fixed++;
|
||||
}
|
||||
|
||||
return { fixed };
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,303 @@
|
||||
/* @vitest-environment node */
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
// Mock the Convex function wrappers so that importing statsMaintenance.ts does
|
||||
// not attempt to load the Convex runtime (convex/server) in the Node test env.
|
||||
vi.mock("./functions", () => ({
|
||||
internalMutation: (def: { handler: unknown }) => def,
|
||||
internalQuery: (def: { handler: unknown }) => def,
|
||||
internalAction: (def: { handler: unknown }) => def,
|
||||
}));
|
||||
|
||||
vi.mock("./_generated/api", () => ({
|
||||
internal: {
|
||||
statsMaintenance: {
|
||||
backfillSkillStatFieldsInternal: Symbol("backfillSkillStatFieldsInternal"),
|
||||
getSkillStatBackfillStateInternal: Symbol("getSkillStatBackfillStateInternal"),
|
||||
setSkillStatBackfillStateInternal: Symbol("setSkillStatBackfillStateInternal"),
|
||||
reconcileSkillStarCounts: Symbol("reconcileSkillStarCounts"),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const { __test, reconcileSkillStarCountsHandler } = await import("./statsMaintenance");
|
||||
const { buildSkillStatPatch } = __test;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Build a minimal skill doc for testing. Only the stat-related fields are
|
||||
* required; everything else is left as `undefined` / cast via `as never`.
|
||||
*/
|
||||
function makeSkill(overrides: {
|
||||
statsDownloads?: number;
|
||||
statsStars?: number;
|
||||
statsInstallsCurrent?: number;
|
||||
statsInstallsAllTime?: number;
|
||||
stats: {
|
||||
downloads: number;
|
||||
stars: number;
|
||||
installsCurrent?: number;
|
||||
installsAllTime?: number;
|
||||
comments: number;
|
||||
};
|
||||
}) {
|
||||
return overrides as never;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// buildSkillStatPatch
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("buildSkillStatPatch", () => {
|
||||
it("scenario 1: top-level fields present and already in sync with nested → returns null", () => {
|
||||
const skill = makeSkill({
|
||||
statsDownloads: 10,
|
||||
statsStars: 5,
|
||||
statsInstallsCurrent: 3,
|
||||
statsInstallsAllTime: 20,
|
||||
stats: { downloads: 10, stars: 5, installsCurrent: 3, installsAllTime: 20, comments: 1 },
|
||||
});
|
||||
|
||||
expect(buildSkillStatPatch(skill)).toBeNull();
|
||||
});
|
||||
|
||||
it("scenario 2: top-level fields present but nested fields are stale → patches nested to match top-level", () => {
|
||||
const skill = makeSkill({
|
||||
statsDownloads: 10,
|
||||
statsStars: 5,
|
||||
statsInstallsCurrent: 3,
|
||||
statsInstallsAllTime: 20,
|
||||
stats: { downloads: 1, stars: 1, installsCurrent: 0, installsAllTime: 0, comments: 0 },
|
||||
});
|
||||
|
||||
const patch = buildSkillStatPatch(skill);
|
||||
expect(patch).not.toBeNull();
|
||||
// Top-level fields must be written with the canonical (top-level) values.
|
||||
expect(patch!.statsDownloads).toBe(10);
|
||||
expect(patch!.statsStars).toBe(5);
|
||||
expect(patch!.statsInstallsCurrent).toBe(3);
|
||||
expect(patch!.statsInstallsAllTime).toBe(20);
|
||||
// Nested fields must be brought in sync with the top-level values.
|
||||
expect(patch!.stats.downloads).toBe(10);
|
||||
expect(patch!.stats.stars).toBe(5);
|
||||
expect(patch!.stats.installsCurrent).toBe(3);
|
||||
expect(patch!.stats.installsAllTime).toBe(20);
|
||||
});
|
||||
|
||||
it("scenario 3: top-level fields absent (pre-migration doc) → reads from nested, writes both sets", () => {
|
||||
const skill = makeSkill({
|
||||
// No statsDownloads / statsStars / etc. — pre-migration document.
|
||||
stats: { downloads: 7, stars: 3, installsCurrent: 2, installsAllTime: 15, comments: 4 },
|
||||
});
|
||||
|
||||
const patch = buildSkillStatPatch(skill);
|
||||
expect(patch).not.toBeNull();
|
||||
// Top-level fields must be populated from the nested values.
|
||||
expect(patch!.statsDownloads).toBe(7);
|
||||
expect(patch!.statsStars).toBe(3);
|
||||
expect(patch!.statsInstallsCurrent).toBe(2);
|
||||
expect(patch!.statsInstallsAllTime).toBe(15);
|
||||
// Nested fields must remain consistent.
|
||||
expect(patch!.stats.downloads).toBe(7);
|
||||
expect(patch!.stats.stars).toBe(3);
|
||||
expect(patch!.stats.installsCurrent).toBe(2);
|
||||
expect(patch!.stats.installsAllTime).toBe(15);
|
||||
});
|
||||
|
||||
it("scenario 4: top-level fields present but nested is out of sync → patches nested to match top-level (not the other way around)", () => {
|
||||
// This is the exact bug that was previously shipped: the old code wrote
|
||||
// nested → top-level instead of top-level → nested.
|
||||
const skill = makeSkill({
|
||||
statsDownloads: 100,
|
||||
statsStars: 50,
|
||||
statsInstallsCurrent: 30,
|
||||
statsInstallsAllTime: 200,
|
||||
stats: { downloads: 1, stars: 1, installsCurrent: 1, installsAllTime: 1, comments: 0 },
|
||||
});
|
||||
|
||||
const patch = buildSkillStatPatch(skill);
|
||||
expect(patch).not.toBeNull();
|
||||
// The canonical top-level values must win.
|
||||
expect(patch!.statsDownloads).toBe(100);
|
||||
expect(patch!.statsStars).toBe(50);
|
||||
expect(patch!.statsInstallsCurrent).toBe(30);
|
||||
expect(patch!.statsInstallsAllTime).toBe(200);
|
||||
// The stale nested values must be overwritten by the top-level values.
|
||||
expect(patch!.stats.downloads).toBe(100);
|
||||
expect(patch!.stats.stars).toBe(50);
|
||||
expect(patch!.stats.installsCurrent).toBe(30);
|
||||
expect(patch!.stats.installsAllTime).toBe(200);
|
||||
});
|
||||
|
||||
it("preserves unrelated nested fields (e.g. comments) when patching stat fields", () => {
|
||||
const skill = makeSkill({
|
||||
statsDownloads: 5,
|
||||
statsStars: 2,
|
||||
statsInstallsCurrent: 1,
|
||||
statsInstallsAllTime: 10,
|
||||
stats: { downloads: 0, stars: 0, installsCurrent: 0, installsAllTime: 0, comments: 99 },
|
||||
});
|
||||
|
||||
const patch = buildSkillStatPatch(skill);
|
||||
expect(patch).not.toBeNull();
|
||||
// comments is not a stat field managed by buildSkillStatPatch — it must be
|
||||
// carried over unchanged from the original nested object.
|
||||
expect(patch!.stats.comments).toBe(99);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// reconcileSkillStarCountsHandler
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("reconcileSkillStarCounts", () => {
|
||||
/**
|
||||
* Build a minimal db mock that returns a single-page result for skills and
|
||||
* configurable star / comment record counts.
|
||||
*/
|
||||
function makeCtx(options: {
|
||||
skill: {
|
||||
_id: string;
|
||||
statsStars?: number;
|
||||
stats: { stars: number; comments: number };
|
||||
softDeletedAt?: number;
|
||||
};
|
||||
actualStarCount: number;
|
||||
actualCommentCount: number;
|
||||
}) {
|
||||
const { skill, actualStarCount, actualCommentCount } = options;
|
||||
|
||||
const starRecords = Array.from({ length: actualStarCount }, (_, i) => ({
|
||||
_id: `stars:${i}`,
|
||||
skillId: skill._id,
|
||||
}));
|
||||
|
||||
const commentRecords = Array.from({ length: actualCommentCount }, (_, i) => ({
|
||||
_id: `comments:${i}`,
|
||||
skillId: skill._id,
|
||||
softDeletedAt: undefined,
|
||||
}));
|
||||
|
||||
const paginate = vi.fn().mockResolvedValue({
|
||||
page: [skill],
|
||||
continueCursor: null,
|
||||
isDone: true,
|
||||
});
|
||||
|
||||
const collect = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(starRecords)
|
||||
.mockResolvedValueOnce(commentRecords);
|
||||
|
||||
const withIndex = vi.fn().mockReturnValue({ collect });
|
||||
|
||||
const patch = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
const ctx = {
|
||||
db: {
|
||||
query: vi.fn().mockReturnValue({
|
||||
order: vi.fn().mockReturnValue({ paginate }),
|
||||
withIndex,
|
||||
}),
|
||||
patch,
|
||||
},
|
||||
} as never;
|
||||
|
||||
return { ctx, patch };
|
||||
}
|
||||
|
||||
it("reads from top-level statsStars (canonical path) when deciding whether to patch", async () => {
|
||||
// statsStars is correct (matches actual count), but stats.stars is stale.
|
||||
// The reconcile job uses the canonical read path (top-level preferred), so
|
||||
// it should NOT trigger a patch based on the star count alone.
|
||||
const skill = {
|
||||
_id: "skills:1",
|
||||
statsStars: 5, // canonical value — correct
|
||||
stats: { stars: 99, comments: 0 }, // legacy value — stale, but not reconcile's concern
|
||||
};
|
||||
|
||||
const { ctx, patch } = makeCtx({ skill, actualStarCount: 5, actualCommentCount: 0 });
|
||||
|
||||
const result = await reconcileSkillStarCountsHandler(ctx, {});
|
||||
|
||||
expect(result.scanned).toBe(1);
|
||||
expect(result.patched).toBe(0);
|
||||
expect(patch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to stats.stars when statsStars is absent (pre-migration doc)", async () => {
|
||||
// Pre-migration doc: no top-level statsStars. The canonical read path
|
||||
// falls back to stats.stars. If that also matches actual count, no patch.
|
||||
const skill = {
|
||||
_id: "skills:1",
|
||||
// statsStars intentionally absent
|
||||
stats: { stars: 3, comments: 0 },
|
||||
};
|
||||
|
||||
const { ctx, patch } = makeCtx({ skill, actualStarCount: 3, actualCommentCount: 0 });
|
||||
|
||||
const result = await reconcileSkillStarCountsHandler(ctx, {});
|
||||
|
||||
expect(result.scanned).toBe(1);
|
||||
expect(result.patched).toBe(0);
|
||||
expect(patch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("patches both statsStars and stats.stars when canonical value drifts from actual count", async () => {
|
||||
const skill = {
|
||||
_id: "skills:1",
|
||||
statsStars: 10, // canonical value — out of sync with actual
|
||||
stats: { stars: 10, comments: 0 },
|
||||
};
|
||||
|
||||
const { ctx, patch } = makeCtx({ skill, actualStarCount: 7, actualCommentCount: 0 });
|
||||
|
||||
const result = await reconcileSkillStarCountsHandler(ctx, {});
|
||||
|
||||
expect(result.scanned).toBe(1);
|
||||
expect(result.patched).toBe(1);
|
||||
expect(patch).toHaveBeenCalledWith("skills:1", expect.objectContaining({
|
||||
statsStars: 7,
|
||||
stats: expect.objectContaining({ stars: 7 }),
|
||||
}));
|
||||
});
|
||||
|
||||
it("patches when comment count drifts even if star count is correct", async () => {
|
||||
const skill = {
|
||||
_id: "skills:1",
|
||||
statsStars: 5,
|
||||
stats: { stars: 5, comments: 10 }, // comments out of sync
|
||||
};
|
||||
|
||||
const { ctx, patch } = makeCtx({ skill, actualStarCount: 5, actualCommentCount: 3 });
|
||||
|
||||
const result = await reconcileSkillStarCountsHandler(ctx, {});
|
||||
|
||||
expect(result.scanned).toBe(1);
|
||||
expect(result.patched).toBe(1);
|
||||
expect(patch).toHaveBeenCalledWith("skills:1", expect.objectContaining({
|
||||
stats: expect.objectContaining({ comments: 3 }),
|
||||
}));
|
||||
});
|
||||
|
||||
it("skips soft-deleted skills", async () => {
|
||||
const skill = {
|
||||
_id: "skills:1",
|
||||
softDeletedAt: 12345,
|
||||
statsStars: 0,
|
||||
stats: { stars: 0, comments: 0 },
|
||||
};
|
||||
|
||||
const { ctx, patch } = makeCtx({ skill, actualStarCount: 5, actualCommentCount: 0 });
|
||||
|
||||
const result = await reconcileSkillStarCountsHandler(ctx, {});
|
||||
|
||||
// Soft-deleted skills are excluded from scanned count and never patched.
|
||||
expect(result.scanned).toBe(0);
|
||||
expect(result.patched).toBe(0);
|
||||
expect(patch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -183,25 +183,53 @@ export const runSkillStatBackfillInternal: ReturnType<typeof internalAction> = i
|
||||
|
||||
function buildSkillStatPatch(skill: Doc<"skills">) {
|
||||
const stats = skill.stats;
|
||||
const nextDownloads = stats.downloads;
|
||||
const nextStars = stats.stars;
|
||||
const nextInstallsCurrent = stats.installsCurrent ?? 0;
|
||||
const nextInstallsAllTime = stats.installsAllTime ?? 0;
|
||||
|
||||
if (
|
||||
// Prefer the top-level stat fields when they exist (they are kept up-to-date
|
||||
// by applySkillStatDeltas on every event flush). Fall back to the legacy
|
||||
// nested `stats` object only for documents that pre-date the migration.
|
||||
const nextDownloads =
|
||||
typeof skill.statsDownloads === "number" ? skill.statsDownloads : stats.downloads;
|
||||
const nextStars =
|
||||
typeof skill.statsStars === "number" ? skill.statsStars : stats.stars;
|
||||
const nextInstallsCurrent =
|
||||
typeof skill.statsInstallsCurrent === "number"
|
||||
? skill.statsInstallsCurrent
|
||||
: (stats.installsCurrent ?? 0);
|
||||
const nextInstallsAllTime =
|
||||
typeof skill.statsInstallsAllTime === "number"
|
||||
? skill.statsInstallsAllTime
|
||||
: (stats.installsAllTime ?? 0);
|
||||
|
||||
// Check whether both sets of fields are already in sync.
|
||||
const topLevelInSync =
|
||||
skill.statsDownloads === nextDownloads &&
|
||||
skill.statsStars === nextStars &&
|
||||
skill.statsInstallsCurrent === nextInstallsCurrent &&
|
||||
skill.statsInstallsAllTime === nextInstallsAllTime
|
||||
) {
|
||||
skill.statsInstallsAllTime === nextInstallsAllTime;
|
||||
|
||||
const nestedInSync =
|
||||
stats.downloads === nextDownloads &&
|
||||
stats.stars === nextStars &&
|
||||
(stats.installsCurrent ?? 0) === nextInstallsCurrent &&
|
||||
(stats.installsAllTime ?? 0) === nextInstallsAllTime;
|
||||
|
||||
if (topLevelInSync && nestedInSync) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Write both sets of fields so they stay in sync.
|
||||
return {
|
||||
statsDownloads: nextDownloads,
|
||||
statsStars: nextStars,
|
||||
statsInstallsCurrent: nextInstallsCurrent,
|
||||
statsInstallsAllTime: nextInstallsAllTime,
|
||||
stats: {
|
||||
...stats,
|
||||
downloads: nextDownloads,
|
||||
stars: nextStars,
|
||||
installsCurrent: nextInstallsCurrent,
|
||||
installsAllTime: nextInstallsAllTime,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -215,63 +243,79 @@ function buildSkillStatPatch(skill: Doc<"skills">) {
|
||||
*
|
||||
* Downloads and installs are event-sourced only (no separate table to count from),
|
||||
* so they cannot be reconciled this way.
|
||||
*
|
||||
* Exported as a standalone function so it can be unit-tested directly without
|
||||
* going through the Convex internalMutation wrapper.
|
||||
*/
|
||||
export async function reconcileSkillStarCountsHandler(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ctx: { db: { query: any; patch: any } },
|
||||
args: { cursor?: string; batchSize?: number },
|
||||
) {
|
||||
const batchSize = clampInt(args.batchSize ?? 50, 1, 200);
|
||||
const now = Date.now();
|
||||
|
||||
const { page, isDone, continueCursor } = await ctx.db
|
||||
.query("skills")
|
||||
.order("asc")
|
||||
.paginate({ cursor: args.cursor ?? null, numItems: batchSize });
|
||||
|
||||
let scanned = 0;
|
||||
let patched = 0;
|
||||
for (const skill of page) {
|
||||
if (skill.softDeletedAt) continue;
|
||||
scanned += 1;
|
||||
// Count actual star records for this skill
|
||||
const starRecords = await ctx.db
|
||||
.query("stars")
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.withIndex("by_skill_user", (q: any) => q.eq("skillId", skill._id))
|
||||
.collect();
|
||||
const actualStars = starRecords.length;
|
||||
|
||||
// Count actual comment records for this skill
|
||||
const commentRecords = await ctx.db
|
||||
.query("comments")
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.withIndex("by_skill", (q: any) => q.eq("skillId", skill._id))
|
||||
.collect();
|
||||
const actualComments = commentRecords.filter((c: { softDeletedAt?: unknown }) => !c.softDeletedAt).length;
|
||||
|
||||
// Check if stats are out of sync (compare against the canonical value
|
||||
// used by toPublicSkill: prefer top-level field, fall back to nested).
|
||||
const currentStars =
|
||||
typeof skill.statsStars === "number" ? skill.statsStars : skill.stats.stars;
|
||||
|
||||
if (currentStars !== actualStars || skill.stats.comments !== actualComments) {
|
||||
const updatedStats = {
|
||||
...skill.stats,
|
||||
stars: actualStars,
|
||||
comments: actualComments,
|
||||
};
|
||||
// Keep both the top-level index field and the legacy nested field in sync.
|
||||
await ctx.db.patch(skill._id, {
|
||||
statsStars: actualStars,
|
||||
stats: updatedStats,
|
||||
updatedAt: now,
|
||||
});
|
||||
patched += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
scanned,
|
||||
patched,
|
||||
cursor: isDone ? null : continueCursor,
|
||||
isDone,
|
||||
};
|
||||
}
|
||||
|
||||
export const reconcileSkillStarCounts = internalMutation({
|
||||
args: {
|
||||
cursor: v.optional(v.string()),
|
||||
batchSize: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const batchSize = clampInt(args.batchSize ?? 50, 1, 200);
|
||||
const now = Date.now();
|
||||
|
||||
const { page, isDone, continueCursor } = await ctx.db
|
||||
.query("skills")
|
||||
.order("asc")
|
||||
.paginate({ cursor: args.cursor ?? null, numItems: batchSize });
|
||||
|
||||
let scanned = 0;
|
||||
let patched = 0;
|
||||
for (const skill of page) {
|
||||
if (skill.softDeletedAt) continue;
|
||||
scanned += 1;
|
||||
// Count actual star records for this skill
|
||||
const starRecords = await ctx.db
|
||||
.query("stars")
|
||||
.withIndex("by_skill_user", (q) => q.eq("skillId", skill._id))
|
||||
.collect();
|
||||
const actualStars = starRecords.length;
|
||||
|
||||
// Count actual comment records for this skill
|
||||
const commentRecords = await ctx.db
|
||||
.query("comments")
|
||||
.withIndex("by_skill", (q) => q.eq("skillId", skill._id))
|
||||
.collect();
|
||||
const actualComments = commentRecords.filter((c) => !c.softDeletedAt).length;
|
||||
|
||||
// Check if stats are out of sync
|
||||
if (skill.stats.stars !== actualStars || skill.stats.comments !== actualComments) {
|
||||
const updatedStats = {
|
||||
...skill.stats,
|
||||
stars: actualStars,
|
||||
comments: actualComments,
|
||||
};
|
||||
await ctx.db.patch(skill._id, {
|
||||
statsStars: actualStars,
|
||||
stats: updatedStats,
|
||||
updatedAt: now,
|
||||
});
|
||||
patched += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
scanned,
|
||||
patched,
|
||||
cursor: isDone ? null : continueCursor,
|
||||
isDone,
|
||||
};
|
||||
},
|
||||
handler: reconcileSkillStarCountsHandler,
|
||||
});
|
||||
|
||||
export const runReconcileSkillStarCountsInternal = internalAction({
|
||||
@@ -308,6 +352,11 @@ function clampInt(value: number, min: number, max: number) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
// Exported for unit testing only — not part of the public API.
|
||||
export const __test = {
|
||||
buildSkillStatPatch,
|
||||
};
|
||||
|
||||
/**
|
||||
* Count a page of skillSearchDigest docs and return the partial public count.
|
||||
* Each query runs in its own transaction (~1000 docs, ~900 KB), well under limits.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockSpawn = vi.fn();
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
vi.mock("node:child_process", () => ({
|
||||
spawn: (...args: unknown[]) => mockSpawn(...args),
|
||||
@@ -26,6 +27,26 @@ function createMockChild() {
|
||||
}
|
||||
|
||||
describe("openInBrowser", () => {
|
||||
it("uses explorer on Windows and preserves query params in the URL argument", () => {
|
||||
const child = createMockChild();
|
||||
mockSpawn.mockReturnValueOnce(child);
|
||||
const url =
|
||||
"https://clawhub.ai/auth?redirect_uri=http%3A%2F%2F127.0.0.1%3A43123%2Fcallback&state=abc123";
|
||||
|
||||
try {
|
||||
Object.defineProperty(process, "platform", { value: "win32" });
|
||||
openInBrowser(url);
|
||||
} finally {
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform });
|
||||
}
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledWith("explorer", [url], {
|
||||
stdio: "ignore",
|
||||
detached: true,
|
||||
});
|
||||
expect(child.unref).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("prints manual URL instructions when browser opener is missing", () => {
|
||||
const child = createMockChild();
|
||||
mockSpawn.mockReturnValueOnce(child);
|
||||
|
||||
@@ -48,7 +48,7 @@ export function openInBrowser(url: string) {
|
||||
process.platform === "darwin"
|
||||
? ["open", url]
|
||||
: process.platform === "win32"
|
||||
? ["cmd", "/c", "start", "", url]
|
||||
? ["explorer", url]
|
||||
: ["xdg-open", url];
|
||||
const [command, ...commandArgs] = args;
|
||||
if (!command) return;
|
||||
|
||||
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 972 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 972 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 792 B |
@@ -1,45 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none">
|
||||
<rect width="512" height="512" rx="0" fill="none"/>
|
||||
<!-- Body segments -->
|
||||
<g fill="#F03020" stroke="#fff" stroke-width="6" stroke-linejoin="round">
|
||||
<!-- Tail fan - center -->
|
||||
<ellipse cx="256" cy="460" rx="42" ry="30"/>
|
||||
<!-- Tail fan - left -->
|
||||
<ellipse cx="210" cy="455" rx="32" ry="24" transform="rotate(25 210 455)"/>
|
||||
<!-- Tail fan - right -->
|
||||
<ellipse cx="302" cy="455" rx="32" ry="24" transform="rotate(-25 302 455)"/>
|
||||
<!-- Lower body segment -->
|
||||
<rect x="208" y="380" width="96" height="55" rx="12"/>
|
||||
<!-- Mid-lower body -->
|
||||
<rect x="204" y="335" width="104" height="55" rx="12"/>
|
||||
<!-- Mid body -->
|
||||
<rect x="200" y="290" width="112" height="55" rx="14"/>
|
||||
<!-- Upper body / thorax -->
|
||||
<ellipse cx="256" cy="255" rx="72" ry="65"/>
|
||||
<!-- Head -->
|
||||
<circle cx="256" cy="195" r="58"/>
|
||||
<!-- Left claw arm -->
|
||||
<path d="M195 230 Q150 190 120 170 Q100 155 95 140" stroke-width="18" fill="none" stroke-linecap="round"/>
|
||||
<!-- Right claw arm -->
|
||||
<path d="M317 230 Q362 190 392 170 Q412 155 417 140" stroke-width="18" fill="none" stroke-linecap="round"/>
|
||||
<!-- Left claw -->
|
||||
<ellipse cx="88" cy="115" rx="52" ry="42" transform="rotate(-20 88 115)"/>
|
||||
<!-- Right claw -->
|
||||
<ellipse cx="424" cy="115" rx="52" ry="42" transform="rotate(20 424 115)"/>
|
||||
<!-- Left claw notch -->
|
||||
<path d="M65 95 Q78 80 72 65" stroke-width="6" fill="none" stroke="#fff" stroke-linecap="round"/>
|
||||
<!-- Right claw notch -->
|
||||
<path d="M447 95 Q434 80 440 65" stroke-width="6" fill="none" stroke="#fff" stroke-linecap="round"/>
|
||||
<!-- Left legs -->
|
||||
<path d="M205 290 Q175 295 155 305" stroke-width="10" fill="none" stroke-linecap="round"/>
|
||||
<path d="M202 320 Q172 330 152 340" stroke-width="10" fill="none" stroke-linecap="round"/>
|
||||
<path d="M205 350 Q178 358 158 370" stroke-width="10" fill="none" stroke-linecap="round"/>
|
||||
<!-- Right legs -->
|
||||
<path d="M307 290 Q337 295 357 305" stroke-width="10" fill="none" stroke-linecap="round"/>
|
||||
<path d="M310 320 Q340 330 360 340" stroke-width="10" fill="none" stroke-linecap="round"/>
|
||||
<path d="M307 350 Q334 358 354 370" stroke-width="10" fill="none" stroke-linecap="round"/>
|
||||
</g>
|
||||
<!-- Eyes -->
|
||||
<circle cx="236" cy="185" r="10" fill="#000"/>
|
||||
<circle cx="276" cy="185" r="10" fill="#000"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 285 KiB |
@@ -21,5 +21,5 @@
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
"background_color": "#0a0a0a"
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
<svg width="1200" height="630" viewBox="0 0 1200 630" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1200" y2="630" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#14110F"/>
|
||||
<stop offset="0.55" stop-color="#1A1512"/>
|
||||
<stop offset="1" stop-color="#14110F"/>
|
||||
</linearGradient>
|
||||
|
||||
<radialGradient id="glowOrange" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(260 60) rotate(120) scale(520 420)">
|
||||
<stop stop-color="#E86A47" stop-opacity="0.55"/>
|
||||
<stop offset="1" stop-color="#E86A47" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
|
||||
<radialGradient id="glowSea" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(1050 120) rotate(140) scale(520 420)">
|
||||
<stop stop-color="#4AD8B7" stop-opacity="0.35"/>
|
||||
<stop offset="1" stop-color="#4AD8B7" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
|
||||
<filter id="softBlur" x="-40%" y="-40%" width="180%" height="180%">
|
||||
<feGaussianBlur stdDeviation="24"/>
|
||||
</filter>
|
||||
|
||||
<filter id="cardShadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="18" stdDeviation="26" flood-color="#000000" flood-opacity="0.6"/>
|
||||
</filter>
|
||||
|
||||
<linearGradient id="pill" x1="0" y1="0" x2="360" y2="0" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E86A47" stop-opacity="0.22"/>
|
||||
<stop offset="1" stop-color="#E86A47" stop-opacity="0.08"/>
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="stroke" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop stop-color="#FFFFFF" stop-opacity="0.16"/>
|
||||
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0.06"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="1200" height="630" fill="url(#bg)"/>
|
||||
<circle cx="260" cy="60" r="520" fill="url(#glowOrange)" filter="url(#softBlur)"/>
|
||||
<circle cx="1050" cy="120" r="520" fill="url(#glowSea)" filter="url(#softBlur)"/>
|
||||
|
||||
<!-- Subtle grain (very light) -->
|
||||
<g opacity="0.08">
|
||||
<path d="M0 84 C160 120 340 40 520 86 C700 132 820 210 1200 160" stroke="#FFFFFF" stroke-opacity="0.10" stroke-width="2"/>
|
||||
<path d="M0 188 C220 240 360 160 560 204 C760 248 900 330 1200 300" stroke="#FFFFFF" stroke-opacity="0.08" stroke-width="2"/>
|
||||
<path d="M0 440 C240 380 420 520 620 470 C820 420 960 500 1200 460" stroke="#FFFFFF" stroke-opacity="0.06" stroke-width="2"/>
|
||||
</g>
|
||||
|
||||
<!-- Right mark -->
|
||||
<g opacity="0.22" filter="url(#softBlur)">
|
||||
<image href="clawd-mark.png" x="740" y="70" width="560" height="560" preserveAspectRatio="xMidYMid meet"/>
|
||||
</g>
|
||||
|
||||
<!-- Content card -->
|
||||
<g filter="url(#cardShadow)">
|
||||
<rect x="72" y="96" width="640" height="438" rx="34" fill="#201B18" fill-opacity="0.92" stroke="url(#stroke)"/>
|
||||
</g>
|
||||
|
||||
<!-- Tiny mark -->
|
||||
<image href="clawd-mark.png" x="108" y="134" width="46" height="46" preserveAspectRatio="xMidYMid meet"/>
|
||||
|
||||
<!-- Pill -->
|
||||
<g>
|
||||
<rect x="166" y="136" width="304" height="42" rx="21" fill="url(#pill)" stroke="#E86A47" stroke-opacity="0.28"/>
|
||||
<text x="186" y="163"
|
||||
fill="#F6EFE4"
|
||||
font-size="18"
|
||||
font-weight="600"
|
||||
font-family="Bricolage Grotesque, Manrope, -apple-system, BlinkMacSystemFont, Helvetica Neue, Helvetica, Arial, sans-serif"
|
||||
opacity="0.92">lobster-light. agent-right.</text>
|
||||
</g>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="112" y="265"
|
||||
fill="#F6EFE4"
|
||||
font-size="92"
|
||||
font-weight="700"
|
||||
font-family="Bricolage Grotesque, Manrope, -apple-system, BlinkMacSystemFont, Helvetica Neue, Helvetica, Arial, sans-serif">ClawHub</text>
|
||||
|
||||
<!-- Subtitle -->
|
||||
<text x="114" y="332"
|
||||
fill="#C6B8A8"
|
||||
font-size="23"
|
||||
font-weight="500"
|
||||
font-family="Manrope, Bricolage Grotesque, -apple-system, BlinkMacSystemFont, Helvetica Neue, Helvetica, Arial, sans-serif">
|
||||
a fast skill registry for agents, with vector search.
|
||||
</text>
|
||||
|
||||
<!-- Accent line + hint -->
|
||||
<rect x="114" y="372" width="110" height="6" rx="3" fill="#E86A47"/>
|
||||
<text x="114" y="430"
|
||||
fill="#F6EFE4"
|
||||
font-size="20"
|
||||
font-weight="600"
|
||||
opacity="0.90"
|
||||
font-family="IBM Plex Mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace">clawhub.ai</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,51 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { renderWithInlineCode } from "../routes/about";
|
||||
|
||||
function renderToContainer(text: string) {
|
||||
const { container } = render(<p>{renderWithInlineCode(text)}</p>);
|
||||
return container.querySelector("p")!;
|
||||
}
|
||||
|
||||
describe("renderWithInlineCode", () => {
|
||||
it("returns plain text unchanged when no backticks present", () => {
|
||||
const el = renderToContainer("No code here.");
|
||||
expect(el.textContent).toBe("No code here.");
|
||||
expect(el.querySelectorAll("code")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("wraps backtick-delimited text in <code> elements", () => {
|
||||
const el = renderToContainer("Run `curl | sh` to install.");
|
||||
const codes = el.querySelectorAll("code");
|
||||
expect(codes).toHaveLength(1);
|
||||
expect(codes[0].textContent).toBe("curl | sh");
|
||||
expect(codes[0].className).toBe("about-inline-code");
|
||||
expect(el.textContent).toBe("Run curl | sh to install.");
|
||||
});
|
||||
|
||||
it("handles multiple code spans in a single string", () => {
|
||||
const el = renderToContainer(
|
||||
"Use `curl | sh` or `npx @latest` for setup."
|
||||
);
|
||||
const codes = el.querySelectorAll("code");
|
||||
expect(codes).toHaveLength(2);
|
||||
expect(codes[0].textContent).toBe("curl | sh");
|
||||
expect(codes[1].textContent).toBe("npx @latest");
|
||||
});
|
||||
|
||||
it("handles empty input string", () => {
|
||||
const el = renderToContainer("");
|
||||
expect(el.textContent).toBe("");
|
||||
expect(el.querySelectorAll("code")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("handles string that is only a code span", () => {
|
||||
const el = renderToContainer("`only-code`");
|
||||
const codes = el.querySelectorAll("code");
|
||||
expect(codes).toHaveLength(1);
|
||||
expect(codes[0].textContent).toBe("only-code");
|
||||
expect(el.textContent).toBe("only-code");
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import Header from "../components/Header";
|
||||
|
||||
const siteModeMock = vi.fn(() => "souls");
|
||||
const navigateMock = vi.fn();
|
||||
|
||||
vi.mock("@tanstack/react-router", () => ({
|
||||
Link: (props: {
|
||||
@@ -19,7 +20,7 @@ vi.mock("@tanstack/react-router", () => ({
|
||||
</a>
|
||||
),
|
||||
useLocation: () => ({ pathname: "/" }),
|
||||
useNavigate: () => vi.fn(),
|
||||
useNavigate: () => navigateMock,
|
||||
}));
|
||||
|
||||
vi.mock("@convex-dev/auth/react", () => ({
|
||||
@@ -113,24 +114,44 @@ describe("Header", () => {
|
||||
it("renders direct desktop theme family controls and plain Skills tab", () => {
|
||||
siteModeMock.mockReturnValue("skills");
|
||||
setThemeMock.mockClear();
|
||||
setModeMock.mockClear();
|
||||
|
||||
render(<Header />);
|
||||
|
||||
expect(screen.getByText("Theme")).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Claw" })).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Hub" })).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: /Cycle theme mode/i })).toBeTruthy();
|
||||
expect(screen.getAllByText("Skills")).toHaveLength(1);
|
||||
expect(screen.getAllByText("Souls")).toHaveLength(1);
|
||||
expect(screen.getAllByText("Users")).toHaveLength(1);
|
||||
expect(screen.getByPlaceholderText("Search skills, plugins, users")).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Hub" }));
|
||||
expect(setThemeMock).toHaveBeenCalledWith("hub");
|
||||
fireEvent.click(screen.getByRole("button", { name: /Cycle theme mode/i }));
|
||||
expect(setModeMock).toHaveBeenCalledWith("light");
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Open menu" }));
|
||||
|
||||
expect(screen.getAllByText("Skills")).toHaveLength(2);
|
||||
expect(screen.getAllByText("Souls")).toHaveLength(2);
|
||||
expect(screen.getAllByText("Users")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("routes soul-mode header searches to the souls browse page", () => {
|
||||
siteModeMock.mockReturnValue("souls");
|
||||
navigateMock.mockReset();
|
||||
|
||||
render(<Header />);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText("Search souls..."), {
|
||||
target: { value: "angler" },
|
||||
});
|
||||
fireEvent.submit(screen.getByRole("search", { name: "Site search" }));
|
||||
|
||||
expect(navigateMock).toHaveBeenCalledWith({
|
||||
to: "/souls",
|
||||
search: {
|
||||
q: "angler",
|
||||
sort: undefined,
|
||||
dir: undefined,
|
||||
view: undefined,
|
||||
focus: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ let loaderDataMock: {
|
||||
nextCursor: string | null;
|
||||
rateLimited: boolean;
|
||||
retryAfterSeconds: number | null;
|
||||
apiError?: boolean;
|
||||
} = {
|
||||
items: [],
|
||||
nextCursor: null,
|
||||
@@ -72,7 +73,13 @@ describe("plugins route", () => {
|
||||
isRateLimitedPackageApiErrorMock.mockClear();
|
||||
navigateMock.mockReset();
|
||||
searchMock = {};
|
||||
loaderDataMock = { items: [], nextCursor: null, rateLimited: false, retryAfterSeconds: null };
|
||||
loaderDataMock = {
|
||||
items: [],
|
||||
nextCursor: null,
|
||||
rateLimited: false,
|
||||
retryAfterSeconds: null,
|
||||
apiError: false,
|
||||
};
|
||||
});
|
||||
|
||||
it("rejects skill family filter in search state", async () => {
|
||||
@@ -223,6 +230,36 @@ describe("plugins route", () => {
|
||||
nextCursor: null,
|
||||
rateLimited: true,
|
||||
retryAfterSeconds: 22,
|
||||
apiError: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("flags API errors for filtered catalog requests", async () => {
|
||||
fetchPluginCatalogMock.mockRejectedValue(new Error("boom"));
|
||||
const route = await loadRoute();
|
||||
const loader = route.__config.loader as (args: {
|
||||
deps: Record<string, unknown>;
|
||||
}) => Promise<{
|
||||
items: Array<{ name: string }>;
|
||||
nextCursor: string | null;
|
||||
rateLimited: boolean;
|
||||
retryAfterSeconds: number | null;
|
||||
apiError?: boolean;
|
||||
}>;
|
||||
|
||||
const result = await loader({
|
||||
deps: {
|
||||
q: "demo",
|
||||
executesCode: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
items: [],
|
||||
nextCursor: null,
|
||||
rateLimited: false,
|
||||
retryAfterSeconds: null,
|
||||
apiError: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import type { ComponentType, ReactNode } from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const navigateMock = vi.fn();
|
||||
let searchMock: { q?: string; type?: "all" | "skills" | "plugins" | "users" } = {};
|
||||
|
||||
vi.mock("@tanstack/react-router", () => ({
|
||||
createFileRoute:
|
||||
() =>
|
||||
(config: { component?: unknown; validateSearch?: unknown }) => ({
|
||||
__config: config,
|
||||
useSearch: () => searchMock,
|
||||
}),
|
||||
useNavigate: () => navigateMock,
|
||||
}));
|
||||
|
||||
vi.mock("../lib/useUnifiedSearch", () => ({
|
||||
useUnifiedSearch: () => ({
|
||||
results: [],
|
||||
skillCount: 0,
|
||||
pluginCount: 0,
|
||||
userCount: 0,
|
||||
isSearching: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../components/PluginListItem", () => ({
|
||||
PluginListItem: ({ item }: { item: { name: string } }) => <div>{item.name}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("../components/SkillListItem", () => ({
|
||||
SkillListItem: ({ skill }: { skill: { slug: string } }) => <div>{skill.slug}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("../components/UserListItem", () => ({
|
||||
UserListItem: ({ user }: { user: { _id: string } }) => <div>{user._id}</div>,
|
||||
}));
|
||||
|
||||
vi.mock("../components/ui/card", () => ({
|
||||
Card: ({ children, className }: { children: ReactNode; className?: string }) => (
|
||||
<div className={className}>{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
async function loadRoute() {
|
||||
return (await import("../routes/search")).Route as unknown as {
|
||||
__config: {
|
||||
component?: ComponentType;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
describe("search route", () => {
|
||||
beforeEach(() => {
|
||||
searchMock = { q: "first" };
|
||||
navigateMock.mockReset();
|
||||
});
|
||||
|
||||
it("keeps the input synced with query param changes while mounted", async () => {
|
||||
const route = await loadRoute();
|
||||
const Component = route.__config.component as ComponentType;
|
||||
const rendered = render(<Component />);
|
||||
|
||||
const input = screen.getByPlaceholderText("Search skills, plugins, users...") as HTMLInputElement;
|
||||
expect(input.value).toBe("first");
|
||||
|
||||
fireEvent.change(input, { target: { value: "draft" } });
|
||||
expect(input.value).toBe("draft");
|
||||
|
||||
searchMock = { q: "second" };
|
||||
rendered.rerender(<Component />);
|
||||
|
||||
expect(
|
||||
(screen.getByPlaceholderText("Search skills, plugins, users...") as HTMLInputElement).value,
|
||||
).toBe("second");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/* @vitest-environment jsdom */
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { createRef, type ComponentProps } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { SkillsToolbar } from '../routes/skills/-SkillsToolbar';
|
||||
|
||||
function renderToolbar(overrides?: Partial<ComponentProps<typeof SkillsToolbar>>) {
|
||||
return render(
|
||||
<SkillsToolbar
|
||||
searchInputRef={createRef<HTMLInputElement>()}
|
||||
query=""
|
||||
hasQuery={false}
|
||||
sort="downloads"
|
||||
dir="desc"
|
||||
view="list"
|
||||
highlightedOnly={false}
|
||||
nonSuspiciousOnly={false}
|
||||
capabilityTag={undefined}
|
||||
onQueryChange={vi.fn()}
|
||||
onToggleHighlighted={vi.fn()}
|
||||
onToggleNonSuspicious={vi.fn()}
|
||||
onCapabilityTagChange={vi.fn()}
|
||||
onSortChange={vi.fn()}
|
||||
onToggleDir={vi.fn()}
|
||||
onToggleView={vi.fn()}
|
||||
{...overrides}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('SkillsToolbar', () => {
|
||||
it('keeps filter chips on a dark-mode surface', () => {
|
||||
renderToolbar();
|
||||
|
||||
const staffPicksButton = screen.getByRole('button', { name: 'Staff Picks' });
|
||||
|
||||
expect(staffPicksButton.className).toContain('dark:bg-[rgba(14,28,37,0.84)]');
|
||||
expect(staffPicksButton.className).toContain('dark:text-[rgba(245,238,232,0.88)]');
|
||||
});
|
||||
|
||||
it('uses a readable active color treatment in dark mode', () => {
|
||||
renderToolbar({ highlightedOnly: true });
|
||||
|
||||
const staffPicksButton = screen.getByRole('button', { name: 'Staff Picks' });
|
||||
|
||||
expect(staffPicksButton.getAttribute('aria-pressed')).toBe('true');
|
||||
expect(staffPicksButton.className).toContain('dark:bg-[rgba(255,131,95,0.14)]');
|
||||
expect(staffPicksButton.className).toContain('dark:text-[#ffd5c9]');
|
||||
});
|
||||
});
|
||||
@@ -1,18 +1,14 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { FOOTER_NAV_SECTIONS } from "../lib/nav-items";
|
||||
import { getSiteName } from "../lib/site";
|
||||
|
||||
export function Footer() {
|
||||
const siteName = getSiteName();
|
||||
return (
|
||||
<footer className="site-footer" role="contentinfo">
|
||||
<div className="site-footer-inner">
|
||||
<div className="site-footer-divider" aria-hidden="true" />
|
||||
<div className="footer-grid">
|
||||
{FOOTER_NAV_SECTIONS.map((section) => (
|
||||
<div key={section.title} className="footer-col">
|
||||
<h4 className="footer-col-title">{section.title}</h4>
|
||||
{section.items.map((item) => {
|
||||
{section.items.filter((item) => item.featureFlag !== false).map((item) => {
|
||||
if (item.kind === "link") {
|
||||
return (
|
||||
<Link key={item.label} to={item.to} search={item.search ?? {}}>
|
||||
@@ -33,18 +29,6 @@ export function Footer() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="footer-bottom">
|
||||
<span>
|
||||
{siteName} — An{" "}
|
||||
<a href="https://openclaw.ai" target="_blank" rel="noreferrer">
|
||||
OpenClaw
|
||||
</a>{" "}
|
||||
project by{" "}
|
||||
<a href="https://steipete.me" target="_blank" rel="noreferrer">
|
||||
Peter Steinberger
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from "../lib/nav-items";
|
||||
import { isModerator } from "../lib/roles";
|
||||
import { getClawHubSiteUrl, getSiteMode, getSiteName } from "../lib/site";
|
||||
import { applyTheme, THEME_OPTIONS, useThemeMode } from "../lib/theme";
|
||||
import { applyTheme, useThemeMode } from "../lib/theme";
|
||||
import { startThemeTransition } from "../lib/theme-transition";
|
||||
import { setAuthError, useAuthError } from "../lib/useAuthError";
|
||||
import { useAuthStatus } from "../lib/useAuthStatus";
|
||||
@@ -40,10 +40,12 @@ const NAV_ICONS: Record<NavIconName, ComponentType<{ size?: number; className?:
|
||||
ghost: Ghost,
|
||||
};
|
||||
|
||||
const THEME_MODE_SEQUENCE: Array<"system" | "light" | "dark"> = ["system", "light", "dark"];
|
||||
|
||||
export default function Header() {
|
||||
const { isAuthenticated, isLoading, me } = useAuthStatus();
|
||||
const { signIn, signOut } = useAuthActions();
|
||||
const { theme, mode, setMode, setTheme } = useThemeMode();
|
||||
const { theme, mode, setMode } = useThemeMode();
|
||||
const toggleRef = useRef<HTMLDivElement | null>(null);
|
||||
const siteMode = getSiteMode();
|
||||
const siteName = useMemo(() => getSiteName(siteMode), [siteMode]);
|
||||
@@ -69,7 +71,7 @@ export default function Header() {
|
||||
const [navSearchQuery, setNavSearchQuery] = useState("");
|
||||
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const themeLabel = THEME_OPTIONS.find((option) => option.value === theme)?.label ?? "Claw";
|
||||
const ThemeModeIcon = getThemeModeIcon(mode);
|
||||
|
||||
const setThemeMode = (next: "system" | "light" | "dark") => {
|
||||
startThemeTransition({
|
||||
@@ -84,9 +86,10 @@ export default function Header() {
|
||||
});
|
||||
};
|
||||
|
||||
const setThemeFamily = (nextTheme: string) => {
|
||||
applyTheme(mode, nextTheme);
|
||||
setTheme(nextTheme);
|
||||
const cycleThemeMode = () => {
|
||||
const currentIndex = Math.max(0, THEME_MODE_SEQUENCE.indexOf(mode));
|
||||
const nextMode = THEME_MODE_SEQUENCE[(currentIndex + 1) % THEME_MODE_SEQUENCE.length] ?? "system";
|
||||
setThemeMode(nextMode);
|
||||
};
|
||||
|
||||
const handleNavSearch = (e: React.FormEvent) => {
|
||||
@@ -94,8 +97,16 @@ export default function Header() {
|
||||
const q = navSearchQuery.trim();
|
||||
if (!q) return;
|
||||
void navigate({
|
||||
to: "/search",
|
||||
search: { q, type: undefined },
|
||||
to: isSoulMode ? "/souls" : "/search",
|
||||
search: isSoulMode
|
||||
? {
|
||||
q,
|
||||
sort: undefined,
|
||||
dir: undefined,
|
||||
view: undefined,
|
||||
focus: undefined,
|
||||
}
|
||||
: { q, type: undefined },
|
||||
});
|
||||
setNavSearchQuery("");
|
||||
setMobileSearchOpen(false);
|
||||
@@ -146,23 +157,6 @@ export default function Header() {
|
||||
</SheetClose>
|
||||
))}
|
||||
</div>
|
||||
<div className="mobile-nav-section">
|
||||
<div className="mobile-nav-section-title">Theme family</div>
|
||||
{THEME_OPTIONS.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
className="mobile-nav-link"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setThemeFamily(option.value);
|
||||
setMobileMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<span>{option.label}</span>
|
||||
{theme === option.value ? <span className="mobile-nav-meta">Selected</span> : null}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="mobile-nav-section">
|
||||
<div className="mobile-nav-section-title">Theme mode</div>
|
||||
<button
|
||||
@@ -208,9 +202,10 @@ export default function Header() {
|
||||
search={{ q: undefined, highlighted: undefined, search: undefined }}
|
||||
className="brand"
|
||||
>
|
||||
<span className="brand-mark">
|
||||
{/* TODO: Re-introduce logo once new asset is ready */}
|
||||
{/* <span className="brand-mark">
|
||||
<img src="/clawd-logo.png" alt="" aria-hidden="true" className="brand-mark-image" />
|
||||
</span>
|
||||
</span> */}
|
||||
<span className="brand-name brand-name-responsive">{siteName}</span>
|
||||
</Link>
|
||||
|
||||
@@ -236,22 +231,16 @@ export default function Header() {
|
||||
<Search size={18} aria-hidden="true" />
|
||||
</button>
|
||||
<div className="theme-toggle" ref={toggleRef}>
|
||||
<div className="theme-picker-desktop" aria-label={`Theme family, current ${themeLabel}`}>
|
||||
<span className="theme-picker-label">Theme</span>
|
||||
<div className="theme-family-toggle" role="group" aria-label="Theme family">
|
||||
{THEME_OPTIONS.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className="theme-family-button"
|
||||
data-state={theme === option.value ? "on" : "off"}
|
||||
aria-pressed={theme === option.value}
|
||||
onClick={() => setThemeFamily(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="theme-cycle-group" aria-label="Theme controls">
|
||||
<button
|
||||
type="button"
|
||||
className="theme-cycle-button theme-cycle-button-mode"
|
||||
onClick={cycleThemeMode}
|
||||
aria-label={`Cycle theme mode. Current: ${mode}`}
|
||||
title={`Theme mode: ${mode}`}
|
||||
>
|
||||
<ThemeModeIcon className="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<ToggleGroup
|
||||
className="theme-mode-toggle"
|
||||
@@ -261,7 +250,7 @@ export default function Header() {
|
||||
if (!value) return;
|
||||
setThemeMode(value as "system" | "light" | "dark");
|
||||
}}
|
||||
aria-label={`Theme mode, ${themeLabel} preset`}
|
||||
aria-label="Theme mode"
|
||||
>
|
||||
<ToggleGroupItem value="system" aria-label="System theme">
|
||||
<Monitor className="h-4 w-4" aria-hidden="true" />
|
||||
@@ -410,3 +399,15 @@ function getCurrentRelativeUrl() {
|
||||
if (typeof window === "undefined") return "/";
|
||||
return `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
}
|
||||
|
||||
function getThemeModeIcon(mode: "system" | "light" | "dark") {
|
||||
switch (mode) {
|
||||
case "light":
|
||||
return Sun;
|
||||
case "dark":
|
||||
return Moon;
|
||||
case "system":
|
||||
default:
|
||||
return Monitor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { formatCompactStat, formatSkillStatsTriplet } from "../lib/numberFormat"
|
||||
import type { PublicPublisher, PublicSkill } from "../lib/publicUser";
|
||||
|
||||
import { type LlmAnalysis, SecurityScanResults } from "./SkillSecurityScanResults";
|
||||
import { SkillInstallCard } from "./SkillInstallCard";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Button } from "./ui/button";
|
||||
import { UserBadge } from "./UserBadge";
|
||||
@@ -110,8 +111,8 @@ export function SkillHeader({
|
||||
onTagSubmit,
|
||||
onTagDelete,
|
||||
tagVersions,
|
||||
clawdis: _clawdis,
|
||||
osLabels: _osLabels,
|
||||
clawdis,
|
||||
osLabels,
|
||||
}: SkillHeaderProps) {
|
||||
const formattedStats = formatSkillStatsTriplet(skill.stats);
|
||||
const suppressScanResults =
|
||||
@@ -354,6 +355,7 @@ export function SkillHeader({
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<SkillInstallCard clawdis={clawdis} osLabels={osLabels} />
|
||||
</div>
|
||||
|
||||
<div className="skill-tag-row">
|
||||
|
||||
@@ -4,11 +4,20 @@ import { SecurityScanResults } from "./SkillSecurityScanResults";
|
||||
|
||||
describe("SecurityScanResults static guidance", () => {
|
||||
it("renders capability-only states without scanner verdicts", () => {
|
||||
render(<SecurityScanResults capabilityTags={["posts-externally", "requires-oauth-token"]} />);
|
||||
render(
|
||||
<SecurityScanResults
|
||||
capabilityTags={[
|
||||
"posts-externally",
|
||||
"requires-oauth-token",
|
||||
"requires-sensitive-credentials",
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Capability signals")).toBeTruthy();
|
||||
expect(screen.getByText("Posts externally")).toBeTruthy();
|
||||
expect(screen.getByText("Requires OAuth token")).toBeTruthy();
|
||||
expect(screen.getByText("Requires sensitive credentials")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders capability labels separately from scan verdicts", () => {
|
||||
|
||||
@@ -14,6 +14,7 @@ const SKILL_CAPABILITY_LABELS: Record<string, string> = {
|
||||
"can-make-purchases": "Can make purchases",
|
||||
"can-sign-transactions": "Can sign transactions",
|
||||
"requires-oauth-token": "Requires OAuth token",
|
||||
"requires-sensitive-credentials": "Requires sensitive credentials",
|
||||
"posts-externally": "Posts externally",
|
||||
};
|
||||
|
||||
|
||||
@@ -47,10 +47,10 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
variant === "outline" &&
|
||||
"border border-[color:var(--border-ui)] bg-transparent text-[color:var(--ink)] hover:not-disabled:border-[color:var(--border-ui-hover)] hover:not-disabled:bg-[color:var(--surface)]",
|
||||
// Size styles
|
||||
size === "default" && "min-h-[44px] rounded-[var(--radius-pill)] px-4 py-[11px] text-sm",
|
||||
size === "sm" && "min-h-[34px] rounded-[var(--radius-pill)] px-3 py-1.5 text-xs",
|
||||
size === "lg" && "min-h-[52px] rounded-[var(--radius-pill)] px-6 py-3 text-base",
|
||||
size === "icon" && "h-[44px] w-[44px] rounded-[var(--radius-pill)] p-0",
|
||||
size === "default" && "min-h-[44px] rounded-[var(--r-btn)] px-4 py-[11px] text-sm",
|
||||
size === "sm" && "min-h-[34px] rounded-[var(--r-btn)] px-3 py-1.5 text-xs",
|
||||
size === "lg" && "min-h-[52px] rounded-[var(--r-btn)] px-6 py-3 text-base",
|
||||
size === "icon" && "h-[44px] w-[44px] rounded-[var(--r-btn)] p-0",
|
||||
className,
|
||||
)}
|
||||
disabled={disabled || loading}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { getRuntimeEnv } from "./runtimeEnv";
|
||||
|
||||
/**
|
||||
* Feature flags — controlled via VITE_FEATURE_* env vars.
|
||||
* Default values are the fallback when the env var is unset.
|
||||
*/
|
||||
|
||||
function flag(name: string, defaultValue: boolean): boolean {
|
||||
const raw = getRuntimeEnv(name);
|
||||
if (raw === undefined) return defaultValue;
|
||||
return raw === "true" || raw === "1";
|
||||
}
|
||||
|
||||
/** Show the Souls section (nav, footer, homepage category, routes). Default: false */
|
||||
export const FEATURE_SOULS = flag("VITE_FEATURE_SOULS", false);
|
||||
@@ -1,3 +1,5 @@
|
||||
import { FEATURE_SOULS } from "./features";
|
||||
|
||||
/**
|
||||
* Shared navigation configuration used by Header and Footer to eliminate
|
||||
* triple duplication of nav link definitions.
|
||||
@@ -25,6 +27,8 @@ export interface NavItem {
|
||||
soulModeHide: boolean;
|
||||
/** Additional path prefixes that should also highlight this nav item (e.g. /skill for /skills) */
|
||||
activePathPrefixes?: string[];
|
||||
/** Feature flag that must be truthy for this item to show */
|
||||
featureFlag?: boolean;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -79,7 +83,7 @@ export const PRIMARY_NAV_ITEMS: NavItem[] = [
|
||||
authRequired: false,
|
||||
staffOnly: false,
|
||||
soulModeOnly: false,
|
||||
soulModeHide: true,
|
||||
soulModeHide: false,
|
||||
activePathPrefixes: ["/plugin/"],
|
||||
},
|
||||
{
|
||||
@@ -90,9 +94,9 @@ export const PRIMARY_NAV_ITEMS: NavItem[] = [
|
||||
authRequired: false,
|
||||
staffOnly: false,
|
||||
soulModeOnly: false,
|
||||
// In soul-mode this is the primary tab; in skills-mode it is also shown.
|
||||
soulModeHide: false,
|
||||
activePathPrefixes: ["/soul/"],
|
||||
featureFlag: FEATURE_SOULS,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -155,9 +159,9 @@ export interface FooterNavSection {
|
||||
}
|
||||
|
||||
export type FooterNavItem =
|
||||
| { kind: "link"; label: string; to: string; search?: Record<string, unknown> }
|
||||
| { kind: "external"; label: string; href: string }
|
||||
| { kind: "text"; label: string };
|
||||
| { kind: "link"; label: string; to: string; search?: Record<string, unknown>; featureFlag?: boolean }
|
||||
| { kind: "external"; label: string; href: string; featureFlag?: boolean }
|
||||
| { kind: "text"; label: string; featureFlag?: boolean };
|
||||
|
||||
export const FOOTER_NAV_SECTIONS: FooterNavSection[] = [
|
||||
{
|
||||
@@ -165,7 +169,7 @@ export const FOOTER_NAV_SECTIONS: FooterNavSection[] = [
|
||||
items: [
|
||||
{ kind: "link", label: "Skills", to: "/skills", search: SKILLS_SEARCH },
|
||||
{ kind: "link", label: "Plugins", to: "/plugins" },
|
||||
{ kind: "link", label: "Souls", to: "/souls", search: SOULS_SEARCH },
|
||||
{ kind: "link", label: "Souls", to: "/souls", search: SOULS_SEARCH, featureFlag: FEATURE_SOULS },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -190,25 +194,18 @@ export const FOOTER_NAV_SECTIONS: FooterNavSection[] = [
|
||||
sourceRepo: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "external",
|
||||
label: "Documentation",
|
||||
href: "https://github.com/openclaw/clawhub",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Community",
|
||||
items: [
|
||||
{ kind: "external", label: "GitHub", href: "https://github.com/openclaw/clawhub" },
|
||||
{ kind: "link", label: "About", to: "/about" },
|
||||
{ kind: "external", label: "OpenClaw", href: "https://openclaw.ai" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Platform",
|
||||
items: [
|
||||
{ kind: "text", label: "MIT Licensed" },
|
||||
{ kind: "external", label: "Deployed on Vercel", href: "https://vercel.com" },
|
||||
{ kind: "external", label: "Powered by Convex", href: "https://www.convex.dev" },
|
||||
],
|
||||
@@ -229,6 +226,7 @@ export function filterNavItems(
|
||||
if (item.soulModeHide && ctx.isSoulMode) return false;
|
||||
if (item.authRequired && !ctx.isAuthenticated) return false;
|
||||
if (item.staffOnly && !ctx.isStaff) return false;
|
||||
if (item.featureFlag === false) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -128,15 +128,36 @@ function normalizeApiPath(path: string) {
|
||||
return path.startsWith("/") ? path : `/${path}`;
|
||||
}
|
||||
|
||||
function resolveAbsoluteBaseUrl(...candidates: Array<string | undefined>) {
|
||||
for (const candidate of candidates) {
|
||||
const value = candidate?.trim();
|
||||
if (!value) continue;
|
||||
try {
|
||||
return new URL(value).toString();
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function packageApiUrl(path: string) {
|
||||
const normalizedPath = normalizeApiPath(path);
|
||||
if (typeof window !== "undefined") {
|
||||
// In production, Vercel rewrites /api/* to the Convex site, so relative
|
||||
// paths work. In local dev, Nitro intercepts the request before Vite's
|
||||
// proxy, so we must use the Convex site URL directly.
|
||||
const convexSiteUrl = getRuntimeEnv("VITE_CONVEX_SITE_URL");
|
||||
if (convexSiteUrl && window.location.hostname === "localhost") {
|
||||
return new URL(normalizedPath, convexSiteUrl);
|
||||
const convexClientBaseUrl = resolveAbsoluteBaseUrl(
|
||||
getRuntimeEnv("VITE_CONVEX_SITE_URL"),
|
||||
getRuntimeEnv("VITE_CONVEX_URL"),
|
||||
);
|
||||
if (
|
||||
convexClientBaseUrl &&
|
||||
(window.location.hostname === "localhost" ||
|
||||
window.location.hostname === "127.0.0.1" ||
|
||||
window.location.hostname === "0.0.0.0")
|
||||
) {
|
||||
return new URL(normalizedPath, convexClientBaseUrl);
|
||||
}
|
||||
return new URL(normalizedPath, window.location.origin);
|
||||
}
|
||||
@@ -144,7 +165,11 @@ async function packageApiUrl(path: string) {
|
||||
// In production, Vercel rewrites /api/* but SSR loaders run server-side
|
||||
// where the rewrite doesn't apply. Using getRequestUrl() would loop back
|
||||
// into TanStack Start / Nitro, which rejects non-HTML requests.
|
||||
const base = getRuntimeEnv("VITE_CONVEX_SITE_URL") ?? getRequiredRuntimeEnv("VITE_CONVEX_URL");
|
||||
const base =
|
||||
resolveAbsoluteBaseUrl(
|
||||
getRuntimeEnv("VITE_CONVEX_SITE_URL"),
|
||||
getRuntimeEnv("VITE_CONVEX_URL"),
|
||||
) ?? getRequiredRuntimeEnv("VITE_CONVEX_URL");
|
||||
return new URL(normalizedPath, base);
|
||||
}
|
||||
|
||||
@@ -277,51 +302,32 @@ export async function fetchPluginCatalog(params: {
|
||||
executesCode?: boolean;
|
||||
limit?: number;
|
||||
}): Promise<PluginCatalogResult> {
|
||||
try {
|
||||
if (params.family) {
|
||||
const response = await fetchPackages({
|
||||
q: params.q,
|
||||
cursor: params.cursor,
|
||||
family: params.family,
|
||||
isOfficial: params.isOfficial,
|
||||
executesCode: params.executesCode,
|
||||
limit: params.limit,
|
||||
});
|
||||
if (hasOwnProperty(response, "results") && Array.isArray(response.results)) {
|
||||
return {
|
||||
items: response.results.map((entry) => entry?.package).filter(Boolean) as PackageListItem[],
|
||||
nextCursor: null,
|
||||
};
|
||||
}
|
||||
|
||||
const browseResponse = response as PackageCatalogBrowseResponse;
|
||||
if (params.family) {
|
||||
const response = await fetchPackages({
|
||||
q: params.q,
|
||||
cursor: params.cursor,
|
||||
family: params.family,
|
||||
isOfficial: params.isOfficial,
|
||||
executesCode: params.executesCode,
|
||||
limit: params.limit,
|
||||
});
|
||||
if (hasOwnProperty(response, "results") && Array.isArray(response.results)) {
|
||||
return {
|
||||
items: browseResponse?.items ?? [],
|
||||
nextCursor: browseResponse?.nextCursor ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
if (params.q?.trim()) {
|
||||
const url = await packageApiUrl(`${ApiRoutes.plugins}/search`);
|
||||
url.searchParams.set("q", params.q.trim());
|
||||
if (typeof params.limit === "number") url.searchParams.set("limit", String(params.limit));
|
||||
if (typeof params.isOfficial === "boolean") {
|
||||
url.searchParams.set("isOfficial", String(params.isOfficial));
|
||||
}
|
||||
if (typeof params.executesCode === "boolean") {
|
||||
url.searchParams.set("executesCode", String(params.executesCode));
|
||||
}
|
||||
const response = await fetchJson<{
|
||||
results?: Array<{ score: number; package: PackageListItem }>;
|
||||
}>(url);
|
||||
return {
|
||||
items: (response?.results ?? []).map((entry) => entry?.package).filter(Boolean) as PackageListItem[],
|
||||
items: response.results.map((entry) => entry?.package).filter(Boolean) as PackageListItem[],
|
||||
nextCursor: null,
|
||||
};
|
||||
}
|
||||
|
||||
const url = await packageApiUrl(ApiRoutes.plugins);
|
||||
if (params.cursor) url.searchParams.set("cursor", params.cursor);
|
||||
const browseResponse = response as PackageCatalogBrowseResponse;
|
||||
return {
|
||||
items: browseResponse?.items ?? [],
|
||||
nextCursor: browseResponse?.nextCursor ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
if (params.q?.trim()) {
|
||||
const url = await packageApiUrl(`${ApiRoutes.plugins}/search`);
|
||||
url.searchParams.set("q", params.q.trim());
|
||||
if (typeof params.limit === "number") url.searchParams.set("limit", String(params.limit));
|
||||
if (typeof params.isOfficial === "boolean") {
|
||||
url.searchParams.set("isOfficial", String(params.isOfficial));
|
||||
@@ -329,29 +335,39 @@ export async function fetchPluginCatalog(params: {
|
||||
if (typeof params.executesCode === "boolean") {
|
||||
url.searchParams.set("executesCode", String(params.executesCode));
|
||||
}
|
||||
const result = await fetchJson<PluginCatalogResult>(url);
|
||||
const response = await fetchJson<{
|
||||
results?: Array<{ score: number; package: PackageListItem }>;
|
||||
}>(url);
|
||||
return {
|
||||
items: result?.items ?? [],
|
||||
nextCursor: result?.nextCursor ?? null,
|
||||
items: (response?.results ?? []).map((entry) => entry?.package).filter(Boolean) as PackageListItem[],
|
||||
nextCursor: null,
|
||||
};
|
||||
} catch {
|
||||
// Return empty result on API error to prevent SSR crashes
|
||||
return { items: [], nextCursor: null };
|
||||
}
|
||||
|
||||
const url = await packageApiUrl(ApiRoutes.plugins);
|
||||
if (params.cursor) url.searchParams.set("cursor", params.cursor);
|
||||
if (typeof params.limit === "number") url.searchParams.set("limit", String(params.limit));
|
||||
if (typeof params.isOfficial === "boolean") {
|
||||
url.searchParams.set("isOfficial", String(params.isOfficial));
|
||||
}
|
||||
if (typeof params.executesCode === "boolean") {
|
||||
url.searchParams.set("executesCode", String(params.executesCode));
|
||||
}
|
||||
const result = await fetchJson<PluginCatalogResult>(url);
|
||||
return {
|
||||
items: result?.items ?? [],
|
||||
nextCursor: result?.nextCursor ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchPackageDetail(name: string): Promise<PackageDetailResponse> {
|
||||
try {
|
||||
const url = await packageApiUrl(`${ApiRoutes.packages}/${encodeURIComponent(name)}`);
|
||||
const response = await packageFetch(url, "application/json");
|
||||
if (response.status === 404 || !response.ok) {
|
||||
return { package: null, owner: null };
|
||||
}
|
||||
return (await response.json()) as PackageDetailResponse;
|
||||
} catch {
|
||||
// Return empty result on API error to prevent SSR crashes
|
||||
const url = await packageApiUrl(`${ApiRoutes.packages}/${encodeURIComponent(name)}`);
|
||||
const response = await packageFetch(url, "application/json");
|
||||
if (response.status === 404) {
|
||||
return { package: null, owner: null };
|
||||
}
|
||||
if (!response.ok) throw await createPackageApiError(response);
|
||||
return (await response.json()) as PackageDetailResponse;
|
||||
}
|
||||
|
||||
export async function fetchPackageVersion(name: string, version: string): Promise<PackageVersionDetail | null> {
|
||||
@@ -367,15 +383,13 @@ export async function fetchPackageVersion(name: string, version: string): Promis
|
||||
}
|
||||
|
||||
export async function fetchPackageReadme(name: string, version?: string | null): Promise<string | null> {
|
||||
try {
|
||||
const url = await packageApiUrl(`${ApiRoutes.packages}/${encodeURIComponent(name)}/file`);
|
||||
url.searchParams.set("path", "README.md");
|
||||
if (version) url.searchParams.set("version", version);
|
||||
const response = await packageFetch(url, "text/plain");
|
||||
if (response.ok) return await response.text();
|
||||
return null;
|
||||
} catch {
|
||||
// Return null on API error to prevent SSR crashes
|
||||
const url = await packageApiUrl(`${ApiRoutes.packages}/${encodeURIComponent(name)}/file`);
|
||||
url.searchParams.set("path", "README.md");
|
||||
if (version) url.searchParams.set("version", version);
|
||||
const response = await packageFetch(url, "text/plain");
|
||||
if (response.ok) return await response.text();
|
||||
if (response.status === 404 || response.status === 423) {
|
||||
return null;
|
||||
}
|
||||
throw await createPackageApiError(response);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
type CustomThemeData,
|
||||
} from './customTheme';
|
||||
|
||||
export type ThemeName = 'claw' | 'hub';
|
||||
export type ThemeName = 'claw';
|
||||
export type ThemeMode = 'system' | 'light' | 'dark';
|
||||
export type ResolvedTheme = 'light' | 'dark';
|
||||
|
||||
@@ -29,16 +29,11 @@ export const THEME_OPTIONS: Array<{ value: ThemeName; label: string; description
|
||||
label: 'Claw',
|
||||
description: 'OpenClaw black, white, and red.',
|
||||
},
|
||||
{
|
||||
value: 'hub',
|
||||
label: 'Hub',
|
||||
description: 'Marketplace monochrome index with terminal-style contrast.',
|
||||
},
|
||||
];
|
||||
|
||||
export const THEME_FAMILY_OPTIONS = THEME_OPTIONS;
|
||||
|
||||
const VALID_THEME_NAMES = new Set<ThemeName>(['claw', 'hub']);
|
||||
const VALID_THEME_NAMES = new Set<ThemeName>(['claw']);
|
||||
const VALID_THEME_MODES = new Set<ThemeMode>(['system', 'light', 'dark']);
|
||||
|
||||
const LEGACY_MAP: Record<string, ThemeSelection> = {
|
||||
@@ -51,8 +46,8 @@ const LEGACY_MAP: Record<string, ThemeSelection> = {
|
||||
landingTheme: { theme: 'claw', mode: 'dark' },
|
||||
newTheme: { theme: 'claw', mode: 'dark' },
|
||||
openknot: { theme: 'claw', mode: 'dark' },
|
||||
fieldmanual: { theme: 'hub', mode: 'dark' },
|
||||
clawdash: { theme: 'hub', mode: 'light' },
|
||||
fieldmanual: { theme: 'claw', mode: 'dark' },
|
||||
clawdash: { theme: 'claw', mode: 'light' },
|
||||
};
|
||||
|
||||
function parseThemeSelection(themeRaw: unknown, modeRaw: unknown): ThemeSelection {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getUserFacingConvexError } from "./convexError";
|
||||
export async function uploadFile(uploadUrl: string, file: File) {
|
||||
const path = file.webkitRelativePath || file.name;
|
||||
const contentType =
|
||||
normalizeTextContentType(path, file.type) ?? file.type ?? "application/octet-stream";
|
||||
normalizeTextContentType(path, file.type) || file.type || "application/octet-stream";
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": contentType },
|
||||
|
||||
|
Before Width: | Height: | Size: 19 KiB |
@@ -97,11 +97,6 @@ export const Route = createRootRoute({
|
||||
href: "/favicon.ico",
|
||||
type: "image/x-icon",
|
||||
},
|
||||
{
|
||||
rel: "icon",
|
||||
href: "/logo.svg",
|
||||
type: "image/svg+xml",
|
||||
},
|
||||
{
|
||||
rel: "apple-touch-icon",
|
||||
href: "/logo192.png",
|
||||
|
||||
@@ -1,41 +1,72 @@
|
||||
import { createFileRoute, Link } from '@tanstack/react-router';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import {
|
||||
Banknote,
|
||||
Drama,
|
||||
Eye,
|
||||
EyeOff,
|
||||
ImageOff,
|
||||
ShieldOff,
|
||||
UserX,
|
||||
} from 'lucide-react';
|
||||
import { Badge } from '../components/ui/badge';
|
||||
import { Button } from '../components/ui/button';
|
||||
import { getSiteMode, getSiteName, getSiteUrlForMode } from '../lib/site';
|
||||
|
||||
const prohibitedCategories = [
|
||||
export function renderWithInlineCode(text: string): (string | JSX.Element)[] {
|
||||
const parts = text.split(/(`[^`]+`)/g);
|
||||
return parts.map((part, i) => {
|
||||
if (part.startsWith('`') && part.endsWith('`')) {
|
||||
return (
|
||||
<code key={i} className="about-inline-code">
|
||||
{part.slice(1, -1)}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
return part;
|
||||
});
|
||||
}
|
||||
|
||||
const prohibitedCategories: { title: string; icon: LucideIcon; examples: string }[] = [
|
||||
{
|
||||
title: 'Bypass and unauthorized access',
|
||||
icon: ShieldOff,
|
||||
examples:
|
||||
'Auth bypass, account takeover, CAPTCHA bypass, Cloudflare or anti-bot evasion, rate-limit bypass, reusable session theft, live call or agent takeover.',
|
||||
},
|
||||
{
|
||||
title: 'Platform abuse and ban evasion',
|
||||
icon: UserX,
|
||||
examples:
|
||||
'Stealth accounts after bans, account warming/farming, fake engagement, multi-account automation, spam posting, marketplace or social automation built to avoid detection.',
|
||||
},
|
||||
{
|
||||
title: 'Fraud and deception',
|
||||
icon: Banknote,
|
||||
examples:
|
||||
'Fake certificates, fake invoices, deceptive payment flows, fake social proof, scam outreach, or synthetic-identity workflows built to create accounts for fraud.',
|
||||
},
|
||||
{
|
||||
title: 'Privacy-invasive surveillance',
|
||||
icon: Eye,
|
||||
examples:
|
||||
'Mass contact scraping for spam, doxxing, stalking, covert monitoring, biometric / face-matching workflows without clear consent, or buying, publishing, downloading, or operationalizing leaked data or breach dumps.',
|
||||
},
|
||||
{
|
||||
title: 'Non-consensual impersonation',
|
||||
icon: Drama,
|
||||
examples:
|
||||
'Face swap, digital twins, cloned influencers, fake personas, or other identity manipulation used to impersonate or mislead.',
|
||||
},
|
||||
{
|
||||
title: 'Explicit sexual content',
|
||||
icon: ImageOff,
|
||||
examples:
|
||||
'NSFW image, video, or text generation, especially wrappers around third-party APIs with safety checks disabled.',
|
||||
},
|
||||
{
|
||||
title: 'Hidden or misleading execution',
|
||||
icon: EyeOff,
|
||||
examples:
|
||||
'Obfuscated install commands, `curl | sh`, undeclared secret requirements, undeclared private-key use, or remote `npx @latest` execution without reviewability.',
|
||||
},
|
||||
@@ -84,88 +115,109 @@ export const Route = createFileRoute('/about')({
|
||||
function AboutPage() {
|
||||
return (
|
||||
<main className="section about-page">
|
||||
<div className="about-hero">
|
||||
<div className="about-hero-copy">
|
||||
<div className="skill-card-tags mb-3">
|
||||
<Badge>About</Badge>
|
||||
<Badge variant="accent">Policy</Badge>
|
||||
<div className="about-bento">
|
||||
<section className="about-panel about-panel-hero">
|
||||
<div className="about-hero-copy">
|
||||
<div className="skill-card-tags mb-3">
|
||||
<Badge>About</Badge>
|
||||
<Badge variant="accent">Policy</Badge>
|
||||
</div>
|
||||
<h1 className="about-title">What ClawHub will not host</h1>
|
||||
<p className="about-lead">
|
||||
ClawHub is for useful agent tooling, not abuse workflows. If a skill is built to
|
||||
evade defenses, scam people, invade privacy, or enable non-consensual behavior, it
|
||||
does not belong here.
|
||||
</p>
|
||||
</div>
|
||||
<h1 className="about-title">What ClawHub will not host</h1>
|
||||
<p className="about-lead">
|
||||
ClawHub is for useful agent tooling, not abuse workflows. If a skill is built to evade
|
||||
defenses, scam people, invade privacy, or enable non-consensual behavior, it does not
|
||||
belong here.
|
||||
</section>
|
||||
|
||||
<aside className="about-panel about-panel-callout">
|
||||
<div className="about-callout">
|
||||
<span className="about-callout-label">Moderation stance</span>
|
||||
<p>
|
||||
We judge end-to-end abuse patterns, not keyword theater. Useful tooling stays.
|
||||
Predatory workflows get removed.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section className="about-panel about-panel-categories">
|
||||
<div className="home-section-header">
|
||||
<h2 className="home-section-title">Rejection Categories</h2>
|
||||
</div>
|
||||
<div className="about-grid">
|
||||
{prohibitedCategories.map((category) => {
|
||||
const Icon = category.icon;
|
||||
return (
|
||||
<article key={category.title} className="about-rule-card">
|
||||
<div className="about-rule-card-header">
|
||||
<div className="about-rule-card-icon">
|
||||
<Icon size={18} />
|
||||
</div>
|
||||
<h3>{category.title}</h3>
|
||||
</div>
|
||||
<p>{renderWithInlineCode(category.examples)}</p>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="about-panel about-panel-patterns">
|
||||
<div className="home-section-header">
|
||||
<h2 className="home-section-title">Recent patterns we are explicitly not okay with</h2>
|
||||
</div>
|
||||
<div className="about-patterns">
|
||||
{recentPatterns.map((pattern) => (
|
||||
<div key={pattern} className="about-pattern">
|
||||
{pattern}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="about-panel about-panel-enforcement">
|
||||
<div>
|
||||
<span className="about-callout-label">Enforcement</span>
|
||||
<div className="management-sublist">
|
||||
<div className="management-subitem">
|
||||
We may hide, remove, or hard-delete violating skills.
|
||||
</div>
|
||||
<div className="management-subitem">
|
||||
We may revoke tokens, soft-delete associated content, and ban repeat or severe
|
||||
offenders.
|
||||
</div>
|
||||
<div className="management-subitem">
|
||||
We do not guarantee warning-first enforcement for obvious abuse.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="about-panel about-panel-actions">
|
||||
<span className="about-callout-label">Next steps</span>
|
||||
<p className="about-panel-copy">
|
||||
If you are reviewing a borderline workflow, use the reviewer doc. If you are browsing,
|
||||
stay in the public catalog.
|
||||
</p>
|
||||
</div>
|
||||
<div className="about-callout">
|
||||
<span className="about-callout-label">Moderation stance</span>
|
||||
<p>
|
||||
We judge end-to-end abuse patterns, not keyword theater. Useful tooling stays.
|
||||
Predatory workflows get removed.
|
||||
</p>
|
||||
</div>
|
||||
<div className="skill-card-tags">
|
||||
<Button asChild variant="primary">
|
||||
<Link to="/skills">
|
||||
Browse Skills
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<a
|
||||
href="https://github.com/openclaw/clawhub/blob/main/docs/acceptable-usage.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Reviewer Doc
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section className="about-section">
|
||||
<div className="home-section-header">
|
||||
<h2 className="home-section-title">Immediate rejection categories</h2>
|
||||
</div>
|
||||
<div className="about-grid">
|
||||
{prohibitedCategories.map((category) => (
|
||||
<article key={category.title} className="about-rule-card">
|
||||
<h2>{category.title}</h2>
|
||||
<p>{category.examples}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="about-section">
|
||||
<div className="home-section-header">
|
||||
<h2 className="home-section-title">Recent patterns we are explicitly not okay with</h2>
|
||||
</div>
|
||||
<div className="about-patterns">
|
||||
{recentPatterns.map((pattern) => (
|
||||
<div key={pattern} className="about-pattern">
|
||||
{pattern}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="about-enforcement">
|
||||
<div>
|
||||
<span className="about-callout-label">Enforcement</span>
|
||||
<div className="management-sublist">
|
||||
<div className="management-subitem">
|
||||
We may hide, remove, or hard-delete violating skills.
|
||||
</div>
|
||||
<div className="management-subitem">
|
||||
We may revoke tokens, soft-delete associated content, and ban repeat or severe
|
||||
offenders.
|
||||
</div>
|
||||
<div className="management-subitem">
|
||||
We do not guarantee warning-first enforcement for obvious abuse.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="skill-card-tags">
|
||||
<Button asChild variant="primary">
|
||||
<Link to="/skills">
|
||||
Browse Skills
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<a
|
||||
href="https://github.com/openclaw/clawhub/blob/main/docs/acceptable-usage.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Reviewer Doc
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
fetchPackageReadme,
|
||||
fetchPackageVersion,
|
||||
getPackageDownloadPath,
|
||||
isRateLimitedPackageApiError,
|
||||
type PackageDetailResponse,
|
||||
type PackageVersionDetail,
|
||||
} from "../../lib/packageApi";
|
||||
@@ -35,7 +36,6 @@ type PluginDetailLoaderData = {
|
||||
|
||||
export const Route = createFileRoute("/plugins/$name")({
|
||||
loader: async ({ params }): Promise<PluginDetailLoaderData> => {
|
||||
// All fetch functions now handle errors internally and return null/empty on failure
|
||||
const requestedName = params.name;
|
||||
const candidateNames = requestedName.includes("/")
|
||||
? [requestedName]
|
||||
@@ -43,9 +43,25 @@ export const Route = createFileRoute("/plugins/$name")({
|
||||
|
||||
let resolvedName = requestedName;
|
||||
let detail: PackageDetailResponse = { package: null, owner: null };
|
||||
|
||||
|
||||
for (const candidateName of candidateNames) {
|
||||
const candidateDetail = await fetchPackageDetail(candidateName);
|
||||
let candidateDetail: PackageDetailResponse;
|
||||
try {
|
||||
candidateDetail = await fetchPackageDetail(candidateName);
|
||||
} catch (error) {
|
||||
if (isRateLimitedPackageApiError(error)) {
|
||||
return {
|
||||
detail: { package: null, owner: null },
|
||||
version: null,
|
||||
readme: null,
|
||||
rateLimited: {
|
||||
scope: "detail",
|
||||
retryAfterSeconds: error.retryAfterSeconds,
|
||||
},
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (candidateDetail.package) {
|
||||
detail = candidateDetail;
|
||||
resolvedName = candidateName;
|
||||
@@ -58,15 +74,29 @@ export const Route = createFileRoute("/plugins/$name")({
|
||||
return { detail, version: null, readme: null, rateLimited: null };
|
||||
}
|
||||
|
||||
// Fetch readme and version in parallel - functions handle errors internally
|
||||
const [version, readme] = await Promise.all([
|
||||
detail.package.latestVersion
|
||||
? fetchPackageVersion(resolvedName, detail.package.latestVersion)
|
||||
: Promise.resolve(null),
|
||||
fetchPackageReadme(resolvedName),
|
||||
]);
|
||||
|
||||
return { detail, version, readme, rateLimited: null };
|
||||
try {
|
||||
const [version, readme] = await Promise.all([
|
||||
detail.package.latestVersion
|
||||
? fetchPackageVersion(resolvedName, detail.package.latestVersion)
|
||||
: Promise.resolve(null),
|
||||
fetchPackageReadme(resolvedName),
|
||||
]);
|
||||
|
||||
return { detail, version, readme, rateLimited: null };
|
||||
} catch (error) {
|
||||
if (isRateLimitedPackageApiError(error)) {
|
||||
return {
|
||||
detail,
|
||||
version: null,
|
||||
readme: null,
|
||||
rateLimited: {
|
||||
scope: "metadata",
|
||||
retryAfterSeconds: error.retryAfterSeconds,
|
||||
},
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
head: ({ params, loaderData }) => ({
|
||||
meta: [
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PluginListItem } from "../../components/PluginListItem";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import {
|
||||
fetchPluginCatalog,
|
||||
isRateLimitedPackageApiError,
|
||||
type PackageListItem,
|
||||
} from "../../lib/packageApi";
|
||||
|
||||
@@ -55,24 +56,42 @@ export const Route = createFileRoute("/plugins/")({
|
||||
}),
|
||||
loaderDeps: ({ search }) => search,
|
||||
loader: async ({ deps }): Promise<PluginsLoaderData> => {
|
||||
// fetchPluginCatalog now handles errors internally and returns empty results
|
||||
const data = await fetchPluginCatalog({
|
||||
q: deps.q,
|
||||
cursor: deps.q ? undefined : deps.cursor,
|
||||
family: deps.family,
|
||||
isOfficial: deps.verified,
|
||||
executesCode: deps.executesCode,
|
||||
limit: 50,
|
||||
});
|
||||
try {
|
||||
const data = await fetchPluginCatalog({
|
||||
q: deps.q,
|
||||
cursor: deps.q ? undefined : deps.cursor,
|
||||
family: deps.family,
|
||||
isOfficial: deps.verified,
|
||||
executesCode: deps.executesCode,
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
const items = data?.items ?? [];
|
||||
return {
|
||||
items,
|
||||
nextCursor: data?.nextCursor ?? null,
|
||||
rateLimited: false,
|
||||
retryAfterSeconds: null,
|
||||
apiError: items.length === 0 && !deps.q && !deps.family && !deps.verified && !deps.executesCode,
|
||||
};
|
||||
return {
|
||||
items: data?.items ?? [],
|
||||
nextCursor: data?.nextCursor ?? null,
|
||||
rateLimited: false,
|
||||
retryAfterSeconds: null,
|
||||
apiError: false,
|
||||
};
|
||||
} catch (error) {
|
||||
if (isRateLimitedPackageApiError(error)) {
|
||||
return {
|
||||
items: [],
|
||||
nextCursor: null,
|
||||
rateLimited: true,
|
||||
retryAfterSeconds: error.retryAfterSeconds,
|
||||
apiError: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
items: [],
|
||||
nextCursor: null,
|
||||
rateLimited: false,
|
||||
retryAfterSeconds: null,
|
||||
apiError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
component: PluginsIndex,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { Search } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { PluginListItem } from "../components/PluginListItem";
|
||||
import { SkillListItem } from "../components/SkillListItem";
|
||||
import { UserListItem } from "../components/UserListItem";
|
||||
@@ -35,6 +35,10 @@ function UnifiedSearchPage() {
|
||||
const activeType = search.type ?? "all";
|
||||
const [query, setQuery] = useState(search.q ?? "");
|
||||
|
||||
useEffect(() => {
|
||||
setQuery(search.q ?? "");
|
||||
}, [search.q]);
|
||||
|
||||
const { results, skillCount, pluginCount, userCount, isSearching } = useUnifiedSearch(
|
||||
search.q ?? "",
|
||||
activeType,
|
||||
|
||||
@@ -52,7 +52,7 @@ import {
|
||||
type ListViewMode,
|
||||
usePreferences,
|
||||
} from "../lib/preferences";
|
||||
import { getThemeFamilyLabel, THEME_FAMILY_OPTIONS, useThemeMode } from "../lib/theme";
|
||||
import { getThemeFamilyLabel, useThemeMode } from "../lib/theme";
|
||||
|
||||
export const Route = createFileRoute("/settings")({
|
||||
component: Settings,
|
||||
@@ -67,7 +67,6 @@ export function Settings() {
|
||||
family: themeFamily,
|
||||
importCustomTheme,
|
||||
mode: themeMode,
|
||||
setFamily: setThemeFamily,
|
||||
setMode: setThemeMode,
|
||||
clearCustomTheme,
|
||||
} = useThemeMode();
|
||||
@@ -91,11 +90,14 @@ export function Settings() {
|
||||
handle: string;
|
||||
displayName: string;
|
||||
kind: "user" | "org";
|
||||
image?: string | null;
|
||||
bio?: string | null;
|
||||
};
|
||||
role: "owner" | "admin" | "publisher";
|
||||
}>
|
||||
| undefined;
|
||||
const createOrg = useMutation(api.publishers.createOrg);
|
||||
const updateOrgProfile = useMutation(api.publishers.updateProfile);
|
||||
const addOrgMember = useMutation(api.publishers.addMember);
|
||||
const removeOrgMember = useMutation(api.publishers.removeMember);
|
||||
const [displayName, setDisplayName] = useState("");
|
||||
@@ -105,6 +107,9 @@ export function Settings() {
|
||||
const [orgHandle, setOrgHandle] = useState("");
|
||||
const [orgDisplayName, setOrgDisplayName] = useState("");
|
||||
const [selectedOrgHandle, setSelectedOrgHandle] = useState("");
|
||||
const [selectedOrgDisplayName, setSelectedOrgDisplayName] = useState("");
|
||||
const [selectedOrgBio, setSelectedOrgBio] = useState("");
|
||||
const [selectedOrgImage, setSelectedOrgImage] = useState("");
|
||||
const [memberHandle, setMemberHandle] = useState("");
|
||||
const [memberRole, setMemberRole] = useState<"owner" | "admin" | "publisher">("publisher");
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
@@ -144,6 +149,18 @@ export function Settings() {
|
||||
}
|
||||
}, [orgs, selectedOrgHandle]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedOrg) {
|
||||
setSelectedOrgDisplayName("");
|
||||
setSelectedOrgBio("");
|
||||
setSelectedOrgImage("");
|
||||
return;
|
||||
}
|
||||
setSelectedOrgDisplayName(selectedOrg.publisher.displayName ?? "");
|
||||
setSelectedOrgBio(selectedOrg.publisher.bio ?? "");
|
||||
setSelectedOrgImage(selectedOrg.publisher.image ?? "");
|
||||
}, [selectedOrg]);
|
||||
|
||||
if (!me) {
|
||||
return (
|
||||
<Container size="narrow" className="py-10">
|
||||
@@ -205,6 +222,17 @@ export function Settings() {
|
||||
toast.success("Removed custom theme");
|
||||
}
|
||||
|
||||
async function onSaveOrgProfile() {
|
||||
if (!selectedOrg) return;
|
||||
await updateOrgProfile({
|
||||
publisherId: selectedOrg.publisher._id,
|
||||
displayName: selectedOrgDisplayName,
|
||||
bio: selectedOrgBio || undefined,
|
||||
image: selectedOrgImage || undefined,
|
||||
});
|
||||
toast.success("Organization updated");
|
||||
}
|
||||
|
||||
return (
|
||||
<Container size="narrow" className="py-10">
|
||||
<main className="flex flex-col gap-6">
|
||||
@@ -291,29 +319,6 @@ export function Settings() {
|
||||
{/* Theme Section */}
|
||||
<div className="space-y-3">
|
||||
<Label id="theme" className="text-sm font-semibold text-[color:var(--ink)]">Theme</Label>
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
{THEME_FAMILY_OPTIONS.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => setThemeFamily(option.value)}
|
||||
className="rounded-[var(--r-md)] border p-4 text-left transition-colors"
|
||||
style={{
|
||||
borderColor:
|
||||
themeFamily === option.value ? "var(--accent)" : "var(--border-ui)",
|
||||
background:
|
||||
themeFamily === option.value
|
||||
? "var(--accent-subtle)"
|
||||
: "var(--surface-muted)",
|
||||
}}
|
||||
>
|
||||
<div className="text-sm font-semibold text-[color:var(--ink)]">{option.label}</div>
|
||||
<div className="mt-1 text-xs text-[color:var(--ink-soft)]">
|
||||
{option.description}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant={themeMode === "light" ? "primary" : "ghost"}
|
||||
@@ -685,6 +690,32 @@ export function Settings() {
|
||||
<>
|
||||
<Separator className="my-2" />
|
||||
|
||||
{selectedOrg ? (
|
||||
<div className="rounded-[var(--radius-sm)] border border-[color:var(--line)] bg-[color:var(--surface-muted)] p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="h-16 w-16">
|
||||
<AvatarImage
|
||||
src={selectedOrgImage.trim() || undefined}
|
||||
alt={selectedOrgDisplayName || selectedOrg.publisher.handle}
|
||||
/>
|
||||
<AvatarFallback className="text-lg">
|
||||
{(selectedOrgDisplayName || selectedOrg.publisher.handle)
|
||||
.charAt(0)
|
||||
.toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0">
|
||||
<div className="text-base font-semibold text-[color:var(--ink)]">
|
||||
{selectedOrgDisplayName || selectedOrg.publisher.handle}
|
||||
</div>
|
||||
<div className="text-sm text-[color:var(--ink-soft)]">
|
||||
@{selectedOrg.publisher.handle}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="settings-manage-org">Manage org</Label>
|
||||
<select
|
||||
@@ -703,6 +734,39 @@ export function Settings() {
|
||||
|
||||
{selectedOrg && selectedOrg.role !== "publisher" ? (
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="settings-selected-org-display-name">Org display name</Label>
|
||||
<Input
|
||||
id="settings-selected-org-display-name"
|
||||
value={selectedOrgDisplayName}
|
||||
onChange={(event) => setSelectedOrgDisplayName(event.target.value)}
|
||||
placeholder="OpenClaw"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="settings-selected-org-image">Profile image URL</Label>
|
||||
<Input
|
||||
id="settings-selected-org-image"
|
||||
value={selectedOrgImage}
|
||||
onChange={(event) => setSelectedOrgImage(event.target.value)}
|
||||
placeholder="https://example.com/logo.png"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="settings-selected-org-bio">Org bio</Label>
|
||||
<Textarea
|
||||
id="settings-selected-org-bio"
|
||||
rows={4}
|
||||
value={selectedOrgBio}
|
||||
onChange={(event) => setSelectedOrgBio(event.target.value)}
|
||||
placeholder="Tell people what this organization publishes."
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button type="button" onClick={() => void onSaveOrgProfile()}>
|
||||
Save org profile
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="settings-add-member">Add member</Label>
|
||||
<Input
|
||||
|
||||
@@ -53,6 +53,7 @@ const SKILL_CAPABILITY_LABELS: Record<string, string> = {
|
||||
"can-make-purchases": "Payments",
|
||||
"can-sign-transactions": "Signs transactions",
|
||||
"requires-oauth-token": "OAuth",
|
||||
"requires-sensitive-credentials": "Sensitive credentials",
|
||||
"posts-externally": "External posting",
|
||||
};
|
||||
|
||||
@@ -260,10 +261,10 @@ function FilterChip({
|
||||
type="button"
|
||||
aria-pressed={active}
|
||||
onClick={onClick}
|
||||
className={`inline-flex items-center gap-1.5 rounded-[var(--radius-sm)] border border-[rgba(29,59,78,0.22)] bg-[rgba(255,255,255,0.94)] px-3.5 min-h-[36px] text-xs font-semibold transition-all duration-150 ${
|
||||
className={`inline-flex min-h-[36px] items-center gap-1.5 rounded-[var(--radius-sm)] border border-[rgba(29,59,78,0.22)] bg-[rgba(255,255,255,0.94)] px-3.5 text-xs font-semibold transition-all duration-150 dark:border-[rgba(255,255,255,0.12)] dark:bg-[rgba(14,28,37,0.84)] ${
|
||||
active
|
||||
? "border-[color:var(--accent)]/30 bg-[color:var(--accent)]/10 text-[color:var(--accent)]"
|
||||
: "text-[color:var(--ink-soft)] hover:border-[color:var(--border-ui-hover)] hover:text-[color:var(--ink)] dark:text-[rgba(245,238,232,0.88)] dark:hover:text-[rgba(245,238,232,0.96)]"
|
||||
? "border-[color:var(--accent)]/30 bg-[color:var(--accent)]/10 text-[color:var(--accent)] dark:border-[rgba(255,131,95,0.34)] dark:bg-[rgba(255,131,95,0.14)] dark:text-[#ffd5c9]"
|
||||
: "text-[color:var(--ink-soft)] hover:border-[color:var(--border-ui-hover)] hover:text-[color:var(--ink)] dark:text-[rgba(245,238,232,0.88)] dark:hover:border-[rgba(255,255,255,0.2)] dark:hover:text-[rgba(245,238,232,0.96)]"
|
||||
}`}
|
||||
>
|
||||
{active && !icon && <Check className="h-3 w-3" />}
|
||||
|
||||
@@ -27,6 +27,7 @@ const SKILL_CAPABILITY_LABELS: Record<string, string> = {
|
||||
"can-make-purchases": "payments",
|
||||
"can-sign-transactions": "signs transactions",
|
||||
"requires-oauth-token": "oauth",
|
||||
"requires-sensitive-credentials": "sensitive credentials",
|
||||
"posts-externally": "external posting",
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getUserFacingConvexError } from "../../lib/convexError";
|
||||
export async function uploadFile(uploadUrl: string, file: File) {
|
||||
const path = file.webkitRelativePath || file.name;
|
||||
const contentType =
|
||||
normalizeTextContentType(path, file.type) ?? file.type ?? "application/octet-stream";
|
||||
normalizeTextContentType(path, file.type) || file.type || "application/octet-stream";
|
||||
const response = await fetch(uploadUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": contentType },
|
||||
|
||||