pilothub 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.local.example +19 -0
- package/.github/workflows/ci.yml +40 -0
- package/.oxlintrc.json +3 -0
- package/AGENTS.md +45 -0
- package/CHANGELOG.md +138 -0
- package/DEPRECATIONS.md +7 -0
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/biome.json +41 -0
- package/convex/_generated/api.d.ts +153 -0
- package/convex/_generated/api.js +23 -0
- package/convex/_generated/dataModel.d.ts +60 -0
- package/convex/_generated/server.d.ts +143 -0
- package/convex/_generated/server.js +93 -0
- package/convex/auth.config.ts +8 -0
- package/convex/auth.ts +19 -0
- package/convex/comments.ts +88 -0
- package/convex/crons.ts +34 -0
- package/convex/devSeed.ts +459 -0
- package/convex/devSeedExtra.ts +541 -0
- package/convex/downloads.ts +78 -0
- package/convex/githubBackups.ts +170 -0
- package/convex/githubBackupsNode.ts +183 -0
- package/convex/githubImport.ts +317 -0
- package/convex/githubSoulBackups.ts +170 -0
- package/convex/githubSoulBackupsNode.ts +186 -0
- package/convex/http.ts +194 -0
- package/convex/httpApi.handlers.test.ts +488 -0
- package/convex/httpApi.test.ts +70 -0
- package/convex/httpApi.ts +305 -0
- package/convex/httpApiV1.handlers.test.ts +584 -0
- package/convex/httpApiV1.ts +1172 -0
- package/convex/leaderboards.ts +39 -0
- package/convex/lib/access.ts +36 -0
- package/convex/lib/apiTokenAuth.ts +36 -0
- package/convex/lib/badges.ts +50 -0
- package/convex/lib/changelog.test.ts +34 -0
- package/convex/lib/changelog.ts +278 -0
- package/convex/lib/embeddings.ts +38 -0
- package/convex/lib/githubBackup.ts +443 -0
- package/convex/lib/githubImport.test.ts +247 -0
- package/convex/lib/githubImport.ts +425 -0
- package/convex/lib/githubSoulBackup.ts +443 -0
- package/convex/lib/leaderboards.ts +103 -0
- package/convex/lib/moderation.ts +42 -0
- package/convex/lib/public.ts +89 -0
- package/convex/lib/searchText.test.ts +46 -0
- package/convex/lib/searchText.ts +27 -0
- package/convex/lib/skillBackfill.test.ts +34 -0
- package/convex/lib/skillBackfill.ts +67 -0
- package/convex/lib/skillPublish.test.ts +28 -0
- package/convex/lib/skillPublish.ts +284 -0
- package/convex/lib/skillStats.ts +80 -0
- package/convex/lib/skills.test.ts +197 -0
- package/convex/lib/skills.ts +273 -0
- package/convex/lib/soulChangelog.ts +273 -0
- package/convex/lib/soulPublish.ts +236 -0
- package/convex/lib/tokens.test.ts +33 -0
- package/convex/lib/tokens.ts +51 -0
- package/convex/lib/webhooks.test.ts +91 -0
- package/convex/lib/webhooks.ts +112 -0
- package/convex/maintenance.test.ts +270 -0
- package/convex/maintenance.ts +840 -0
- package/convex/rateLimits.ts +50 -0
- package/convex/schema.ts +472 -0
- package/convex/search.test.ts +12 -0
- package/convex/search.ts +254 -0
- package/convex/seed.test.ts +37 -0
- package/convex/seed.ts +254 -0
- package/convex/seedSouls.ts +111 -0
- package/convex/skillStatEvents.ts +568 -0
- package/convex/skills.ts +1606 -0
- package/convex/soulComments.ts +88 -0
- package/convex/soulDownloads.ts +14 -0
- package/convex/soulStars.ts +71 -0
- package/convex/souls.ts +570 -0
- package/convex/stars.ts +108 -0
- package/convex/statsMaintenance.ts +205 -0
- package/convex/telemetry.ts +434 -0
- package/convex/tokens.ts +88 -0
- package/convex/tsconfig.json +7 -0
- package/convex/uploads.ts +20 -0
- package/convex/users.ts +122 -0
- package/convex/webhooks.ts +50 -0
- package/convex.json +3 -0
- package/docs/README.md +32 -0
- package/docs/api.md +51 -0
- package/docs/architecture.md +61 -0
- package/docs/auth.md +54 -0
- package/docs/cli.md +117 -0
- package/docs/deploy.md +78 -0
- package/docs/diffing.md +84 -0
- package/docs/github-import.md +171 -0
- package/docs/http-api.md +187 -0
- package/docs/manual-testing.md +64 -0
- package/docs/mintlify.md +43 -0
- package/docs/quickstart.md +120 -0
- package/docs/skill-format.md +58 -0
- package/docs/soul-format.md +37 -0
- package/docs/spec.md +177 -0
- package/docs/telemetry.md +91 -0
- package/docs/troubleshooting.md +49 -0
- package/docs/webhook.md +51 -0
- package/e2e/menu-smoke.pw.test.ts +49 -0
- package/e2e/pilothub.e2e.test.ts +494 -0
- package/e2e/search-exact.pw.test.ts +97 -0
- package/package.json +84 -0
- package/packages/pilothub/LICENSE +22 -0
- package/packages/pilothub/README.md +57 -0
- package/packages/pilothub/bin/pilothub.js +2 -0
- package/packages/pilothub/package.json +41 -0
- package/packages/pilothub/src/browserAuth.test.ts +96 -0
- package/packages/pilothub/src/browserAuth.ts +174 -0
- package/packages/pilothub/src/cli/buildInfo.ts +94 -0
- package/packages/pilothub/src/cli/commands/auth.ts +97 -0
- package/packages/pilothub/src/cli/commands/delete.test.ts +73 -0
- package/packages/pilothub/src/cli/commands/delete.ts +83 -0
- package/packages/pilothub/src/cli/commands/publish.test.ts +122 -0
- package/packages/pilothub/src/cli/commands/publish.ts +108 -0
- package/packages/pilothub/src/cli/commands/skills.test.ts +191 -0
- package/packages/pilothub/src/cli/commands/skills.ts +380 -0
- package/packages/pilothub/src/cli/commands/star.ts +46 -0
- package/packages/pilothub/src/cli/commands/sync.test.ts +310 -0
- package/packages/pilothub/src/cli/commands/sync.ts +200 -0
- package/packages/pilothub/src/cli/commands/syncHelpers.test.ts +26 -0
- package/packages/pilothub/src/cli/commands/syncHelpers.ts +427 -0
- package/packages/pilothub/src/cli/commands/syncTypes.ts +27 -0
- package/packages/pilothub/src/cli/commands/unstar.ts +48 -0
- package/packages/pilothub/src/cli/helpStyle.ts +45 -0
- package/packages/pilothub/src/cli/pilotbotConfig.test.ts +159 -0
- package/packages/pilothub/src/cli/pilotbotConfig.ts +147 -0
- package/packages/pilothub/src/cli/registry.test.ts +63 -0
- package/packages/pilothub/src/cli/registry.ts +43 -0
- package/packages/pilothub/src/cli/scanSkills.test.ts +64 -0
- package/packages/pilothub/src/cli/scanSkills.ts +84 -0
- package/packages/pilothub/src/cli/slug.ts +16 -0
- package/packages/pilothub/src/cli/types.ts +12 -0
- package/packages/pilothub/src/cli/ui.ts +75 -0
- package/packages/pilothub/src/cli.ts +311 -0
- package/packages/pilothub/src/config.ts +36 -0
- package/packages/pilothub/src/discovery.test.ts +75 -0
- package/packages/pilothub/src/discovery.ts +19 -0
- package/packages/pilothub/src/http.test.ts +156 -0
- package/packages/pilothub/src/http.ts +301 -0
- package/packages/pilothub/src/schema/ark.ts +29 -0
- package/packages/pilothub/src/schema/index.ts +5 -0
- package/packages/pilothub/src/schema/routes.ts +22 -0
- package/packages/pilothub/src/schema/schemas.ts +260 -0
- package/packages/pilothub/src/schema/textFiles.test.ts +23 -0
- package/packages/pilothub/src/schema/textFiles.ts +66 -0
- package/packages/pilothub/src/skills.test.ts +191 -0
- package/packages/pilothub/src/skills.ts +172 -0
- package/packages/pilothub/src/types.ts +10 -0
- package/packages/pilothub/tsconfig.json +14 -0
- package/packages/schema/README.md +3 -0
- package/packages/schema/dist/ark.d.ts +4 -0
- package/packages/schema/dist/ark.js +26 -0
- package/packages/schema/dist/ark.js.map +1 -0
- package/packages/schema/dist/index.d.ts +5 -0
- package/packages/schema/dist/index.js +5 -0
- package/packages/schema/dist/index.js.map +1 -0
- package/packages/schema/dist/routes.d.ts +21 -0
- package/packages/schema/dist/routes.js +22 -0
- package/packages/schema/dist/routes.js.map +1 -0
- package/packages/schema/dist/schemas.d.ts +297 -0
- package/packages/schema/dist/schemas.js +243 -0
- package/packages/schema/dist/schemas.js.map +1 -0
- package/packages/schema/dist/textFiles.d.ts +5 -0
- package/packages/schema/dist/textFiles.js +66 -0
- package/packages/schema/dist/textFiles.js.map +1 -0
- package/packages/schema/package.json +26 -0
- package/packages/schema/src/ark.ts +29 -0
- package/packages/schema/src/index.ts +5 -0
- package/packages/schema/src/routes.ts +22 -0
- package/packages/schema/src/schemas.test.ts +123 -0
- package/packages/schema/src/schemas.ts +287 -0
- package/packages/schema/src/textFiles.test.ts +23 -0
- package/packages/schema/src/textFiles.ts +66 -0
- package/packages/schema/tsconfig.json +15 -0
- package/pilothub +46 -0
- package/playwright.config.ts +33 -0
- package/public/.well-known/pilothub.json +6 -0
- package/public/api/v1/openapi.json +379 -0
- package/public/favicon.ico +0 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +25 -0
- package/public/og.png +0 -0
- package/public/og.svg +98 -0
- package/public/pilot-logo.png +0 -0
- package/public/pilot-mark.png +0 -0
- package/public/robots.txt +3 -0
- package/public/tanstack-circle-logo.png +0 -0
- package/public/tanstack-word-logo-white.svg +1 -0
- package/scripts/check-peer-deps.ts +56 -0
- package/scripts/docs-list.ts +148 -0
- package/scripts/run-playwright-local.sh +14 -0
- package/server/og/fetchSkillOgMeta.ts +27 -0
- package/server/og/fetchSoulOgMeta.ts +27 -0
- package/server/og/ogAssets.ts +80 -0
- package/server/og/skillOgSvg.test.ts +59 -0
- package/server/og/skillOgSvg.ts +258 -0
- package/server/og/soulOgSvg.ts +209 -0
- package/server/routes/og/skill.png.ts +103 -0
- package/server/routes/og/soul.png.ts +111 -0
- package/src/__tests__/skill-detail-page.test.tsx +86 -0
- package/src/__tests__/skills-index.test.tsx +145 -0
- package/src/__tests__/upload.route.test.tsx +228 -0
- package/src/components/AppProviders.tsx +19 -0
- package/src/components/ClientOnly.tsx +18 -0
- package/src/components/Footer.tsx +29 -0
- package/src/components/Header.tsx +295 -0
- package/src/components/InstallSwitcher.tsx +53 -0
- package/src/components/SkillCard.tsx +36 -0
- package/src/components/SkillDetailPage.tsx +817 -0
- package/src/components/SkillDiffCard.tsx +485 -0
- package/src/components/SoulCard.tsx +19 -0
- package/src/components/SoulDetailPage.tsx +263 -0
- package/src/components/UserBootstrap.tsx +18 -0
- package/src/components/ui/dropdown-menu.tsx +67 -0
- package/src/components/ui/toggle-group.tsx +35 -0
- package/src/convex/client.ts +3 -0
- package/src/lib/badges.ts +29 -0
- package/src/lib/diffing.test.ts +163 -0
- package/src/lib/diffing.ts +106 -0
- package/src/lib/gravatar.test.ts +9 -0
- package/src/lib/gravatar.ts +158 -0
- package/src/lib/og.test.ts +142 -0
- package/src/lib/og.ts +156 -0
- package/src/lib/publicUser.ts +39 -0
- package/src/lib/roles.ts +19 -0
- package/src/lib/site.test.ts +130 -0
- package/src/lib/site.ts +84 -0
- package/src/lib/theme-transition.test.ts +134 -0
- package/src/lib/theme-transition.ts +134 -0
- package/src/lib/theme.test.tsx +88 -0
- package/src/lib/theme.ts +43 -0
- package/src/lib/uploadFiles.jsdom.test.ts +33 -0
- package/src/lib/uploadFiles.test.ts +123 -0
- package/src/lib/uploadFiles.ts +245 -0
- package/src/lib/uploadUtils.test.ts +78 -0
- package/src/lib/uploadUtils.ts +93 -0
- package/src/lib/useAuthStatus.ts +12 -0
- package/src/lib/utils.test.ts +9 -0
- package/src/lib/utils.ts +6 -0
- package/src/logo.svg +12 -0
- package/src/routeTree.gen.ts +345 -0
- package/src/router.tsx +17 -0
- package/src/routes/$owner/$slug.tsx +55 -0
- package/src/routes/__root.tsx +136 -0
- package/src/routes/admin.tsx +11 -0
- package/src/routes/cli/auth.tsx +168 -0
- package/src/routes/dashboard.tsx +97 -0
- package/src/routes/import.tsx +415 -0
- package/src/routes/index.tsx +252 -0
- package/src/routes/management.tsx +529 -0
- package/src/routes/settings.tsx +203 -0
- package/src/routes/skills/index.tsx +422 -0
- package/src/routes/souls/$slug.tsx +55 -0
- package/src/routes/souls/index.tsx +243 -0
- package/src/routes/stars.tsx +68 -0
- package/src/routes/u/$handle.tsx +307 -0
- package/src/routes/upload/utils.ts +81 -0
- package/src/routes/upload.tsx +499 -0
- package/src/styles.css +2718 -0
- package/tsconfig.json +24 -0
- package/tsconfig.oxlint.json +16 -0
- package/vercel.json +8 -0
- package/vite.config.ts +48 -0
- package/vitest.config.ts +47 -0
- package/vitest.e2e.config.ts +11 -0
- package/vitest.setup.ts +1 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { gravatarUrl } from './gravatar'
|
|
3
|
+
|
|
4
|
+
describe('gravatarUrl', () => {
|
|
5
|
+
it('generates a stable hash', () => {
|
|
6
|
+
const url = gravatarUrl('MyEmailAddress@example.com ')
|
|
7
|
+
expect(url).toContain('0bc83cb571cd1c50ba6f3e8a78ef1346')
|
|
8
|
+
})
|
|
9
|
+
})
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
function md5cycle(x: number[], k: number[]) {
|
|
2
|
+
let [a, b, c, d] = x
|
|
3
|
+
|
|
4
|
+
a = ff(a, b, c, d, k[0], 7, -680876936)
|
|
5
|
+
d = ff(d, a, b, c, k[1], 12, -389564586)
|
|
6
|
+
c = ff(c, d, a, b, k[2], 17, 606105819)
|
|
7
|
+
b = ff(b, c, d, a, k[3], 22, -1044525330)
|
|
8
|
+
a = ff(a, b, c, d, k[4], 7, -176418897)
|
|
9
|
+
d = ff(d, a, b, c, k[5], 12, 1200080426)
|
|
10
|
+
c = ff(c, d, a, b, k[6], 17, -1473231341)
|
|
11
|
+
b = ff(b, c, d, a, k[7], 22, -45705983)
|
|
12
|
+
a = ff(a, b, c, d, k[8], 7, 1770035416)
|
|
13
|
+
d = ff(d, a, b, c, k[9], 12, -1958414417)
|
|
14
|
+
c = ff(c, d, a, b, k[10], 17, -42063)
|
|
15
|
+
b = ff(b, c, d, a, k[11], 22, -1990404162)
|
|
16
|
+
a = ff(a, b, c, d, k[12], 7, 1804603682)
|
|
17
|
+
d = ff(d, a, b, c, k[13], 12, -40341101)
|
|
18
|
+
c = ff(c, d, a, b, k[14], 17, -1502002290)
|
|
19
|
+
b = ff(b, c, d, a, k[15], 22, 1236535329)
|
|
20
|
+
|
|
21
|
+
a = gg(a, b, c, d, k[1], 5, -165796510)
|
|
22
|
+
d = gg(d, a, b, c, k[6], 9, -1069501632)
|
|
23
|
+
c = gg(c, d, a, b, k[11], 14, 643717713)
|
|
24
|
+
b = gg(b, c, d, a, k[0], 20, -373897302)
|
|
25
|
+
a = gg(a, b, c, d, k[5], 5, -701558691)
|
|
26
|
+
d = gg(d, a, b, c, k[10], 9, 38016083)
|
|
27
|
+
c = gg(c, d, a, b, k[15], 14, -660478335)
|
|
28
|
+
b = gg(b, c, d, a, k[4], 20, -405537848)
|
|
29
|
+
a = gg(a, b, c, d, k[9], 5, 568446438)
|
|
30
|
+
d = gg(d, a, b, c, k[14], 9, -1019803690)
|
|
31
|
+
c = gg(c, d, a, b, k[3], 14, -187363961)
|
|
32
|
+
b = gg(b, c, d, a, k[8], 20, 1163531501)
|
|
33
|
+
a = gg(a, b, c, d, k[13], 5, -1444681467)
|
|
34
|
+
d = gg(d, a, b, c, k[2], 9, -51403784)
|
|
35
|
+
c = gg(c, d, a, b, k[7], 14, 1735328473)
|
|
36
|
+
b = gg(b, c, d, a, k[12], 20, -1926607734)
|
|
37
|
+
|
|
38
|
+
a = hh(a, b, c, d, k[5], 4, -378558)
|
|
39
|
+
d = hh(d, a, b, c, k[8], 11, -2022574463)
|
|
40
|
+
c = hh(c, d, a, b, k[11], 16, 1839030562)
|
|
41
|
+
b = hh(b, c, d, a, k[14], 23, -35309556)
|
|
42
|
+
a = hh(a, b, c, d, k[1], 4, -1530992060)
|
|
43
|
+
d = hh(d, a, b, c, k[4], 11, 1272893353)
|
|
44
|
+
c = hh(c, d, a, b, k[7], 16, -155497632)
|
|
45
|
+
b = hh(b, c, d, a, k[10], 23, -1094730640)
|
|
46
|
+
a = hh(a, b, c, d, k[13], 4, 681279174)
|
|
47
|
+
d = hh(d, a, b, c, k[0], 11, -358537222)
|
|
48
|
+
c = hh(c, d, a, b, k[3], 16, -722521979)
|
|
49
|
+
b = hh(b, c, d, a, k[6], 23, 76029189)
|
|
50
|
+
a = hh(a, b, c, d, k[9], 4, -640364487)
|
|
51
|
+
d = hh(d, a, b, c, k[12], 11, -421815835)
|
|
52
|
+
c = hh(c, d, a, b, k[15], 16, 530742520)
|
|
53
|
+
b = hh(b, c, d, a, k[2], 23, -995338651)
|
|
54
|
+
|
|
55
|
+
a = ii(a, b, c, d, k[0], 6, -198630844)
|
|
56
|
+
d = ii(d, a, b, c, k[7], 10, 1126891415)
|
|
57
|
+
c = ii(c, d, a, b, k[14], 15, -1416354905)
|
|
58
|
+
b = ii(b, c, d, a, k[5], 21, -57434055)
|
|
59
|
+
a = ii(a, b, c, d, k[12], 6, 1700485571)
|
|
60
|
+
d = ii(d, a, b, c, k[3], 10, -1894986606)
|
|
61
|
+
c = ii(c, d, a, b, k[10], 15, -1051523)
|
|
62
|
+
b = ii(b, c, d, a, k[1], 21, -2054922799)
|
|
63
|
+
a = ii(a, b, c, d, k[8], 6, 1873313359)
|
|
64
|
+
d = ii(d, a, b, c, k[15], 10, -30611744)
|
|
65
|
+
c = ii(c, d, a, b, k[6], 15, -1560198380)
|
|
66
|
+
b = ii(b, c, d, a, k[13], 21, 1309151649)
|
|
67
|
+
a = ii(a, b, c, d, k[4], 6, -145523070)
|
|
68
|
+
d = ii(d, a, b, c, k[11], 10, -1120210379)
|
|
69
|
+
c = ii(c, d, a, b, k[2], 15, 718787259)
|
|
70
|
+
b = ii(b, c, d, a, k[9], 21, -343485551)
|
|
71
|
+
|
|
72
|
+
x[0] = add32(a, x[0])
|
|
73
|
+
x[1] = add32(b, x[1])
|
|
74
|
+
x[2] = add32(c, x[2])
|
|
75
|
+
x[3] = add32(d, x[3])
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function cmn(q: number, a: number, b: number, x: number, s: number, t: number) {
|
|
79
|
+
a = add32(add32(a, q), add32(x, t))
|
|
80
|
+
return add32((a << s) | (a >>> (32 - s)), b)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function ff(a: number, b: number, c: number, d: number, x: number, s: number, t: number) {
|
|
84
|
+
return cmn((b & c) | (~b & d), a, b, x, s, t)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function gg(a: number, b: number, c: number, d: number, x: number, s: number, t: number) {
|
|
88
|
+
return cmn((b & d) | (c & ~d), a, b, x, s, t)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function hh(a: number, b: number, c: number, d: number, x: number, s: number, t: number) {
|
|
92
|
+
return cmn(b ^ c ^ d, a, b, x, s, t)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function ii(a: number, b: number, c: number, d: number, x: number, s: number, t: number) {
|
|
96
|
+
return cmn(c ^ (b | ~d), a, b, x, s, t)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function md51(s: string) {
|
|
100
|
+
const n = s.length
|
|
101
|
+
const state = [1732584193, -271733879, -1732584194, 271733878]
|
|
102
|
+
let i = 0
|
|
103
|
+
for (i = 64; i <= n; i += 64) {
|
|
104
|
+
md5cycle(state, md5blk(s.substring(i - 64, i)))
|
|
105
|
+
}
|
|
106
|
+
s = s.substring(i - 64)
|
|
107
|
+
const tail = Array(16).fill(0) as number[]
|
|
108
|
+
for (i = 0; i < s.length; i += 1) {
|
|
109
|
+
tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3)
|
|
110
|
+
}
|
|
111
|
+
tail[i >> 2] |= 0x80 << ((i % 4) << 3)
|
|
112
|
+
if (i > 55) {
|
|
113
|
+
md5cycle(state, tail)
|
|
114
|
+
for (let j = 0; j < 16; j += 1) tail[j] = 0
|
|
115
|
+
}
|
|
116
|
+
tail[14] = n * 8
|
|
117
|
+
md5cycle(state, tail)
|
|
118
|
+
return state
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function md5blk(s: string) {
|
|
122
|
+
const md5blks: number[] = []
|
|
123
|
+
for (let i = 0; i < 64; i += 4) {
|
|
124
|
+
md5blks[i >> 2] =
|
|
125
|
+
s.charCodeAt(i) +
|
|
126
|
+
(s.charCodeAt(i + 1) << 8) +
|
|
127
|
+
(s.charCodeAt(i + 2) << 16) +
|
|
128
|
+
(s.charCodeAt(i + 3) << 24)
|
|
129
|
+
}
|
|
130
|
+
return md5blks
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function rhex(n: number) {
|
|
134
|
+
const s = '0123456789abcdef'
|
|
135
|
+
let j = 0
|
|
136
|
+
let out = ''
|
|
137
|
+
for (; j < 4; j += 1) {
|
|
138
|
+
out += s.charAt((n >> (j * 8 + 4)) & 0x0f) + s.charAt((n >> (j * 8)) & 0x0f)
|
|
139
|
+
}
|
|
140
|
+
return out
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function hex(x: number[]) {
|
|
144
|
+
for (let i = 0; i < x.length; i += 1) {
|
|
145
|
+
x[i] = Number(x[i])
|
|
146
|
+
}
|
|
147
|
+
return x.map(rhex).join('')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function add32(a: number, b: number) {
|
|
151
|
+
return (a + b) & 0xffffffff
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function gravatarUrl(email: string, size = 160) {
|
|
155
|
+
const normalized = email.trim().toLowerCase()
|
|
156
|
+
const hash = hex(md51(normalized))
|
|
157
|
+
return `https://www.gravatar.com/avatar/${hash}?d=identicon&s=${size}`
|
|
158
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { buildSkillMeta, buildSoulMeta, fetchSkillMeta, fetchSoulMeta } from './og'
|
|
3
|
+
|
|
4
|
+
describe('og helpers', () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
vi.restoreAllMocks()
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('builds metadata with owner and summary', () => {
|
|
10
|
+
const meta = buildSkillMeta({
|
|
11
|
+
slug: 'weather',
|
|
12
|
+
owner: 'steipete',
|
|
13
|
+
displayName: 'Weather',
|
|
14
|
+
summary: 'Forecasts for your area.',
|
|
15
|
+
version: '1.2.3',
|
|
16
|
+
})
|
|
17
|
+
expect(meta.title).toBe('Weather — PilotHub')
|
|
18
|
+
expect(meta.description).toBe('Forecasts for your area.')
|
|
19
|
+
expect(meta.url).toContain('/steipete/weather')
|
|
20
|
+
expect(meta.owner).toBe('steipete')
|
|
21
|
+
expect(meta.image).toContain('/og/skill.png?')
|
|
22
|
+
expect(meta.image).toContain('v=5')
|
|
23
|
+
expect(meta.image).toContain('slug=weather')
|
|
24
|
+
expect(meta.image).toContain('owner=steipete')
|
|
25
|
+
expect(meta.image).toContain('version=1.2.3')
|
|
26
|
+
expect(meta.image).not.toContain('title=')
|
|
27
|
+
expect(meta.image).not.toContain('description=')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('builds soul metadata with summary', () => {
|
|
31
|
+
const meta = buildSoulMeta({
|
|
32
|
+
slug: 'north-star',
|
|
33
|
+
owner: 'someone',
|
|
34
|
+
displayName: 'North Star',
|
|
35
|
+
summary: 'Personal north star notes.',
|
|
36
|
+
version: '0.1.0',
|
|
37
|
+
})
|
|
38
|
+
expect(meta.title).toBe('North Star — SoulHub')
|
|
39
|
+
expect(meta.description).toBe('Personal north star notes.')
|
|
40
|
+
expect(meta.url).toContain('/souls/north-star')
|
|
41
|
+
expect(meta.owner).toBe('someone')
|
|
42
|
+
expect(meta.image).toContain('/og/soul.png?')
|
|
43
|
+
expect(meta.image).toContain('v=1')
|
|
44
|
+
expect(meta.image).toContain('slug=north-star')
|
|
45
|
+
expect(meta.image).toContain('owner=someone')
|
|
46
|
+
expect(meta.image).toContain('version=0.1.0')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('uses defaults when owner and summary are missing', () => {
|
|
50
|
+
const meta = buildSkillMeta({ slug: 'parser' })
|
|
51
|
+
expect(meta.title).toBe('parser — PilotHub')
|
|
52
|
+
expect(meta.description).toMatch(/PilotHub — a fast skill registry/i)
|
|
53
|
+
expect(meta.url).toContain('/unknown/parser')
|
|
54
|
+
expect(meta.owner).toBeNull()
|
|
55
|
+
expect(meta.image).toContain('slug=parser')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('uses soul defaults when owner and summary are missing', () => {
|
|
59
|
+
const meta = buildSoulMeta({ slug: 'signal' })
|
|
60
|
+
expect(meta.title).toBe('signal — SoulHub')
|
|
61
|
+
expect(meta.description).toMatch(/SoulHub — the home for SOUL.md/i)
|
|
62
|
+
expect(meta.url).toContain('/souls/signal')
|
|
63
|
+
expect(meta.owner).toBeNull()
|
|
64
|
+
expect(meta.image).toContain('slug=signal')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('truncates long descriptions', () => {
|
|
68
|
+
const longSummary = 'a'.repeat(240)
|
|
69
|
+
const meta = buildSkillMeta({ slug: 'long', summary: longSummary })
|
|
70
|
+
expect(meta.description.length).toBe(200)
|
|
71
|
+
expect(meta.description.endsWith('…')).toBe(true)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('fetches skill metadata when response is ok', async () => {
|
|
75
|
+
const fetchMock = vi.fn(async () => ({
|
|
76
|
+
ok: true,
|
|
77
|
+
json: async () => ({
|
|
78
|
+
skill: { displayName: 'Weather', summary: 'Forecasts' },
|
|
79
|
+
owner: { handle: 'steipete', userId: 'users:1' },
|
|
80
|
+
latestVersion: { version: '1.2.3' },
|
|
81
|
+
}),
|
|
82
|
+
}))
|
|
83
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
84
|
+
|
|
85
|
+
const meta = await fetchSkillMeta('weather')
|
|
86
|
+
expect(meta).toEqual({
|
|
87
|
+
displayName: 'Weather',
|
|
88
|
+
summary: 'Forecasts',
|
|
89
|
+
owner: 'steipete',
|
|
90
|
+
ownerId: 'users:1',
|
|
91
|
+
version: '1.2.3',
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('fetches soul metadata when response is ok', async () => {
|
|
96
|
+
const fetchMock = vi.fn(async () => ({
|
|
97
|
+
ok: true,
|
|
98
|
+
json: async () => ({
|
|
99
|
+
soul: { displayName: 'North Star', summary: 'Signal' },
|
|
100
|
+
owner: { handle: 'steipete' },
|
|
101
|
+
latestVersion: { version: '0.1.0' },
|
|
102
|
+
}),
|
|
103
|
+
}))
|
|
104
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
105
|
+
|
|
106
|
+
const meta = await fetchSoulMeta('north-star')
|
|
107
|
+
expect(meta).toEqual({
|
|
108
|
+
displayName: 'North Star',
|
|
109
|
+
summary: 'Signal',
|
|
110
|
+
owner: 'steipete',
|
|
111
|
+
version: '0.1.0',
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('returns null when response is not ok', async () => {
|
|
116
|
+
const fetchMock = vi.fn(async () => ({ ok: false }))
|
|
117
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
118
|
+
|
|
119
|
+
const meta = await fetchSkillMeta('weather')
|
|
120
|
+
expect(meta).toBeNull()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('returns null when fetch throws', async () => {
|
|
124
|
+
const fetchMock = vi.fn(async () => {
|
|
125
|
+
throw new Error('network')
|
|
126
|
+
})
|
|
127
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
128
|
+
|
|
129
|
+
const meta = await fetchSkillMeta('weather')
|
|
130
|
+
expect(meta).toBeNull()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('returns null when soul fetch throws', async () => {
|
|
134
|
+
const fetchMock = vi.fn(async () => {
|
|
135
|
+
throw new Error('network')
|
|
136
|
+
})
|
|
137
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
138
|
+
|
|
139
|
+
const meta = await fetchSoulMeta('north-star')
|
|
140
|
+
expect(meta).toBeNull()
|
|
141
|
+
})
|
|
142
|
+
})
|
package/src/lib/og.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { getPilotHubSiteUrl, getOnlyCrabsSiteUrl } from './site'
|
|
2
|
+
|
|
3
|
+
type SkillMetaSource = {
|
|
4
|
+
slug: string
|
|
5
|
+
owner?: string | null
|
|
6
|
+
ownerId?: string | null
|
|
7
|
+
displayName?: string | null
|
|
8
|
+
summary?: string | null
|
|
9
|
+
version?: string | null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type SkillMeta = {
|
|
13
|
+
title: string
|
|
14
|
+
description: string
|
|
15
|
+
image: string
|
|
16
|
+
url: string
|
|
17
|
+
owner: string | null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type SoulMetaSource = {
|
|
21
|
+
slug: string
|
|
22
|
+
owner?: string | null
|
|
23
|
+
displayName?: string | null
|
|
24
|
+
summary?: string | null
|
|
25
|
+
version?: string | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type SoulMeta = {
|
|
29
|
+
title: string
|
|
30
|
+
description: string
|
|
31
|
+
image: string
|
|
32
|
+
url: string
|
|
33
|
+
owner: string | null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const DEFAULT_DESCRIPTION = 'PilotHub — a fast skill registry for agents, with vector search.'
|
|
37
|
+
const DEFAULT_SOUL_DESCRIPTION = 'SoulHub — the home for SOUL.md bundles and personal system lore.'
|
|
38
|
+
const OG_SKILL_IMAGE_LAYOUT_VERSION = '5'
|
|
39
|
+
const OG_SOUL_IMAGE_LAYOUT_VERSION = '1'
|
|
40
|
+
|
|
41
|
+
export function getSiteUrl() {
|
|
42
|
+
return getPilotHubSiteUrl()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getSoulSiteUrl() {
|
|
46
|
+
return getOnlyCrabsSiteUrl()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getApiBase() {
|
|
50
|
+
const explicit = import.meta.env.VITE_CONVEX_SITE_URL?.trim()
|
|
51
|
+
return explicit || getSiteUrl()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function fetchSkillMeta(slug: string) {
|
|
55
|
+
try {
|
|
56
|
+
const apiBase = getApiBase()
|
|
57
|
+
const url = new URL(`/api/v1/skills/${encodeURIComponent(slug)}`, apiBase)
|
|
58
|
+
const response = await fetch(url.toString(), { headers: { Accept: 'application/json' } })
|
|
59
|
+
if (!response.ok) return null
|
|
60
|
+
const payload = (await response.json()) as {
|
|
61
|
+
skill?: { displayName?: string; summary?: string | null } | null
|
|
62
|
+
owner?: { handle?: string | null; userId?: string | null } | null
|
|
63
|
+
latestVersion?: { version?: string | null } | null
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
displayName: payload.skill?.displayName ?? null,
|
|
67
|
+
summary: payload.skill?.summary ?? null,
|
|
68
|
+
owner: payload.owner?.handle ?? null,
|
|
69
|
+
ownerId: payload.owner?.userId ?? null,
|
|
70
|
+
version: payload.latestVersion?.version ?? null,
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function fetchSoulMeta(slug: string) {
|
|
78
|
+
try {
|
|
79
|
+
const apiBase = getApiBase()
|
|
80
|
+
const url = new URL(`/api/v1/souls/${encodeURIComponent(slug)}`, apiBase)
|
|
81
|
+
const response = await fetch(url.toString(), { headers: { Accept: 'application/json' } })
|
|
82
|
+
if (!response.ok) return null
|
|
83
|
+
const payload = (await response.json()) as {
|
|
84
|
+
soul?: { displayName?: string; summary?: string | null } | null
|
|
85
|
+
owner?: { handle?: string | null } | null
|
|
86
|
+
latestVersion?: { version?: string | null } | null
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
displayName: payload.soul?.displayName ?? null,
|
|
90
|
+
summary: payload.soul?.summary ?? null,
|
|
91
|
+
owner: payload.owner?.handle ?? null,
|
|
92
|
+
version: payload.latestVersion?.version ?? null,
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function buildSkillMeta(source: SkillMetaSource): SkillMeta {
|
|
100
|
+
const siteUrl = getSiteUrl()
|
|
101
|
+
const owner = clean(source.owner)
|
|
102
|
+
const ownerId = clean(source.ownerId)
|
|
103
|
+
const displayName = clean(source.displayName) || clean(source.slug)
|
|
104
|
+
const summary = clean(source.summary)
|
|
105
|
+
const version = clean(source.version)
|
|
106
|
+
const title = `${displayName} — PilotHub`
|
|
107
|
+
const description =
|
|
108
|
+
summary || (owner ? `Agent skill by @${owner} on PilotHub.` : DEFAULT_DESCRIPTION)
|
|
109
|
+
const ownerPath = owner || ownerId || 'unknown'
|
|
110
|
+
const url = `${siteUrl}/${ownerPath}/${source.slug}`
|
|
111
|
+
const imageParams = new URLSearchParams()
|
|
112
|
+
imageParams.set('v', OG_SKILL_IMAGE_LAYOUT_VERSION)
|
|
113
|
+
imageParams.set('slug', source.slug)
|
|
114
|
+
if (owner) imageParams.set('owner', owner)
|
|
115
|
+
if (version) imageParams.set('version', version)
|
|
116
|
+
return {
|
|
117
|
+
title,
|
|
118
|
+
description: truncate(description, 200),
|
|
119
|
+
image: `${siteUrl}/og/skill.png?${imageParams.toString()}`,
|
|
120
|
+
url,
|
|
121
|
+
owner: owner || null,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function buildSoulMeta(source: SoulMetaSource): SoulMeta {
|
|
126
|
+
const siteUrl = getSoulSiteUrl()
|
|
127
|
+
const owner = clean(source.owner)
|
|
128
|
+
const displayName = clean(source.displayName) || clean(source.slug)
|
|
129
|
+
const summary = clean(source.summary)
|
|
130
|
+
const version = clean(source.version)
|
|
131
|
+
const title = `${displayName} — SoulHub`
|
|
132
|
+
const description =
|
|
133
|
+
summary || (owner ? `Soul by @${owner} on SoulHub.` : DEFAULT_SOUL_DESCRIPTION)
|
|
134
|
+
const url = `${siteUrl}/souls/${source.slug}`
|
|
135
|
+
const imageParams = new URLSearchParams()
|
|
136
|
+
imageParams.set('v', OG_SOUL_IMAGE_LAYOUT_VERSION)
|
|
137
|
+
imageParams.set('slug', source.slug)
|
|
138
|
+
if (owner) imageParams.set('owner', owner)
|
|
139
|
+
if (version) imageParams.set('version', version)
|
|
140
|
+
return {
|
|
141
|
+
title,
|
|
142
|
+
description: truncate(description, 200),
|
|
143
|
+
image: `${siteUrl}/og/soul.png?${imageParams.toString()}`,
|
|
144
|
+
url,
|
|
145
|
+
owner: owner || null,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function clean(value?: string | null) {
|
|
150
|
+
return value?.trim() ?? ''
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function truncate(value: string, max: number) {
|
|
154
|
+
if (value.length <= max) return value
|
|
155
|
+
return `${value.slice(0, max - 1).trim()}…`
|
|
156
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Doc } from '../../convex/_generated/dataModel'
|
|
2
|
+
|
|
3
|
+
export type PublicUser = Pick<
|
|
4
|
+
Doc<'users'>,
|
|
5
|
+
'_id' | '_creationTime' | 'handle' | 'name' | 'displayName' | 'image' | 'bio'
|
|
6
|
+
>
|
|
7
|
+
|
|
8
|
+
export type PublicSkill = Pick<
|
|
9
|
+
Doc<'skills'>,
|
|
10
|
+
| '_id'
|
|
11
|
+
| '_creationTime'
|
|
12
|
+
| 'slug'
|
|
13
|
+
| 'displayName'
|
|
14
|
+
| 'summary'
|
|
15
|
+
| 'ownerUserId'
|
|
16
|
+
| 'canonicalSkillId'
|
|
17
|
+
| 'forkOf'
|
|
18
|
+
| 'latestVersionId'
|
|
19
|
+
| 'tags'
|
|
20
|
+
| 'badges'
|
|
21
|
+
| 'stats'
|
|
22
|
+
| 'createdAt'
|
|
23
|
+
| 'updatedAt'
|
|
24
|
+
>
|
|
25
|
+
|
|
26
|
+
export type PublicSoul = Pick<
|
|
27
|
+
Doc<'souls'>,
|
|
28
|
+
| '_id'
|
|
29
|
+
| '_creationTime'
|
|
30
|
+
| 'slug'
|
|
31
|
+
| 'displayName'
|
|
32
|
+
| 'summary'
|
|
33
|
+
| 'ownerUserId'
|
|
34
|
+
| 'latestVersionId'
|
|
35
|
+
| 'tags'
|
|
36
|
+
| 'stats'
|
|
37
|
+
| 'createdAt'
|
|
38
|
+
| 'updatedAt'
|
|
39
|
+
>
|
package/src/lib/roles.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Doc } from '../../convex/_generated/dataModel'
|
|
2
|
+
import type { PublicSkill } from './publicUser'
|
|
3
|
+
|
|
4
|
+
type User = Doc<'users'> | null | undefined
|
|
5
|
+
|
|
6
|
+
type Skill = PublicSkill | null | undefined
|
|
7
|
+
|
|
8
|
+
export function isAdmin(user: User) {
|
|
9
|
+
return user?.role === 'admin'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isModerator(user: User) {
|
|
13
|
+
return user?.role === 'admin' || user?.role === 'moderator'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function canManageSkill(user: User, skill: Skill) {
|
|
17
|
+
if (!user || !skill) return false
|
|
18
|
+
return user._id === skill.ownerUserId || isModerator(user)
|
|
19
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/* @vitest-environment node */
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
detectSiteMode,
|
|
7
|
+
detectSiteModeFromUrl,
|
|
8
|
+
getPilotHubSiteUrl,
|
|
9
|
+
getOnlyCrabsHost,
|
|
10
|
+
getOnlyCrabsSiteUrl,
|
|
11
|
+
getSiteDescription,
|
|
12
|
+
getSiteMode,
|
|
13
|
+
getSiteName,
|
|
14
|
+
getSiteUrlForMode,
|
|
15
|
+
} from './site'
|
|
16
|
+
|
|
17
|
+
function withMetaEnv<T>(values: Record<string, string | undefined>, run: () => T): T {
|
|
18
|
+
const env = import.meta.env as unknown as Record<string, unknown>
|
|
19
|
+
const previous = new Map<string, unknown>()
|
|
20
|
+
for (const [key, value] of Object.entries(values)) {
|
|
21
|
+
previous.set(key, env[key])
|
|
22
|
+
if (value === undefined) {
|
|
23
|
+
delete env[key]
|
|
24
|
+
} else {
|
|
25
|
+
env[key] = value
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return run()
|
|
30
|
+
} finally {
|
|
31
|
+
for (const [key, value] of previous.entries()) {
|
|
32
|
+
if (value === undefined) delete env[key]
|
|
33
|
+
else env[key] = value
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
vi.unstubAllGlobals()
|
|
40
|
+
vi.unstubAllEnvs()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('site helpers', () => {
|
|
44
|
+
it('returns default and env configured site URLs', () => {
|
|
45
|
+
expect(getPilotHubSiteUrl()).toBe('https://pilothub.com')
|
|
46
|
+
withMetaEnv({ VITE_SITE_URL: 'https://example.com' }, () => {
|
|
47
|
+
expect(getPilotHubSiteUrl()).toBe('https://example.com')
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('picks SoulHub URL from explicit env', () => {
|
|
52
|
+
withMetaEnv({ VITE_SOULHUB_SITE_URL: 'https://souls.example.com' }, () => {
|
|
53
|
+
expect(getOnlyCrabsSiteUrl()).toBe('https://souls.example.com')
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('derives SoulHub URL from local VITE_SITE_URL', () => {
|
|
58
|
+
withMetaEnv({ VITE_SITE_URL: 'http://localhost:3000' }, () => {
|
|
59
|
+
expect(getOnlyCrabsSiteUrl()).toBe('http://localhost:3000')
|
|
60
|
+
})
|
|
61
|
+
withMetaEnv({ VITE_SITE_URL: 'http://127.0.0.1:3000' }, () => {
|
|
62
|
+
expect(getOnlyCrabsSiteUrl()).toBe('http://127.0.0.1:3000')
|
|
63
|
+
})
|
|
64
|
+
withMetaEnv({ VITE_SITE_URL: 'http://0.0.0.0:3000' }, () => {
|
|
65
|
+
expect(getOnlyCrabsSiteUrl()).toBe('http://0.0.0.0:3000')
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('falls back to default SoulHub URL for invalid VITE_SITE_URL', () => {
|
|
70
|
+
withMetaEnv({ VITE_SITE_URL: 'not a url' }, () => {
|
|
71
|
+
expect(getOnlyCrabsSiteUrl()).toBe('https://onlycrabs.ai')
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('detects site mode from host and URLs', () => {
|
|
76
|
+
expect(detectSiteMode(null)).toBe('skills')
|
|
77
|
+
|
|
78
|
+
withMetaEnv({ VITE_SOULHUB_HOST: 'souls.example.com' }, () => {
|
|
79
|
+
expect(getOnlyCrabsHost()).toBe('souls.example.com')
|
|
80
|
+
expect(detectSiteMode('souls.example.com')).toBe('souls')
|
|
81
|
+
expect(detectSiteMode('sub.souls.example.com')).toBe('souls')
|
|
82
|
+
expect(detectSiteMode('pilothub.com')).toBe('skills')
|
|
83
|
+
|
|
84
|
+
expect(detectSiteModeFromUrl('https://souls.example.com/x')).toBe('souls')
|
|
85
|
+
expect(detectSiteModeFromUrl('souls.example.com')).toBe('souls')
|
|
86
|
+
expect(detectSiteModeFromUrl('https://pilothub.com')).toBe('skills')
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('detects site mode from window when available', () => {
|
|
91
|
+
withMetaEnv({ VITE_SOULHUB_HOST: 'onlycrabs.ai' }, () => {
|
|
92
|
+
vi.stubGlobal('window', { location: { hostname: 'onlycrabs.ai' } } as unknown as Window)
|
|
93
|
+
expect(getSiteMode()).toBe('souls')
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('detects site mode from env on the server', () => {
|
|
98
|
+
withMetaEnv({ VITE_SITE_MODE: 'souls', VITE_SOULHUB_HOST: 'onlycrabs.ai' }, () => {
|
|
99
|
+
expect(getSiteMode()).toBe('souls')
|
|
100
|
+
})
|
|
101
|
+
withMetaEnv({ VITE_SITE_MODE: 'skills', VITE_SOULHUB_HOST: 'onlycrabs.ai' }, () => {
|
|
102
|
+
expect(getSiteMode()).toBe('skills')
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('detects site mode from VITE_SOULHUB_SITE_URL and SITE_URL fallback', () => {
|
|
107
|
+
withMetaEnv(
|
|
108
|
+
{ VITE_SITE_MODE: undefined, VITE_SOULHUB_SITE_URL: 'https://onlycrabs.ai' },
|
|
109
|
+
() => {
|
|
110
|
+
expect(getSiteMode()).toBe('souls')
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
withMetaEnv({ VITE_SOULHUB_SITE_URL: undefined, VITE_SITE_URL: undefined }, () => {
|
|
115
|
+
vi.stubEnv('SITE_URL', 'https://onlycrabs.ai')
|
|
116
|
+
expect(getSiteMode()).toBe('souls')
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('derives site metadata from mode', () => {
|
|
121
|
+
expect(getSiteName('skills')).toBe('PilotHub')
|
|
122
|
+
expect(getSiteName('souls')).toBe('SoulHub')
|
|
123
|
+
|
|
124
|
+
expect(getSiteDescription('skills')).toContain('PilotHub')
|
|
125
|
+
expect(getSiteDescription('souls')).toContain('SoulHub')
|
|
126
|
+
|
|
127
|
+
expect(getSiteUrlForMode('skills')).toBe('https://pilothub.com')
|
|
128
|
+
expect(getSiteUrlForMode('souls')).toBe('https://onlycrabs.ai')
|
|
129
|
+
})
|
|
130
|
+
})
|