mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 17:02:11 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca70633b92 | ||
|
|
b8f2e4d7bf |
@@ -15,12 +15,10 @@ jobs:
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.6
|
||||
bun-version: 1.3.5
|
||||
|
||||
- name: Install
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Peer deps
|
||||
run: bun run check:peers
|
||||
|
||||
- name: Lint
|
||||
run: bun run lint
|
||||
@@ -38,3 +36,4 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
run: bun run build
|
||||
|
||||
|
||||
+1
-8
@@ -1,18 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## 0.2.1 - Unreleased
|
||||
|
||||
### Added
|
||||
- CLI: add `explore` command for latest updates, with limit clamping + tests/docs (thanks @jdrhyne, #14).
|
||||
|
||||
### Fixed
|
||||
- Web: keep search mode navigation and state in sync (thanks @NACC96, #12).
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "clawdhub",
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.37.4",
|
||||
"@auth/core": "^0.34.3",
|
||||
"@convex-dev/auth": "^0.0.90",
|
||||
"@fontsource/bricolage-grotesque": "^5.2.10",
|
||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||
@@ -16,10 +15,10 @@
|
||||
"@resvg/resvg-wasm": "^2.6.2",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-devtools": "^0.9.2",
|
||||
"@tanstack/react-router": "^1.151.6",
|
||||
"@tanstack/react-router-devtools": "^1.151.6",
|
||||
"@tanstack/react-start": "^1.152.0",
|
||||
"@tanstack/router-plugin": "^1.151.6",
|
||||
"@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": "workspace:*",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -61,7 +60,7 @@
|
||||
},
|
||||
"packages/clawdhub": {
|
||||
"name": "clawdhub",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.0",
|
||||
"bin": {
|
||||
"clawdhub": "bin/clawdhub.js",
|
||||
},
|
||||
@@ -86,10 +85,10 @@
|
||||
"name": "clawdhub-schema",
|
||||
"version": "0.0.2",
|
||||
"dependencies": {
|
||||
"arktype": "^2.1.29",
|
||||
"arktype": "latest",
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3",
|
||||
"typescript": "latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -106,7 +105,7 @@
|
||||
|
||||
"@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
|
||||
|
||||
"@auth/core": ["@auth/core@0.37.4", "", { "dependencies": { "@panva/hkdf": "^1.2.1", "jose": "^5.9.6", "oauth4webapi": "^3.1.1", "preact": "10.24.3", "preact-render-to-string": "6.5.11" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^6.8.0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-HOXJwXWXQRhbBDHlMU0K/6FT1v+wjtzdKhsNg0ZN7/gne6XPsIrjZ4daMcFnbq0Z/vsAbYBinQhhua0d77v7qw=="],
|
||||
"@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=="],
|
||||
|
||||
@@ -552,37 +551,37 @@
|
||||
|
||||
"@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.151.6", "", { "dependencies": { "@tanstack/history": "1.151.1", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.151.6", "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-KDbz7kacZCOoDrUwYljz4I/qjqVGq+bgUhpi/CWubi7by0GZ3JEECwFl/+k+4V6ATinJDjTNmCGwFcdwqjQDtA=="],
|
||||
"@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.151.6", "", { "dependencies": { "@tanstack/router-devtools-core": "1.151.6" }, "peerDependencies": { "@tanstack/react-router": "^1.151.6", "@tanstack/router-core": "^1.151.6", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-mRRFzIAIOAWYcZrEr0FYy/1FmM51iWwUdK0J3nWuXjAIeEb7uizS0HkeNbzX5yxfGZgkplk23eCXIUmJcDuVRQ=="],
|
||||
"@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.152.0", "", { "dependencies": { "@tanstack/react-router": "1.151.6", "@tanstack/react-start-client": "1.152.0", "@tanstack/react-start-server": "1.152.0", "@tanstack/router-utils": "^1.143.11", "@tanstack/start-client-core": "1.152.0", "@tanstack/start-plugin-core": "1.152.0", "@tanstack/start-server-core": "1.152.0", "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-btRNNIJGnXVEmD9yuiyNRvMITX5aocVJVdv/LQHxca4EcxntPEn1+HboPXFr6SDl9UNekH/6NZqv3LPz9tDm7A=="],
|
||||
"@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.152.0", "", { "dependencies": { "@tanstack/react-router": "1.151.6", "@tanstack/router-core": "1.151.6", "@tanstack/start-client-core": "1.152.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-YI2VVdCLk86QP5Q0oMXZWdk551haIisuF5AIr+S9ZAF415s4AgDpKRlXX251aqiHXYvv4rFnV1c9o7w02031Cw=="],
|
||||
"@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.152.0", "", { "dependencies": { "@tanstack/history": "1.151.1", "@tanstack/react-router": "1.151.6", "@tanstack/router-core": "1.151.6", "@tanstack/start-client-core": "1.152.0", "@tanstack/start-server-core": "1.152.0" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-oz3J3Ipj04IUAfVSE1DD41x8+7DDWHZez+fCqLnk8sw63ct013CeKwIvW9v1n0OtU6axekuVHHYBOsF5wO5/lg=="],
|
||||
"@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.151.6", "", { "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-eyqWx6vhKffkINWLujDF2sxAG9GE/XUdi3HrlD94ddJO9MBi/90a1HJaTYFSV8LmngjcRv8A3tt7OvFdv/UqhA=="],
|
||||
"@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.151.6", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.151.6", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-OHGGvEtnANEbEwjYCChbvCyCLk/3Cqh9G5bhM5DVqrZ+b9wfeu46IdEsbSi1JfuK2sCHNMS5MrJaE2HZPsFx6Q=="],
|
||||
"@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.151.6", "", { "dependencies": { "@tanstack/router-core": "1.151.6", "@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-IS4tkrkLIwI2EViGlUXCVgnKJ4EhWMM6w75XoJqd0X4t6K0/OiHkr3AQ0f2qZXbNciqLGxS88GLe5UiSAOS5Vw=="],
|
||||
"@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.151.6", "", { "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.6", "@tanstack/router-generator": "1.151.6", "@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.6", "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-Kz9wmAgcylung1KoXvEEVTW91PNh4U65MgVwSmz5fYQP7UqNHPvHqBFWKEu17a45SfZ4LufsNW3LTFT35tWGXg=="],
|
||||
"@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.152.0", "", { "dependencies": { "@tanstack/router-core": "1.151.6", "@tanstack/start-fn-stubs": "1.151.3", "@tanstack/start-storage-context": "1.151.6", "seroval": "^1.4.1", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-G88urpJImiGZttawtciSj46Ko57TXO2pd11Zef6Yw1VLD6fJs+RF0q9N0P98SGs9Jm0oUjLusi3Ti550fkQ2qw=="],
|
||||
"@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.151.3", "", {}, "sha512-/zWBnfsOwact936Bn0CxigudU1QRZdiNTsK7ME/LMXXA66XsDxkryX5+5FeGwU5ETNPfLAx6pRUet1mtUKnLCg=="],
|
||||
|
||||
"@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.152.0", "", { "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.6", "@tanstack/router-generator": "1.151.6", "@tanstack/router-plugin": "1.151.6", "@tanstack/router-utils": "1.143.11", "@tanstack/start-client-core": "1.152.0", "@tanstack/start-server-core": "1.152.0", "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-XT+ECwpmi6cQj6gb9QHo1XOOw+7zGXOvwnrijg3HSzNKFDlawHycgOotnViTOjtVjhHLpW1/yGyivWX5OC8wTw=="],
|
||||
"@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.152.0", "", { "dependencies": { "@tanstack/history": "1.151.1", "@tanstack/router-core": "1.151.6", "@tanstack/start-client-core": "1.152.0", "@tanstack/start-storage-context": "1.151.6", "h3-v2": "npm:h3@2.0.1-rc.7", "seroval": "^1.4.1", "tiny-invariant": "^1.3.3" } }, "sha512-bMmc4tIhR3FClb1iWLw0zuhvY71b9k2iz8Jw0tSK/KKi++gl1bGyJjuPtx8kDbx5Ix1SoXp4QNdXuo7dv5INfQ=="],
|
||||
"@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.151.6", "", { "dependencies": { "@tanstack/router-core": "1.151.6" } }, "sha512-MvTcT40qnqatIpKjWSfMRxFzTkprGBxhX2c+em58iZLEsGksitMUWbprknD6AIUqjHty8V3LuhULks/o6tSugQ=="],
|
||||
"@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=="],
|
||||
|
||||
@@ -606,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=="],
|
||||
@@ -740,7 +741,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -1074,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=="],
|
||||
|
||||
@@ -1118,9 +1119,9 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -1344,6 +1345,10 @@
|
||||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Z82FDl1ByxqPEPrAYYeTQVlx2FSHPe1qwX465c+96IRS3fTdSYRoJcRxg3g2fEG5I69z1dSEWQlNRRr0/677mg=="],
|
||||
@@ -1380,6 +1385,8 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -26,8 +26,6 @@ import {
|
||||
soulsDeleteRouterV1Http,
|
||||
soulsGetRouterV1Http,
|
||||
soulsPostRouterV1Http,
|
||||
starsDeleteRouterV1Http,
|
||||
starsPostRouterV1Http,
|
||||
whoamiV1Http,
|
||||
} from './httpApiV1'
|
||||
|
||||
@@ -83,18 +81,6 @@ http.route({
|
||||
handler: skillsDeleteRouterV1Http,
|
||||
})
|
||||
|
||||
http.route({
|
||||
pathPrefix: `${ApiRoutes.stars}/`,
|
||||
method: 'POST',
|
||||
handler: starsPostRouterV1Http,
|
||||
})
|
||||
|
||||
http.route({
|
||||
pathPrefix: `${ApiRoutes.stars}/`,
|
||||
method: 'DELETE',
|
||||
handler: starsDeleteRouterV1Http,
|
||||
})
|
||||
|
||||
http.route({
|
||||
path: ApiRoutes.whoami,
|
||||
method: 'GET',
|
||||
|
||||
@@ -498,62 +498,4 @@ describe('httpApiV1 handlers', () => {
|
||||
)
|
||||
expect(response2.status).toBe(200)
|
||||
})
|
||||
|
||||
it('stars require auth', async () => {
|
||||
vi.mocked(requireApiTokenUser).mockRejectedValueOnce(new Error('Unauthorized'))
|
||||
const runMutation = vi.fn().mockResolvedValue(okRate())
|
||||
const response = await __handlers.starsPostRouterV1Handler(
|
||||
makeCtx({ runMutation }),
|
||||
new Request('https://example.com/api/v1/stars/demo', { method: 'POST' }),
|
||||
)
|
||||
expect(response.status).toBe(401)
|
||||
})
|
||||
|
||||
it('stars add succeeds', async () => {
|
||||
vi.mocked(requireApiTokenUser).mockResolvedValue({
|
||||
userId: 'users:1',
|
||||
user: { handle: 'p' },
|
||||
} as never)
|
||||
const runQuery = vi.fn().mockResolvedValue({ _id: 'skills:1' })
|
||||
const runMutation = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(okRate())
|
||||
.mockResolvedValueOnce(okRate())
|
||||
.mockResolvedValueOnce({ ok: true, starred: true, alreadyStarred: false })
|
||||
const response = await __handlers.starsPostRouterV1Handler(
|
||||
makeCtx({ runQuery, runMutation }),
|
||||
new Request('https://example.com/api/v1/stars/demo', {
|
||||
method: 'POST',
|
||||
headers: { Authorization: 'Bearer clh_test' },
|
||||
}),
|
||||
)
|
||||
expect(response.status).toBe(200)
|
||||
const json = await response.json()
|
||||
expect(json.ok).toBe(true)
|
||||
expect(json.starred).toBe(true)
|
||||
})
|
||||
|
||||
it('stars delete succeeds', async () => {
|
||||
vi.mocked(requireApiTokenUser).mockResolvedValue({
|
||||
userId: 'users:1',
|
||||
user: { handle: 'p' },
|
||||
} as never)
|
||||
const runQuery = vi.fn().mockResolvedValue({ _id: 'skills:1' })
|
||||
const runMutation = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(okRate())
|
||||
.mockResolvedValueOnce(okRate())
|
||||
.mockResolvedValueOnce({ ok: true, unstarred: true, alreadyUnstarred: false })
|
||||
const response = await __handlers.starsDeleteRouterV1Handler(
|
||||
makeCtx({ runQuery, runMutation }),
|
||||
new Request('https://example.com/api/v1/stars/demo', {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: 'Bearer clh_test' },
|
||||
}),
|
||||
)
|
||||
expect(response.status).toBe(200)
|
||||
const json = await response.json()
|
||||
expect(json.ok).toBe(true)
|
||||
expect(json.unstarred).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1047,56 +1047,6 @@ async function soulsDeleteRouterV1Handler(ctx: ActionCtx, request: Request) {
|
||||
}
|
||||
|
||||
export const soulsDeleteRouterV1Http = httpAction(soulsDeleteRouterV1Handler)
|
||||
|
||||
async function starsPostRouterV1Handler(ctx: ActionCtx, request: Request) {
|
||||
const rate = await applyRateLimit(ctx, request, 'write')
|
||||
if (!rate.ok) return rate.response
|
||||
|
||||
const segments = getPathSegments(request, '/api/v1/stars/')
|
||||
if (segments.length !== 1) return text('Not found', 404, rate.headers)
|
||||
const slug = segments[0]?.trim().toLowerCase() ?? ''
|
||||
|
||||
try {
|
||||
const { userId } = await requireApiTokenUser(ctx, request)
|
||||
const skill = await ctx.runQuery(internal.skills.getSkillBySlugInternal, { slug })
|
||||
if (!skill) return text('Skill not found', 404, rate.headers)
|
||||
|
||||
const result = await ctx.runMutation(internal.stars.addStarInternal, {
|
||||
userId,
|
||||
skillId: skill._id,
|
||||
})
|
||||
return json(result, 200, rate.headers)
|
||||
} catch {
|
||||
return text('Unauthorized', 401, rate.headers)
|
||||
}
|
||||
}
|
||||
|
||||
export const starsPostRouterV1Http = httpAction(starsPostRouterV1Handler)
|
||||
|
||||
async function starsDeleteRouterV1Handler(ctx: ActionCtx, request: Request) {
|
||||
const rate = await applyRateLimit(ctx, request, 'write')
|
||||
if (!rate.ok) return rate.response
|
||||
|
||||
const segments = getPathSegments(request, '/api/v1/stars/')
|
||||
if (segments.length !== 1) return text('Not found', 404, rate.headers)
|
||||
const slug = segments[0]?.trim().toLowerCase() ?? ''
|
||||
|
||||
try {
|
||||
const { userId } = await requireApiTokenUser(ctx, request)
|
||||
const skill = await ctx.runQuery(internal.skills.getSkillBySlugInternal, { slug })
|
||||
if (!skill) return text('Skill not found', 404, rate.headers)
|
||||
|
||||
const result = await ctx.runMutation(internal.stars.removeStarInternal, {
|
||||
userId,
|
||||
skillId: skill._id,
|
||||
})
|
||||
return json(result, 200, rate.headers)
|
||||
} catch {
|
||||
return text('Unauthorized', 401, rate.headers)
|
||||
}
|
||||
}
|
||||
|
||||
export const starsDeleteRouterV1Http = httpAction(starsDeleteRouterV1Handler)
|
||||
export const __handlers = {
|
||||
searchSkillsV1Handler,
|
||||
resolveSkillVersionV1Handler,
|
||||
@@ -1110,7 +1060,5 @@ export const __handlers = {
|
||||
publishSoulV1Handler,
|
||||
soulsPostRouterV1Handler,
|
||||
soulsDeleteRouterV1Handler,
|
||||
starsPostRouterV1Handler,
|
||||
starsDeleteRouterV1Handler,
|
||||
whoamiV1Handler,
|
||||
}
|
||||
|
||||
+1
-48
@@ -1,6 +1,6 @@
|
||||
import { v } from 'convex/values'
|
||||
import type { Doc } from './_generated/dataModel'
|
||||
import { internalMutation, mutation, query } from './_generated/server'
|
||||
import { mutation, query } from './_generated/server'
|
||||
import { requireUser } from './lib/access'
|
||||
|
||||
export const isStarred = query({
|
||||
@@ -68,50 +68,3 @@ export const listByUser = query({
|
||||
return skills
|
||||
},
|
||||
})
|
||||
|
||||
export const addStarInternal = internalMutation({
|
||||
args: { userId: v.id('users'), skillId: v.id('skills') },
|
||||
handler: async (ctx, args) => {
|
||||
const skill = await ctx.db.get(args.skillId)
|
||||
if (!skill) throw new Error('Skill not found')
|
||||
const existing = await ctx.db
|
||||
.query('stars')
|
||||
.withIndex('by_skill_user', (q) => q.eq('skillId', args.skillId).eq('userId', args.userId))
|
||||
.unique()
|
||||
if (existing) return { ok: true as const, starred: true, alreadyStarred: true }
|
||||
|
||||
await ctx.db.insert('stars', {
|
||||
skillId: args.skillId,
|
||||
userId: args.userId,
|
||||
createdAt: Date.now(),
|
||||
})
|
||||
|
||||
await ctx.db.patch(skill._id, {
|
||||
stats: { ...skill.stats, stars: skill.stats.stars + 1 },
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
|
||||
return { ok: true as const, starred: true, alreadyStarred: false }
|
||||
},
|
||||
})
|
||||
|
||||
export const removeStarInternal = internalMutation({
|
||||
args: { userId: v.id('users'), skillId: v.id('skills') },
|
||||
handler: async (ctx, args) => {
|
||||
const skill = await ctx.db.get(args.skillId)
|
||||
if (!skill) throw new Error('Skill not found')
|
||||
const existing = await ctx.db
|
||||
.query('stars')
|
||||
.withIndex('by_skill_user', (q) => q.eq('skillId', args.skillId).eq('userId', args.userId))
|
||||
.unique()
|
||||
if (!existing) return { ok: true as const, unstarred: false, alreadyUnstarred: true }
|
||||
|
||||
await ctx.db.delete(existing._id)
|
||||
await ctx.db.patch(skill._id, {
|
||||
stats: { ...skill.stats, stars: Math.max(0, skill.stats.stars - 1) },
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
|
||||
return { ok: true as const, unstarred: true, alreadyUnstarred: false }
|
||||
},
|
||||
})
|
||||
|
||||
@@ -47,12 +47,6 @@ Stores your API token + cached registry URL.
|
||||
|
||||
- Verifies the stored token via `/api/v1/whoami`.
|
||||
|
||||
### `star <slug>` / `unstar <slug>`
|
||||
|
||||
- Adds/removes a skill from your highlights.
|
||||
- Calls `POST /api/v1/stars/<slug>` and `DELETE /api/v1/stars/<slug>`.
|
||||
- `--yes` skips confirmation.
|
||||
|
||||
### `search <query...>`
|
||||
|
||||
- Calls `/api/v1/search?q=...`.
|
||||
|
||||
@@ -140,20 +140,6 @@ Publishes a new version.
|
||||
|
||||
Soft-delete / restore a skill (owner/admin only).
|
||||
|
||||
### `POST /api/v1/stars/{slug}` / `DELETE /api/v1/stars/{slug}`
|
||||
|
||||
Add/remove a star (highlights). Both endpoints are idempotent.
|
||||
|
||||
Responses:
|
||||
|
||||
```json
|
||||
{ "ok": true, "starred": true, "alreadyStarred": false }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "ok": true, "unstarred": true, "alreadyUnstarred": false }
|
||||
```
|
||||
|
||||
## Legacy CLI endpoints (deprecated)
|
||||
|
||||
Still supported for older CLI versions:
|
||||
|
||||
+5
-6
@@ -10,7 +10,6 @@
|
||||
"build": "bun --bun vite build",
|
||||
"preview": "bun --bun vite preview",
|
||||
"docs:list": "bun scripts/docs-list.ts",
|
||||
"check:peers": "bun scripts/check-peer-deps.ts",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:e2e": "vitest run -c vitest.e2e.config.ts",
|
||||
@@ -24,7 +23,7 @@
|
||||
"format": "biome format --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.37.4",
|
||||
"@auth/core": "^0.34.3",
|
||||
"@convex-dev/auth": "^0.0.90",
|
||||
"@fontsource/bricolage-grotesque": "^5.2.10",
|
||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||
@@ -35,10 +34,10 @@
|
||||
"@resvg/resvg-wasm": "^2.6.2",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-devtools": "^0.9.2",
|
||||
"@tanstack/react-router": "^1.151.6",
|
||||
"@tanstack/react-router-devtools": "^1.151.6",
|
||||
"@tanstack/react-start": "^1.152.0",
|
||||
"@tanstack/router-plugin": "^1.151.6",
|
||||
"@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": "workspace:*",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "clawdhub",
|
||||
"version": "0.2.1",
|
||||
"description": "ClawdHub CLI \\u2014 install, update, search, and publish agent skills.",
|
||||
"version": "0.2.0",
|
||||
"description": "ClawdHub CLI \u2014 install, update, search, and publish agent skills.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
@@ -8,9 +8,7 @@ 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 { cmdExplore, cmdInstall, cmdList, cmdSearch, cmdUpdate } from './cli/commands/skills.js'
|
||||
import { cmdStarSkill } from './cli/commands/star.js'
|
||||
import { cmdSync } from './cli/commands/sync.js'
|
||||
import { cmdUnstarSkill } from './cli/commands/unstar.js'
|
||||
import { configureCommanderHelp, styleEnvBlock, styleTitle } from './cli/helpStyle.js'
|
||||
import { DEFAULT_REGISTRY, DEFAULT_SITE } from './cli/registry.js'
|
||||
import type { GlobalOpts } from './cli/types.js'
|
||||
@@ -236,26 +234,6 @@ program
|
||||
await cmdUndeleteSkill(opts, slug, options, isInputAllowed())
|
||||
})
|
||||
|
||||
program
|
||||
.command('star')
|
||||
.description('Add a skill to your highlights')
|
||||
.argument('<slug>', 'Skill slug')
|
||||
.option('--yes', 'Skip confirmation')
|
||||
.action(async (slug, options) => {
|
||||
const opts = await resolveGlobalOpts()
|
||||
await cmdStarSkill(opts, slug, options, isInputAllowed())
|
||||
})
|
||||
|
||||
program
|
||||
.command('unstar')
|
||||
.description('Remove a skill from your highlights')
|
||||
.argument('<slug>', 'Skill slug')
|
||||
.option('--yes', 'Skip confirmation')
|
||||
.action(async (slug, options) => {
|
||||
const opts = await resolveGlobalOpts()
|
||||
await cmdUnstarSkill(opts, slug, options, isInputAllowed())
|
||||
})
|
||||
|
||||
program
|
||||
.command('sync')
|
||||
.description('Scan local skills and publish new/updated ones')
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { readGlobalConfig } from '../../config.js'
|
||||
import { apiRequest } from '../../http.js'
|
||||
import { ApiRoutes, ApiV1StarResponseSchema } from '../../schema/index.js'
|
||||
import { getRegistry } from '../registry.js'
|
||||
import type { GlobalOpts } from '../types.js'
|
||||
import { createSpinner, fail, formatError, isInteractive, promptConfirm } from '../ui.js'
|
||||
|
||||
async function requireToken() {
|
||||
const cfg = await readGlobalConfig()
|
||||
const token = cfg?.token
|
||||
if (!token) fail('Not logged in. Run: clawdhub login')
|
||||
return token
|
||||
}
|
||||
|
||||
export async function cmdStarSkill(
|
||||
opts: GlobalOpts,
|
||||
slugArg: string,
|
||||
options: { yes?: boolean },
|
||||
inputAllowed: boolean,
|
||||
) {
|
||||
const slug = slugArg.trim().toLowerCase()
|
||||
if (!slug) fail('Slug required')
|
||||
const allowPrompt = isInteractive() && inputAllowed !== false
|
||||
|
||||
if (!options.yes) {
|
||||
if (!allowPrompt) fail('Pass --yes (no input)')
|
||||
const ok = await promptConfirm(`Star ${slug}?`)
|
||||
if (!ok) return
|
||||
}
|
||||
|
||||
const token = await requireToken()
|
||||
const registry = await getRegistry(opts, { cache: true })
|
||||
const spinner = createSpinner(`Starring ${slug}`)
|
||||
try {
|
||||
const result = await apiRequest(
|
||||
registry,
|
||||
{ method: 'POST', path: `${ApiRoutes.stars}/${encodeURIComponent(slug)}`, token },
|
||||
ApiV1StarResponseSchema,
|
||||
)
|
||||
spinner.succeed(result.alreadyStarred ? `OK. ${slug} already starred.` : `OK. Starred ${slug}`)
|
||||
return result
|
||||
} catch (error) {
|
||||
spinner.fail(formatError(error))
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { readGlobalConfig } from '../../config.js'
|
||||
import { apiRequest } from '../../http.js'
|
||||
import { ApiRoutes, ApiV1UnstarResponseSchema } from '../../schema/index.js'
|
||||
import { getRegistry } from '../registry.js'
|
||||
import type { GlobalOpts } from '../types.js'
|
||||
import { createSpinner, fail, formatError, isInteractive, promptConfirm } from '../ui.js'
|
||||
|
||||
async function requireToken() {
|
||||
const cfg = await readGlobalConfig()
|
||||
const token = cfg?.token
|
||||
if (!token) fail('Not logged in. Run: clawdhub login')
|
||||
return token
|
||||
}
|
||||
|
||||
export async function cmdUnstarSkill(
|
||||
opts: GlobalOpts,
|
||||
slugArg: string,
|
||||
options: { yes?: boolean },
|
||||
inputAllowed: boolean,
|
||||
) {
|
||||
const slug = slugArg.trim().toLowerCase()
|
||||
if (!slug) fail('Slug required')
|
||||
const allowPrompt = isInteractive() && inputAllowed !== false
|
||||
|
||||
if (!options.yes) {
|
||||
if (!allowPrompt) fail('Pass --yes (no input)')
|
||||
const ok = await promptConfirm(`Unstar ${slug}?`)
|
||||
if (!ok) return
|
||||
}
|
||||
|
||||
const token = await requireToken()
|
||||
const registry = await getRegistry(opts, { cache: true })
|
||||
const spinner = createSpinner(`Unstarring ${slug}`)
|
||||
try {
|
||||
const result = await apiRequest(
|
||||
registry,
|
||||
{ method: 'DELETE', path: `${ApiRoutes.stars}/${encodeURIComponent(slug)}`, token },
|
||||
ApiV1UnstarResponseSchema,
|
||||
)
|
||||
spinner.succeed(
|
||||
result.alreadyUnstarred ? `OK. ${slug} already unstarred.` : `OK. Unstarred ${slug}`,
|
||||
)
|
||||
return result
|
||||
} catch (error) {
|
||||
spinner.fail(formatError(error))
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ export const ApiRoutes = {
|
||||
resolve: '/api/v1/resolve',
|
||||
download: '/api/v1/download',
|
||||
skills: '/api/v1/skills',
|
||||
stars: '/api/v1/stars',
|
||||
souls: '/api/v1/souls',
|
||||
whoami: '/api/v1/whoami',
|
||||
} as const
|
||||
|
||||
@@ -215,18 +215,6 @@ export const ApiV1DeleteResponseSchema = type({
|
||||
ok: 'true',
|
||||
})
|
||||
|
||||
export const ApiV1StarResponseSchema = type({
|
||||
ok: 'true',
|
||||
starred: 'boolean',
|
||||
alreadyStarred: 'boolean',
|
||||
})
|
||||
|
||||
export const ApiV1UnstarResponseSchema = type({
|
||||
ok: 'true',
|
||||
unstarred: 'boolean',
|
||||
alreadyUnstarred: 'boolean',
|
||||
})
|
||||
|
||||
export const SkillInstallSpecSchema = type({
|
||||
id: 'string?',
|
||||
kind: '"brew"|"node"|"go"|"uv"',
|
||||
|
||||
Vendored
-1
@@ -15,7 +15,6 @@ export declare const ApiRoutes: {
|
||||
readonly resolve: "/api/v1/resolve";
|
||||
readonly download: "/api/v1/download";
|
||||
readonly skills: "/api/v1/skills";
|
||||
readonly stars: "/api/v1/stars";
|
||||
readonly souls: "/api/v1/souls";
|
||||
readonly whoami: "/api/v1/whoami";
|
||||
};
|
||||
|
||||
Vendored
-1
@@ -15,7 +15,6 @@ export const ApiRoutes = {
|
||||
resolve: '/api/v1/resolve',
|
||||
download: '/api/v1/download',
|
||||
skills: '/api/v1/skills',
|
||||
stars: '/api/v1/stars',
|
||||
souls: '/api/v1/souls',
|
||||
whoami: '/api/v1/whoami',
|
||||
};
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,QAAQ,EAAE,eAAe;IACzB,MAAM,EAAE,aAAa;IACrB,KAAK,EAAE,YAAY;IACnB,YAAY,EAAE,oBAAoB;IAClC,SAAS,EAAE,iBAAiB;IAC5B,YAAY,EAAE,qBAAqB;IACnC,UAAU,EAAE,kBAAkB;IAC9B,gBAAgB,EAAE,yBAAyB;IAC3C,cAAc,EAAE,uBAAuB;IACvC,gBAAgB,EAAE,yBAAyB;CACnC,CAAA;AAEV,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,MAAM,EAAE,gBAAgB;IACxB,OAAO,EAAE,iBAAiB;IAC1B,QAAQ,EAAE,kBAAkB;IAC5B,MAAM,EAAE,gBAAgB;IACxB,KAAK,EAAE,eAAe;IACtB,KAAK,EAAE,eAAe;IACtB,MAAM,EAAE,gBAAgB;CAChB,CAAA"}
|
||||
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,QAAQ,EAAE,eAAe;IACzB,MAAM,EAAE,aAAa;IACrB,KAAK,EAAE,YAAY;IACnB,YAAY,EAAE,oBAAoB;IAClC,SAAS,EAAE,iBAAiB;IAC5B,YAAY,EAAE,qBAAqB;IACnC,UAAU,EAAE,kBAAkB;IAC9B,gBAAgB,EAAE,yBAAyB;IAC3C,cAAc,EAAE,uBAAuB;IACvC,gBAAgB,EAAE,yBAAyB;CACnC,CAAA;AAEV,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,MAAM,EAAE,gBAAgB;IACxB,OAAO,EAAE,iBAAiB;IAC1B,QAAQ,EAAE,kBAAkB;IAC5B,MAAM,EAAE,gBAAgB;IACxB,KAAK,EAAE,eAAe;IACtB,MAAM,EAAE,gBAAgB;CAChB,CAAA"}
|
||||
Vendored
-10
@@ -221,16 +221,6 @@ export declare const ApiV1PublishResponseSchema: import("arktype/internal/varian
|
||||
export declare const ApiV1DeleteResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
||||
ok: true;
|
||||
}, {}>;
|
||||
export declare const ApiV1StarResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
||||
ok: true;
|
||||
starred: boolean;
|
||||
alreadyStarred: boolean;
|
||||
}, {}>;
|
||||
export declare const ApiV1UnstarResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
||||
ok: true;
|
||||
unstarred: boolean;
|
||||
alreadyUnstarred: boolean;
|
||||
}, {}>;
|
||||
export declare const SkillInstallSpecSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
||||
kind: "brew" | "node" | "go" | "uv";
|
||||
id?: string | undefined;
|
||||
|
||||
Vendored
+1
-11
@@ -192,16 +192,6 @@ export const ApiV1PublishResponseSchema = type({
|
||||
export const ApiV1DeleteResponseSchema = type({
|
||||
ok: 'true',
|
||||
});
|
||||
export const ApiV1StarResponseSchema = type({
|
||||
ok: 'true',
|
||||
starred: 'boolean',
|
||||
alreadyStarred: 'boolean',
|
||||
});
|
||||
export const ApiV1UnstarResponseSchema = type({
|
||||
ok: 'true',
|
||||
unstarred: 'boolean',
|
||||
alreadyUnstarred: 'boolean',
|
||||
});
|
||||
export const SkillInstallSpecSchema = type({
|
||||
id: 'string?',
|
||||
kind: '"brew"|"node"|"go"|"uv"',
|
||||
@@ -240,4 +230,4 @@ export const ClawdisSkillMetadataSchema = type({
|
||||
nix: NixPluginSpecSchema.optional(),
|
||||
config: ClawdbotConfigSpecSchema.optional(),
|
||||
});
|
||||
//# sourceMappingURL=schemas.js.map
|
||||
//# sourceMappingURL=schemas.js.map
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -16,7 +16,6 @@ export const ApiRoutes = {
|
||||
resolve: '/api/v1/resolve',
|
||||
download: '/api/v1/download',
|
||||
skills: '/api/v1/skills',
|
||||
stars: '/api/v1/stars',
|
||||
souls: '/api/v1/souls',
|
||||
whoami: '/api/v1/whoami',
|
||||
} as const
|
||||
|
||||
@@ -226,18 +226,6 @@ export const ApiV1DeleteResponseSchema = type({
|
||||
ok: 'true',
|
||||
})
|
||||
|
||||
export const ApiV1StarResponseSchema = type({
|
||||
ok: 'true',
|
||||
starred: 'boolean',
|
||||
alreadyStarred: 'boolean',
|
||||
})
|
||||
|
||||
export const ApiV1UnstarResponseSchema = type({
|
||||
ok: 'true',
|
||||
unstarred: 'boolean',
|
||||
alreadyUnstarred: 'boolean',
|
||||
})
|
||||
|
||||
export const SkillInstallSpecSchema = type({
|
||||
id: 'string?',
|
||||
kind: '"brew"|"node"|"go"|"uv"',
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import semver from 'semver'
|
||||
|
||||
type PackageJson = {
|
||||
name?: string
|
||||
version?: string
|
||||
dependencies?: Record<string, string>
|
||||
peerDependencies?: Record<string, string>
|
||||
}
|
||||
|
||||
async function readJson(path: string): Promise<PackageJson> {
|
||||
const raw = await readFile(path, 'utf8')
|
||||
return JSON.parse(raw) as PackageJson
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const root = process.cwd()
|
||||
const rootPkgPath = join(root, 'package.json')
|
||||
const authPkgPath = join(root, 'node_modules', '@convex-dev', 'auth', 'package.json')
|
||||
const corePkgPath = join(root, 'node_modules', '@auth', 'core', 'package.json')
|
||||
|
||||
const rootPkg = await readJson(rootPkgPath)
|
||||
const authPkg = await readJson(authPkgPath)
|
||||
const corePkg = await readJson(corePkgPath)
|
||||
|
||||
const peerRange = authPkg.peerDependencies?.['@auth/core']
|
||||
const declaredRange = rootPkg.dependencies?.['@auth/core']
|
||||
const installedVersion = corePkg.version
|
||||
|
||||
if (!peerRange) {
|
||||
throw new Error('Missing @auth/core peer range in @convex-dev/auth package.json')
|
||||
}
|
||||
if (!declaredRange) {
|
||||
throw new Error('Missing @auth/core dependency in root package.json')
|
||||
}
|
||||
if (!installedVersion) {
|
||||
throw new Error('Missing @auth/core version in node_modules')
|
||||
}
|
||||
|
||||
if (!semver.intersects(declaredRange, peerRange, { includePrerelease: true })) {
|
||||
throw new Error(
|
||||
`@auth/core range mismatch: package.json declares "${declaredRange}" but @convex-dev/auth requires "${peerRange}"`,
|
||||
)
|
||||
}
|
||||
|
||||
if (!semver.satisfies(installedVersion, peerRange, { includePrerelease: true })) {
|
||||
throw new Error(
|
||||
`@auth/core version mismatch: installed "${installedVersion}" does not satisfy @convex-dev/auth peer "${peerRange}"`,
|
||||
)
|
||||
}
|
||||
|
||||
console.log(`peer ok: @auth/core ${installedVersion} satisfies @convex-dev/auth (${peerRange})`)
|
||||
}
|
||||
|
||||
await main()
|
||||
@@ -1,60 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@tanstack/react-router', () => ({
|
||||
createFileRoute: () => (config: { beforeLoad?: unknown }) => ({ __config: config }),
|
||||
redirect: (options: unknown) => ({ redirect: options }),
|
||||
}))
|
||||
|
||||
import { Route } from '../routes/search'
|
||||
|
||||
describe('search route', () => {
|
||||
it('redirects to home with search mode enabled', () => {
|
||||
const beforeLoad = Route.__config.beforeLoad as (args: {
|
||||
search: { q?: string; highlighted?: boolean }
|
||||
}) => void
|
||||
let thrown: unknown
|
||||
|
||||
try {
|
||||
beforeLoad({ search: { q: 'crab', highlighted: true } })
|
||||
} catch (error) {
|
||||
thrown = error
|
||||
}
|
||||
|
||||
expect(thrown).toEqual({
|
||||
redirect: {
|
||||
to: '/',
|
||||
search: {
|
||||
q: 'crab',
|
||||
highlighted: true,
|
||||
search: true,
|
||||
},
|
||||
replace: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('redirects to home with search flag even without query', () => {
|
||||
const beforeLoad = Route.__config.beforeLoad as (args: {
|
||||
search: { q?: string; highlighted?: boolean }
|
||||
}) => void
|
||||
let thrown: unknown
|
||||
|
||||
try {
|
||||
beforeLoad({ search: {} })
|
||||
} catch (error) {
|
||||
thrown = error
|
||||
}
|
||||
|
||||
expect(thrown).toEqual({
|
||||
redirect: {
|
||||
to: '/',
|
||||
search: {
|
||||
q: undefined,
|
||||
highlighted: undefined,
|
||||
search: true,
|
||||
},
|
||||
replace: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -4,7 +4,6 @@ import { vi } from 'vitest'
|
||||
import { SkillDetailPage } from '../components/SkillDetailPage'
|
||||
|
||||
const navigateMock = vi.fn()
|
||||
const useAuthStatusMock = vi.fn()
|
||||
|
||||
vi.mock('@tanstack/react-router', () => ({
|
||||
useNavigate: () => navigateMock,
|
||||
@@ -14,27 +13,18 @@ const useQueryMock = vi.fn()
|
||||
const getReadmeMock = vi.fn()
|
||||
|
||||
vi.mock('convex/react', () => ({
|
||||
useConvexAuth: () => ({ isAuthenticated: false }),
|
||||
useQuery: (...args: unknown[]) => useQueryMock(...args),
|
||||
useMutation: () => vi.fn(),
|
||||
useAction: () => getReadmeMock,
|
||||
}))
|
||||
|
||||
vi.mock('../lib/useAuthStatus', () => ({
|
||||
useAuthStatus: () => useAuthStatusMock(),
|
||||
}))
|
||||
|
||||
describe('SkillDetailPage', () => {
|
||||
beforeEach(() => {
|
||||
useQueryMock.mockReset()
|
||||
getReadmeMock.mockReset()
|
||||
navigateMock.mockReset()
|
||||
useAuthStatusMock.mockReset()
|
||||
getReadmeMock.mockResolvedValue({ text: '' })
|
||||
useAuthStatusMock.mockReturnValue({
|
||||
isAuthenticated: false,
|
||||
isLoading: false,
|
||||
me: null,
|
||||
})
|
||||
useQueryMock.mockImplementation((_fn: unknown, args: unknown) => {
|
||||
if (args === 'skip') return undefined
|
||||
return undefined
|
||||
@@ -42,7 +32,9 @@ describe('SkillDetailPage', () => {
|
||||
})
|
||||
|
||||
it('shows a loading indicator while loading', () => {
|
||||
useQueryMock.mockImplementationOnce(() => undefined) // getBySlug
|
||||
useQueryMock
|
||||
.mockImplementationOnce(() => null) // me
|
||||
.mockImplementationOnce(() => undefined) // getBySlug
|
||||
|
||||
render(<SkillDetailPage slug="weather" />)
|
||||
expect(screen.getByText(/Loading skill/i)).toBeTruthy()
|
||||
@@ -50,26 +42,30 @@ describe('SkillDetailPage', () => {
|
||||
})
|
||||
|
||||
it('shows not found when skill query resolves to null', async () => {
|
||||
useQueryMock.mockImplementationOnce(() => null) // getBySlug
|
||||
useQueryMock
|
||||
.mockImplementationOnce(() => null) // me
|
||||
.mockImplementationOnce(() => null) // getBySlug
|
||||
|
||||
render(<SkillDetailPage slug="missing-skill" />)
|
||||
expect(await screen.findByText(/Skill not found/i)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('redirects legacy routes to canonical owner/slug', async () => {
|
||||
useQueryMock.mockImplementationOnce(() => ({
|
||||
skill: {
|
||||
_id: 'skills:1',
|
||||
slug: 'weather',
|
||||
displayName: 'Weather',
|
||||
summary: 'Get current weather.',
|
||||
ownerUserId: 'users:1',
|
||||
tags: {},
|
||||
stats: { stars: 0, downloads: 0 },
|
||||
},
|
||||
owner: { handle: 'steipete', name: 'Peter' },
|
||||
latestVersion: { _id: 'skillVersions:1', version: '1.0.0', parsed: {} },
|
||||
}))
|
||||
useQueryMock
|
||||
.mockImplementationOnce(() => null) // me
|
||||
.mockImplementationOnce(() => ({
|
||||
skill: {
|
||||
_id: 'skills:1',
|
||||
slug: 'weather',
|
||||
displayName: 'Weather',
|
||||
summary: 'Get current weather.',
|
||||
ownerUserId: 'users:1',
|
||||
tags: {},
|
||||
stats: { stars: 0, downloads: 0 },
|
||||
},
|
||||
owner: { handle: 'steipete', name: 'Peter' },
|
||||
latestVersion: { _id: 'skillVersions:1', version: '1.0.0', parsed: {} },
|
||||
}))
|
||||
|
||||
render(<SkillDetailPage slug="weather" redirectToCanonical />)
|
||||
expect(screen.getByText(/Loading skill/i)).toBeTruthy()
|
||||
|
||||
@@ -15,10 +15,10 @@ const publishVersion = vi.fn()
|
||||
const generateChangelogPreview = vi.fn()
|
||||
const fetchMock = vi.fn()
|
||||
const useQueryMock = vi.fn()
|
||||
const useAuthStatusMock = vi.fn()
|
||||
let useActionCallCount = 0
|
||||
|
||||
vi.mock('convex/react', () => ({
|
||||
useConvexAuth: () => ({ isAuthenticated: true }),
|
||||
useQuery: (...args: unknown[]) => useQueryMock(...args),
|
||||
useMutation: () => generateUploadUrl,
|
||||
useAction: () => {
|
||||
@@ -27,10 +27,6 @@ vi.mock('convex/react', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../lib/useAuthStatus', () => ({
|
||||
useAuthStatus: () => useAuthStatusMock(),
|
||||
}))
|
||||
|
||||
describe('Upload route', () => {
|
||||
beforeEach(() => {
|
||||
generateUploadUrl.mockReset()
|
||||
@@ -38,13 +34,7 @@ describe('Upload route', () => {
|
||||
generateChangelogPreview.mockReset()
|
||||
fetchMock.mockReset()
|
||||
useQueryMock.mockReset()
|
||||
useAuthStatusMock.mockReset()
|
||||
useActionCallCount = 0
|
||||
useAuthStatusMock.mockReturnValue({
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
me: { _id: 'users:1' },
|
||||
})
|
||||
useQueryMock.mockImplementation((_fn: unknown, args: unknown) => {
|
||||
if (args === 'skip') return undefined
|
||||
return null
|
||||
|
||||
@@ -3,13 +3,15 @@ import { convex } from '../convex/client'
|
||||
import { UserBootstrap } from './UserBootstrap'
|
||||
|
||||
export function AppProviders({ children }: { children: React.ReactNode }) {
|
||||
if (typeof window === 'undefined') {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
return (
|
||||
<ConvexAuthProvider
|
||||
client={convex}
|
||||
replaceURL={(relativeUrl) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.history.replaceState(null, '', relativeUrl)
|
||||
}
|
||||
window.history.replaceState(null, '', relativeUrl)
|
||||
}}
|
||||
>
|
||||
<UserBootstrap />
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useAuthActions } from '@convex-dev/auth/react'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { useConvexAuth, useQuery } from 'convex/react'
|
||||
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, getSiteMode, getSiteName } from '../lib/site'
|
||||
import { applyTheme, useThemeMode } from '../lib/theme'
|
||||
import { startThemeTransition } from '../lib/theme-transition'
|
||||
import { useAuthStatus } from '../lib/useAuthStatus'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -17,8 +18,9 @@ import {
|
||||
import { ToggleGroup, ToggleGroupItem } from './ui/toggle-group'
|
||||
|
||||
export default function Header() {
|
||||
const { isAuthenticated, isLoading, me } = useAuthStatus()
|
||||
const { isAuthenticated, isLoading } = useConvexAuth()
|
||||
const { signIn, signOut } = useAuthActions()
|
||||
const me = useQuery(api.users.me)
|
||||
const { mode, setMode } = useThemeMode()
|
||||
const toggleRef = useRef<HTMLDivElement | null>(null)
|
||||
const siteMode = getSiteMode()
|
||||
@@ -47,11 +49,7 @@ export default function Header() {
|
||||
return (
|
||||
<header className="navbar">
|
||||
<div className="navbar-inner">
|
||||
<Link
|
||||
to="/"
|
||||
search={{ q: undefined, highlighted: undefined, search: undefined }}
|
||||
className="brand"
|
||||
>
|
||||
<Link to="/" search={{ q: undefined, highlighted: undefined }} className="brand">
|
||||
<span className="brand-mark">
|
||||
<img src="/clawd-logo.png" alt="" aria-hidden="true" />
|
||||
</span>
|
||||
@@ -79,7 +77,7 @@ export default function Header() {
|
||||
Upload
|
||||
</Link>
|
||||
{isSoulMode ? null : <Link to="/import">Import</Link>}
|
||||
<Link to="/" search={{ q: undefined, highlighted: undefined, search: true }}>
|
||||
<Link to="/search" search={{ q: undefined, highlighted: undefined }}>
|
||||
Search
|
||||
</Link>
|
||||
{me ? <Link to="/stars">Stars</Link> : null}
|
||||
@@ -128,7 +126,7 @@ export default function Header() {
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/" search={{ q: undefined, highlighted: undefined, search: true }}>
|
||||
<Link to="/search" search={{ q: undefined, highlighted: undefined }}>
|
||||
Search
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import type { ClawdisSkillMetadata, SkillInstallSpec } from 'clawdhub-schema'
|
||||
import { useAction, useMutation, useQuery } from 'convex/react'
|
||||
import { useAction, useConvexAuth, useMutation, useQuery } from 'convex/react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { api } from '../../convex/_generated/api'
|
||||
import type { Doc, Id } from '../../convex/_generated/dataModel'
|
||||
import { useAuthStatus } from '../lib/useAuthStatus'
|
||||
import { SkillDiffCard } from './SkillDiffCard'
|
||||
|
||||
type SkillDetailPageProps = {
|
||||
@@ -21,7 +20,8 @@ export function SkillDetailPage({
|
||||
redirectToCanonical,
|
||||
}: SkillDetailPageProps) {
|
||||
const navigate = useNavigate()
|
||||
const { isAuthenticated, me } = useAuthStatus()
|
||||
const { isAuthenticated } = useConvexAuth()
|
||||
const me = useQuery(api.users.me)
|
||||
const result = useQuery(api.skills.getBySlug, { slug })
|
||||
const toggleStar = useMutation(api.stars.toggle)
|
||||
const addComment = useMutation(api.comments.add)
|
||||
|
||||
@@ -418,7 +418,7 @@ function buildDiffOptions(viewMode: 'split' | 'inline'): DiffEditorProps['option
|
||||
|
||||
function applyMonacoTheme(monaco: NonNullable<ReturnType<typeof useMonaco>>) {
|
||||
const styles = getComputedStyle(document.documentElement)
|
||||
const surface = normalizeHex(styles.getPropertyValue('--surface').trim() || '#ffffff')
|
||||
const surface = styles.getPropertyValue('--surface').trim() || '#ffffff'
|
||||
const surfaceMuted = styles.getPropertyValue('--surface-muted').trim() || '#f6f1ec'
|
||||
const ink = styles.getPropertyValue('--ink').trim() || '#1d1a17'
|
||||
const inkSoft = styles.getPropertyValue('--ink-soft').trim() || '#4c463f'
|
||||
@@ -468,7 +468,7 @@ function applyMonacoTheme(monaco: NonNullable<ReturnType<typeof useMonaco>>) {
|
||||
}
|
||||
|
||||
function normalizeHex(value: string) {
|
||||
if (!value.startsWith('#')) return value
|
||||
if (!value.startsWith('#')) return '#000000'
|
||||
if (value.length === 4) {
|
||||
return `#${value[1]}${value[1]}${value[2]}${value[2]}${value[3]}${value[3]}`
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useAction, useMutation, useQuery } from 'convex/react'
|
||||
import { useAction, useConvexAuth, useMutation, useQuery } from 'convex/react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { api } from '../../convex/_generated/api'
|
||||
import type { Doc } from '../../convex/_generated/dataModel'
|
||||
import { useAuthStatus } from '../lib/useAuthStatus'
|
||||
|
||||
type SoulDetailPageProps = {
|
||||
slug: string
|
||||
}
|
||||
|
||||
export function SoulDetailPage({ slug }: SoulDetailPageProps) {
|
||||
const { isAuthenticated, me } = useAuthStatus()
|
||||
const { isAuthenticated } = useConvexAuth()
|
||||
const me = useQuery(api.users.me)
|
||||
const result = useQuery(api.souls.getBySlug, { slug })
|
||||
const toggleStar = useMutation(api.soulStars.toggle)
|
||||
const addComment = useMutation(api.soulComments.add)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useMutation } from 'convex/react'
|
||||
import { useConvexAuth, useMutation } from 'convex/react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { api } from '../../convex/_generated/api'
|
||||
import { useAuthStatus } from '../lib/useAuthStatus'
|
||||
|
||||
export function UserBootstrap() {
|
||||
const { isAuthenticated, isLoading } = useAuthStatus()
|
||||
const { isAuthenticated, isLoading } = useConvexAuth()
|
||||
const ensureUser = useMutation(api.users.ensure)
|
||||
const didRun = useRef(false)
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { useQuery } from 'convex/react'
|
||||
import { api } from '../../convex/_generated/api'
|
||||
|
||||
export function useAuthStatus() {
|
||||
const me = useQuery(api.users.me)
|
||||
return {
|
||||
me,
|
||||
isLoading: me === undefined,
|
||||
isAuthenticated: Boolean(me),
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useAuthActions } from '@convex-dev/auth/react'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { useMutation } from 'convex/react'
|
||||
import { useConvexAuth, useMutation, useQuery } from 'convex/react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { api } from '../../../convex/_generated/api'
|
||||
import { useAuthStatus } from '../../lib/useAuthStatus'
|
||||
|
||||
export const Route = createFileRoute('/cli/auth')({
|
||||
component: CliAuth,
|
||||
})
|
||||
|
||||
function CliAuth() {
|
||||
const { isAuthenticated, isLoading, me } = useAuthStatus()
|
||||
const { isAuthenticated, isLoading } = useConvexAuth()
|
||||
const { signIn } = useAuthActions()
|
||||
const me = useQuery(api.users.me)
|
||||
const createToken = useMutation(api.tokens.create)
|
||||
|
||||
const search = Route.useSearch() as {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||
import { useAction } from 'convex/react'
|
||||
import { useAction, useConvexAuth } from 'convex/react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { api } from '../../convex/_generated/api'
|
||||
import { formatBytes } from '../lib/uploadUtils'
|
||||
import { useAuthStatus } from '../lib/useAuthStatus'
|
||||
|
||||
export const Route = createFileRoute('/import')({
|
||||
component: ImportGitHub,
|
||||
@@ -38,7 +37,7 @@ type CandidatePreview = {
|
||||
}
|
||||
|
||||
function ImportGitHub() {
|
||||
const { isAuthenticated, isLoading } = useAuthStatus()
|
||||
const { isAuthenticated, isLoading } = useConvexAuth()
|
||||
const previewImport = useAction(api.githubImport.previewGitHubImport)
|
||||
const previewCandidate = useAction(api.githubImport.previewGitHubImportCandidate)
|
||||
const importSkill = useAction(api.githubImport.importGitHubSkill)
|
||||
|
||||
+16
-40
@@ -11,14 +11,7 @@ import { getSiteMode } from '../lib/site'
|
||||
export const Route = createFileRoute('/')({
|
||||
validateSearch: (search) => ({
|
||||
q: typeof search.q === 'string' && search.q.trim() ? search.q : undefined,
|
||||
highlighted:
|
||||
search.highlighted === '1' || search.highlighted === 'true' || search.highlighted === true
|
||||
? true
|
||||
: undefined,
|
||||
search:
|
||||
search.search === '1' || search.search === 'true' || search.search === true
|
||||
? true
|
||||
: undefined,
|
||||
highlighted: search.highlighted === '1' || search.highlighted === 'true' ? true : undefined,
|
||||
}),
|
||||
component: Home,
|
||||
})
|
||||
@@ -41,9 +34,7 @@ function SkillsHome() {
|
||||
Array<{ skill: Doc<'skills'>; version: Doc<'skillVersions'> | null; score: number }>
|
||||
>([])
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
const [searchMode, setSearchMode] = useState(
|
||||
Boolean(search.q || search.highlighted || search.search),
|
||||
)
|
||||
const [searchMode, setSearchMode] = useState(Boolean(search.q || search.highlighted))
|
||||
const searchRequest = useRef(0)
|
||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||
const trimmedQuery = useMemo(() => query.trim(), [query])
|
||||
@@ -52,29 +43,20 @@ function SkillsHome() {
|
||||
useEffect(() => {
|
||||
setQuery(search.q ?? '')
|
||||
setHighlightedOnly(search.highlighted ?? false)
|
||||
if (search.q || search.highlighted || search.search) {
|
||||
if (search.q || search.highlighted) {
|
||||
setSearchMode(true)
|
||||
} else {
|
||||
setSearchMode(false)
|
||||
}
|
||||
}, [search.highlighted, search.q, search.search])
|
||||
}, [search.highlighted, search.q])
|
||||
|
||||
useEffect(() => {
|
||||
void navigate({
|
||||
search: () => ({
|
||||
q: trimmedQuery || undefined,
|
||||
highlighted: highlightedOnly ? true : undefined,
|
||||
search: searchMode && !trimmedQuery && !highlightedOnly ? true : undefined,
|
||||
}),
|
||||
replace: true,
|
||||
})
|
||||
}, [highlightedOnly, navigate, searchMode, trimmedQuery])
|
||||
|
||||
useEffect(() => {
|
||||
if (searchMode && inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
}
|
||||
}, [searchMode])
|
||||
}, [highlightedOnly, navigate, trimmedQuery])
|
||||
|
||||
useEffect(() => {
|
||||
if (!trimmedQuery) {
|
||||
@@ -121,13 +103,16 @@ function SkillsHome() {
|
||||
<Link to="/upload" search={{ updateSlug: undefined }} className="btn btn-primary">
|
||||
Publish a skill
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
search={{ q: undefined, highlighted: undefined, search: true }}
|
||||
<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">
|
||||
@@ -292,7 +277,7 @@ function OnlyCrabsHome() {
|
||||
Array<{ soul: Doc<'souls'>; version: Doc<'soulVersions'> | null; score: number }>
|
||||
>([])
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
const [searchMode, setSearchMode] = useState(Boolean(search.q || search.search))
|
||||
const [searchMode, setSearchMode] = useState(Boolean(search.q))
|
||||
const searchRequest = useRef(0)
|
||||
const seedEnsuredRef = useRef(false)
|
||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||
@@ -301,12 +286,10 @@ function OnlyCrabsHome() {
|
||||
|
||||
useEffect(() => {
|
||||
setQuery(search.q ?? '')
|
||||
if (search.q || search.search) {
|
||||
if (search.q) {
|
||||
setSearchMode(true)
|
||||
} else {
|
||||
setSearchMode(false)
|
||||
}
|
||||
}, [search.q, search.search])
|
||||
}, [search.q])
|
||||
|
||||
useEffect(() => {
|
||||
if (seedEnsuredRef.current) return
|
||||
@@ -319,17 +302,10 @@ function OnlyCrabsHome() {
|
||||
search: () => ({
|
||||
q: trimmedQuery || undefined,
|
||||
highlighted: undefined,
|
||||
search: searchMode && !trimmedQuery ? true : undefined,
|
||||
}),
|
||||
replace: true,
|
||||
})
|
||||
}, [navigate, searchMode, trimmedQuery])
|
||||
|
||||
useEffect(() => {
|
||||
if (searchMode && inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
}
|
||||
}, [searchMode])
|
||||
}, [navigate, trimmedQuery])
|
||||
|
||||
useEffect(() => {
|
||||
if (!trimmedQuery) {
|
||||
|
||||
+19
-9
@@ -1,19 +1,29 @@
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export const Route = createFileRoute('/search')({
|
||||
validateSearch: (search) => ({
|
||||
q: typeof search.q === 'string' && search.q.trim() ? search.q : undefined,
|
||||
highlighted: search.highlighted === '1' || search.highlighted === 'true' ? true : undefined,
|
||||
}),
|
||||
beforeLoad: ({ search }) => {
|
||||
throw redirect({
|
||||
component: SearchRedirect,
|
||||
})
|
||||
|
||||
function SearchRedirect() {
|
||||
const navigate = Route.useNavigate()
|
||||
const search = Route.useSearch()
|
||||
|
||||
useEffect(() => {
|
||||
void navigate({
|
||||
to: '/',
|
||||
search: {
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
q: search.q || undefined,
|
||||
highlighted: search.highlighted || undefined,
|
||||
search: true,
|
||||
},
|
||||
highlighted: search.highlighted ? true : undefined,
|
||||
}),
|
||||
replace: true,
|
||||
})
|
||||
},
|
||||
})
|
||||
}, [navigate, search.highlighted, search.q])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
+7
-26
@@ -1,5 +1,5 @@
|
||||
import { createFileRoute, Link } from '@tanstack/react-router'
|
||||
import { useMutation, useQuery } from 'convex/react'
|
||||
import { useQuery } from 'convex/react'
|
||||
import { api } from '../../convex/_generated/api'
|
||||
import type { Doc } from '../../convex/_generated/dataModel'
|
||||
|
||||
@@ -14,8 +14,6 @@ function Stars() {
|
||||
| Doc<'skills'>[]
|
||||
| undefined) ?? []
|
||||
|
||||
const toggleStar = useMutation(api.stars.toggle)
|
||||
|
||||
if (!me) {
|
||||
return (
|
||||
<main className="section">
|
||||
@@ -33,29 +31,12 @@ function Stars() {
|
||||
<div className="card">No stars yet.</div>
|
||||
) : (
|
||||
skills.map((skill) => (
|
||||
<div key={skill._id} className="card skill-card">
|
||||
<Link to="/skills/$slug" params={{ slug: skill.slug }}>
|
||||
<h3 className="skill-card-title">{skill.displayName}</h3>
|
||||
</Link>
|
||||
<div className="skill-card-footer skill-card-footer-inline">
|
||||
<span className="stat">⭐ {skill.stats.stars}</span>
|
||||
<button
|
||||
className="star-toggle is-active"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await toggleStar({ skillId: skill._id })
|
||||
} catch (error) {
|
||||
console.error('Failed to unstar skill:', error)
|
||||
window.alert('Unable to unstar this skill. Please try again.')
|
||||
}
|
||||
}}
|
||||
aria-label={`Unstar ${skill.displayName}`}
|
||||
>
|
||||
<span aria-hidden="true">★</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Link key={skill._id} to="/skills/$slug" params={{ slug: skill.slug }} className="card">
|
||||
<h3 className="section-title" style={{ fontSize: '1.2rem', margin: 0 }}>
|
||||
{skill.displayName}
|
||||
</h3>
|
||||
<div className="stat">⭐ {skill.stats.stars}</div>
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router'
|
||||
import { useAction, useMutation, useQuery } from 'convex/react'
|
||||
import { useAction, useConvexAuth, useMutation, useQuery } from 'convex/react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import semver from 'semver'
|
||||
import { api } from '../../convex/_generated/api'
|
||||
import { getSiteMode } from '../lib/site'
|
||||
import { expandFiles } from '../lib/uploadFiles'
|
||||
import { useAuthStatus } from '../lib/useAuthStatus'
|
||||
import {
|
||||
formatBytes,
|
||||
formatPublishError,
|
||||
@@ -25,7 +24,7 @@ export const Route = createFileRoute('/upload')({
|
||||
})
|
||||
|
||||
export function Upload() {
|
||||
const { isAuthenticated } = useAuthStatus()
|
||||
const { isAuthenticated } = useConvexAuth()
|
||||
const { updateSlug } = useSearch({ from: '/upload' })
|
||||
const siteMode = getSiteMode()
|
||||
const isSoulMode = siteMode === 'souls'
|
||||
|
||||
@@ -1002,13 +1002,6 @@ code {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.skill-card-footer-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hero-install {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createRequire } from 'node:module'
|
||||
import { dirname, join } from 'node:path'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { devtools } from '@tanstack/devtools-vite'
|
||||
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
|
||||
@@ -20,26 +19,7 @@ const plexMonoPath = require.resolve(
|
||||
'@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-500-normal.woff2',
|
||||
)
|
||||
|
||||
const convexEntry = require.resolve('convex')
|
||||
const convexRoot = dirname(dirname(dirname(convexEntry)))
|
||||
const convexReactPath = join(convexRoot, 'dist/esm/react/index.js')
|
||||
const convexBrowserPath = join(convexRoot, 'dist/esm/browser/index.js')
|
||||
const convexValuesPath = join(convexRoot, 'dist/esm/values/index.js')
|
||||
const convexAuthReactPath = require.resolve('@convex-dev/auth/react')
|
||||
|
||||
const config = defineConfig({
|
||||
resolve: {
|
||||
dedupe: ['convex', '@convex-dev/auth', 'react', 'react-dom'],
|
||||
alias: {
|
||||
'convex/react': convexReactPath,
|
||||
'convex/browser': convexBrowserPath,
|
||||
'convex/values': convexValuesPath,
|
||||
'@convex-dev/auth/react': convexAuthReactPath,
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['convex/react', 'convex/browser'],
|
||||
},
|
||||
plugins: [
|
||||
devtools(),
|
||||
nitro({
|
||||
|
||||
Reference in New Issue
Block a user