mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 17:02:11 +00:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca70633b92 | ||
|
|
b8f2e4d7bf | ||
|
|
9a912ee5eb | ||
|
|
11b257a062 | ||
|
|
02e509404a | ||
|
|
2cf6182991 | ||
|
|
18bfea5035 | ||
|
|
5f93ddb390 | ||
|
|
2b552c4803 | ||
|
|
ece0b830fb | ||
|
|
57afea9de5 | ||
|
|
1e788b2e0c | ||
|
|
f025721261 | ||
|
|
c2cc61df24 | ||
|
|
7a8a3f75ce | ||
|
|
ac0bdf0375 | ||
|
|
6c7dd2ec6d | ||
|
|
c72e3007a1 | ||
|
|
b66d28a184 | ||
|
|
dddee828af | ||
|
|
106cb1896a |
@@ -21,3 +21,6 @@ todos.json
|
||||
.vscode
|
||||
.env*.local
|
||||
coverage
|
||||
playwright-report
|
||||
test-results
|
||||
.playwright
|
||||
|
||||
+3
-1
@@ -1,10 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
## 0.2.0 - 2026-01-13
|
||||
|
||||
### Added
|
||||
- Web: dynamic OG image cards for skills (name, description, version).
|
||||
- CLI: auto-scan Clawdbot skill roots (per-agent workspaces, shared skills, extraDirs).
|
||||
- CLI: add `explore` command for latest updates, with limit clamping + tests (thanks @jdrhyne, #14).
|
||||
- Web: import skills from public GitHub URLs (auto-detect `SKILL.md`, smart file selection, provenance).
|
||||
- Web/API: SoulHub (SOUL.md registry) with v1 endpoints and first-run auto-seed.
|
||||
|
||||
@@ -14,6 +15,7 @@
|
||||
- Registry: make SoulHub auto-seed idempotent and non-user-owned.
|
||||
- Registry: keep GitHub backup state + publish backups intact (thanks @joshp123, #1).
|
||||
- CLI/Registry: restore fork lineage on sync + clamp bulk list queries (thanks @joshp123, #1).
|
||||
- CLI: default workdir falls back to Clawdbot workspace (override with `--workdir` / `CLAWDHUB_WORKDIR`).
|
||||
|
||||
## 0.0.6 - 2026-01-07
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"!**/.output",
|
||||
"!**/coverage",
|
||||
"!**/convex/_generated",
|
||||
"!**/test-results",
|
||||
"!**/src/routeTree.gen.ts",
|
||||
"!**/.tanstack",
|
||||
"!**/public",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"": {
|
||||
"name": "clawdhub",
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.41.1",
|
||||
"@auth/core": "^0.34.3",
|
||||
"@convex-dev/auth": "^0.0.90",
|
||||
"@fontsource/bricolage-grotesque": "^5.2.10",
|
||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||
@@ -14,17 +14,17 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@resvg/resvg-wasm": "^2.6.2",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-devtools": "^0.9.0",
|
||||
"@tanstack/react-router": "^1.144.0",
|
||||
"@tanstack/react-router-devtools": "^1.144.0",
|
||||
"@tanstack/react-start": "^1.145.3",
|
||||
"@tanstack/router-plugin": "^1.145.2",
|
||||
"@tanstack/react-devtools": "^0.9.2",
|
||||
"@tanstack/react-router": "^1.151.3",
|
||||
"@tanstack/react-router-devtools": "^1.151.3",
|
||||
"@tanstack/react-start": "^1.151.5",
|
||||
"@tanstack/router-plugin": "^1.151.5",
|
||||
"@vercel/analytics": "^1.6.1",
|
||||
"clawdhub-schema": "^0.0.2",
|
||||
"clawdhub-schema": "workspace:*",
|
||||
"clsx": "^2.1.1",
|
||||
"convex": "^1.31.2",
|
||||
"convex": "^1.31.5",
|
||||
"fflate": "^0.8.2",
|
||||
"h3": "2.0.1-rc.5",
|
||||
"h3": "2.0.1-rc.8",
|
||||
"lucide-react": "^0.562.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"nitro": "^3.0.1-alpha.1",
|
||||
@@ -35,31 +35,32 @@
|
||||
"semver": "^7.7.3",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vite-tsconfig-paths": "^6.0.3",
|
||||
"vite-tsconfig-paths": "^6.0.4",
|
||||
"yaml": "^2.8.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.11",
|
||||
"@tanstack/devtools-vite": "^0.4.0",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@tanstack/devtools-vite": "^0.4.1",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/node": "^25.0.9",
|
||||
"@types/react": "^19.2.8",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"@vitest/coverage-v8": "^4.0.16",
|
||||
"@vitest/coverage-v8": "^4.0.17",
|
||||
"jsdom": "^27.4.0",
|
||||
"oxlint": "^1.36.0",
|
||||
"oxlint-tsgolint": "^0.10.1",
|
||||
"oxlint": "^1.39.0",
|
||||
"oxlint-tsgolint": "^0.11.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.0",
|
||||
"vitest": "^4.0.16",
|
||||
"vite": "^7.3.1",
|
||||
"vitest": "^4.0.17",
|
||||
},
|
||||
},
|
||||
"packages/clawdhub": {
|
||||
"name": "clawdhub",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"bin": {
|
||||
"clawdhub": "bin/clawdhub.js",
|
||||
},
|
||||
@@ -76,7 +77,7 @@
|
||||
"semver": "^7.7.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/node": "^25.0.9",
|
||||
"typescript": "^5.9.3",
|
||||
},
|
||||
},
|
||||
@@ -84,10 +85,10 @@
|
||||
"name": "clawdhub-schema",
|
||||
"version": "0.0.2",
|
||||
"dependencies": {
|
||||
"arktype": "^2.1.29",
|
||||
"arktype": "latest",
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3",
|
||||
"typescript": "latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -104,7 +105,7 @@
|
||||
|
||||
"@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
|
||||
|
||||
"@auth/core": ["@auth/core@0.41.1", "", { "dependencies": { "@panva/hkdf": "^1.2.1", "jose": "^6.0.6", "oauth4webapi": "^3.3.0", "preact": "10.24.3", "preact-render-to-string": "6.5.11" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^7.0.7" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-t9cJ2zNYAdWMacGRMT6+r4xr1uybIdmYa49calBPeTqwgAFPV/88ac9TEvCR85pvATiSPt8VaNf+Gt24JIT/uw=="],
|
||||
"@auth/core": ["@auth/core@0.34.3", "", { "dependencies": { "@panva/hkdf": "^1.1.1", "@types/cookie": "0.6.0", "cookie": "0.6.0", "jose": "^5.1.3", "oauth4webapi": "^2.10.4", "preact": "10.11.3", "preact-render-to-string": "5.2.3" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^7" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-jMjY/S0doZnWYNV90x0jmU3B+UcrsfGYnukxYrRbj0CVvGI/MX3JbHsxSrx2d4mbnXaUsqJmAcDfoQWA6r0lOw=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
@@ -354,36 +355,38 @@
|
||||
|
||||
"@oxc-transform/binding-win32-x64-msvc": ["@oxc-transform/binding-win32-x64-msvc@0.96.0", "", { "os": "win32", "cpu": "x64" }, "sha512-0fI0P0W7bSO/GCP/N5dkmtB9vBqCA4ggo1WmXTnxNJVmFFOtcA1vYm1I9jl8fxo+sucW2WnlpnI4fjKdo3JKxA=="],
|
||||
|
||||
"@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.10.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-KGC4++BeEqrIcmDHiJt/e6/860PWJmUJjjp0mE+smpBmRXMjmOFFjrPmN+ZyCyVgf1WdmhPkQXsRSPeTR+2omw=="],
|
||||
"@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.11.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UJIOFeJZpFTJIGS+bMdFXcvjslvnXBEouMvzynfQD7RTazcFIRLbokYgEbhrN2P6B352Ut1TUtvR0CLAp/9QfA=="],
|
||||
|
||||
"@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@0.10.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-tvmrDgj3Q0tdc+zMWfCVLVq8EQDEUqasm1zaWgSMYIszpID6qdgqbT+OpWWXV9fLZgtvrkoXGwxkHAUJzdVZXQ=="],
|
||||
"@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@0.11.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-68O8YvexIm+ISZKl2vBFII1dMfLrteDyPcuCIecDuiBIj2tV0KYq13zpSCMz4dvJUWJW6RmOOGZKrkkvOAy6uQ=="],
|
||||
|
||||
"@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@0.10.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-7kD28z6/ykGx8WetKTPRZt30pd+ziassxg/8cM24lhjUI+hNXyRHVtHes73dh9D6glJKno+1ut+3amUdZBZcpQ=="],
|
||||
"@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@0.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-hXBInrFxPNbPPbPQYozo8YpSsFFYdtHBWRUiLMxul71vTy1CdSA7H5Qq2KbrKomr/ASmhvIDVAQZxh9hIJNHMA=="],
|
||||
|
||||
"@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@0.10.1", "", { "os": "linux", "cpu": "x64" }, "sha512-NmJmiqdzYUTHIxteSTyX6IFFgnIsOAjRWXfrS6Jbo5xlB3g39WHniSF3asB/khLJNtwSg4InUS34NprYM7zrEw=="],
|
||||
"@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@0.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-aMaGctlwrJhaIQPOdVJR+AGHZGPm4D1pJ457l0SqZt4dLXAhuUt2ene6cUUGF+864R7bDyFVGZqbZHODYpENyA=="],
|
||||
|
||||
"@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@0.10.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-3KrT80vl3nXUkjuJI/z8dF6xWsKx0t9Tz4ZQHgQw3fYw+CoihBRWGklrdlmCz+EGfMyVaQLqBV9PZckhSqLe2A=="],
|
||||
"@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@0.11.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-ipOs6kKo8fz5n5LSHvcbyZFmEpEIsh2m7+B03RW3jGjBEPMiXb4PfKNuxnusFYTtJM9WaR3bCVm5UxeJTA8r3w=="],
|
||||
|
||||
"@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@0.10.1", "", { "os": "win32", "cpu": "x64" }, "sha512-hW1fSJZVxG51sLdGq1sQjOzb1tsQ23z/BquJfUwL7CqBobxr7TJvGmoINL+9KryOJt0jCoaiMfWe4yoYw5XfIA=="],
|
||||
"@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@0.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-m2apsAXg6qU3ulQG45W/qshyEpOjoL+uaQyXJG5dBoDoa66XPtCaSkBlKltD0EwGu0aoB8lM4I5I3OzQ6raNhw=="],
|
||||
|
||||
"@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.36.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MJkj82GH+nhvWKJhSIM6KlZ8tyGKdogSQXtNdpIyP02r/tTayFJQaAEWayG2Jhsn93kske+nimg5MYFhwO/rlg=="],
|
||||
"@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.39.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lT3hNhIa02xCujI6YGgjmYGg3Ht/X9ag5ipUVETaMpx5Rd4BbTNWUPif1WN1YZHxt3KLCIqaAe7zVhatv83HOQ=="],
|
||||
|
||||
"@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.36.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-VvEhfkqj/99dCTqOcfkyFXOSbx4lIy5u2m2GHbK4WCMDySokOcMTNRHGw8fH/WgQ5cDrDMSTYIGQTmnBGi9tiQ=="],
|
||||
"@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.39.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-UT+rfTWd+Yr7iJeSLd/7nF8X4gTYssKh+n77hxl6Oilp3NnG1CKRHxZDy3o3lIBnwgzJkdyUAiYWO1bTMXQ1lA=="],
|
||||
|
||||
"@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.36.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EMx92X5q+hHc3olTuj/kgkx9+yP0p/AVs4yvHbUfzZhBekXNpUWxWvg4hIKmQWn+Ee2j4o80/0ACGO0hDYJ9mg=="],
|
||||
"@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.39.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qocBkvS2V6rH0t9AT3DfQunMnj3xkM7srs5/Ycj2j5ZqMoaWd/FxHNVJDFP++35roKSvsRJoS0mtA8/77jqm6Q=="],
|
||||
|
||||
"@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.36.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-7YCxtrPIctVYLqWrWkk8pahdCxch6PtsaucfMLC7TOlDt4nODhnQd4yzEscKqJ8Gjrw1bF4g+Ngob1gB+Qr9Fw=="],
|
||||
"@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.39.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-arZzAc1PPcz9epvGBBCMHICeyQloKtHX3eoOe62B3Dskn7gf6Q14wnDHr1r9Vp4vtcBATNq6HlKV14smdlC/qA=="],
|
||||
|
||||
"@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.36.0", "", { "os": "linux", "cpu": "x64" }, "sha512-lnaJVlx5r3NWmoOMesfQXJSf78jHTn8Z+sdAf795Kgteo72+qGC1Uax2SToCJVN2J8PNG3oRV5bLriiCNR2i6Q=="],
|
||||
"@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.39.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZVt5qsECpuNprdWxAPpDBwoixr1VTcZ4qAEQA2l/wmFyVPDYFD3oBY/SWACNnWBddMrswjTg9O8ALxYWoEpmXw=="],
|
||||
|
||||
"@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.36.0", "", { "os": "linux", "cpu": "x64" }, "sha512-AhuEU2Qdl66lSfTGu/Htirq8r/8q2YnZoG3yEXLMQWnPMn7efy8spD/N1NA7kH0Hll+cdfwgQkQqC2G4MS2lPQ=="],
|
||||
"@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.39.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pB0hlGyKPbxr9NMIV783lD6cWL3MpaqnZRM9MWni4yBdHPTKyFNYdg5hGD0Bwg+UP4S2rOevq/+OO9x9Bi7E6g=="],
|
||||
|
||||
"@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.36.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-GlWCBjUJY2QgvBFuNRkiRJu7K/djLmM0UQKfZV8IN+UXbP/JbjZHWKRdd4LXlQmzoz7M5Hd6p+ElCej8/90FCg=="],
|
||||
"@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.39.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gg2SFaJohI9+tIQVKXlPw3FsPQFi/eCSWiCgwPtPn5uzQxHRTeQEZKuluz1fuzR5U70TXubb2liZi4Dgl8LJQA=="],
|
||||
|
||||
"@oxlint/win32-x64": ["@oxlint/win32-x64@1.36.0", "", { "os": "win32", "cpu": "x64" }, "sha512-J+Vc00Utcf8p77lZPruQgb0QnQXuKnFogN88kCnOqs2a83I+vTBB8ILr0+L9sTwVRvIDMSC0pLdeQH4svWGFZg=="],
|
||||
"@oxlint/win32-x64": ["@oxlint/win32-x64@1.39.0", "", { "os": "win32", "cpu": "x64" }, "sha512-sbi25lfj74hH+6qQtb7s1wEvd1j8OQbTaH8v3xTcDjrwm579Cyh0HBv1YSZ2+gsnVwfVDiCTL1D0JsNqYXszVA=="],
|
||||
|
||||
"@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="],
|
||||
|
||||
"@playwright/test": ["@playwright/test@1.57.0", "", { "dependencies": { "playwright": "1.57.0" }, "bin": { "playwright": "cli.js" } }, "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA=="],
|
||||
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||
|
||||
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.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-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
|
||||
@@ -532,7 +535,7 @@
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
|
||||
|
||||
"@tanstack/devtools": ["@tanstack/devtools@0.10.1", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/keyboard": "^1.3.3", "@solid-primitives/resize-observer": "^2.1.3", "@tanstack/devtools-client": "0.0.5", "@tanstack/devtools-event-bus": "0.4.0", "@tanstack/devtools-ui": "0.4.4", "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.9" } }, "sha512-1gtPmCDXV4Pl1nVtoqwjV0tc4E9GMuFtlkBX1Lz1KfqI3W9JojT5YsVifOQ/g8BTQ5w5+tyIANwHU7WYgLq/MQ=="],
|
||||
"@tanstack/devtools": ["@tanstack/devtools@0.10.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/keyboard": "^1.3.3", "@solid-primitives/resize-observer": "^2.1.3", "@tanstack/devtools-client": "0.0.5", "@tanstack/devtools-event-bus": "0.4.0", "@tanstack/devtools-ui": "0.4.4", "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.9" } }, "sha512-M2HnKtaNf3Z8JDTNDq+X7/1gwOqSwTnCyC0GR+TYiRZM9mkY9GpvTqp6p6bx3DT8onu2URJiVxgHD9WK2e3MNQ=="],
|
||||
|
||||
"@tanstack/devtools-client": ["@tanstack/devtools-client@0.0.5", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.0" } }, "sha512-hsNDE3iu4frt9cC2ppn1mNRnLKo2uc1/1hXAyY9z4UYb+o40M2clFAhiFoo4HngjfGJDV3x18KVVIq7W4Un+zA=="],
|
||||
|
||||
@@ -542,47 +545,47 @@
|
||||
|
||||
"@tanstack/devtools-ui": ["@tanstack/devtools-ui@0.4.4", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.9" } }, "sha512-5xHXFyX3nom0UaNfiOM92o6ziaHjGo3mcSGe2HD5Xs8dWRZNpdZ0Smd0B9ddEhy0oB+gXyMzZgUJb9DmrZV0Mg=="],
|
||||
|
||||
"@tanstack/devtools-vite": ["@tanstack/devtools-vite@0.4.0", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/generator": "^7.28.3", "@babel/parser": "^7.28.4", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@tanstack/devtools-client": "0.0.5", "@tanstack/devtools-event-bus": "0.4.0", "chalk": "^5.6.2", "launch-editor": "^2.11.1", "picomatch": "^4.0.3" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0" } }, "sha512-vZ5SsjcLSLC+lBb4N6QDJEdrsrORs8OtIcwQafexAR7aJOv6SxGNoqERujEbTzfWY+PAypa1oYxPqtEAOcitDw=="],
|
||||
"@tanstack/devtools-vite": ["@tanstack/devtools-vite@0.4.1", "", { "dependencies": { "@babel/core": "^7.28.4", "@babel/generator": "^7.28.3", "@babel/parser": "^7.28.4", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@tanstack/devtools-client": "0.0.5", "@tanstack/devtools-event-bus": "0.4.0", "chalk": "^5.6.2", "launch-editor": "^2.11.1", "picomatch": "^4.0.3" }, "peerDependencies": { "vite": "^6.0.0 || ^7.0.0" } }, "sha512-PkMOomcWnl/pUkCqIjqL/csjPHtkMVBirDpJVOZR7XJZDxo5CuD7B+3KsujFCF4Dsn6QYlae97gCZvxi/CB76Q=="],
|
||||
|
||||
"@tanstack/history": ["@tanstack/history@1.141.0", "", {}, "sha512-LS54XNyxyTs5m/pl1lkwlg7uZM3lvsv2FIIV1rsJgnfwVCnI+n4ZGZ2CcjNT13BPu/3hPP+iHmliBSscJxW5FQ=="],
|
||||
"@tanstack/history": ["@tanstack/history@1.151.1", "", {}, "sha512-Z/eymNBuUGHYIea7nNX3xR5feqx418ChlwWOKklVpCVzEQ5Q3kNTUw+WK4HYUKxF+1uXFN01Dbuhhl7SmW1LJA=="],
|
||||
|
||||
"@tanstack/react-devtools": ["@tanstack/react-devtools@0.9.0", "", { "dependencies": { "@tanstack/devtools": "0.10.1" }, "peerDependencies": { "@types/react": ">=16.8", "@types/react-dom": ">=16.8", "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-Lq0svXOTG5N61SHgx8F0on6zz2GB0kmFjN/yyfNLrJyRgJ+U3jYFRd9ti3uBPABsXzHQMHYYujnTXrOYp/OaUg=="],
|
||||
"@tanstack/react-devtools": ["@tanstack/react-devtools@0.9.2", "", { "dependencies": { "@tanstack/devtools": "0.10.3" }, "peerDependencies": { "@types/react": ">=16.8", "@types/react-dom": ">=16.8", "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-JNXvBO3jgq16GzTVm7p65n5zHNfMhnqF6Bm7CawjoqZrjEakxbM6Yvy63aKSIpbrdf+Wun2Xn8P0qD+vp56e1g=="],
|
||||
|
||||
"@tanstack/react-router": ["@tanstack/react-router@1.144.0", "", { "dependencies": { "@tanstack/history": "1.141.0", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.144.0", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-GmRyIGmHtGj3VLTHXepIwXAxTcHyL5W7Vw7O1CnVEtFxQQWKMVOnWgI7tPY6FhlNwMKVb3n0mPFWz9KMYyd2GA=="],
|
||||
"@tanstack/react-router": ["@tanstack/react-router@1.151.3", "", { "dependencies": { "@tanstack/history": "1.151.1", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.151.3", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-Xb8444OgVpmwCl9hpUA+N/X0dsik/WWYWdldIYNAcmBAl1ZYYXm8kKnfjgHS2aMZzPnqN1BjKocKgc5jg3lK9Q=="],
|
||||
|
||||
"@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.144.0", "", { "dependencies": { "@tanstack/router-devtools-core": "1.144.0" }, "peerDependencies": { "@tanstack/react-router": "^1.144.0", "@tanstack/router-core": "^1.144.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-nstjZvZbOM4U0/Hzi82rtsP1DsR2tfigBidK+WuaDRVVstBsnwVor3DQXTGY5CcfgIiMI3eKzI17VOy3SQDDoQ=="],
|
||||
"@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.151.3", "", { "dependencies": { "@tanstack/router-devtools-core": "1.151.3" }, "peerDependencies": { "@tanstack/react-router": "^1.151.3", "@tanstack/router-core": "^1.151.3", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-WQ8cA4BkDBIglieexdHFvHbhvZOdtgCWO5BDR58MQoz+ZQwsWo9R0m6t6yVOQzkPi69t6Og9s1NogE4BF0/NYA=="],
|
||||
|
||||
"@tanstack/react-start": ["@tanstack/react-start@1.145.3", "", { "dependencies": { "@tanstack/react-router": "1.144.0", "@tanstack/react-start-client": "1.145.0", "@tanstack/react-start-server": "1.145.3", "@tanstack/router-utils": "^1.143.11", "@tanstack/start-client-core": "1.145.0", "@tanstack/start-plugin-core": "1.145.3", "@tanstack/start-server-core": "1.145.3", "pathe": "^2.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" } }, "sha512-ZRd0VbcpPSmYTGdR7PF5LdyPnB7rd4zfyuf8bjtUbjphh4P0wjE3DUTA7Mk29RMvvo6sS7Advjsax9ZqEevLgg=="],
|
||||
"@tanstack/react-start": ["@tanstack/react-start@1.151.5", "", { "dependencies": { "@tanstack/react-router": "1.151.3", "@tanstack/react-start-client": "1.151.3", "@tanstack/react-start-server": "1.151.3", "@tanstack/router-utils": "^1.143.11", "@tanstack/start-client-core": "1.151.3", "@tanstack/start-plugin-core": "1.151.5", "@tanstack/start-server-core": "1.151.3", "pathe": "^2.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" } }, "sha512-V3di0M9L6Y/dGCTJrqjTWvxHPxXkSnT2+Dt1ffmVSixlO0HiUDrc1BfEZb3avERPkJ0SL8zxSz6xFYsQZew/yg=="],
|
||||
|
||||
"@tanstack/react-start-client": ["@tanstack/react-start-client@1.145.0", "", { "dependencies": { "@tanstack/react-router": "1.144.0", "@tanstack/router-core": "1.144.0", "@tanstack/start-client-core": "1.145.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-UC/+ONaOzuFnlHbOEudYS+AHOrcwAJaqbnfh9zZ5pUtTkJToBawW3YabDbMnS3o6lEiKggc8uGpsiCglUJrBcA=="],
|
||||
"@tanstack/react-start-client": ["@tanstack/react-start-client@1.151.3", "", { "dependencies": { "@tanstack/react-router": "1.151.3", "@tanstack/router-core": "1.151.3", "@tanstack/start-client-core": "1.151.3", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-oW6hyCnetWu5dpPROUUxoX3Rpvc1Y7bEwqj3zfHCzGBQ0CsQqryjvtWifd9IBIVYW6W4qKD9ScLyzoLl7uw+ww=="],
|
||||
|
||||
"@tanstack/react-start-server": ["@tanstack/react-start-server@1.145.3", "", { "dependencies": { "@tanstack/history": "1.141.0", "@tanstack/react-router": "1.144.0", "@tanstack/router-core": "1.144.0", "@tanstack/start-client-core": "1.145.0", "@tanstack/start-server-core": "1.145.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-HHFq8KTUUsgjifNpYfU7o1jJaVmrwhrjtqQuabGiRseaeIRd4qIGsIS6M1bmOM4+5sYZLKm+lkP6oxgOBuvvaQ=="],
|
||||
"@tanstack/react-start-server": ["@tanstack/react-start-server@1.151.3", "", { "dependencies": { "@tanstack/history": "1.151.1", "@tanstack/react-router": "1.151.3", "@tanstack/router-core": "1.151.3", "@tanstack/start-client-core": "1.151.3", "@tanstack/start-server-core": "1.151.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-Zdlw5gyNkffd52E6u8IFCNu/xwUFfuLnQId3DIkGzPBFi9rg5JfcRXANbjUL0SrnM3+nksyhnbnluYTlCrEmvg=="],
|
||||
|
||||
"@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "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-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="],
|
||||
|
||||
"@tanstack/router-core": ["@tanstack/router-core@1.144.0", "", { "dependencies": { "@tanstack/history": "1.141.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.1", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-6oVERtK9XDHCP4XojgHsdHO56ZSj11YaWjF5g/zw39LhyA6Lx+/X86AEIHO4y0BUrMQaJfcjdAQMVSAs6Vjtdg=="],
|
||||
"@tanstack/router-core": ["@tanstack/router-core@1.151.3", "", { "dependencies": { "@tanstack/history": "1.151.1", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.1", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-JVaNgR/4xl6/5BhVVZCnaZrN+VgpIaHVIWnMiC8dgpZZAI+FfIUOXoTQ+95167cp1igjR8pKDAKEX+gDgD3yKw=="],
|
||||
|
||||
"@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.144.0", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.144.0", "csstype": "^3.0.10", "solid-js": ">=1.9.5" }, "optionalPeers": ["csstype"] }, "sha512-rbpQn1aHUtcfY3U3SyJqOZRqDu0a2uPK+TE2CH50HieJApmCuNKj5RsjVQYHgwiFFvR0w0LUmueTnl2X2hiWTg=="],
|
||||
"@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.151.3", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.151.3", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-C4ZjhYz3ASkromu0PkXvr6MVwHWsipfQ3fttPODMgjQ1Crvulixmd6yaeZjPoqYXWWTFw75qfiY1Fmh89mD+oQ=="],
|
||||
|
||||
"@tanstack/router-generator": ["@tanstack/router-generator@1.145.2", "", { "dependencies": { "@tanstack/router-core": "1.144.0", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.141.0", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-6DLwfqhexgxw2T2QuS9Y349Vb49hCXBIz9mjWyynjMrpejLXJL+PaHaKJw0Y+H7Ao6RE2vlvXCc2cMjgbz5c7Q=="],
|
||||
"@tanstack/router-generator": ["@tanstack/router-generator@1.151.5", "", { "dependencies": { "@tanstack/router-core": "1.151.3", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.145.4", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-YxXCVil0hMBwSYHYL1kGY9ENI0mi3FUDTqVty8XudddT3Jvx21lW94BAUU56VZAp7kuDIVpn8nsyk++ogDJgOg=="],
|
||||
|
||||
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.145.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.144.0", "@tanstack/router-generator": "1.145.2", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.141.0", "babel-dead-code-elimination": "^1.0.11", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.144.0", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-dOABjCE4M2KxB+f/mY71dDZduwVTpf+tCPb4NxmAqbF5Rxes24QaaIZQmiU12jte/L8zYyIA/yX9fi93xZue5Q=="],
|
||||
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.151.5", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.151.3", "@tanstack/router-generator": "1.151.5", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.145.4", "babel-dead-code-elimination": "^1.0.11", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.151.3", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-l+4jmNACXHDamjbP/bFcOLheqzzqGCpEMjMS/w+BqBEYIu0W+Jqwj8wOwMaEBcuk/Vhnvmkp+WN1RYdM6+GVLg=="],
|
||||
|
||||
"@tanstack/router-utils": ["@tanstack/router-utils@1.143.11", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-N24G4LpfyK8dOlnP8BvNdkuxg1xQljkyl6PcrdiPSA301pOjatRT1y8wuCCJZKVVD8gkd0MpCZ0VEjRMGILOtA=="],
|
||||
|
||||
"@tanstack/start-client-core": ["@tanstack/start-client-core@1.145.0", "", { "dependencies": { "@tanstack/router-core": "1.144.0", "@tanstack/start-fn-stubs": "1.143.8", "@tanstack/start-storage-context": "1.144.0", "seroval": "^1.4.1", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-pqINeN7ZqdfTZrkhy9C7isxRr8U3cByH5ZtLVnUxJp9fvLgwX7LlI+OWpGI0q3E8f/mHMUqJdeE56+atSs8Khw=="],
|
||||
"@tanstack/start-client-core": ["@tanstack/start-client-core@1.151.3", "", { "dependencies": { "@tanstack/router-core": "1.151.3", "@tanstack/start-fn-stubs": "1.151.3", "@tanstack/start-storage-context": "1.151.3", "seroval": "^1.4.1", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-44i4lPsY7rmQYY9kZYicc+x+/zDXkDGEn6qNVOA0Q4O6evNXr92eXPVBAYFly1y7m+wlFgeFWddBXbW08gUA7g=="],
|
||||
|
||||
"@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.143.8", "", {}, "sha512-2IKUPh/TlxwzwHMiHNeFw95+L2sD4M03Es27SxMR0A60Qc4WclpaD6gpC8FsbuNASM2jBxk2UyeYClJxW1GOAQ=="],
|
||||
"@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.151.3", "", {}, "sha512-/zWBnfsOwact936Bn0CxigudU1QRZdiNTsK7ME/LMXXA66XsDxkryX5+5FeGwU5ETNPfLAx6pRUet1mtUKnLCg=="],
|
||||
|
||||
"@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.145.3", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@rolldown/pluginutils": "1.0.0-beta.40", "@tanstack/router-core": "1.144.0", "@tanstack/router-generator": "1.145.2", "@tanstack/router-plugin": "1.145.2", "@tanstack/router-utils": "1.143.11", "@tanstack/start-client-core": "1.145.0", "@tanstack/start-server-core": "1.145.3", "babel-dead-code-elimination": "^1.0.11", "cheerio": "^1.0.0", "exsolve": "^1.0.7", "pathe": "^2.0.3", "srvx": "^0.10.0", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^3.24.2" }, "peerDependencies": { "vite": ">=7.0.0" } }, "sha512-PUWKI/8OMyvq8Yjn8ccbEwenASBs5YPEHpXmUjeZ0qb8REGJ6v71Twlqtuva6/fBqZrAKl+2CZrWjgbYZr/h8g=="],
|
||||
"@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.151.5", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@rolldown/pluginutils": "1.0.0-beta.40", "@tanstack/router-core": "1.151.3", "@tanstack/router-generator": "1.151.5", "@tanstack/router-plugin": "1.151.5", "@tanstack/router-utils": "1.143.11", "@tanstack/start-client-core": "1.151.3", "@tanstack/start-server-core": "1.151.3", "babel-dead-code-elimination": "^1.0.11", "cheerio": "^1.0.0", "exsolve": "^1.0.7", "pathe": "^2.0.3", "srvx": "^0.10.0", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^3.24.2" }, "peerDependencies": { "vite": ">=7.0.0" } }, "sha512-IA3XJ5jzJu2JcLFCTDgEqsR8SgsX1drJKZbvVgoYk6RS3scBtIs7PaBlJ/hn61TvYUx14UTQsJzFzxU6DRH1fQ=="],
|
||||
|
||||
"@tanstack/start-server-core": ["@tanstack/start-server-core@1.145.3", "", { "dependencies": { "@tanstack/history": "1.141.0", "@tanstack/router-core": "1.144.0", "@tanstack/start-client-core": "1.145.0", "@tanstack/start-storage-context": "1.144.0", "h3-v2": "npm:h3@2.0.1-rc.7", "seroval": "^1.4.1", "tiny-invariant": "^1.3.3" } }, "sha512-atsi0fyzymG9BRDJL4kb0oJjhCdB+Wqds+OGPDiWj5VOteCXLpop0ulDlak6wNL2QJZbqqv5BgtGbTQ6rlNyJg=="],
|
||||
"@tanstack/start-server-core": ["@tanstack/start-server-core@1.151.3", "", { "dependencies": { "@tanstack/history": "1.151.1", "@tanstack/router-core": "1.151.3", "@tanstack/start-client-core": "1.151.3", "@tanstack/start-storage-context": "1.151.3", "h3-v2": "npm:h3@2.0.1-rc.7", "seroval": "^1.4.1", "tiny-invariant": "^1.3.3" } }, "sha512-Hg+VwS2PKkx7+EW9MGZeT3arY7K+JUKvjhuQ/Ugs05f8x/dks3cn91aK5DYkPN0YRMpTNbDwZJX/DePBSnse8w=="],
|
||||
|
||||
"@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.144.0", "", { "dependencies": { "@tanstack/router-core": "1.144.0" } }, "sha512-DuUx5CXfLNettyJlsHDQp66y5haeqzXJkUor7kp5p10SVv24p76dTYqBOpw+wQz//RfJlOciIZFVBcKezXXY0w=="],
|
||||
"@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.151.3", "", { "dependencies": { "@tanstack/router-core": "1.151.3" } }, "sha512-3gUiBFfcV/OYbdwex6Dl4BWY2oTHy46fDF9CzOzTpujDcz3hI7pqWs68sxatChdyoQVMZR2Xf8os4VRLMgF3ig=="],
|
||||
|
||||
"@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="],
|
||||
|
||||
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.141.0", "", {}, "sha512-CJrWtr6L9TVzEImm9S7dQINx+xJcYP/aDkIi6gnaWtIgbZs1pnzsE0yJc2noqXZ+yAOqLx3TBGpBEs9tS0P9/A=="],
|
||||
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.145.4", "", {}, "sha512-CI75JrfqSluhdGwLssgVeQBaCphgfkMQpi8MCY3UJX1hoGzXa8kHYJcUuIFMOLs1q7zqHy++EVVtMK03osR5wQ=="],
|
||||
|
||||
"@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
|
||||
|
||||
@@ -602,6 +605,8 @@
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||
|
||||
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||
|
||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||
@@ -616,9 +621,9 @@
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
||||
"@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
|
||||
"@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
@@ -634,21 +639,21 @@
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="],
|
||||
|
||||
"@vitest/coverage-v8": ["@vitest/coverage-v8@4.0.16", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.0.16", "ast-v8-to-istanbul": "^0.3.8", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", "obug": "^2.1.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.0.16", "vitest": "4.0.16" }, "optionalPeers": ["@vitest/browser"] }, "sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A=="],
|
||||
"@vitest/coverage-v8": ["@vitest/coverage-v8@4.0.17", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.0.17", "ast-v8-to-istanbul": "^0.3.10", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", "obug": "^2.1.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.0.17", "vitest": "4.0.17" }, "optionalPeers": ["@vitest/browser"] }, "sha512-/6zU2FLGg0jsd+ePZcwHRy3+WpNTBBhDY56P4JTRqUN/Dp6CvOEa9HrikcQ4KfV2b2kAHUFB4dl1SuocWXSFEw=="],
|
||||
|
||||
"@vitest/expect": ["@vitest/expect@4.0.16", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA=="],
|
||||
"@vitest/expect": ["@vitest/expect@4.0.17", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.17", "@vitest/utils": "4.0.17", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-mEoqP3RqhKlbmUmntNDDCJeTDavDR+fVYkSOw8qRwJFaW/0/5zA9zFeTrHqNtcmwh6j26yMmwx2PqUDPzt5ZAQ=="],
|
||||
|
||||
"@vitest/mocker": ["@vitest/mocker@4.0.16", "", { "dependencies": { "@vitest/spy": "4.0.16", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg=="],
|
||||
"@vitest/mocker": ["@vitest/mocker@4.0.17", "", { "dependencies": { "@vitest/spy": "4.0.17", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-+ZtQhLA3lDh1tI2wxe3yMsGzbp7uuJSWBM1iTIKCbppWTSBN09PUC+L+fyNlQApQoR+Ps8twt2pbSSXg2fQVEQ=="],
|
||||
|
||||
"@vitest/pretty-format": ["@vitest/pretty-format@4.0.16", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA=="],
|
||||
"@vitest/pretty-format": ["@vitest/pretty-format@4.0.17", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw=="],
|
||||
|
||||
"@vitest/runner": ["@vitest/runner@4.0.16", "", { "dependencies": { "@vitest/utils": "4.0.16", "pathe": "^2.0.3" } }, "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q=="],
|
||||
"@vitest/runner": ["@vitest/runner@4.0.17", "", { "dependencies": { "@vitest/utils": "4.0.17", "pathe": "^2.0.3" } }, "sha512-JmuQyf8aMWoo/LmNFppdpkfRVHJcsgzkbCA+/Bk7VfNH7RE6Ut2qxegeyx2j3ojtJtKIbIGy3h+KxGfYfk28YQ=="],
|
||||
|
||||
"@vitest/snapshot": ["@vitest/snapshot@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA=="],
|
||||
"@vitest/snapshot": ["@vitest/snapshot@4.0.17", "", { "dependencies": { "@vitest/pretty-format": "4.0.17", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-npPelD7oyL+YQM2gbIYvlavlMVWUfNNGZPcu0aEUQXt7FXTuqhmgiYupPnAanhKvyP6Srs2pIbWo30K0RbDtRQ=="],
|
||||
|
||||
"@vitest/spy": ["@vitest/spy@4.0.16", "", {}, "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw=="],
|
||||
"@vitest/spy": ["@vitest/spy@4.0.17", "", {}, "sha512-I1bQo8QaP6tZlTomQNWKJE6ym4SHf3oLS7ceNjozxxgzavRAgZDc06T7kD8gb9bXKEgcLNt00Z+kZO6KaJ62Ew=="],
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="],
|
||||
"@vitest/utils": ["@vitest/utils@4.0.17", "", { "dependencies": { "@vitest/pretty-format": "4.0.17", "tinyrainbow": "^3.0.3" } }, "sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
@@ -734,9 +739,9 @@
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"convex": ["convex@1.31.2", "", { "dependencies": { "esbuild": "0.25.4", "prettier": "^3.0.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-RFuJOwlL2bM5X63egvBI5ZZZH6wESREpAbHsLjODxzDeJuewTLKrEnbvHV/NWp1uJYpgEFJziuGHmZ0tnAmmJg=="],
|
||||
"convex": ["convex@1.31.5", "", { "dependencies": { "esbuild": "0.27.0", "prettier": "^3.0.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-E1IuJKFwMCHDToNGukBPs6c7RFaarR3t8chLF9n98TM5/Tgmj8lM6l7sKM1aJ3VwqGaB4wbeUAPY8osbCOXBhQ=="],
|
||||
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
||||
|
||||
"cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="],
|
||||
|
||||
@@ -836,7 +841,7 @@
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="],
|
||||
"h3": ["h3@2.0.1-rc.8", "", { "dependencies": { "rou3": "^0.7.12", "srvx": "^0.10.0" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-IIMQG7qnXx1Ls75suuMHH4xtcvTFxsUguDIZB+dgdYr1RftLj59FkeWF1dOr+jnejDs8Eo+ZKV1CMqogFeqGRQ=="],
|
||||
|
||||
"h3-v2": ["h3@2.0.1-rc.7", "", { "dependencies": { "rou3": "^0.7.12", "srvx": "^0.10.0" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qbrRu1OLXmUYnysWOCVrYhtC/m8ZuXu/zCbo3U/KyphJxbPFiC76jHYwVrmEcss9uNAHO5BoUguQ46yEpgI2PA=="],
|
||||
|
||||
@@ -896,13 +901,11 @@
|
||||
|
||||
"istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="],
|
||||
|
||||
"istanbul-lib-source-maps": ["istanbul-lib-source-maps@5.0.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0" } }, "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A=="],
|
||||
|
||||
"istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="],
|
||||
|
||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||
"jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
@@ -1072,7 +1075,7 @@
|
||||
|
||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||
|
||||
"oauth4webapi": ["oauth4webapi@3.8.3", "", {}, "sha512-pQ5BsX3QRTgnt5HxgHwgunIRaDXBdkT23tf8dfzmtTIL2LTpdmxgbpbBm0VgFWAIDlezQvQCTgnVIUmHupXHxw=="],
|
||||
"oauth4webapi": ["oauth4webapi@2.17.0", "", {}, "sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ=="],
|
||||
|
||||
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||
|
||||
@@ -1088,9 +1091,9 @@
|
||||
|
||||
"oxc-transform": ["oxc-transform@0.96.0", "", { "optionalDependencies": { "@oxc-transform/binding-android-arm64": "0.96.0", "@oxc-transform/binding-darwin-arm64": "0.96.0", "@oxc-transform/binding-darwin-x64": "0.96.0", "@oxc-transform/binding-freebsd-x64": "0.96.0", "@oxc-transform/binding-linux-arm-gnueabihf": "0.96.0", "@oxc-transform/binding-linux-arm-musleabihf": "0.96.0", "@oxc-transform/binding-linux-arm64-gnu": "0.96.0", "@oxc-transform/binding-linux-arm64-musl": "0.96.0", "@oxc-transform/binding-linux-riscv64-gnu": "0.96.0", "@oxc-transform/binding-linux-s390x-gnu": "0.96.0", "@oxc-transform/binding-linux-x64-gnu": "0.96.0", "@oxc-transform/binding-linux-x64-musl": "0.96.0", "@oxc-transform/binding-wasm32-wasi": "0.96.0", "@oxc-transform/binding-win32-arm64-msvc": "0.96.0", "@oxc-transform/binding-win32-x64-msvc": "0.96.0" } }, "sha512-dQPNIF+gHpSkmC0+Vg9IktNyhcn28Y8R3eTLyzn52UNymkasLicl3sFAtz7oEVuFmCpgGjaUTKkwk+jW2cHpDQ=="],
|
||||
|
||||
"oxlint": ["oxlint@1.36.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.36.0", "@oxlint/darwin-x64": "1.36.0", "@oxlint/linux-arm64-gnu": "1.36.0", "@oxlint/linux-arm64-musl": "1.36.0", "@oxlint/linux-x64-gnu": "1.36.0", "@oxlint/linux-x64-musl": "1.36.0", "@oxlint/win32-arm64": "1.36.0", "@oxlint/win32-x64": "1.36.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.10.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxc_language_server": "bin/oxc_language_server", "oxlint": "bin/oxlint" } }, "sha512-IicUdXfXgI8OKrDPnoSjvBfeEF8PkKtm+CoLlg4LYe4ypc8U+T4r7730XYshdBGZdelg+JRw8GtCb2w/KaaZvw=="],
|
||||
"oxlint": ["oxlint@1.39.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.39.0", "@oxlint/darwin-x64": "1.39.0", "@oxlint/linux-arm64-gnu": "1.39.0", "@oxlint/linux-arm64-musl": "1.39.0", "@oxlint/linux-x64-gnu": "1.39.0", "@oxlint/linux-x64-musl": "1.39.0", "@oxlint/win32-arm64": "1.39.0", "@oxlint/win32-x64": "1.39.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.10.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-wSiLr0wjG+KTU6c1LpVoQk7JZ7l8HCKlAkVDVTJKWmCGazsNxexxnOXl7dsar92mQcRnzko5g077ggP3RINSjA=="],
|
||||
|
||||
"oxlint-tsgolint": ["oxlint-tsgolint@0.10.1", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.10.1", "@oxlint-tsgolint/darwin-x64": "0.10.1", "@oxlint-tsgolint/linux-arm64": "0.10.1", "@oxlint-tsgolint/linux-x64": "0.10.1", "@oxlint-tsgolint/win32-arm64": "0.10.1", "@oxlint-tsgolint/win32-x64": "0.10.1" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-EEHNdo5cW2w1xwYdBQ7d3IXDqWAtMkfVFrh+9gQ4kYbYJwygY4QXSh1eH80/xVipZdVKujAwBgg/nNNHk56kxQ=="],
|
||||
"oxlint-tsgolint": ["oxlint-tsgolint@0.11.1", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.11.1", "@oxlint-tsgolint/darwin-x64": "0.11.1", "@oxlint-tsgolint/linux-arm64": "0.11.1", "@oxlint-tsgolint/linux-x64": "0.11.1", "@oxlint-tsgolint/win32-arm64": "0.11.1", "@oxlint-tsgolint/win32-x64": "0.11.1" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-WulCp+0/6RvpM4zPv+dAXybf03QvRA8ATxaBlmj4XMIQqTs5jeq3cUTk48WCt4CpLwKhyyGZPHmjLl1KHQ/cvA=="],
|
||||
|
||||
"p-retry": ["p-retry@7.1.1", "", { "dependencies": { "is-network-error": "^1.1.0" } }, "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w=="],
|
||||
|
||||
@@ -1110,11 +1113,15 @@
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"playwright": ["playwright@1.57.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="],
|
||||
|
||||
"playwright-core": ["playwright-core@1.57.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"preact": ["preact@10.24.3", "", {}, "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA=="],
|
||||
"preact": ["preact@10.11.3", "", {}, "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg=="],
|
||||
|
||||
"preact-render-to-string": ["preact-render-to-string@6.5.11", "", { "peerDependencies": { "preact": ">=10" } }, "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw=="],
|
||||
"preact-render-to-string": ["preact-render-to-string@5.2.3", "", { "dependencies": { "pretty-format": "^3.8.0" }, "peerDependencies": { "preact": ">=10" } }, "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA=="],
|
||||
|
||||
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
|
||||
|
||||
@@ -1192,7 +1199,7 @@
|
||||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"srvx": ["srvx@0.9.8", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ=="],
|
||||
"srvx": ["srvx@0.10.0", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-NqIsR+wQCfkvvwczBh8J8uM4wTZx41K2lLSEp/3oMp917ODVVMtW5Me4epCmQ3gH8D+0b+/t4xxkUKutyhimTA=="],
|
||||
|
||||
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||
|
||||
@@ -1292,13 +1299,13 @@
|
||||
|
||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||
|
||||
"vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="],
|
||||
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||
|
||||
"vite-tsconfig-paths": ["vite-tsconfig-paths@6.0.3", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-7bL7FPX/DSviaZGYUKowWF1AiDVWjMjxNbE8lyaVGDezkedWqfGhlnQ4BZXre0ZN5P4kAgIJfAlgFDVyjrCIyg=="],
|
||||
"vite-tsconfig-paths": ["vite-tsconfig-paths@6.0.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3", "vite": "*" } }, "sha512-iIsEJ+ek5KqRTK17pmxtgIxXtqr3qDdE6OxrP9mVeGhVDNXRJTKN/l9oMbujTQNzMLe6XZ8qmpztfbkPu2TiFQ=="],
|
||||
|
||||
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
|
||||
|
||||
"vitest": ["vitest@4.0.16", "", { "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", "@vitest/pretty-format": "4.0.16", "@vitest/runner": "4.0.16", "@vitest/snapshot": "4.0.16", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.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.0.16", "@vitest/browser-preview": "4.0.16", "@vitest/browser-webdriverio": "4.0.16", "@vitest/ui": "4.0.16", "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-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q=="],
|
||||
"vitest": ["vitest@4.0.17", "", { "dependencies": { "@vitest/expect": "4.0.17", "@vitest/mocker": "4.0.17", "@vitest/pretty-format": "4.0.17", "@vitest/runner": "4.0.17", "@vitest/snapshot": "4.0.17", "@vitest/spy": "4.0.17", "@vitest/utils": "4.0.17", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.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.0.17", "@vitest/browser-preview": "4.0.17", "@vitest/browser-webdriverio": "4.0.17", "@vitest/ui": "4.0.17", "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-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg=="],
|
||||
|
||||
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
|
||||
|
||||
@@ -1338,7 +1345,9 @@
|
||||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@convex-dev/auth/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
|
||||
"@convex-dev/auth/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"@convex-dev/auth/oauth4webapi": ["oauth4webapi@3.8.3", "", {}, "sha512-pQ5BsX3QRTgnt5HxgHwgunIRaDXBdkT23tf8dfzmtTIL2LTpdmxgbpbBm0VgFWAIDlezQvQCTgnVIUmHupXHxw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-ryJnSmj4UhrGLZZPJ6PKVb4wNPAIkW6iyLy+0TRwazd3L1u0wzMe8RfqevAh2HbcSkoeLiSYnOVDOys4JSGYyg=="],
|
||||
|
||||
@@ -1354,19 +1363,19 @@
|
||||
|
||||
"@tanstack/start-plugin-core/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.40", "", {}, "sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w=="],
|
||||
|
||||
"@tanstack/start-plugin-core/srvx": ["srvx@0.10.0", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-NqIsR+wQCfkvvwczBh8J8uM4wTZx41K2lLSEp/3oMp917ODVVMtW5Me4epCmQ3gH8D+0b+/t4xxkUKutyhimTA=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"ast-v8-to-istanbul/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||
|
||||
"cheerio/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
|
||||
"convex/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
|
||||
"convex/esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="],
|
||||
|
||||
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"h3-v2/srvx": ["srvx@0.10.0", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-NqIsR+wQCfkvvwczBh8J8uM4wTZx41K2lLSEp/3oMp917ODVVMtW5Me4epCmQ3gH8D+0b+/t4xxkUKutyhimTA=="],
|
||||
"nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="],
|
||||
|
||||
"nitro/srvx": ["srvx@0.9.8", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
@@ -1374,6 +1383,10 @@
|
||||
|
||||
"parse5-parser-stream/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
|
||||
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||
|
||||
"preact-render-to-string/pretty-format": ["pretty-format@3.8.0", "", {}, "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="],
|
||||
|
||||
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
@@ -1384,54 +1397,56 @@
|
||||
|
||||
"strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"convex/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
|
||||
"convex/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="],
|
||||
|
||||
"convex/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
|
||||
"convex/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="],
|
||||
|
||||
"convex/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="],
|
||||
"convex/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ=="],
|
||||
|
||||
"convex/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="],
|
||||
"convex/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.0", "", { "os": "android", "cpu": "x64" }, "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q=="],
|
||||
|
||||
"convex/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="],
|
||||
"convex/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg=="],
|
||||
|
||||
"convex/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="],
|
||||
"convex/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g=="],
|
||||
|
||||
"convex/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="],
|
||||
"convex/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw=="],
|
||||
|
||||
"convex/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="],
|
||||
"convex/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g=="],
|
||||
|
||||
"convex/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="],
|
||||
"convex/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ=="],
|
||||
|
||||
"convex/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="],
|
||||
"convex/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ=="],
|
||||
|
||||
"convex/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="],
|
||||
"convex/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw=="],
|
||||
|
||||
"convex/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="],
|
||||
"convex/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg=="],
|
||||
|
||||
"convex/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="],
|
||||
"convex/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg=="],
|
||||
|
||||
"convex/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="],
|
||||
"convex/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA=="],
|
||||
|
||||
"convex/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="],
|
||||
"convex/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ=="],
|
||||
|
||||
"convex/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="],
|
||||
"convex/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w=="],
|
||||
|
||||
"convex/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="],
|
||||
"convex/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw=="],
|
||||
|
||||
"convex/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="],
|
||||
"convex/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w=="],
|
||||
|
||||
"convex/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="],
|
||||
"convex/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.0", "", { "os": "none", "cpu": "x64" }, "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA=="],
|
||||
|
||||
"convex/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="],
|
||||
"convex/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ=="],
|
||||
|
||||
"convex/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="],
|
||||
"convex/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A=="],
|
||||
|
||||
"convex/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="],
|
||||
"convex/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA=="],
|
||||
|
||||
"convex/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="],
|
||||
"convex/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA=="],
|
||||
|
||||
"convex/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="],
|
||||
"convex/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg=="],
|
||||
|
||||
"convex/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
|
||||
"convex/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ=="],
|
||||
|
||||
"convex/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="],
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+4
@@ -28,6 +28,7 @@ import type * as lib_embeddings from "../lib/embeddings.js";
|
||||
import type * as lib_githubBackup from "../lib/githubBackup.js";
|
||||
import type * as lib_githubImport from "../lib/githubImport.js";
|
||||
import type * as lib_githubSoulBackup from "../lib/githubSoulBackup.js";
|
||||
import type * as lib_searchText from "../lib/searchText.js";
|
||||
import type * as lib_skillBackfill from "../lib/skillBackfill.js";
|
||||
import type * as lib_skillPublish from "../lib/skillPublish.js";
|
||||
import type * as lib_skills from "../lib/skills.js";
|
||||
@@ -39,6 +40,7 @@ import type * as maintenance from "../maintenance.js";
|
||||
import type * as rateLimits from "../rateLimits.js";
|
||||
import type * as search from "../search.js";
|
||||
import type * as seed from "../seed.js";
|
||||
import type * as seedSouls from "../seedSouls.js";
|
||||
import type * as skills from "../skills.js";
|
||||
import type * as soulComments from "../soulComments.js";
|
||||
import type * as soulDownloads from "../soulDownloads.js";
|
||||
@@ -78,6 +80,7 @@ declare const fullApi: ApiFromModules<{
|
||||
"lib/githubBackup": typeof lib_githubBackup;
|
||||
"lib/githubImport": typeof lib_githubImport;
|
||||
"lib/githubSoulBackup": typeof lib_githubSoulBackup;
|
||||
"lib/searchText": typeof lib_searchText;
|
||||
"lib/skillBackfill": typeof lib_skillBackfill;
|
||||
"lib/skillPublish": typeof lib_skillPublish;
|
||||
"lib/skills": typeof lib_skills;
|
||||
@@ -89,6 +92,7 @@ declare const fullApi: ApiFromModules<{
|
||||
rateLimits: typeof rateLimits;
|
||||
search: typeof search;
|
||||
seed: typeof seed;
|
||||
seedSouls: typeof seedSouls;
|
||||
skills: typeof skills;
|
||||
soulComments: typeof soulComments;
|
||||
soulDownloads: typeof soulDownloads;
|
||||
|
||||
@@ -11,7 +11,7 @@ vi.mock('./skills', () => ({
|
||||
|
||||
const { requireApiTokenUser } = await import('./lib/apiTokenAuth')
|
||||
const { publishVersionForUser } = await import('./skills')
|
||||
const { __handlers } = await import('./httpApi')
|
||||
const { __handlers, cliSkillDeleteHttp, cliSkillUndeleteHttp } = await import('./httpApi')
|
||||
const { hashSkillFiles } = await import('./lib/skills')
|
||||
|
||||
function makeCtx(partial: Record<string, unknown>) {
|
||||
@@ -55,6 +55,19 @@ describe('httpApi handlers', () => {
|
||||
expect(json.results[0].slug).toBe('a')
|
||||
})
|
||||
|
||||
it('searchSkillsHttp omits approvedOnly when false', async () => {
|
||||
const runAction = vi.fn().mockResolvedValue([])
|
||||
await __handlers.searchSkillsHandler(
|
||||
makeCtx({ runAction }),
|
||||
new Request('https://example.com/api/search?q=test&approvedOnly=false'),
|
||||
)
|
||||
expect(runAction).toHaveBeenCalledWith(expect.anything(), {
|
||||
query: 'test',
|
||||
limit: undefined,
|
||||
approvedOnly: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('getSkillHttp validates slug', async () => {
|
||||
const response = await __handlers.getSkillHandler(
|
||||
makeCtx({ runQuery: vi.fn() }),
|
||||
@@ -104,6 +117,30 @@ describe('httpApi handlers', () => {
|
||||
expect(json.owner.handle).toBe('p')
|
||||
})
|
||||
|
||||
it('getSkillHttp returns payload with null owner/latestVersion', async () => {
|
||||
const runQuery = vi.fn().mockResolvedValue({
|
||||
skill: {
|
||||
slug: 'demo',
|
||||
displayName: 'Demo',
|
||||
summary: null,
|
||||
tags: {},
|
||||
stats: {},
|
||||
createdAt: 1,
|
||||
updatedAt: 2,
|
||||
},
|
||||
latestVersion: null,
|
||||
owner: null,
|
||||
})
|
||||
const response = await __handlers.getSkillHandler(
|
||||
makeCtx({ runQuery }),
|
||||
new Request('https://example.com/api/skill?slug=demo'),
|
||||
)
|
||||
expect(response.status).toBe(200)
|
||||
const json = await response.json()
|
||||
expect(json.latestVersion).toBeNull()
|
||||
expect(json.owner).toBeNull()
|
||||
})
|
||||
|
||||
it('resolveSkillVersionHttp validates hash', async () => {
|
||||
const response = await __handlers.resolveSkillVersionHandler(
|
||||
makeCtx({ runQuery: vi.fn() }),
|
||||
@@ -186,6 +223,65 @@ describe('httpApi handlers', () => {
|
||||
expect(runMutation).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('cliTelemetrySyncHttp returns 400 on invalid payload', async () => {
|
||||
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({ userId: 'users:1' } as never)
|
||||
const response = await __handlers.cliTelemetrySyncHandler(
|
||||
makeCtx({ runMutation: vi.fn() }),
|
||||
new Request('https://x/api/cli/telemetry/sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ roots: 'nope' }),
|
||||
}),
|
||||
)
|
||||
expect(response.status).toBe(400)
|
||||
})
|
||||
|
||||
it('cliTelemetrySyncHttp forwards skill versions when provided', async () => {
|
||||
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({ userId: 'users:1' } as never)
|
||||
const runMutation = vi.fn().mockResolvedValue(null)
|
||||
await __handlers.cliTelemetrySyncHandler(
|
||||
makeCtx({ runMutation }),
|
||||
new Request('https://x/api/cli/telemetry/sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
roots: [
|
||||
{
|
||||
rootId: 'abc',
|
||||
label: '~/skills',
|
||||
skills: [{ slug: 'weather', version: '1.0.0' }],
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
)
|
||||
expect(runMutation).toHaveBeenCalledWith(expect.anything(), {
|
||||
userId: 'users:1',
|
||||
roots: [
|
||||
{ rootId: 'abc', label: '~/skills', skills: [{ slug: 'weather', version: '1.0.0' }] },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('cliTelemetrySyncHttp returns 400 on invalid json', async () => {
|
||||
const request = new Request('https://x/api/cli/telemetry/sync', { method: 'POST', body: '{' })
|
||||
const response = await __handlers.cliTelemetrySyncHandler(makeCtx({}), request)
|
||||
expect(response.status).toBe(400)
|
||||
})
|
||||
|
||||
it('cliTelemetrySyncHttp returns 401 when unauthorized', async () => {
|
||||
vi.mocked(requireApiTokenUser).mockRejectedValueOnce(new Error('Unauthorized'))
|
||||
const response = await __handlers.cliTelemetrySyncHandler(
|
||||
makeCtx({}),
|
||||
new Request('https://x/api/cli/telemetry/sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ roots: [] }),
|
||||
}),
|
||||
)
|
||||
expect(response.status).toBe(401)
|
||||
})
|
||||
|
||||
it('cliUploadUrlHttp returns uploadUrl', async () => {
|
||||
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({ userId: 'user1' } as never)
|
||||
const runMutation = vi.fn().mockResolvedValue('https://upload.local')
|
||||
@@ -316,6 +412,48 @@ describe('httpApi handlers', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('cliSkillUndeleteHttp calls delete handler with deleted=false', async () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({ userId: 'user1' } as never)
|
||||
const runMutation = vi.fn().mockResolvedValue({ ok: true })
|
||||
const response = await cliSkillUndeleteHttp(
|
||||
makeCtx({ runMutation }),
|
||||
new Request('https://x/api/cli/skill/undelete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ slug: 'demo' }),
|
||||
}),
|
||||
)
|
||||
expect(response.status).toBe(200)
|
||||
expect(runMutation).toHaveBeenCalledWith(expect.anything(), {
|
||||
userId: 'user1',
|
||||
slug: 'demo',
|
||||
deleted: false,
|
||||
})
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('cliSkillDeleteHttp calls delete handler with deleted=true', async () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({ userId: 'user1' } as never)
|
||||
const runMutation = vi.fn().mockResolvedValue({ ok: true })
|
||||
const response = await cliSkillDeleteHttp(
|
||||
makeCtx({ runMutation }),
|
||||
new Request('https://x/api/cli/skill/delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ slug: 'demo' }),
|
||||
}),
|
||||
)
|
||||
expect(response.status).toBe(200)
|
||||
expect(runMutation).toHaveBeenCalledWith(expect.anything(), {
|
||||
userId: 'user1',
|
||||
slug: 'demo',
|
||||
deleted: true,
|
||||
})
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('cliSkillDeleteHandler returns 400 on invalid json', async () => {
|
||||
const request = new Request('https://x/api/cli/skill/delete', { method: 'POST', body: '{' })
|
||||
const response = await __handlers.cliSkillDeleteHandler(makeCtx({}), request, true)
|
||||
|
||||
@@ -25,6 +25,29 @@ describe('httpApi', () => {
|
||||
expect(parsed.files[0]?.path).toBe('SKILL.md')
|
||||
})
|
||||
|
||||
it('normalizes optional fields in publish payload', () => {
|
||||
const parsed = __test.parsePublishBody({
|
||||
slug: 'cool-skill',
|
||||
displayName: 'Cool Skill',
|
||||
version: '1.2.3',
|
||||
changelog: '',
|
||||
tags: [],
|
||||
forkOf: { slug: 'base-skill' },
|
||||
files: [
|
||||
{
|
||||
path: 'SKILL.md',
|
||||
size: 5,
|
||||
storageId: 'fakeStorageId',
|
||||
sha256: 'abcd',
|
||||
contentType: 'text/markdown',
|
||||
},
|
||||
],
|
||||
})
|
||||
expect(parsed.tags).toBeUndefined()
|
||||
expect(parsed.source).toBeUndefined()
|
||||
expect(parsed.forkOf).toEqual({ slug: 'base-skill', version: undefined })
|
||||
})
|
||||
|
||||
it('rejects invalid publish payloads', () => {
|
||||
expect(() => __test.parsePublishBody(null)).toThrow(/Publish payload/i)
|
||||
expect(() =>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/* @vitest-environment node */
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { __test, matchesExactTokens, tokenize } from './searchText'
|
||||
|
||||
describe('searchText', () => {
|
||||
it('tokenize lowercases and splits on punctuation', () => {
|
||||
expect(tokenize('Minimax Usage /minimax-usage')).toEqual([
|
||||
'minimax',
|
||||
'usage',
|
||||
'minimax',
|
||||
'usage',
|
||||
])
|
||||
})
|
||||
|
||||
it('matchesExactTokens requires all query tokens', () => {
|
||||
const queryTokens = tokenize('Remind Me')
|
||||
expect(matchesExactTokens(queryTokens, ['Remind Me', '/remind-me', 'Short summary'])).toBe(true)
|
||||
expect(matchesExactTokens(queryTokens, ['Reminder tool', '/reminder', 'Short summary'])).toBe(
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
it('matchesExactTokens ignores empty inputs', () => {
|
||||
expect(matchesExactTokens([], ['text'])).toBe(false)
|
||||
expect(matchesExactTokens(['token'], [' ', null, undefined])).toBe(false)
|
||||
})
|
||||
|
||||
it('normalize uses lowercase', () => {
|
||||
expect(__test.normalize('AbC')).toBe('abc')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
const WORD_RE = /[a-z0-9]+/g
|
||||
|
||||
function normalize(value: string) {
|
||||
return value.toLowerCase()
|
||||
}
|
||||
|
||||
export function tokenize(value: string): string[] {
|
||||
if (!value) return []
|
||||
return normalize(value).match(WORD_RE) ?? []
|
||||
}
|
||||
|
||||
export function matchesExactTokens(
|
||||
queryTokens: string[],
|
||||
parts: Array<string | null | undefined>,
|
||||
): boolean {
|
||||
if (queryTokens.length === 0) return false
|
||||
const text = parts.filter((part) => Boolean(part?.trim())).join(' ')
|
||||
if (!text) return false
|
||||
const textTokens = tokenize(text)
|
||||
if (textTokens.length === 0) return false
|
||||
const textSet = new Set(textTokens)
|
||||
return queryTokens.every((token) => textSet.has(token))
|
||||
}
|
||||
|
||||
export const __test = { normalize, tokenize, matchesExactTokens }
|
||||
+26
-13
@@ -1,20 +1,33 @@
|
||||
/* @vitest-environment node */
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { API_TOKEN_PREFIX, generateToken, hashToken } from './tokens'
|
||||
import { __test, generateToken, hashToken } from './tokens'
|
||||
|
||||
describe('tokens', () => {
|
||||
it('generates token with prefix and url-safe chars', () => {
|
||||
const { token, prefix } = generateToken()
|
||||
expect(token.startsWith(API_TOKEN_PREFIX)).toBe(true)
|
||||
expect(prefix).toBe(token.slice(0, 12))
|
||||
expect(token).toMatch(/^[a-z0-9_-]+$/i)
|
||||
it('hashToken returns sha256 hex', async () => {
|
||||
await expect(hashToken('test')).resolves.toBe(
|
||||
'9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08',
|
||||
)
|
||||
})
|
||||
|
||||
it('hashes tokens deterministically', async () => {
|
||||
const a = await hashToken('clh_test')
|
||||
const b = await hashToken('clh_test')
|
||||
const c = await hashToken('clh_other')
|
||||
expect(a).toBe(b)
|
||||
expect(a).not.toBe(c)
|
||||
expect(a).toMatch(/^[a-f0-9]{64}$/)
|
||||
it('generateToken returns token + prefix', () => {
|
||||
const { token, prefix } = generateToken()
|
||||
expect(token).toMatch(/^clh_[A-Za-z0-9_-]+$/)
|
||||
expect(prefix).toBe(token.slice(0, 12))
|
||||
})
|
||||
|
||||
it('toHex encodes bytes', () => {
|
||||
expect(__test.toHex(new Uint8Array([0, 15, 255]))).toBe('000fff')
|
||||
})
|
||||
|
||||
it('toBase64 encodes 1/2/3-byte tails', () => {
|
||||
expect(__test.toBase64(new Uint8Array([0xff]))).toBe('/w==')
|
||||
expect(__test.toBase64(new Uint8Array([0xff, 0xee]))).toBe('/+4=')
|
||||
expect(__test.toBase64(new Uint8Array([0xff, 0xee, 0xdd]))).toBe('/+7d')
|
||||
})
|
||||
|
||||
it('toBase64Url replaces alphabet and strips padding', () => {
|
||||
expect(__test.toBase64Url(new Uint8Array([0xff]))).toBe('_w')
|
||||
expect(__test.toBase64Url(new Uint8Array([0xfa, 0x00, 0x00]))).toBe('-gAA')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -43,3 +43,9 @@ function toBase64(bytes: Uint8Array) {
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
export const __test = {
|
||||
toHex,
|
||||
toBase64,
|
||||
toBase64Url,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/* @vitest-environment node */
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { __test } from './search'
|
||||
|
||||
describe('search helpers', () => {
|
||||
it('advances candidate limit until max', () => {
|
||||
expect(__test.getNextCandidateLimit(50, 1000)).toBe(100)
|
||||
expect(__test.getNextCandidateLimit(800, 1000)).toBe(1000)
|
||||
expect(__test.getNextCandidateLimit(1000, 1000)).toBeNull()
|
||||
})
|
||||
})
|
||||
+91
-27
@@ -3,6 +3,7 @@ import { internal } from './_generated/api'
|
||||
import type { Doc, Id } from './_generated/dataModel'
|
||||
import { action, internalQuery } from './_generated/server'
|
||||
import { generateEmbedding } from './lib/embeddings'
|
||||
import { matchesExactTokens, tokenize } from './lib/searchText'
|
||||
|
||||
type HydratedEntry = {
|
||||
embeddingId: Id<'skillEmbeddings'>
|
||||
@@ -12,6 +13,11 @@ type HydratedEntry = {
|
||||
|
||||
type SearchResult = HydratedEntry & { score: number }
|
||||
|
||||
function getNextCandidateLimit(current: number, max: number) {
|
||||
const next = Math.min(current * 2, max)
|
||||
return next > current ? next : null
|
||||
}
|
||||
|
||||
export const searchSkills: ReturnType<typeof action> = action({
|
||||
args: {
|
||||
query: v.string(),
|
||||
@@ -21,31 +27,59 @@ export const searchSkills: ReturnType<typeof action> = action({
|
||||
handler: async (ctx, args): Promise<SearchResult[]> => {
|
||||
const query = args.query.trim()
|
||||
if (!query) return []
|
||||
const queryTokens = tokenize(query)
|
||||
if (queryTokens.length === 0) return []
|
||||
const vector = await generateEmbedding(query)
|
||||
const results = await ctx.vectorSearch('skillEmbeddings', 'by_embedding', {
|
||||
vector,
|
||||
limit: args.limit ?? 10,
|
||||
filter: (q) => q.or(q.eq('visibility', 'latest'), q.eq('visibility', 'latest-approved')),
|
||||
})
|
||||
const limit = args.limit ?? 10
|
||||
const maxCandidate = Math.min(Math.max(limit * 10, 200), 1000)
|
||||
let candidateLimit = Math.max(limit * 3, 50)
|
||||
let hydrated: HydratedEntry[] = []
|
||||
let scoreById = new Map<Id<'skillEmbeddings'>, number>()
|
||||
let exactMatches: HydratedEntry[] = []
|
||||
|
||||
const hydrated = (await ctx.runQuery(internal.search.hydrateResults, {
|
||||
embeddingIds: results.map((result) => result._id),
|
||||
})) as HydratedEntry[]
|
||||
while (candidateLimit <= maxCandidate) {
|
||||
const results = await ctx.vectorSearch('skillEmbeddings', 'by_embedding', {
|
||||
vector,
|
||||
limit: candidateLimit,
|
||||
filter: (q) => q.or(q.eq('visibility', 'latest'), q.eq('visibility', 'latest-approved')),
|
||||
})
|
||||
|
||||
const scoreById = new Map<Id<'skillEmbeddings'>, number>(
|
||||
results.map((result) => [result._id, result._score]),
|
||||
)
|
||||
hydrated = (await ctx.runQuery(internal.search.hydrateResults, {
|
||||
embeddingIds: results.map((result) => result._id),
|
||||
})) as HydratedEntry[]
|
||||
|
||||
const filtered = args.highlightedOnly
|
||||
? hydrated.filter((entry) => entry.skill?.batch === 'highlighted')
|
||||
: hydrated
|
||||
scoreById = new Map<Id<'skillEmbeddings'>, number>(
|
||||
results.map((result) => [result._id, result._score]),
|
||||
)
|
||||
|
||||
return filtered
|
||||
const filtered = args.highlightedOnly
|
||||
? hydrated.filter((entry) => entry.skill?.batch === 'highlighted')
|
||||
: hydrated
|
||||
|
||||
exactMatches = filtered.filter((entry) =>
|
||||
matchesExactTokens(queryTokens, [
|
||||
entry.skill?.displayName,
|
||||
entry.skill?.slug,
|
||||
entry.skill?.summary,
|
||||
]),
|
||||
)
|
||||
|
||||
if (exactMatches.length >= limit || results.length < candidateLimit) {
|
||||
break
|
||||
}
|
||||
|
||||
const nextLimit = getNextCandidateLimit(candidateLimit, maxCandidate)
|
||||
if (!nextLimit) break
|
||||
candidateLimit = nextLimit
|
||||
}
|
||||
|
||||
return exactMatches
|
||||
.map((entry) => ({
|
||||
...entry,
|
||||
score: scoreById.get(entry.embeddingId) ?? 0,
|
||||
}))
|
||||
.filter((entry) => entry.skill)
|
||||
.slice(0, limit)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -83,27 +117,55 @@ export const searchSouls: ReturnType<typeof action> = action({
|
||||
handler: async (ctx, args): Promise<SoulSearchResult[]> => {
|
||||
const query = args.query.trim()
|
||||
if (!query) return []
|
||||
const queryTokens = tokenize(query)
|
||||
if (queryTokens.length === 0) return []
|
||||
const vector = await generateEmbedding(query)
|
||||
const results = await ctx.vectorSearch('soulEmbeddings', 'by_embedding', {
|
||||
vector,
|
||||
limit: args.limit ?? 10,
|
||||
filter: (q) => q.or(q.eq('visibility', 'latest'), q.eq('visibility', 'latest-approved')),
|
||||
})
|
||||
const limit = args.limit ?? 10
|
||||
const maxCandidate = Math.min(Math.max(limit * 10, 200), 1000)
|
||||
let candidateLimit = Math.max(limit * 3, 50)
|
||||
let hydrated: HydratedSoulEntry[] = []
|
||||
let scoreById = new Map<Id<'soulEmbeddings'>, number>()
|
||||
let exactMatches: HydratedSoulEntry[] = []
|
||||
|
||||
const hydrated = (await ctx.runQuery(internal.search.hydrateSoulResults, {
|
||||
embeddingIds: results.map((result) => result._id),
|
||||
})) as HydratedSoulEntry[]
|
||||
while (candidateLimit <= maxCandidate) {
|
||||
const results = await ctx.vectorSearch('soulEmbeddings', 'by_embedding', {
|
||||
vector,
|
||||
limit: candidateLimit,
|
||||
filter: (q) => q.or(q.eq('visibility', 'latest'), q.eq('visibility', 'latest-approved')),
|
||||
})
|
||||
|
||||
const scoreById = new Map<Id<'soulEmbeddings'>, number>(
|
||||
results.map((result) => [result._id, result._score]),
|
||||
)
|
||||
hydrated = (await ctx.runQuery(internal.search.hydrateSoulResults, {
|
||||
embeddingIds: results.map((result) => result._id),
|
||||
})) as HydratedSoulEntry[]
|
||||
|
||||
return hydrated
|
||||
scoreById = new Map<Id<'soulEmbeddings'>, number>(
|
||||
results.map((result) => [result._id, result._score]),
|
||||
)
|
||||
|
||||
exactMatches = hydrated.filter((entry) =>
|
||||
matchesExactTokens(queryTokens, [
|
||||
entry.soul?.displayName,
|
||||
entry.soul?.slug,
|
||||
entry.soul?.summary,
|
||||
]),
|
||||
)
|
||||
|
||||
if (exactMatches.length >= limit || results.length < candidateLimit) {
|
||||
break
|
||||
}
|
||||
|
||||
const nextLimit = getNextCandidateLimit(candidateLimit, maxCandidate)
|
||||
if (!nextLimit) break
|
||||
candidateLimit = nextLimit
|
||||
}
|
||||
|
||||
return exactMatches
|
||||
.map((entry) => ({
|
||||
...entry,
|
||||
score: scoreById.get(entry.embeddingId) ?? 0,
|
||||
}))
|
||||
.filter((entry) => entry.soul)
|
||||
.slice(0, limit)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -124,3 +186,5 @@ export const hydrateSoulResults = internalQuery({
|
||||
return entries
|
||||
},
|
||||
})
|
||||
|
||||
export const __test = { getNextCandidateLimit }
|
||||
|
||||
+9
-1
@@ -17,7 +17,7 @@ bun clawdhub --help
|
||||
|
||||
## Global flags
|
||||
|
||||
- `--workdir <dir>`: working directory (default: cwd)
|
||||
- `--workdir <dir>`: working directory (default: cwd; falls back to Clawdbot workspace if configured)
|
||||
- `--dir <dir>`: install dir under workdir (default: `skills`)
|
||||
- `--site <url>`: base URL for browser login (default: `https://clawdhub.com`)
|
||||
- `--registry <url>`: API base URL (default: discovered, else `https://clawdhub.com`)
|
||||
@@ -27,6 +27,7 @@ Env equivalents:
|
||||
|
||||
- `CLAWDHUB_SITE`
|
||||
- `CLAWDHUB_REGISTRY`
|
||||
- `CLAWDHUB_WORKDIR`
|
||||
|
||||
## Config file
|
||||
|
||||
@@ -50,6 +51,13 @@ Stores your API token + cached registry URL.
|
||||
|
||||
- Calls `/api/v1/search?q=...`.
|
||||
|
||||
### `explore`
|
||||
|
||||
- Lists latest updated skills via `/api/v1/skills?limit=...` (sorted by `updatedAt` desc).
|
||||
- Flags:
|
||||
- `--limit <n>` (1–50, default: 25)
|
||||
- Output: `<slug> v<version> <age> <summary>` (summary truncated to 50 chars).
|
||||
|
||||
### `install <slug>`
|
||||
|
||||
- Resolves latest version via `/api/v1/skills/<slug>`.
|
||||
|
||||
@@ -48,3 +48,17 @@ read_when:
|
||||
|
||||
## Sync
|
||||
- `bun clawdhub sync --dry-run --all`
|
||||
|
||||
## Playwright (menu smoke)
|
||||
|
||||
Run against prod:
|
||||
|
||||
```
|
||||
PLAYWRIGHT_BASE_URL=https://clawdhub.com bun run test:pw
|
||||
```
|
||||
|
||||
Run against a local preview server:
|
||||
|
||||
```
|
||||
bun run test:e2e:local
|
||||
```
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ bun clawdhub whoami
|
||||
bun clawdhub search gif --limit 5
|
||||
```
|
||||
|
||||
Install a skill into `./skills/<slug>`:
|
||||
Install a skill into `./skills/<slug>` (if Clawdbot is configured, installs into that workspace instead):
|
||||
|
||||
```bash
|
||||
bun clawdhub install <slug>
|
||||
|
||||
@@ -11,9 +11,23 @@ import {
|
||||
parseArk,
|
||||
} from 'clawdhub-schema'
|
||||
import { unzipSync } from 'fflate'
|
||||
import { Agent, setGlobalDispatcher } from 'undici'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readGlobalConfig } from '../packages/clawdhub/src/config'
|
||||
|
||||
const REQUEST_TIMEOUT_MS = 15_000
|
||||
|
||||
try {
|
||||
setGlobalDispatcher(
|
||||
new Agent({
|
||||
allowH2: true,
|
||||
connect: { timeout: REQUEST_TIMEOUT_MS },
|
||||
}),
|
||||
)
|
||||
} catch {
|
||||
// ignore dispatcher setup failures
|
||||
}
|
||||
|
||||
function mustGetToken() {
|
||||
const fromEnv = process.env.CLAWDHUB_E2E_TOKEN?.trim()
|
||||
if (fromEnv) return fromEnv
|
||||
@@ -31,6 +45,16 @@ async function makeTempConfig(registry: string, token: string | null) {
|
||||
return { dir, path }
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(input: RequestInfo | URL, init?: RequestInit) {
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort('Timeout'), REQUEST_TIMEOUT_MS)
|
||||
try {
|
||||
return await fetch(input, { ...init, signal: controller.signal })
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
|
||||
describe('clawdhub e2e', () => {
|
||||
it('prints CLI version via --cli-version', async () => {
|
||||
const result = spawnSync('bun', ['clawdhub', '--cli-version'], {
|
||||
@@ -47,7 +71,9 @@ describe('clawdhub e2e', () => {
|
||||
url.searchParams.set('q', 'gif')
|
||||
url.searchParams.set('limit', '5')
|
||||
|
||||
const response = await fetch(url.toString(), { headers: { Accept: 'application/json' } })
|
||||
const response = await fetchWithTimeout(url.toString(), {
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
expect(response.ok).toBe(true)
|
||||
const json = (await response.json()) as unknown
|
||||
const parsed = parseArk(ApiV1SearchResponseSchema, json, 'API response')
|
||||
@@ -103,7 +129,7 @@ describe('clawdhub e2e', () => {
|
||||
const cfg = await makeTempConfig(registry, token)
|
||||
try {
|
||||
const whoamiUrl = new URL(ApiRoutes.whoami, registry)
|
||||
const whoamiRes = await fetch(whoamiUrl.toString(), {
|
||||
const whoamiRes = await fetchWithTimeout(whoamiUrl.toString(), {
|
||||
headers: { Accept: 'application/json', Authorization: `Bearer ${token}` },
|
||||
})
|
||||
expect(whoamiRes.ok).toBe(true)
|
||||
@@ -310,7 +336,7 @@ describe('clawdhub e2e', () => {
|
||||
const downloadUrl = new URL(ApiRoutes.download, registry)
|
||||
downloadUrl.searchParams.set('slug', slug)
|
||||
downloadUrl.searchParams.set('version', '1.0.1')
|
||||
const zipRes = await fetch(downloadUrl.toString())
|
||||
const zipRes = await fetchWithTimeout(downloadUrl.toString())
|
||||
expect(zipRes.ok).toBe(true)
|
||||
const zipBytes = new Uint8Array(await zipRes.arrayBuffer())
|
||||
const unzipped = unzipSync(zipBytes)
|
||||
@@ -374,7 +400,8 @@ describe('clawdhub e2e', () => {
|
||||
)
|
||||
expect(update.status).toBe(0)
|
||||
|
||||
const metaRes = await fetch(`${registry}${ApiRoutes.skills}/${slug}`, {
|
||||
const metaUrl = new URL(`${ApiRoutes.skills}/${slug}`, registry)
|
||||
const metaRes = await fetchWithTimeout(metaUrl.toString(), {
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
expect(metaRes.status).toBe(200)
|
||||
@@ -401,12 +428,12 @@ describe('clawdhub e2e', () => {
|
||||
)
|
||||
expect(del.status).toBe(0)
|
||||
|
||||
const metaAfterDelete = await fetch(metaUrl.toString(), {
|
||||
const metaAfterDelete = await fetchWithTimeout(metaUrl.toString(), {
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
expect(metaAfterDelete.status).toBe(404)
|
||||
|
||||
const downloadAfterDelete = await fetch(downloadUrl.toString())
|
||||
const downloadAfterDelete = await fetchWithTimeout(downloadUrl.toString())
|
||||
expect(downloadAfterDelete.status).toBe(404)
|
||||
|
||||
const undelete = spawnSync(
|
||||
@@ -431,7 +458,7 @@ describe('clawdhub e2e', () => {
|
||||
)
|
||||
expect(undelete.status).toBe(0)
|
||||
|
||||
const metaAfterUndelete = await fetch(metaUrl.toString(), {
|
||||
const metaAfterUndelete = await fetchWithTimeout(metaUrl.toString(), {
|
||||
headers: { Accept: 'application/json' },
|
||||
})
|
||||
expect(metaAfterUndelete.status).toBe(200)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
const navLabels = ['Skills', 'Upload', 'Import', 'Search']
|
||||
|
||||
test('skills loads without error', async ({ page }) => {
|
||||
await page.goto('/skills', { waitUntil: 'domcontentloaded' })
|
||||
await expect(page.locator('text=Something went wrong!')).toHaveCount(0)
|
||||
await expect(page.locator('h1', { hasText: 'Skills' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('souls loads without error', async ({ page }) => {
|
||||
await page.goto('/souls', { waitUntil: 'domcontentloaded' })
|
||||
await expect(page.locator('text=Something went wrong!')).toHaveCount(0)
|
||||
await expect(page.locator('h1', { hasText: 'Souls' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('header menu routes render', async ({ page }) => {
|
||||
await page.goto('/', { waitUntil: 'domcontentloaded' })
|
||||
|
||||
for (const label of navLabels) {
|
||||
const link = page.getByRole('link', { name: label }).first()
|
||||
await expect(link).toBeVisible()
|
||||
await link.click()
|
||||
|
||||
if (label === 'Skills') {
|
||||
await expect(page).toHaveURL(/\/skills/)
|
||||
await expect(page.locator('h1', { hasText: 'Skills' })).toBeVisible()
|
||||
}
|
||||
|
||||
if (label === 'Upload') {
|
||||
await expect(page).toHaveURL(/\/upload/)
|
||||
const heading = page.locator('h1.section-title', { hasText: /^Publish a /i })
|
||||
const signInCard = page.locator('text=Sign in to upload')
|
||||
await expect(heading.or(signInCard)).toBeVisible()
|
||||
}
|
||||
|
||||
if (label === 'Import') {
|
||||
await expect(page).toHaveURL(/\/import/)
|
||||
const heading = page.getByRole('heading', { name: 'Import from GitHub' })
|
||||
const signInCard = page.locator('text=Sign in to import and publish skills.')
|
||||
await expect(heading.or(signInCard)).toBeVisible()
|
||||
}
|
||||
|
||||
if (label === 'Search') {
|
||||
await expect(page).toHaveURL(/\/?(\?|$)/)
|
||||
await expect(page.locator('h1', { hasText: 'ClawdHub' })).toBeVisible()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,97 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test('skills search paginates exact results', async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
const makeSearchResults = (count: number) =>
|
||||
Array.from({ length: count }, (_, index) => ({
|
||||
score: 0.9,
|
||||
skill: {
|
||||
_id: `skill_${index}`,
|
||||
slug: `skill-${index}`,
|
||||
displayName: `Skill ${index}`,
|
||||
summary: `Summary ${index}`,
|
||||
tags: {},
|
||||
stats: {
|
||||
downloads: 0,
|
||||
installsCurrent: 0,
|
||||
installsAllTime: 0,
|
||||
stars: 0,
|
||||
versions: 1,
|
||||
comments: 0,
|
||||
},
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
},
|
||||
version: null,
|
||||
}))
|
||||
|
||||
class MockWebSocket {
|
||||
url: string
|
||||
readyState = 0
|
||||
onopen?: () => void
|
||||
onmessage?: (event: { data: string }) => void
|
||||
onclose?: (event: { code: number; reason: string }) => void
|
||||
onerror?: () => void
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url
|
||||
window.setTimeout(() => {
|
||||
this.readyState = 1
|
||||
this.onopen?.()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
send(data: string) {
|
||||
try {
|
||||
const message = JSON.parse(data) as {
|
||||
type?: string
|
||||
requestId?: number
|
||||
udfPath?: string
|
||||
args?: Array<Record<string, unknown>>
|
||||
}
|
||||
if (message.type === 'Action' && message.udfPath?.includes('searchSkills')) {
|
||||
const [args] = message.args ?? []
|
||||
const limit = typeof args?.limit === 'number' ? args.limit : 10
|
||||
const limits = (window as typeof window & { __searchLimits: number[] }).__searchLimits
|
||||
limits.push(limit)
|
||||
const response = {
|
||||
type: 'ActionResponse',
|
||||
requestId: message.requestId,
|
||||
success: true,
|
||||
result: makeSearchResults(limit),
|
||||
logLines: [],
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
this.onmessage?.({ data: JSON.stringify(response) })
|
||||
}, 0)
|
||||
}
|
||||
} catch {
|
||||
this.onerror?.()
|
||||
}
|
||||
}
|
||||
|
||||
close(code = 1000, reason = 'closed') {
|
||||
this.readyState = 3
|
||||
this.onclose?.({ code, reason })
|
||||
}
|
||||
}
|
||||
|
||||
;(window as typeof window & { __searchLimits: number[] }).__searchLimits = []
|
||||
window.WebSocket = MockWebSocket as unknown as typeof WebSocket
|
||||
})
|
||||
|
||||
await page.goto('/skills', { waitUntil: 'domcontentloaded' })
|
||||
await expect(page.getByRole('heading', { name: 'Skills' })).toBeVisible()
|
||||
|
||||
const input = page.getByPlaceholder('Filter by name, slug, or summary…')
|
||||
await input.fill('remind')
|
||||
await expect(page.getByText('Skill 0')).toBeVisible()
|
||||
await expect(page.getByText('Scroll to load more')).toBeVisible()
|
||||
|
||||
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight))
|
||||
await expect(page.getByText('Skill 75')).toBeVisible()
|
||||
const limits = await page.evaluate(
|
||||
() => (window as typeof window & { __searchLimits: number[] }).__searchLimits,
|
||||
)
|
||||
expect(limits).toEqual([50, 100])
|
||||
})
|
||||
+21
-18
@@ -13,6 +13,8 @@
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:e2e": "vitest run -c vitest.e2e.config.ts",
|
||||
"test:e2e:local": "bash scripts/run-playwright-local.sh",
|
||||
"test:pw": "playwright test",
|
||||
"coverage": "vitest run --coverage",
|
||||
"convex:deploy": "bunx convex deploy --typecheck=disable --yes",
|
||||
"lint": "bun run lint:biome && bun run lint:oxlint",
|
||||
@@ -21,7 +23,7 @@
|
||||
"format": "biome format --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.41.1",
|
||||
"@auth/core": "^0.34.3",
|
||||
"@convex-dev/auth": "^0.0.90",
|
||||
"@fontsource/bricolage-grotesque": "^5.2.10",
|
||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||
@@ -31,17 +33,17 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@resvg/resvg-wasm": "^2.6.2",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-devtools": "^0.9.0",
|
||||
"@tanstack/react-router": "^1.144.0",
|
||||
"@tanstack/react-router-devtools": "^1.144.0",
|
||||
"@tanstack/react-start": "^1.145.3",
|
||||
"@tanstack/router-plugin": "^1.145.2",
|
||||
"@tanstack/react-devtools": "^0.9.2",
|
||||
"@tanstack/react-router": "^1.151.3",
|
||||
"@tanstack/react-router-devtools": "^1.151.3",
|
||||
"@tanstack/react-start": "^1.151.5",
|
||||
"@tanstack/router-plugin": "^1.151.5",
|
||||
"@vercel/analytics": "^1.6.1",
|
||||
"clawdhub-schema": "^0.0.2",
|
||||
"clawdhub-schema": "workspace:*",
|
||||
"clsx": "^2.1.1",
|
||||
"convex": "^1.31.2",
|
||||
"convex": "^1.31.5",
|
||||
"fflate": "^0.8.2",
|
||||
"h3": "2.0.1-rc.5",
|
||||
"h3": "2.0.1-rc.8",
|
||||
"lucide-react": "^0.562.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"nitro": "^3.0.1-alpha.1",
|
||||
@@ -52,25 +54,26 @@
|
||||
"semver": "^7.7.3",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vite-tsconfig-paths": "^6.0.3",
|
||||
"vite-tsconfig-paths": "^6.0.4",
|
||||
"yaml": "^2.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.3.11",
|
||||
"@tanstack/devtools-vite": "^0.4.0",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@tanstack/devtools-vite": "^0.4.1",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/node": "^25.0.9",
|
||||
"@types/react": "^19.2.8",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"@vitest/coverage-v8": "^4.0.16",
|
||||
"@vitest/coverage-v8": "^4.0.17",
|
||||
"jsdom": "^27.4.0",
|
||||
"oxlint": "^1.36.0",
|
||||
"oxlint-tsgolint": "^0.10.1",
|
||||
"oxlint": "^1.39.0",
|
||||
"oxlint-tsgolint": "^0.11.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.0",
|
||||
"vitest": "^4.0.16"
|
||||
"vite": "^7.3.1",
|
||||
"vitest": "^4.0.17"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,5 +53,5 @@ clawdhub sync --root ../clawdis/skills --all --dry-run
|
||||
|
||||
- Site: `https://clawdhub.com` (override via `--site` or `CLAWDHUB_SITE`)
|
||||
- Registry: discovered from `/.well-known/clawdhub.json` on the site (override via `--registry` or `CLAWDHUB_REGISTRY`)
|
||||
- Workdir: current directory (override via `--workdir`)
|
||||
- Workdir: current directory (falls back to Clawdbot workspace if configured; override via `--workdir` or `CLAWDHUB_WORKDIR`)
|
||||
- Install dir: `./skills` under workdir (override via `--dir`)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clawdhub",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"description": "ClawdHub CLI \u2014 install, update, search, and publish agent skills.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -31,7 +31,7 @@
|
||||
"semver": "^7.7.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/node": "^25.0.9",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -21,12 +21,23 @@ describe('browserAuth', () => {
|
||||
expect(url).toContain('state=')
|
||||
})
|
||||
|
||||
it('builds auth url without label', () => {
|
||||
const url = buildCliAuthUrl({
|
||||
siteUrl: 'https://example.com',
|
||||
redirectUri: 'http://127.0.0.1:1234/callback',
|
||||
state: 'state123',
|
||||
})
|
||||
expect(url).toContain('https://example.com/cli/auth?')
|
||||
expect(url).not.toContain('label_b64=')
|
||||
})
|
||||
|
||||
it('accepts only loopback http redirect uris', () => {
|
||||
expect(isAllowedLoopbackRedirectUri('http://127.0.0.1:1234/callback')).toBe(true)
|
||||
expect(isAllowedLoopbackRedirectUri('http://localhost:1234/callback')).toBe(true)
|
||||
expect(isAllowedLoopbackRedirectUri('http://[::1]:1234/callback')).toBe(true)
|
||||
expect(isAllowedLoopbackRedirectUri('https://127.0.0.1:1234/callback')).toBe(false)
|
||||
expect(isAllowedLoopbackRedirectUri('http://evil.com/callback')).toBe(false)
|
||||
expect(isAllowedLoopbackRedirectUri('not a url')).toBe(false)
|
||||
})
|
||||
|
||||
it('receives token via loopback server', async () => {
|
||||
@@ -43,4 +54,43 @@ describe('browserAuth', () => {
|
||||
})
|
||||
await expect(server.waitForResult()).resolves.toEqual(payload)
|
||||
})
|
||||
|
||||
it('serves callback html', async () => {
|
||||
const server = await startLoopbackAuthServer({ timeoutMs: 2000 })
|
||||
const response = await fetch(server.redirectUri)
|
||||
expect(response.status).toBe(200)
|
||||
const text = await response.text()
|
||||
expect(text).toContain('ClawdHub CLI Login')
|
||||
server.close()
|
||||
})
|
||||
|
||||
it('returns 404 for unknown routes', async () => {
|
||||
const server = await startLoopbackAuthServer({ timeoutMs: 2000 })
|
||||
const response = await fetch(server.redirectUri.replace('/callback', '/nope'))
|
||||
expect(response.status).toBe(404)
|
||||
server.close()
|
||||
})
|
||||
|
||||
it('rejects invalid json payloads', async () => {
|
||||
const server = await startLoopbackAuthServer({ timeoutMs: 2000 })
|
||||
const tokenUrl = server.redirectUri.replace('/callback', '/token')
|
||||
const response = await fetch(tokenUrl, { method: 'POST', body: '{' })
|
||||
expect(response.status).toBe(400)
|
||||
await expect(server.waitForResult()).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('rejects state mismatches', async () => {
|
||||
const server = await startLoopbackAuthServer({ timeoutMs: 2000 })
|
||||
await fetch(server.redirectUri.replace('/callback', '/token'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: 'clh_test', registry: 'https://example.com', state: 'nope' }),
|
||||
})
|
||||
await expect(server.waitForResult()).rejects.toThrow(/state mismatch/i)
|
||||
})
|
||||
|
||||
it('times out waiting for login', async () => {
|
||||
const server = await startLoopbackAuthServer({ timeoutMs: 25 })
|
||||
await expect(server.waitForResult()).rejects.toThrow(/timed out waiting for browser login/i)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
import { resolve } from 'node:path'
|
||||
import { stat } from 'node:fs/promises'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { Command } from 'commander'
|
||||
import { getCliBuildLabel, getCliVersion } from './cli/buildInfo.js'
|
||||
import { resolveClawdbotDefaultWorkspace } from './cli/clawdbotConfig.js'
|
||||
import { cmdLoginFlow, cmdLogout, cmdWhoami } from './cli/commands/auth.js'
|
||||
import { cmdDeleteSkill, cmdUndeleteSkill } from './cli/commands/delete.js'
|
||||
import { cmdPublish } from './cli/commands/publish.js'
|
||||
import { cmdInstall, cmdList, cmdSearch, cmdUpdate } from './cli/commands/skills.js'
|
||||
import { cmdExplore, cmdInstall, cmdList, cmdSearch, cmdUpdate } from './cli/commands/skills.js'
|
||||
import { cmdSync } from './cli/commands/sync.js'
|
||||
import { configureCommanderHelp, styleEnvBlock, styleTitle } from './cli/helpStyle.js'
|
||||
import { DEFAULT_REGISTRY, DEFAULT_SITE } from './cli/registry.js'
|
||||
@@ -28,13 +30,16 @@ const program = new Command()
|
||||
.option('--no-input', 'Disable prompts')
|
||||
.showHelpAfterError()
|
||||
.showSuggestionAfterError()
|
||||
.addHelpText('after', styleEnvBlock('\nEnv:\n CLAWDHUB_SITE\n CLAWDHUB_REGISTRY\n'))
|
||||
.addHelpText(
|
||||
'after',
|
||||
styleEnvBlock('\nEnv:\n CLAWDHUB_SITE\n CLAWDHUB_REGISTRY\n CLAWDHUB_WORKDIR\n'),
|
||||
)
|
||||
|
||||
configureCommanderHelp(program)
|
||||
|
||||
function resolveGlobalOpts(): GlobalOpts {
|
||||
async function resolveGlobalOpts(): Promise<GlobalOpts> {
|
||||
const raw = program.opts<{ workdir?: string; dir?: string; site?: string; registry?: string }>()
|
||||
const workdir = resolve(raw.workdir ?? process.cwd())
|
||||
const workdir = await resolveWorkdir(raw.workdir)
|
||||
const dir = resolve(workdir, raw.dir ?? 'skills')
|
||||
const site = raw.site ?? process.env.CLAWDHUB_SITE ?? DEFAULT_SITE
|
||||
const registrySource = raw.registry ? 'cli' : process.env.CLAWDHUB_REGISTRY ? 'env' : 'default'
|
||||
@@ -47,6 +52,35 @@ function isInputAllowed() {
|
||||
return globalFlags.input !== false
|
||||
}
|
||||
|
||||
async function resolveWorkdir(explicit?: string) {
|
||||
if (explicit?.trim()) return resolve(explicit.trim())
|
||||
const envWorkdir = process.env.CLAWDHUB_WORKDIR?.trim()
|
||||
if (envWorkdir) return resolve(envWorkdir)
|
||||
|
||||
const cwd = resolve(process.cwd())
|
||||
const hasMarker = await hasClawdhubMarker(cwd)
|
||||
if (hasMarker) return cwd
|
||||
|
||||
const clawdbotWorkspace = await resolveClawdbotDefaultWorkspace()
|
||||
return clawdbotWorkspace ? resolve(clawdbotWorkspace) : cwd
|
||||
}
|
||||
|
||||
async function hasClawdhubMarker(workdir: string) {
|
||||
const lockfile = join(workdir, '.clawdhub', 'lock.json')
|
||||
if (await pathExists(lockfile)) return true
|
||||
const markerDir = join(workdir, '.clawdhub')
|
||||
return pathExists(markerDir)
|
||||
}
|
||||
|
||||
async function pathExists(path: string) {
|
||||
try {
|
||||
await stat(path)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
program
|
||||
.command('login')
|
||||
.description('Log in (opens browser or stores token)')
|
||||
@@ -54,7 +88,7 @@ program
|
||||
.option('--label <label>', 'Token label (browser flow only)', 'CLI token')
|
||||
.option('--no-browser', 'Do not open browser (requires --token)')
|
||||
.action(async (options) => {
|
||||
const opts = resolveGlobalOpts()
|
||||
const opts = await resolveGlobalOpts()
|
||||
await cmdLoginFlow(opts, options, isInputAllowed())
|
||||
})
|
||||
|
||||
@@ -62,7 +96,7 @@ program
|
||||
.command('logout')
|
||||
.description('Remove stored token')
|
||||
.action(async () => {
|
||||
const opts = resolveGlobalOpts()
|
||||
const opts = await resolveGlobalOpts()
|
||||
await cmdLogout(opts)
|
||||
})
|
||||
|
||||
@@ -70,7 +104,7 @@ program
|
||||
.command('whoami')
|
||||
.description('Validate token')
|
||||
.action(async () => {
|
||||
const opts = resolveGlobalOpts()
|
||||
const opts = await resolveGlobalOpts()
|
||||
await cmdWhoami(opts)
|
||||
})
|
||||
|
||||
@@ -87,7 +121,7 @@ auth
|
||||
.option('--label <label>', 'Token label (browser flow only)', 'CLI token')
|
||||
.option('--no-browser', 'Do not open browser (requires --token)')
|
||||
.action(async (options) => {
|
||||
const opts = resolveGlobalOpts()
|
||||
const opts = await resolveGlobalOpts()
|
||||
await cmdLoginFlow(opts, options, isInputAllowed())
|
||||
})
|
||||
|
||||
@@ -95,7 +129,7 @@ auth
|
||||
.command('logout')
|
||||
.description('Remove stored token')
|
||||
.action(async () => {
|
||||
const opts = resolveGlobalOpts()
|
||||
const opts = await resolveGlobalOpts()
|
||||
await cmdLogout(opts)
|
||||
})
|
||||
|
||||
@@ -103,7 +137,7 @@ auth
|
||||
.command('whoami')
|
||||
.description('Validate token')
|
||||
.action(async () => {
|
||||
const opts = resolveGlobalOpts()
|
||||
const opts = await resolveGlobalOpts()
|
||||
await cmdWhoami(opts)
|
||||
})
|
||||
|
||||
@@ -113,7 +147,7 @@ program
|
||||
.argument('<query...>', 'Query string')
|
||||
.option('--limit <n>', 'Max results', (value) => Number.parseInt(value, 10))
|
||||
.action(async (queryParts, options) => {
|
||||
const opts = resolveGlobalOpts()
|
||||
const opts = await resolveGlobalOpts()
|
||||
const query = queryParts.join(' ').trim()
|
||||
await cmdSearch(opts, query, options.limit)
|
||||
})
|
||||
@@ -125,7 +159,7 @@ program
|
||||
.option('--version <version>', 'Version to install')
|
||||
.option('--force', 'Overwrite existing folder')
|
||||
.action(async (slug, options) => {
|
||||
const opts = resolveGlobalOpts()
|
||||
const opts = await resolveGlobalOpts()
|
||||
await cmdInstall(opts, slug, options.version, options.force)
|
||||
})
|
||||
|
||||
@@ -137,7 +171,7 @@ program
|
||||
.option('--version <version>', 'Update to specific version (single slug only)')
|
||||
.option('--force', 'Overwrite when local files do not match any version')
|
||||
.action(async (slug, options) => {
|
||||
const opts = resolveGlobalOpts()
|
||||
const opts = await resolveGlobalOpts()
|
||||
await cmdUpdate(opts, slug, options, isInputAllowed())
|
||||
})
|
||||
|
||||
@@ -145,10 +179,26 @@ program
|
||||
.command('list')
|
||||
.description('List installed skills (from lockfile)')
|
||||
.action(async () => {
|
||||
const opts = resolveGlobalOpts()
|
||||
const opts = await resolveGlobalOpts()
|
||||
await cmdList(opts)
|
||||
})
|
||||
|
||||
program
|
||||
.command('explore')
|
||||
.description('Browse latest updated skills from the registry')
|
||||
.option(
|
||||
'--limit <n>',
|
||||
'Number of skills to show (max 50)',
|
||||
(value) => Number.parseInt(value, 10),
|
||||
25,
|
||||
)
|
||||
.action(async (options) => {
|
||||
const opts = await resolveGlobalOpts()
|
||||
const limit =
|
||||
typeof options.limit === 'number' && Number.isFinite(options.limit) ? options.limit : 25
|
||||
await cmdExplore(opts, limit)
|
||||
})
|
||||
|
||||
program
|
||||
.command('publish')
|
||||
.description('Publish skill from folder')
|
||||
@@ -160,7 +210,7 @@ program
|
||||
.option('--changelog <text>', 'Changelog text')
|
||||
.option('--tags <tags>', 'Comma-separated tags', 'latest')
|
||||
.action(async (folder, options) => {
|
||||
const opts = resolveGlobalOpts()
|
||||
const opts = await resolveGlobalOpts()
|
||||
await cmdPublish(opts, folder, options)
|
||||
})
|
||||
|
||||
@@ -170,7 +220,7 @@ program
|
||||
.argument('<slug>', 'Skill slug')
|
||||
.option('--yes', 'Skip confirmation')
|
||||
.action(async (slug, options) => {
|
||||
const opts = resolveGlobalOpts()
|
||||
const opts = await resolveGlobalOpts()
|
||||
await cmdDeleteSkill(opts, slug, options, isInputAllowed())
|
||||
})
|
||||
|
||||
@@ -180,7 +230,7 @@ program
|
||||
.argument('<slug>', 'Skill slug')
|
||||
.option('--yes', 'Skip confirmation')
|
||||
.action(async (slug, options) => {
|
||||
const opts = resolveGlobalOpts()
|
||||
const opts = await resolveGlobalOpts()
|
||||
await cmdUndeleteSkill(opts, slug, options, isInputAllowed())
|
||||
})
|
||||
|
||||
@@ -195,7 +245,7 @@ program
|
||||
.option('--tags <tags>', 'Comma-separated tags', 'latest')
|
||||
.option('--concurrency <n>', 'Concurrent registry checks (default: 4)', '4')
|
||||
.action(async (options) => {
|
||||
const opts = resolveGlobalOpts()
|
||||
const opts = await resolveGlobalOpts()
|
||||
const bump = String(options.bump ?? 'patch') as 'patch' | 'minor' | 'major'
|
||||
if (!['patch', 'minor', 'major'].includes(bump)) fail('--bump must be patch|minor|major')
|
||||
const concurrencyRaw = Number(options.concurrency ?? 4)
|
||||
@@ -217,7 +267,7 @@ program
|
||||
})
|
||||
|
||||
program.action(async () => {
|
||||
const opts = resolveGlobalOpts()
|
||||
const opts = await resolveGlobalOpts()
|
||||
const cfg = await readGlobalConfig()
|
||||
if (cfg?.token) {
|
||||
await cmdSync(opts, {}, isInputAllowed())
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { resolveClawdbotSkillRoots } from './clawdbotConfig.js'
|
||||
import { resolveClawdbotDefaultWorkspace, resolveClawdbotSkillRoots } from './clawdbotConfig.js'
|
||||
|
||||
const originalEnv = { ...process.env }
|
||||
|
||||
@@ -24,7 +24,15 @@ describe('resolveClawdbotSkillRoots', () => {
|
||||
|
||||
const config = `{
|
||||
// JSON5 comments + trailing commas supported
|
||||
agent: { workspace: '~/clawd-main', },
|
||||
agents: {
|
||||
defaults: { workspace: '~/clawd-main', },
|
||||
list: [
|
||||
{ id: 'work', name: 'Work Bot', workspace: '~/clawd-work', },
|
||||
{ id: 'family', workspace: '~/clawd-family', },
|
||||
],
|
||||
},
|
||||
// legacy entries still supported
|
||||
agent: { workspace: '~/clawd-legacy', },
|
||||
routing: {
|
||||
agents: {
|
||||
work: { name: 'Work Bot', workspace: '~/clawd-work', },
|
||||
@@ -57,6 +65,56 @@ describe('resolveClawdbotSkillRoots', () => {
|
||||
expect(labels[resolve('/opt/skills')]).toBe('Extra: skills')
|
||||
})
|
||||
|
||||
it('resolves default workspace from agents.defaults and agents.list', async () => {
|
||||
const base = await mkdtemp(join(tmpdir(), 'clawdhub-clawdbot-default-'))
|
||||
const home = join(base, 'home')
|
||||
const stateDir = join(base, 'state')
|
||||
const configPath = join(base, 'clawdbot.json')
|
||||
const workspaceMain = join(base, 'workspace-main')
|
||||
const workspaceList = join(base, 'workspace-list')
|
||||
|
||||
process.env.HOME = home
|
||||
process.env.CLAWDBOT_STATE_DIR = stateDir
|
||||
process.env.CLAWDBOT_CONFIG_PATH = configPath
|
||||
|
||||
const config = `{
|
||||
agents: {
|
||||
defaults: { workspace: "${workspaceMain}", },
|
||||
list: [
|
||||
{ id: 'main', workspace: "${workspaceList}", default: true },
|
||||
],
|
||||
},
|
||||
}`
|
||||
await writeFile(configPath, config, 'utf8')
|
||||
|
||||
const workspace = await resolveClawdbotDefaultWorkspace()
|
||||
expect(workspace).toBe(resolve(workspaceMain))
|
||||
})
|
||||
|
||||
it('falls back to default agent in agents.list when defaults missing', async () => {
|
||||
const base = await mkdtemp(join(tmpdir(), 'clawdhub-clawdbot-list-'))
|
||||
const home = join(base, 'home')
|
||||
const configPath = join(base, 'clawdbot.json')
|
||||
const workspaceMain = join(base, 'workspace-main')
|
||||
const workspaceWork = join(base, 'workspace-work')
|
||||
|
||||
process.env.HOME = home
|
||||
process.env.CLAWDBOT_CONFIG_PATH = configPath
|
||||
|
||||
const config = `{
|
||||
agents: {
|
||||
list: [
|
||||
{ id: 'main', workspace: "${workspaceMain}", default: true },
|
||||
{ id: 'work', workspace: "${workspaceWork}" },
|
||||
],
|
||||
},
|
||||
}`
|
||||
await writeFile(configPath, config, 'utf8')
|
||||
|
||||
const workspace = await resolveClawdbotDefaultWorkspace()
|
||||
expect(workspace).toBe(resolve(workspaceMain))
|
||||
})
|
||||
|
||||
it('respects CLAWDBOT_STATE_DIR and CLAWDBOT_CONFIG_PATH overrides', async () => {
|
||||
const base = await mkdtemp(join(tmpdir(), 'clawdhub-clawdbot-override-'))
|
||||
const home = join(base, 'home')
|
||||
|
||||
@@ -5,6 +5,15 @@ import JSON5 from 'json5'
|
||||
|
||||
type ClawdbotConfig = {
|
||||
agent?: { workspace?: string }
|
||||
agents?: {
|
||||
defaults?: { workspace?: string }
|
||||
list?: Array<{
|
||||
id?: string
|
||||
name?: string
|
||||
workspace?: string
|
||||
default?: boolean
|
||||
}>
|
||||
}
|
||||
routing?: {
|
||||
agents?: Record<
|
||||
string,
|
||||
@@ -37,11 +46,21 @@ export async function resolveClawdbotSkillRoots(): Promise<ClawdbotSkillRoots> {
|
||||
const config = await readClawdbotConfig()
|
||||
if (!config) return { roots, labels }
|
||||
|
||||
const mainWorkspace = resolveUserPath(config.agent?.workspace ?? '')
|
||||
const mainWorkspace = resolveUserPath(
|
||||
config.agents?.defaults?.workspace ?? config.agent?.workspace ?? '',
|
||||
)
|
||||
if (mainWorkspace) {
|
||||
pushRoot(roots, labels, join(mainWorkspace, 'skills'), 'Agent: main')
|
||||
}
|
||||
|
||||
const listedAgents = config.agents?.list ?? []
|
||||
for (const entry of listedAgents) {
|
||||
const workspace = resolveUserPath(entry?.workspace ?? '')
|
||||
if (!workspace) continue
|
||||
const name = entry?.name?.trim() || entry?.id?.trim() || 'agent'
|
||||
pushRoot(roots, labels, join(workspace, 'skills'), `Agent: ${name}`)
|
||||
}
|
||||
|
||||
const agents = config.routing?.agents ?? {}
|
||||
for (const [agentId, entry] of Object.entries(agents)) {
|
||||
const workspace = resolveUserPath(entry?.workspace ?? '')
|
||||
@@ -61,6 +80,22 @@ export async function resolveClawdbotSkillRoots(): Promise<ClawdbotSkillRoots> {
|
||||
return { roots, labels }
|
||||
}
|
||||
|
||||
export async function resolveClawdbotDefaultWorkspace(): Promise<string | null> {
|
||||
const config = await readClawdbotConfig()
|
||||
if (!config) return null
|
||||
|
||||
const defaultsWorkspace = resolveUserPath(
|
||||
config.agents?.defaults?.workspace ?? config.agent?.workspace ?? '',
|
||||
)
|
||||
if (defaultsWorkspace) return defaultsWorkspace
|
||||
|
||||
const listedAgents = config.agents?.list ?? []
|
||||
const defaultAgent =
|
||||
listedAgents.find((entry) => entry.default) ?? listedAgents.find((entry) => entry.id === 'main')
|
||||
const listWorkspace = resolveUserPath(defaultAgent?.workspace ?? '')
|
||||
return listWorkspace || null
|
||||
}
|
||||
|
||||
function resolveClawdbotStateDir() {
|
||||
const override = process.env.CLAWDBOT_STATE_DIR?.trim()
|
||||
if (override) return resolveUserPath(override)
|
||||
@@ -96,8 +131,17 @@ async function readClawdbotConfig(): Promise<ClawdbotConfig | null> {
|
||||
function pushRoot(roots: string[], labels: Record<string, string>, root: string, label?: string) {
|
||||
const resolved = resolveUserPath(root)
|
||||
if (!resolved) return
|
||||
roots.push(resolved)
|
||||
if (!roots.includes(resolved)) roots.push(resolved)
|
||||
if (!label) return
|
||||
const existing = labels[resolved]
|
||||
labels[resolved] = existing ? `${existing}, ${label}` : label
|
||||
if (!existing) {
|
||||
labels[resolved] = label
|
||||
return
|
||||
}
|
||||
const parts = existing
|
||||
.split(', ')
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean)
|
||||
if (parts.includes(label)) return
|
||||
labels[resolved] = `${existing}, ${label}`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/* @vitest-environment node */
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { GlobalOpts } from '../types'
|
||||
|
||||
const mockApiRequest = vi.fn()
|
||||
vi.mock('../../http.js', () => ({
|
||||
apiRequest: (...args: unknown[]) => mockApiRequest(...args),
|
||||
}))
|
||||
|
||||
const mockGetRegistry = vi.fn(async () => 'https://clawdhub.com')
|
||||
vi.mock('../registry.js', () => ({
|
||||
getRegistry: () => mockGetRegistry(),
|
||||
}))
|
||||
|
||||
const mockSpinner = { stop: vi.fn(), fail: vi.fn() }
|
||||
vi.mock('../ui.js', () => ({
|
||||
createSpinner: vi.fn(() => mockSpinner),
|
||||
formatError: (error: unknown) => (error instanceof Error ? error.message : String(error)),
|
||||
}))
|
||||
|
||||
const { clampLimit, cmdExplore, formatExploreLine } = await import('./skills')
|
||||
|
||||
const mockLog = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
function makeOpts(): GlobalOpts {
|
||||
return {
|
||||
workdir: '/work',
|
||||
dir: '/work/skills',
|
||||
site: 'https://clawdhub.com',
|
||||
registry: 'https://clawdhub.com',
|
||||
registrySource: 'default',
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('explore helpers', () => {
|
||||
it('clamps explore limits and handles non-finite values', () => {
|
||||
expect(clampLimit(-5)).toBe(1)
|
||||
expect(clampLimit(0)).toBe(1)
|
||||
expect(clampLimit(1)).toBe(1)
|
||||
expect(clampLimit(50)).toBe(50)
|
||||
expect(clampLimit(99)).toBe(50)
|
||||
expect(clampLimit(Number.NaN)).toBe(25)
|
||||
expect(clampLimit(Number.POSITIVE_INFINITY)).toBe(25)
|
||||
expect(clampLimit(Number.NaN, 10)).toBe(10)
|
||||
})
|
||||
|
||||
it('formats explore lines with relative time and truncation', () => {
|
||||
const now = 4 * 60 * 60 * 1000
|
||||
const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(now)
|
||||
const summary = 'a'.repeat(60)
|
||||
const line = formatExploreLine({
|
||||
slug: 'weather',
|
||||
summary,
|
||||
updatedAt: now - 2 * 60 * 60 * 1000,
|
||||
latestVersion: null,
|
||||
})
|
||||
expect(line).toBe(`weather v? 2h ago ${'a'.repeat(49)}…`)
|
||||
nowSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('cmdExplore', () => {
|
||||
it('clamps limit and handles empty results', async () => {
|
||||
mockApiRequest.mockResolvedValue({ items: [] })
|
||||
|
||||
await cmdExplore(makeOpts(), 0)
|
||||
|
||||
const [, args] = mockApiRequest.mock.calls[0] ?? []
|
||||
const url = new URL(String(args?.url))
|
||||
expect(url.searchParams.get('limit')).toBe('1')
|
||||
expect(mockLog).toHaveBeenCalledWith('No skills found.')
|
||||
})
|
||||
|
||||
it('prints formatted results', async () => {
|
||||
const now = 10 * 60 * 1000
|
||||
const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(now)
|
||||
const item = {
|
||||
slug: 'gog',
|
||||
summary: 'Google Workspace CLI for Gmail, Calendar, Drive and more.',
|
||||
updatedAt: now - 90 * 1000,
|
||||
latestVersion: { version: '1.2.3' },
|
||||
}
|
||||
mockApiRequest.mockResolvedValue({ items: [item] })
|
||||
|
||||
await cmdExplore(makeOpts(), 100)
|
||||
|
||||
const [, args] = mockApiRequest.mock.calls[0] ?? []
|
||||
const url = new URL(String(args?.url))
|
||||
expect(url.searchParams.get('limit')).toBe('50')
|
||||
expect(mockLog).toHaveBeenCalledWith(formatExploreLine(item))
|
||||
nowSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
@@ -5,6 +5,7 @@ import { apiRequest, downloadZip } from '../../http.js'
|
||||
import {
|
||||
ApiRoutes,
|
||||
ApiV1SearchResponseSchema,
|
||||
ApiV1SkillListResponseSchema,
|
||||
ApiV1SkillResolveResponseSchema,
|
||||
ApiV1SkillResponseSchema,
|
||||
} from '../../schema/index.js'
|
||||
@@ -241,6 +242,74 @@ export async function cmdList(opts: GlobalOpts) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdExplore(opts: GlobalOpts, limit = 25) {
|
||||
const registry = await getRegistry(opts, { cache: true })
|
||||
const spinner = createSpinner('Fetching latest skills')
|
||||
try {
|
||||
const url = new URL(ApiRoutes.skills, registry)
|
||||
const boundedLimit = clampLimit(limit)
|
||||
url.searchParams.set('limit', String(boundedLimit))
|
||||
const result = await apiRequest(
|
||||
registry,
|
||||
{ method: 'GET', url: url.toString() },
|
||||
ApiV1SkillListResponseSchema,
|
||||
)
|
||||
|
||||
spinner.stop()
|
||||
if (result.items.length === 0) {
|
||||
console.log('No skills found.')
|
||||
return
|
||||
}
|
||||
|
||||
for (const item of result.items) {
|
||||
console.log(formatExploreLine(item))
|
||||
}
|
||||
} catch (error) {
|
||||
spinner.fail(formatError(error))
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function formatExploreLine(item: {
|
||||
slug: string
|
||||
summary?: string | null
|
||||
updatedAt: number
|
||||
latestVersion?: { version: string } | null
|
||||
}) {
|
||||
const version = item.latestVersion?.version ?? '?'
|
||||
const age = formatRelativeTime(item.updatedAt)
|
||||
const summary = item.summary ? ` ${truncate(item.summary, 50)}` : ''
|
||||
return `${item.slug} v${version} ${age}${summary}`
|
||||
}
|
||||
|
||||
export function clampLimit(limit: number, fallback = 25) {
|
||||
if (!Number.isFinite(limit)) return fallback
|
||||
return Math.min(Math.max(1, limit), 50)
|
||||
}
|
||||
|
||||
function formatRelativeTime(timestamp: number): string {
|
||||
const now = Date.now()
|
||||
const diff = now - timestamp
|
||||
const seconds = Math.floor(diff / 1000)
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const days = Math.floor(hours / 24)
|
||||
|
||||
if (days > 30) {
|
||||
const months = Math.floor(days / 30)
|
||||
return `${months}mo ago`
|
||||
}
|
||||
if (days > 0) return `${days}d ago`
|
||||
if (hours > 0) return `${hours}h ago`
|
||||
if (minutes > 0) return `${minutes}m ago`
|
||||
return 'just now'
|
||||
}
|
||||
|
||||
function truncate(str: string, maxLen: number): string {
|
||||
if (str.length <= maxLen) return str
|
||||
return `${str.slice(0, maxLen - 1)}…`
|
||||
}
|
||||
|
||||
async function resolveSkillVersion(registry: string, slug: string, hash: string) {
|
||||
const url = new URL(ApiRoutes.resolve, registry)
|
||||
url.searchParams.set('slug', slug)
|
||||
|
||||
@@ -33,4 +33,43 @@ describe('discovery', () => {
|
||||
minCliVersion: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('parses apiBase config', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(
|
||||
async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
apiBase: 'https://api.example.com',
|
||||
authBase: 'https://auth.example.com',
|
||||
minCliVersion: '1.2.3',
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
),
|
||||
) as unknown as typeof fetch,
|
||||
)
|
||||
await expect(discoverRegistryFromSite('https://example.com')).resolves.toEqual({
|
||||
apiBase: 'https://api.example.com',
|
||||
authBase: 'https://auth.example.com',
|
||||
minCliVersion: '1.2.3',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns null when apiBase is empty', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(
|
||||
async () =>
|
||||
new Response(JSON.stringify({ apiBase: '' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
) as unknown as typeof fetch,
|
||||
)
|
||||
await expect(discoverRegistryFromSite('https://example.com')).resolves.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* @vitest-environment node */
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { apiRequest, downloadZip } from './http'
|
||||
import { apiRequest, apiRequestForm, downloadZip } from './http'
|
||||
import { ApiV1WhoamiResponseSchema } from './schema/index.js'
|
||||
|
||||
describe('apiRequest', () => {
|
||||
@@ -80,4 +80,77 @@ describe('apiRequest', () => {
|
||||
expect(url).toContain('version=1.0.0')
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('does not retry on non-retryable errors', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
text: async () => 'nope',
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
await expect(downloadZip('https://example.com', { slug: 'demo' })).rejects.toThrow('nope')
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
})
|
||||
|
||||
describe('apiRequestForm', () => {
|
||||
it('posts form data and returns json', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ ok: true }),
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
const form = new FormData()
|
||||
form.append('x', '1')
|
||||
const result = await apiRequestForm('https://example.com', {
|
||||
method: 'POST',
|
||||
path: '/upload',
|
||||
token: 'clh_token',
|
||||
form,
|
||||
})
|
||||
expect(result).toEqual({ ok: true })
|
||||
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
|
||||
expect(init.body).toBe(form)
|
||||
expect((init.headers as Record<string, string>).Authorization).toBe('Bearer clh_token')
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('retries on 429', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 429,
|
||||
text: async () => 'rate limited',
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
await expect(
|
||||
apiRequestForm('https://example.com', {
|
||||
method: 'POST',
|
||||
path: '/upload',
|
||||
form: new FormData(),
|
||||
}),
|
||||
).rejects.toThrow('rate limited')
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3)
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('falls back to HTTP status when body cannot be read', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 400,
|
||||
text: async () => {
|
||||
throw new Error('boom')
|
||||
},
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
await expect(
|
||||
apiRequestForm('https://example.com', {
|
||||
method: 'POST',
|
||||
path: '/upload',
|
||||
form: new FormData(),
|
||||
}),
|
||||
).rejects.toThrow('HTTP 400')
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,29 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import pRetry, { AbortError } from 'p-retry'
|
||||
import { Agent, setGlobalDispatcher } from 'undici'
|
||||
import type { ArkValidator } from './schema/index.js'
|
||||
import { ApiRoutes, parseArk } from './schema/index.js'
|
||||
|
||||
const REQUEST_TIMEOUT_MS = 15_000
|
||||
const REQUEST_TIMEOUT_SECONDS = Math.ceil(REQUEST_TIMEOUT_MS / 1000)
|
||||
const isBun = typeof process !== 'undefined' && Boolean(process.versions?.bun)
|
||||
|
||||
if (typeof process !== 'undefined' && process.versions?.node) {
|
||||
try {
|
||||
setGlobalDispatcher(
|
||||
new Agent({
|
||||
allowH2: true,
|
||||
connect: { timeout: REQUEST_TIMEOUT_MS },
|
||||
}),
|
||||
)
|
||||
} catch {
|
||||
// ignore dispatcher setup failures in non-node runtimes
|
||||
}
|
||||
}
|
||||
|
||||
type RequestArgs =
|
||||
| { method: 'GET' | 'POST' | 'DELETE'; path: string; token?: string; body?: unknown }
|
||||
| { method: 'GET' | 'POST' | 'DELETE'; url: string; token?: string; body?: unknown }
|
||||
@@ -20,6 +42,10 @@ export async function apiRequest<T>(
|
||||
const url = 'url' in args ? args.url : new URL(args.path, registry).toString()
|
||||
const json = await pRetry(
|
||||
async () => {
|
||||
if (isBun) {
|
||||
return await fetchJsonViaCurl(url, args)
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = { Accept: 'application/json' }
|
||||
if (args.token) headers.Authorization = `Bearer ${args.token}`
|
||||
let body: string | undefined
|
||||
@@ -27,7 +53,15 @@ export async function apiRequest<T>(
|
||||
headers['Content-Type'] = 'application/json'
|
||||
body = JSON.stringify(args.body ?? {})
|
||||
}
|
||||
const response = await fetch(url, { method: args.method, headers, body })
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort('Timeout'), REQUEST_TIMEOUT_MS)
|
||||
const response = await fetch(url, {
|
||||
method: args.method,
|
||||
headers,
|
||||
body,
|
||||
signal: controller.signal,
|
||||
})
|
||||
clearTimeout(timeout)
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => '')
|
||||
const message = text || `HTTP ${response.status}`
|
||||
@@ -62,9 +96,21 @@ export async function apiRequestForm<T>(
|
||||
const url = 'url' in args ? args.url : new URL(args.path, registry).toString()
|
||||
const json = await pRetry(
|
||||
async () => {
|
||||
if (isBun) {
|
||||
return await fetchJsonFormViaCurl(url, args)
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = { Accept: 'application/json' }
|
||||
if (args.token) headers.Authorization = `Bearer ${args.token}`
|
||||
const response = await fetch(url, { method: args.method, headers, body: args.form })
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort('Timeout'), REQUEST_TIMEOUT_MS)
|
||||
const response = await fetch(url, {
|
||||
method: args.method,
|
||||
headers,
|
||||
body: args.form,
|
||||
signal: controller.signal,
|
||||
})
|
||||
clearTimeout(timeout)
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => '')
|
||||
const message = text || `HTTP ${response.status}`
|
||||
@@ -87,7 +133,14 @@ export async function downloadZip(registry: string, args: { slug: string; versio
|
||||
if (args.version) url.searchParams.set('version', args.version)
|
||||
return pRetry(
|
||||
async () => {
|
||||
const response = await fetch(url.toString(), { method: 'GET' })
|
||||
if (isBun) {
|
||||
return await fetchBinaryViaCurl(url.toString())
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort('Timeout'), REQUEST_TIMEOUT_MS)
|
||||
const response = await fetch(url.toString(), { method: 'GET', signal: controller.signal })
|
||||
clearTimeout(timeout)
|
||||
if (!response.ok) {
|
||||
const message = (await response.text().catch(() => '')) || `HTTP ${response.status}`
|
||||
if (response.status === 429 || response.status >= 500) {
|
||||
@@ -100,3 +153,149 @@ export async function downloadZip(registry: string, args: { slug: string; versio
|
||||
{ retries: 2 },
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchJsonViaCurl(url: string, args: RequestArgs) {
|
||||
const headers = ['-H', 'Accept: application/json']
|
||||
if (args.token) {
|
||||
headers.push('-H', `Authorization: Bearer ${args.token}`)
|
||||
}
|
||||
const curlArgs = [
|
||||
'--silent',
|
||||
'--show-error',
|
||||
'--location',
|
||||
'--max-time',
|
||||
String(REQUEST_TIMEOUT_SECONDS),
|
||||
'--write-out',
|
||||
'\n%{http_code}',
|
||||
'-X',
|
||||
args.method,
|
||||
...headers,
|
||||
url,
|
||||
]
|
||||
if (args.method === 'POST') {
|
||||
curlArgs.push('-H', 'Content-Type: application/json')
|
||||
curlArgs.push('--data-binary', JSON.stringify(args.body ?? {}))
|
||||
}
|
||||
|
||||
const result = spawnSync('curl', curlArgs, { encoding: 'utf8' })
|
||||
if (result.status !== 0) {
|
||||
throw new Error(result.stderr || 'curl failed')
|
||||
}
|
||||
const output = result.stdout ?? ''
|
||||
const splitAt = output.lastIndexOf('\n')
|
||||
if (splitAt === -1) throw new Error('curl response missing status')
|
||||
const body = output.slice(0, splitAt)
|
||||
const status = Number(output.slice(splitAt + 1).trim())
|
||||
if (!Number.isFinite(status)) throw new Error('curl response missing status')
|
||||
if (status < 200 || status >= 300) {
|
||||
if (status === 429 || status >= 500) {
|
||||
throw new Error(body || `HTTP ${status}`)
|
||||
}
|
||||
throw new AbortError(body || `HTTP ${status}`)
|
||||
}
|
||||
return JSON.parse(body || 'null') as unknown
|
||||
}
|
||||
|
||||
async function fetchJsonFormViaCurl(url: string, args: FormRequestArgs) {
|
||||
const headers = ['-H', 'Accept: application/json']
|
||||
if (args.token) {
|
||||
headers.push('-H', `Authorization: Bearer ${args.token}`)
|
||||
}
|
||||
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'clawdhub-upload-'))
|
||||
try {
|
||||
const formArgs: string[] = []
|
||||
for (const [key, value] of args.form.entries()) {
|
||||
if (value instanceof Blob) {
|
||||
const filename = typeof (value as File).name === 'string' ? (value as File).name : 'file'
|
||||
const filePath = join(tempDir, filename)
|
||||
const bytes = new Uint8Array(await value.arrayBuffer())
|
||||
await writeFile(filePath, bytes)
|
||||
formArgs.push('-F', `${key}=@${filePath};filename=${filename}`)
|
||||
} else {
|
||||
formArgs.push('-F', `${key}=${String(value)}`)
|
||||
}
|
||||
}
|
||||
|
||||
const curlArgs = [
|
||||
'--silent',
|
||||
'--show-error',
|
||||
'--location',
|
||||
'--max-time',
|
||||
String(REQUEST_TIMEOUT_SECONDS),
|
||||
'--write-out',
|
||||
'\n%{http_code}',
|
||||
'-X',
|
||||
args.method,
|
||||
...headers,
|
||||
...formArgs,
|
||||
url,
|
||||
]
|
||||
|
||||
const result = spawnSync('curl', curlArgs, { encoding: 'utf8' })
|
||||
if (result.status !== 0) {
|
||||
throw new Error(result.stderr || 'curl failed')
|
||||
}
|
||||
const output = result.stdout ?? ''
|
||||
const splitAt = output.lastIndexOf('\n')
|
||||
if (splitAt === -1) throw new Error('curl response missing status')
|
||||
const body = output.slice(0, splitAt)
|
||||
const status = Number(output.slice(splitAt + 1).trim())
|
||||
if (!Number.isFinite(status)) throw new Error('curl response missing status')
|
||||
if (status < 200 || status >= 300) {
|
||||
if (status === 429 || status >= 500) {
|
||||
throw new Error(body || `HTTP ${status}`)
|
||||
}
|
||||
throw new AbortError(body || `HTTP ${status}`)
|
||||
}
|
||||
return JSON.parse(body || 'null') as unknown
|
||||
} finally {
|
||||
await rm(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchBinaryViaCurl(url: string) {
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'clawdhub-download-'))
|
||||
const filePath = join(tempDir, 'payload.bin')
|
||||
try {
|
||||
const curlArgs = [
|
||||
'--silent',
|
||||
'--show-error',
|
||||
'--location',
|
||||
'--max-time',
|
||||
String(REQUEST_TIMEOUT_SECONDS),
|
||||
'-o',
|
||||
filePath,
|
||||
'--write-out',
|
||||
'%{http_code}',
|
||||
url,
|
||||
]
|
||||
const result = spawnSync('curl', curlArgs, { encoding: 'utf8' })
|
||||
if (result.status !== 0) {
|
||||
throw new Error(result.stderr || 'curl failed')
|
||||
}
|
||||
const status = Number((result.stdout ?? '').trim())
|
||||
if (!Number.isFinite(status)) throw new Error('curl response missing status')
|
||||
if (status < 200 || status >= 300) {
|
||||
const body = await readFileSafe(filePath)
|
||||
const message = body ? new TextDecoder().decode(body) : `HTTP ${status}`
|
||||
if (status === 429 || status >= 500) {
|
||||
throw new Error(message)
|
||||
}
|
||||
throw new AbortError(message)
|
||||
}
|
||||
const bytes = await readFileSafe(filePath)
|
||||
return bytes ? new Uint8Array(bytes) : new Uint8Array()
|
||||
} finally {
|
||||
await rm(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
async function readFileSafe(path: string) {
|
||||
try {
|
||||
const { readFile } = await import('node:fs/promises')
|
||||
return await readFile(path)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/* @vitest-environment node */
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import * as schema from '.'
|
||||
import { isTextContentType, TEXT_FILE_EXTENSION_SET } from './textFiles'
|
||||
|
||||
describe('packages/clawdhub schema textFiles', () => {
|
||||
it('exports text-file extension set', () => {
|
||||
expect(TEXT_FILE_EXTENSION_SET.has('md')).toBe(true)
|
||||
expect(TEXT_FILE_EXTENSION_SET.has('exe')).toBe(false)
|
||||
})
|
||||
|
||||
it('detects text content types with parameters', () => {
|
||||
expect(isTextContentType('text/plain; charset=utf-8')).toBe(true)
|
||||
expect(isTextContentType('application/json; charset=utf-8')).toBe(true)
|
||||
expect(isTextContentType('application/octet-stream')).toBe(false)
|
||||
})
|
||||
|
||||
it('re-exports helpers from index', () => {
|
||||
expect(typeof schema.isTextContentType).toBe('function')
|
||||
expect(schema.isTextContentType('application/markdown')).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
const port = Number(process.env.PLAYWRIGHT_PORT || 4173)
|
||||
const baseURL = process.env.PLAYWRIGHT_BASE_URL || `http://127.0.0.1:${port}`
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
testMatch: /.*\.pw\.test\.ts/,
|
||||
timeout: 60_000,
|
||||
expect: { timeout: 10_000 },
|
||||
fullyParallel: true,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : [['list']],
|
||||
use: {
|
||||
baseURL,
|
||||
trace: 'retain-on-failure',
|
||||
},
|
||||
webServer: process.env.PLAYWRIGHT_BASE_URL
|
||||
? undefined
|
||||
: {
|
||||
command: 'bun run preview -- --host 127.0.0.1 --port 4173',
|
||||
url: baseURL,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
stdout: 'ignore',
|
||||
stderr: 'pipe',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
})
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PORT="${PLAYWRIGHT_PORT:-4173}"
|
||||
|
||||
if [[ -n "${PLAYWRIGHT_BASE_URL:-}" ]]; then
|
||||
echo "Running against $PLAYWRIGHT_BASE_URL"
|
||||
bun run test:pw
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Running against local preview server on http://127.0.0.1:$PORT"
|
||||
bun run build
|
||||
PLAYWRIGHT_PORT="$PORT" bun run test:pw
|
||||
@@ -79,8 +79,8 @@ function wrapText(value: string, maxChars: number, maxLines: number) {
|
||||
}
|
||||
|
||||
export function buildSoulOgSvg(params: SoulOgSvgParams) {
|
||||
const rawTitle = params.title.trim() || 'onlycrabs.ai'
|
||||
const rawDescription = params.description.trim() || 'SOUL.md bundle on onlycrabs.ai.'
|
||||
const rawTitle = params.title.trim() || 'SoulHub'
|
||||
const rawDescription = params.description.trim() || 'SOUL.md bundle on SoulHub.'
|
||||
|
||||
const cardX = 72
|
||||
const cardY = 96
|
||||
|
||||
@@ -46,10 +46,9 @@ async function ensureWasm() {
|
||||
await wasmInitPromise
|
||||
}
|
||||
|
||||
function buildFooter(host: string | null, slug: string, owner: string | null) {
|
||||
const base = host ? host : 'onlycrabs.ai'
|
||||
if (owner) return `${base}/@${owner}/${slug}`
|
||||
return `${base}/souls/${slug}`
|
||||
function buildFooter(slug: string, owner: string | null) {
|
||||
if (owner) return `@${owner}/${slug}`
|
||||
return `souls/${slug}`
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -75,9 +74,9 @@ export default defineEventHandler(async (event) => {
|
||||
const title = titleFromQuery || meta?.displayName || slug
|
||||
const description = descriptionFromQuery || meta?.summary || ''
|
||||
|
||||
const ownerLabel = owner ? `@${owner}` : 'onlycrabs.ai'
|
||||
const ownerLabel = owner ? `@${owner}` : 'SoulHub'
|
||||
const versionLabel = version ? `v${version}` : 'latest'
|
||||
const footer = buildFooter(getRequestHost(event), slug, owner || null)
|
||||
const footer = buildFooter(slug, owner || null)
|
||||
|
||||
const cacheKey = version ? 'public, max-age=31536000, immutable' : 'public, max-age=3600'
|
||||
setHeader(event, 'Cache-Control', cacheKey)
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/* @vitest-environment jsdom */
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { SkillsIndex } from '../routes/skills/index'
|
||||
|
||||
const navigateMock = vi.fn()
|
||||
const useQueryMock = vi.fn()
|
||||
const useActionMock = vi.fn()
|
||||
let searchMock: Record<string, unknown> = {}
|
||||
|
||||
vi.mock('@tanstack/react-router', () => ({
|
||||
createFileRoute: () => (_config: { component: unknown; validateSearch: unknown }) => ({
|
||||
useNavigate: () => navigateMock,
|
||||
useSearch: () => searchMock,
|
||||
}),
|
||||
Link: (props: { children: unknown }) => <a href="/">{props.children}</a>,
|
||||
}))
|
||||
|
||||
vi.mock('convex/react', () => ({
|
||||
useAction: (...args: unknown[]) => useActionMock(...args),
|
||||
useQuery: (...args: unknown[]) => useQueryMock(...args),
|
||||
}))
|
||||
|
||||
describe('SkillsIndex', () => {
|
||||
beforeEach(() => {
|
||||
useQueryMock.mockReset()
|
||||
useActionMock.mockReset()
|
||||
navigateMock.mockReset()
|
||||
searchMock = {}
|
||||
useActionMock.mockReturnValue(() => Promise.resolve([]))
|
||||
useQueryMock.mockReturnValue({ items: [], nextCursor: null })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('requests the first skills page', () => {
|
||||
render(<SkillsIndex />)
|
||||
expect(useQueryMock).toHaveBeenCalledWith(expect.anything(), {
|
||||
cursor: undefined,
|
||||
limit: 50,
|
||||
})
|
||||
})
|
||||
|
||||
it('renders an empty state when no skills are returned', () => {
|
||||
render(<SkillsIndex />)
|
||||
expect(screen.getByText('No skills match that filter.')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('skips list query and calls search when query is set', async () => {
|
||||
searchMock = { q: 'remind' }
|
||||
const actionFn = vi.fn().mockResolvedValue([])
|
||||
useActionMock.mockReturnValue(actionFn)
|
||||
useQueryMock.mockReturnValue(undefined)
|
||||
vi.useFakeTimers()
|
||||
|
||||
render(<SkillsIndex />)
|
||||
|
||||
expect(useQueryMock).toHaveBeenCalledWith(expect.anything(), 'skip')
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync()
|
||||
})
|
||||
expect(actionFn).toHaveBeenCalledWith({
|
||||
query: 'remind',
|
||||
highlightedOnly: false,
|
||||
limit: 50,
|
||||
})
|
||||
})
|
||||
|
||||
it('loads more results when search pagination is requested', async () => {
|
||||
searchMock = { q: 'remind' }
|
||||
vi.stubGlobal('IntersectionObserver', undefined)
|
||||
const actionFn = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(makeSearchResults(50))
|
||||
.mockResolvedValueOnce(makeSearchResults(100))
|
||||
useActionMock.mockReturnValue(actionFn)
|
||||
useQueryMock.mockReturnValue(undefined)
|
||||
vi.useFakeTimers()
|
||||
|
||||
render(<SkillsIndex />)
|
||||
await act(async () => {
|
||||
await vi.runAllTimersAsync()
|
||||
})
|
||||
|
||||
const loadMoreButton = screen.getByRole('button', { name: 'Load more' })
|
||||
await act(async () => {
|
||||
fireEvent.click(loadMoreButton)
|
||||
await vi.runAllTimersAsync()
|
||||
})
|
||||
|
||||
expect(actionFn).toHaveBeenLastCalledWith({
|
||||
query: 'remind',
|
||||
highlightedOnly: false,
|
||||
limit: 100,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function makeSearchResults(count: number) {
|
||||
return Array.from({ length: count }, (_, index) => ({
|
||||
score: 0.9,
|
||||
skill: {
|
||||
_id: `skill_${index}`,
|
||||
slug: `skill-${index}`,
|
||||
displayName: `Skill ${index}`,
|
||||
summary: `Summary ${index}`,
|
||||
tags: {},
|
||||
stats: {
|
||||
downloads: 0,
|
||||
installsCurrent: 0,
|
||||
installsAllTime: 0,
|
||||
stars: 0,
|
||||
versions: 1,
|
||||
comments: 0,
|
||||
},
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
},
|
||||
version: null,
|
||||
}))
|
||||
}
|
||||
@@ -9,8 +9,8 @@ export function Footer() {
|
||||
<div className="site-footer-row">
|
||||
<div className="site-footer-copy">
|
||||
{siteName} · A{' '}
|
||||
<a href="https://clawdbot.com" target="_blank" rel="noreferrer">
|
||||
ClawdBot
|
||||
<a href="https://clawd.bot" target="_blank" rel="noreferrer">
|
||||
Clawdbot
|
||||
</a>{' '}
|
||||
project ·{' '}
|
||||
<a href="https://github.com/clawdbot/clawdhub" target="_blank" rel="noreferrer">
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Menu, Monitor, Moon, Sun } from 'lucide-react'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { api } from '../../convex/_generated/api'
|
||||
import { gravatarUrl } from '../lib/gravatar'
|
||||
import { getClawdHubSiteUrl, getOnlyCrabsSiteUrl, getSiteMode, getSiteName } from '../lib/site'
|
||||
import { getClawdHubSiteUrl, getSiteMode, getSiteName } from '../lib/site'
|
||||
import { applyTheme, useThemeMode } from '../lib/theme'
|
||||
import { startThemeTransition } from '../lib/theme-transition'
|
||||
import {
|
||||
@@ -26,7 +26,6 @@ export default function Header() {
|
||||
const siteMode = getSiteMode()
|
||||
const siteName = useMemo(() => getSiteName(siteMode), [siteMode])
|
||||
const isSoulMode = siteMode === 'souls'
|
||||
const onlyCrabsUrl = getOnlyCrabsSiteUrl()
|
||||
const clawdHubUrl = getClawdHubSiteUrl()
|
||||
|
||||
const avatar = me?.image ?? (me?.email ? gravatarUrl(me.email) : undefined)
|
||||
@@ -57,11 +56,7 @@ export default function Header() {
|
||||
<span className="brand-name">{siteName}</span>
|
||||
</Link>
|
||||
<nav className="nav-links">
|
||||
{isSoulMode ? (
|
||||
<a href={clawdHubUrl}>ClawdHub</a>
|
||||
) : (
|
||||
<a href={onlyCrabsUrl}>onlycrabs.ai</a>
|
||||
)}
|
||||
{isSoulMode ? <a href={clawdHubUrl}>ClawdHub</a> : null}
|
||||
<Link
|
||||
to={isSoulMode ? '/souls' : '/skills'}
|
||||
search={
|
||||
@@ -97,13 +92,11 @@ export default function Header() {
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem asChild>
|
||||
{isSoulMode ? (
|
||||
{isSoulMode ? (
|
||||
<DropdownMenuItem asChild>
|
||||
<a href={clawdHubUrl}>ClawdHub</a>
|
||||
) : (
|
||||
<a href={onlyCrabsUrl}>onlycrabs.ai</a>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
to={isSoulMode ? '/souls' : '/skills'}
|
||||
|
||||
+3
-3
@@ -35,7 +35,7 @@ describe('og helpers', () => {
|
||||
summary: 'Personal north star notes.',
|
||||
version: '0.1.0',
|
||||
})
|
||||
expect(meta.title).toBe('North Star — onlycrabs.ai')
|
||||
expect(meta.title).toBe('North Star — SoulHub')
|
||||
expect(meta.description).toBe('Personal north star notes.')
|
||||
expect(meta.url).toContain('/souls/north-star')
|
||||
expect(meta.owner).toBe('someone')
|
||||
@@ -57,8 +57,8 @@ describe('og helpers', () => {
|
||||
|
||||
it('uses soul defaults when owner and summary are missing', () => {
|
||||
const meta = buildSoulMeta({ slug: 'signal' })
|
||||
expect(meta.title).toBe('signal — onlycrabs.ai')
|
||||
expect(meta.description).toMatch(/onlycrabs\.ai — the home for SOUL.md/i)
|
||||
expect(meta.title).toBe('signal — SoulHub')
|
||||
expect(meta.description).toMatch(/SoulHub — the home for SOUL.md/i)
|
||||
expect(meta.url).toContain('/souls/signal')
|
||||
expect(meta.owner).toBeNull()
|
||||
expect(meta.image).toContain('slug=signal')
|
||||
|
||||
+3
-4
@@ -33,8 +33,7 @@ type SoulMeta = {
|
||||
}
|
||||
|
||||
const DEFAULT_DESCRIPTION = 'ClawdHub — a fast skill registry for agents, with vector search.'
|
||||
const DEFAULT_SOUL_DESCRIPTION =
|
||||
'onlycrabs.ai — the home for SOUL.md bundles and personal system lore.'
|
||||
const DEFAULT_SOUL_DESCRIPTION = 'SoulHub — the home for SOUL.md bundles and personal system lore.'
|
||||
const OG_SKILL_IMAGE_LAYOUT_VERSION = '5'
|
||||
const OG_SOUL_IMAGE_LAYOUT_VERSION = '1'
|
||||
|
||||
@@ -125,9 +124,9 @@ export function buildSoulMeta(source: SoulMetaSource): SoulMeta {
|
||||
const displayName = clean(source.displayName) || clean(source.slug)
|
||||
const summary = clean(source.summary)
|
||||
const version = clean(source.version)
|
||||
const title = `${displayName} — onlycrabs.ai`
|
||||
const title = `${displayName} — SoulHub`
|
||||
const description =
|
||||
summary || (owner ? `Soul by @${owner} on onlycrabs.ai.` : DEFAULT_SOUL_DESCRIPTION)
|
||||
summary || (owner ? `Soul by @${owner} on SoulHub.` : DEFAULT_SOUL_DESCRIPTION)
|
||||
const url = `${siteUrl}/souls/${source.slug}`
|
||||
const imageParams = new URLSearchParams()
|
||||
imageParams.set('v', OG_SOUL_IMAGE_LAYOUT_VERSION)
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/* @vitest-environment node */
|
||||
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
detectSiteMode,
|
||||
detectSiteModeFromUrl,
|
||||
getClawdHubSiteUrl,
|
||||
getOnlyCrabsHost,
|
||||
getOnlyCrabsSiteUrl,
|
||||
getSiteDescription,
|
||||
getSiteMode,
|
||||
getSiteName,
|
||||
getSiteUrlForMode,
|
||||
} from './site'
|
||||
|
||||
function withMetaEnv<T>(values: Record<string, string | undefined>, run: () => T): T {
|
||||
const env = import.meta.env as unknown as Record<string, unknown>
|
||||
const previous = new Map<string, unknown>()
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
previous.set(key, env[key])
|
||||
if (value === undefined) {
|
||||
delete env[key]
|
||||
} else {
|
||||
env[key] = value
|
||||
}
|
||||
}
|
||||
try {
|
||||
return run()
|
||||
} finally {
|
||||
for (const [key, value] of previous.entries()) {
|
||||
if (value === undefined) delete env[key]
|
||||
else env[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
vi.unstubAllEnvs()
|
||||
})
|
||||
|
||||
describe('site helpers', () => {
|
||||
it('returns default and env configured site URLs', () => {
|
||||
expect(getClawdHubSiteUrl()).toBe('https://clawdhub.com')
|
||||
withMetaEnv({ VITE_SITE_URL: 'https://example.com' }, () => {
|
||||
expect(getClawdHubSiteUrl()).toBe('https://example.com')
|
||||
})
|
||||
})
|
||||
|
||||
it('picks SoulHub URL from explicit env', () => {
|
||||
withMetaEnv({ VITE_SOULHUB_SITE_URL: 'https://souls.example.com' }, () => {
|
||||
expect(getOnlyCrabsSiteUrl()).toBe('https://souls.example.com')
|
||||
})
|
||||
})
|
||||
|
||||
it('derives SoulHub URL from local VITE_SITE_URL', () => {
|
||||
withMetaEnv({ VITE_SITE_URL: 'http://localhost:3000' }, () => {
|
||||
expect(getOnlyCrabsSiteUrl()).toBe('http://localhost:3000')
|
||||
})
|
||||
withMetaEnv({ VITE_SITE_URL: 'http://127.0.0.1:3000' }, () => {
|
||||
expect(getOnlyCrabsSiteUrl()).toBe('http://127.0.0.1:3000')
|
||||
})
|
||||
withMetaEnv({ VITE_SITE_URL: 'http://0.0.0.0:3000' }, () => {
|
||||
expect(getOnlyCrabsSiteUrl()).toBe('http://0.0.0.0:3000')
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to default SoulHub URL for invalid VITE_SITE_URL', () => {
|
||||
withMetaEnv({ VITE_SITE_URL: 'not a url' }, () => {
|
||||
expect(getOnlyCrabsSiteUrl()).toBe('https://onlycrabs.ai')
|
||||
})
|
||||
})
|
||||
|
||||
it('detects site mode from host and URLs', () => {
|
||||
expect(detectSiteMode(null)).toBe('skills')
|
||||
|
||||
withMetaEnv({ VITE_SOULHUB_HOST: 'souls.example.com' }, () => {
|
||||
expect(getOnlyCrabsHost()).toBe('souls.example.com')
|
||||
expect(detectSiteMode('souls.example.com')).toBe('souls')
|
||||
expect(detectSiteMode('sub.souls.example.com')).toBe('souls')
|
||||
expect(detectSiteMode('clawdhub.com')).toBe('skills')
|
||||
|
||||
expect(detectSiteModeFromUrl('https://souls.example.com/x')).toBe('souls')
|
||||
expect(detectSiteModeFromUrl('souls.example.com')).toBe('souls')
|
||||
expect(detectSiteModeFromUrl('https://clawdhub.com')).toBe('skills')
|
||||
})
|
||||
})
|
||||
|
||||
it('detects site mode from window when available', () => {
|
||||
withMetaEnv({ VITE_SOULHUB_HOST: 'onlycrabs.ai' }, () => {
|
||||
vi.stubGlobal('window', { location: { hostname: 'onlycrabs.ai' } } as unknown as Window)
|
||||
expect(getSiteMode()).toBe('souls')
|
||||
})
|
||||
})
|
||||
|
||||
it('detects site mode from env on the server', () => {
|
||||
withMetaEnv({ VITE_SITE_MODE: 'souls', VITE_SOULHUB_HOST: 'onlycrabs.ai' }, () => {
|
||||
expect(getSiteMode()).toBe('souls')
|
||||
})
|
||||
withMetaEnv({ VITE_SITE_MODE: 'skills', VITE_SOULHUB_HOST: 'onlycrabs.ai' }, () => {
|
||||
expect(getSiteMode()).toBe('skills')
|
||||
})
|
||||
})
|
||||
|
||||
it('detects site mode from VITE_SOULHUB_SITE_URL and SITE_URL fallback', () => {
|
||||
withMetaEnv(
|
||||
{ VITE_SITE_MODE: undefined, VITE_SOULHUB_SITE_URL: 'https://onlycrabs.ai' },
|
||||
() => {
|
||||
expect(getSiteMode()).toBe('souls')
|
||||
},
|
||||
)
|
||||
|
||||
withMetaEnv({ VITE_SOULHUB_SITE_URL: undefined, VITE_SITE_URL: undefined }, () => {
|
||||
vi.stubEnv('SITE_URL', 'https://onlycrabs.ai')
|
||||
expect(getSiteMode()).toBe('souls')
|
||||
})
|
||||
})
|
||||
|
||||
it('derives site metadata from mode', () => {
|
||||
expect(getSiteName('skills')).toBe('ClawdHub')
|
||||
expect(getSiteName('souls')).toBe('SoulHub')
|
||||
|
||||
expect(getSiteDescription('skills')).toContain('ClawdHub')
|
||||
expect(getSiteDescription('souls')).toContain('SoulHub')
|
||||
|
||||
expect(getSiteUrlForMode('skills')).toBe('https://clawdhub.com')
|
||||
expect(getSiteUrlForMode('souls')).toBe('https://onlycrabs.ai')
|
||||
})
|
||||
})
|
||||
+2
-2
@@ -70,12 +70,12 @@ export function getSiteMode(): SiteMode {
|
||||
}
|
||||
|
||||
export function getSiteName(mode: SiteMode = getSiteMode()) {
|
||||
return mode === 'souls' ? 'onlycrabs.ai' : 'ClawdHub'
|
||||
return mode === 'souls' ? 'SoulHub' : 'ClawdHub'
|
||||
}
|
||||
|
||||
export function getSiteDescription(mode: SiteMode = getSiteMode()) {
|
||||
return mode === 'souls'
|
||||
? 'onlycrabs.ai — the home for SOUL.md bundles and personal system lore.'
|
||||
? 'SoulHub — the home for SOUL.md bundles and personal system lore.'
|
||||
: 'ClawdHub — a fast skill registry for agents, with vector search.'
|
||||
}
|
||||
|
||||
|
||||
@@ -115,8 +115,8 @@ export interface FileRoutesByFullPath {
|
||||
'/skills/$slug': typeof SkillsSlugRoute
|
||||
'/souls/$slug': typeof SoulsSlugRoute
|
||||
'/u/$handle': typeof UHandleRoute
|
||||
'/skills': typeof SkillsIndexRoute
|
||||
'/souls': typeof SoulsIndexRoute
|
||||
'/skills/': typeof SkillsIndexRoute
|
||||
'/souls/': typeof SoulsIndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
@@ -169,8 +169,8 @@ export interface FileRouteTypes {
|
||||
| '/skills/$slug'
|
||||
| '/souls/$slug'
|
||||
| '/u/$handle'
|
||||
| '/skills'
|
||||
| '/souls'
|
||||
| '/skills/'
|
||||
| '/souls/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
@@ -286,14 +286,14 @@ declare module '@tanstack/react-router' {
|
||||
'/souls/': {
|
||||
id: '/souls/'
|
||||
path: '/souls'
|
||||
fullPath: '/souls'
|
||||
fullPath: '/souls/'
|
||||
preLoaderRoute: typeof SoulsIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/skills/': {
|
||||
id: '/skills/'
|
||||
path: '/skills'
|
||||
fullPath: '/skills'
|
||||
fullPath: '/skills/'
|
||||
preLoaderRoute: typeof SkillsIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
|
||||
+10
-3
@@ -103,9 +103,16 @@ function SkillsHome() {
|
||||
<Link to="/upload" search={{ updateSlug: undefined }} className="btn btn-primary">
|
||||
Publish a skill
|
||||
</Link>
|
||||
<Link to="/search" search={{ q: undefined, highlighted: undefined }} className="btn">
|
||||
<button
|
||||
className="btn"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSearchMode(true)
|
||||
inputRef.current?.focus()
|
||||
}}
|
||||
>
|
||||
Explore search
|
||||
</Link>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hero-card hero-search-card fade-up" data-delay="2">
|
||||
@@ -336,7 +343,7 @@ function OnlyCrabsHome() {
|
||||
<div className="hero-inner">
|
||||
<div className="hero-copy fade-up" data-delay="1">
|
||||
<span className="hero-badge">SOUL.md, shared.</span>
|
||||
<h1 className="hero-title">onlycrabs.ai, where system lore lives.</h1>
|
||||
<h1 className="hero-title">SoulHub, where system lore lives.</h1>
|
||||
<p className="hero-subtitle">
|
||||
Share SOUL.md bundles, version them like docs, and keep personal system lore in one
|
||||
public place.
|
||||
|
||||
+153
-27
@@ -1,11 +1,12 @@
|
||||
import { createFileRoute, Link } from '@tanstack/react-router'
|
||||
import { useQuery } from 'convex/react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useAction, useQuery } from 'convex/react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { api } from '../../../convex/_generated/api'
|
||||
import type { Doc } from '../../../convex/_generated/dataModel'
|
||||
import { SkillCard } from '../../components/SkillCard'
|
||||
|
||||
const sortKeys = ['newest', 'downloads', 'installs', 'stars', 'name', 'updated'] as const
|
||||
const pageSize = 50
|
||||
type SortKey = (typeof sortKeys)[number]
|
||||
type SortDir = 'asc' | 'desc'
|
||||
|
||||
@@ -33,7 +34,7 @@ export const Route = createFileRoute('/skills/')({
|
||||
component: SkillsIndex,
|
||||
})
|
||||
|
||||
function SkillsIndex() {
|
||||
export function SkillsIndex() {
|
||||
const navigate = Route.useNavigate()
|
||||
const search = Route.useSearch()
|
||||
const sort = search.sort ?? 'newest'
|
||||
@@ -41,29 +42,107 @@ function SkillsIndex() {
|
||||
const view = search.view ?? 'list'
|
||||
const highlightedOnly = search.highlighted ?? false
|
||||
const [query, setQuery] = useState(search.q ?? '')
|
||||
const searchSkills = useAction(api.search.searchSkills)
|
||||
const [pages, setPages] = useState<
|
||||
Array<{ skill: Doc<'skills'>; latestVersion: Doc<'skillVersions'> | null }>
|
||||
>([])
|
||||
const [cursor, setCursor] = useState<string | null>(null)
|
||||
const [nextCursor, setNextCursor] = useState<string | null>(null)
|
||||
const [searchResults, setSearchResults] = useState<
|
||||
Array<{ skill: Doc<'skills'>; version: Doc<'skillVersions'> | null; score: number }>
|
||||
>([])
|
||||
const [searchLimit, setSearchLimit] = useState(pageSize)
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
const searchRequest = useRef(0)
|
||||
const loadMoreRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const items = useQuery(api.skills.listWithLatest, { limit: 500 }) as
|
||||
| Array<{ skill: Doc<'skills'>; latestVersion: Doc<'skillVersions'> | null }>
|
||||
const trimmedQuery = useMemo(() => query.trim(), [query])
|
||||
const hasQuery = trimmedQuery.length > 0
|
||||
const searchKey = trimmedQuery ? `${trimmedQuery}::${highlightedOnly ? '1' : '0'}` : ''
|
||||
|
||||
const listPage = useQuery(
|
||||
api.skills.listPublicPage,
|
||||
hasQuery ? 'skip' : { cursor: cursor ?? undefined, limit: pageSize },
|
||||
) as
|
||||
| {
|
||||
items: Array<{ skill: Doc<'skills'>; latestVersion: Doc<'skillVersions'> | null }>
|
||||
nextCursor: string | null
|
||||
}
|
||||
| undefined
|
||||
const isLoadingSkills = items === undefined
|
||||
const isLoadingList = !hasQuery && pages.length === 0 && listPage === undefined
|
||||
|
||||
useEffect(() => {
|
||||
setQuery(search.q ?? '')
|
||||
}, [search.q])
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const value = query.trim().toLowerCase()
|
||||
const all = (items ?? []).filter((entry) =>
|
||||
highlightedOnly ? entry.skill.batch === 'highlighted' : true,
|
||||
)
|
||||
if (!value) return all
|
||||
return all.filter((entry) => {
|
||||
const skill = entry.skill
|
||||
if (skill.slug.toLowerCase().includes(value)) return true
|
||||
if (skill.displayName.toLowerCase().includes(value)) return true
|
||||
return (skill.summary ?? '').toLowerCase().includes(value)
|
||||
})
|
||||
}, [highlightedOnly, query, items])
|
||||
useEffect(() => {
|
||||
if (hasQuery) return
|
||||
setPages([])
|
||||
setCursor(null)
|
||||
setNextCursor(null)
|
||||
}, [hasQuery])
|
||||
|
||||
useEffect(() => {
|
||||
if (hasQuery || !listPage) return
|
||||
setNextCursor(listPage.nextCursor)
|
||||
setPages((prev) => (cursor ? [...prev, ...listPage.items] : listPage.items))
|
||||
}, [cursor, hasQuery, listPage])
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchKey) {
|
||||
setSearchResults([])
|
||||
setIsSearching(false)
|
||||
return
|
||||
}
|
||||
setSearchResults([])
|
||||
setSearchLimit(pageSize)
|
||||
}, [searchKey])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasQuery) return
|
||||
searchRequest.current += 1
|
||||
const requestId = searchRequest.current
|
||||
setIsSearching(true)
|
||||
const handle = window.setTimeout(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
const data = (await searchSkills({
|
||||
query: trimmedQuery,
|
||||
highlightedOnly,
|
||||
limit: searchLimit,
|
||||
})) as Array<{
|
||||
skill: Doc<'skills'>
|
||||
version: Doc<'skillVersions'> | null
|
||||
score: number
|
||||
}>
|
||||
if (requestId === searchRequest.current) {
|
||||
setSearchResults(data)
|
||||
}
|
||||
} finally {
|
||||
if (requestId === searchRequest.current) {
|
||||
setIsSearching(false)
|
||||
}
|
||||
}
|
||||
})()
|
||||
}, 220)
|
||||
return () => window.clearTimeout(handle)
|
||||
}, [hasQuery, highlightedOnly, searchLimit, searchSkills, trimmedQuery])
|
||||
|
||||
const baseItems = useMemo(() => {
|
||||
if (hasQuery) {
|
||||
return searchResults.map((entry) => ({
|
||||
skill: entry.skill,
|
||||
latestVersion: entry.version,
|
||||
}))
|
||||
}
|
||||
return pages
|
||||
}, [hasQuery, pages, searchResults])
|
||||
|
||||
const filtered = useMemo(
|
||||
() =>
|
||||
baseItems.filter((entry) => (highlightedOnly ? entry.skill.batch === 'highlighted' : true)),
|
||||
[baseItems, highlightedOnly],
|
||||
)
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
const multiplier = dir === 'asc' ? 1 : -1
|
||||
@@ -93,10 +172,39 @@ function SkillsIndex() {
|
||||
return results
|
||||
}, [dir, filtered, sort])
|
||||
|
||||
const showing = sorted.length
|
||||
const total = items?.filter((entry) =>
|
||||
highlightedOnly ? entry.skill.batch === 'highlighted' : true,
|
||||
).length
|
||||
const isLoadingSkills = hasQuery ? isSearching && searchResults.length === 0 : isLoadingList
|
||||
const canLoadMore = hasQuery
|
||||
? !isSearching && searchResults.length === searchLimit && searchResults.length > 0
|
||||
: nextCursor !== null
|
||||
const isLoadingMore = hasQuery
|
||||
? isSearching && searchResults.length > 0
|
||||
: listPage === undefined && pages.length > 0
|
||||
const canAutoLoad = typeof IntersectionObserver !== 'undefined'
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (isLoadingMore || !canLoadMore) return
|
||||
if (hasQuery) {
|
||||
setSearchLimit((value) => value + pageSize)
|
||||
} else if (nextCursor) {
|
||||
setCursor(nextCursor)
|
||||
}
|
||||
}, [canLoadMore, hasQuery, isLoadingMore, nextCursor])
|
||||
|
||||
useEffect(() => {
|
||||
if (!canLoadMore || typeof IntersectionObserver === 'undefined') return
|
||||
const target = loadMoreRef.current
|
||||
if (!target) return
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries.some((entry) => entry.isIntersecting)) {
|
||||
loadMore()
|
||||
}
|
||||
},
|
||||
{ rootMargin: '200px' },
|
||||
)
|
||||
observer.observe(target)
|
||||
return () => observer.disconnect()
|
||||
}, [canLoadMore, loadMore])
|
||||
|
||||
return (
|
||||
<main className="section">
|
||||
@@ -108,9 +216,7 @@ function SkillsIndex() {
|
||||
<p className="section-subtitle" style={{ marginBottom: 0 }}>
|
||||
{isLoadingSkills
|
||||
? 'Loading skills…'
|
||||
: `${showing}${typeof total === 'number' ? ` of ${total}` : ''} skills${
|
||||
highlightedOnly ? ' (highlighted)' : ''
|
||||
}.`}
|
||||
: `Browse the skill library${highlightedOnly ? ' (highlighted)' : ''}.`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="skills-toolbar">
|
||||
@@ -209,7 +315,7 @@ function SkillsIndex() {
|
||||
<div className="card">
|
||||
<div className="loading-indicator">Loading skills…</div>
|
||||
</div>
|
||||
) : showing === 0 ? (
|
||||
) : sorted.length === 0 ? (
|
||||
<div className="card">No skills match that filter.</div>
|
||||
) : view === 'cards' ? (
|
||||
<div className="grid">
|
||||
@@ -276,6 +382,26 @@ function SkillsIndex() {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canLoadMore ? (
|
||||
<div
|
||||
ref={canAutoLoad ? loadMoreRef : null}
|
||||
className="card"
|
||||
style={{ marginTop: 16, display: 'flex', justifyContent: 'center' }}
|
||||
>
|
||||
{canAutoLoad ? (
|
||||
isLoadingMore ? (
|
||||
'Loading more…'
|
||||
) : (
|
||||
'Scroll to load more'
|
||||
)
|
||||
) : (
|
||||
<button className="btn" type="button" onClick={loadMore} disabled={isLoadingMore}>
|
||||
{isLoadingMore ? 'Loading…' : 'Load more'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
+7
-2
@@ -2357,7 +2357,7 @@ code {
|
||||
color: var(--accent-deep);
|
||||
}
|
||||
|
||||
.markdown code {
|
||||
.markdown :not(pre) > code {
|
||||
background: rgba(255, 107, 74, 0.12);
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
@@ -2397,11 +2397,16 @@ code {
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .markdown code {
|
||||
[data-theme="dark"] .markdown :not(pre) > code {
|
||||
background: rgba(255, 131, 95, 0.2);
|
||||
color: #ffe9de;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .markdown pre code {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.fade-up {
|
||||
animation: fadeUp 0.6s ease forwards;
|
||||
opacity: 0;
|
||||
|
||||
Reference in New Issue
Block a user