pilothub 0.0.1 → 0.0.2
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/LICENSE +1 -0
- package/README.md +36 -129
- package/dist/browserAuth.d.ts +20 -0
- package/dist/browserAuth.js +156 -0
- package/dist/browserAuth.js.map +1 -0
- package/dist/browserAuth.test.d.ts +1 -0
- package/dist/browserAuth.test.js +83 -0
- package/dist/browserAuth.test.js.map +1 -0
- package/dist/cli/buildInfo.d.ts +3 -0
- package/dist/cli/buildInfo.js +103 -0
- package/dist/cli/buildInfo.js.map +1 -0
- package/dist/cli/commands/auth.d.ts +9 -0
- package/dist/cli/commands/auth.js +75 -0
- package/dist/cli/commands/auth.js.map +1 -0
- package/dist/cli/commands/delete.d.ts +11 -0
- package/dist/cli/commands/delete.js +67 -0
- package/dist/cli/commands/delete.js.map +1 -0
- package/dist/cli/commands/delete.test.d.ts +1 -0
- package/dist/cli/commands/delete.test.js +52 -0
- package/dist/cli/commands/delete.test.js.map +1 -0
- package/dist/cli/commands/publish.d.ts +9 -0
- package/dist/cli/commands/publish.js +87 -0
- package/dist/cli/commands/publish.js.map +1 -0
- package/dist/cli/commands/publish.test.d.ts +1 -0
- package/dist/cli/commands/publish.test.js +104 -0
- package/dist/cli/commands/publish.test.js.map +1 -0
- package/dist/cli/commands/skills.d.ts +23 -0
- package/dist/cli/commands/skills.js +298 -0
- package/dist/cli/commands/skills.js.map +1 -0
- package/dist/cli/commands/skills.test.d.ts +1 -0
- package/dist/cli/commands/skills.test.js +156 -0
- package/dist/cli/commands/skills.test.js.map +1 -0
- package/dist/cli/commands/star.d.ts +8 -0
- package/dist/cli/commands/star.js +38 -0
- package/dist/cli/commands/star.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +3 -0
- package/dist/cli/commands/sync.js +160 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/commands/sync.test.d.ts +1 -0
- package/dist/cli/commands/sync.test.js +277 -0
- package/dist/cli/commands/sync.test.js.map +1 -0
- package/dist/cli/commands/syncHelpers.d.ts +76 -0
- package/dist/cli/commands/syncHelpers.js +349 -0
- package/dist/cli/commands/syncHelpers.js.map +1 -0
- package/dist/cli/commands/syncHelpers.test.d.ts +1 -0
- package/dist/cli/commands/syncHelpers.test.js +22 -0
- package/dist/cli/commands/syncHelpers.test.js.map +1 -0
- package/dist/cli/commands/syncTypes.d.ts +24 -0
- package/dist/cli/commands/syncTypes.js +2 -0
- package/dist/cli/commands/syncTypes.js.map +1 -0
- package/dist/cli/commands/unstar.d.ts +8 -0
- package/dist/cli/commands/unstar.js +38 -0
- package/dist/cli/commands/unstar.js.map +1 -0
- package/dist/cli/helpStyle.d.ts +13 -0
- package/dist/cli/helpStyle.js +38 -0
- package/dist/cli/helpStyle.js.map +1 -0
- package/dist/cli/pilotbotConfig.d.ts +6 -0
- package/dist/cli/pilotbotConfig.js +110 -0
- package/dist/cli/pilotbotConfig.js.map +1 -0
- package/dist/cli/pilotbotConfig.test.d.ts +1 -0
- package/dist/cli/pilotbotConfig.test.js +133 -0
- package/dist/cli/pilotbotConfig.test.js.map +1 -0
- package/dist/cli/registry.d.ts +7 -0
- package/dist/cli/registry.js +42 -0
- package/dist/cli/registry.js.map +1 -0
- package/dist/cli/registry.test.d.ts +1 -0
- package/dist/cli/registry.test.js +48 -0
- package/dist/cli/registry.test.js.map +1 -0
- package/dist/cli/scanSkills.d.ts +7 -0
- package/dist/cli/scanSkills.js +75 -0
- package/dist/cli/scanSkills.js.map +1 -0
- package/dist/cli/scanSkills.test.d.ts +1 -0
- package/dist/cli/scanSkills.test.js +60 -0
- package/dist/cli/scanSkills.test.js.map +1 -0
- package/dist/cli/slug.d.ts +2 -0
- package/dist/cli/slug.js +16 -0
- package/dist/cli/slug.js.map +1 -0
- package/dist/cli/types.d.ts +15 -0
- package/dist/cli/types.js +2 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/ui.d.ts +7 -0
- package/dist/cli/ui.js +72 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +268 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +38 -0
- package/dist/config.js.map +1 -0
- package/dist/discovery.d.ts +5 -0
- package/dist/discovery.js +21 -0
- package/dist/discovery.js.map +1 -0
- package/dist/discovery.test.d.ts +1 -0
- package/dist/discovery.test.js +46 -0
- package/dist/discovery.test.js.map +1 -0
- package/dist/http.d.ts +32 -0
- package/dist/http.js +261 -0
- package/dist/http.js.map +1 -0
- package/dist/http.test.d.ts +1 -0
- package/dist/http.test.js +135 -0
- package/dist/http.test.js.map +1 -0
- package/dist/schema/ark.js.map +1 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/routes.js.map +1 -0
- package/{packages/schema/dist → dist/schema}/schemas.d.ts +0 -39
- package/{packages/schema/dist → dist/schema}/schemas.js +0 -22
- package/dist/schema/schemas.js.map +1 -0
- package/dist/schema/textFiles.js.map +1 -0
- package/dist/schema/textFiles.test.d.ts +1 -0
- package/dist/schema/textFiles.test.js +20 -0
- package/dist/schema/textFiles.test.js.map +1 -0
- package/dist/skills.d.ts +43 -0
- package/dist/skills.js +163 -0
- package/dist/skills.js.map +1 -0
- package/dist/skills.test.d.ts +1 -0
- package/dist/skills.test.js +144 -0
- package/dist/skills.test.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +27 -70
- package/.env.local.example +0 -19
- package/.github/workflows/ci.yml +0 -40
- package/.oxlintrc.json +0 -3
- package/AGENTS.md +0 -45
- package/CHANGELOG.md +0 -138
- package/DEPRECATIONS.md +0 -7
- package/biome.json +0 -41
- package/convex/_generated/api.d.ts +0 -153
- package/convex/_generated/api.js +0 -23
- package/convex/_generated/dataModel.d.ts +0 -60
- package/convex/_generated/server.d.ts +0 -143
- package/convex/_generated/server.js +0 -93
- package/convex/auth.config.ts +0 -8
- package/convex/auth.ts +0 -19
- package/convex/comments.ts +0 -88
- package/convex/crons.ts +0 -34
- package/convex/devSeed.ts +0 -459
- package/convex/devSeedExtra.ts +0 -541
- package/convex/downloads.ts +0 -78
- package/convex/githubBackups.ts +0 -170
- package/convex/githubBackupsNode.ts +0 -183
- package/convex/githubImport.ts +0 -317
- package/convex/githubSoulBackups.ts +0 -170
- package/convex/githubSoulBackupsNode.ts +0 -186
- package/convex/http.ts +0 -194
- package/convex/httpApi.handlers.test.ts +0 -488
- package/convex/httpApi.test.ts +0 -70
- package/convex/httpApi.ts +0 -305
- package/convex/httpApiV1.handlers.test.ts +0 -584
- package/convex/httpApiV1.ts +0 -1172
- package/convex/leaderboards.ts +0 -39
- package/convex/lib/access.ts +0 -36
- package/convex/lib/apiTokenAuth.ts +0 -36
- package/convex/lib/badges.ts +0 -50
- package/convex/lib/changelog.test.ts +0 -34
- package/convex/lib/changelog.ts +0 -278
- package/convex/lib/embeddings.ts +0 -38
- package/convex/lib/githubBackup.ts +0 -443
- package/convex/lib/githubImport.test.ts +0 -247
- package/convex/lib/githubImport.ts +0 -425
- package/convex/lib/githubSoulBackup.ts +0 -443
- package/convex/lib/leaderboards.ts +0 -103
- package/convex/lib/moderation.ts +0 -42
- package/convex/lib/public.ts +0 -89
- package/convex/lib/searchText.test.ts +0 -46
- package/convex/lib/searchText.ts +0 -27
- package/convex/lib/skillBackfill.test.ts +0 -34
- package/convex/lib/skillBackfill.ts +0 -67
- package/convex/lib/skillPublish.test.ts +0 -28
- package/convex/lib/skillPublish.ts +0 -284
- package/convex/lib/skillStats.ts +0 -80
- package/convex/lib/skills.test.ts +0 -197
- package/convex/lib/skills.ts +0 -273
- package/convex/lib/soulChangelog.ts +0 -273
- package/convex/lib/soulPublish.ts +0 -236
- package/convex/lib/tokens.test.ts +0 -33
- package/convex/lib/tokens.ts +0 -51
- package/convex/lib/webhooks.test.ts +0 -91
- package/convex/lib/webhooks.ts +0 -112
- package/convex/maintenance.test.ts +0 -270
- package/convex/maintenance.ts +0 -840
- package/convex/rateLimits.ts +0 -50
- package/convex/schema.ts +0 -472
- package/convex/search.test.ts +0 -12
- package/convex/search.ts +0 -254
- package/convex/seed.test.ts +0 -37
- package/convex/seed.ts +0 -254
- package/convex/seedSouls.ts +0 -111
- package/convex/skillStatEvents.ts +0 -568
- package/convex/skills.ts +0 -1606
- package/convex/soulComments.ts +0 -88
- package/convex/soulDownloads.ts +0 -14
- package/convex/soulStars.ts +0 -71
- package/convex/souls.ts +0 -570
- package/convex/stars.ts +0 -108
- package/convex/statsMaintenance.ts +0 -205
- package/convex/telemetry.ts +0 -434
- package/convex/tokens.ts +0 -88
- package/convex/tsconfig.json +0 -7
- package/convex/uploads.ts +0 -20
- package/convex/users.ts +0 -122
- package/convex/webhooks.ts +0 -50
- package/convex.json +0 -3
- package/docs/README.md +0 -32
- package/docs/api.md +0 -51
- package/docs/architecture.md +0 -61
- package/docs/auth.md +0 -54
- package/docs/cli.md +0 -117
- package/docs/deploy.md +0 -78
- package/docs/diffing.md +0 -84
- package/docs/github-import.md +0 -171
- package/docs/http-api.md +0 -187
- package/docs/manual-testing.md +0 -64
- package/docs/mintlify.md +0 -43
- package/docs/quickstart.md +0 -120
- package/docs/skill-format.md +0 -58
- package/docs/soul-format.md +0 -37
- package/docs/spec.md +0 -177
- package/docs/telemetry.md +0 -91
- package/docs/troubleshooting.md +0 -49
- package/docs/webhook.md +0 -51
- package/e2e/menu-smoke.pw.test.ts +0 -49
- package/e2e/pilothub.e2e.test.ts +0 -494
- package/e2e/search-exact.pw.test.ts +0 -97
- package/packages/pilothub/LICENSE +0 -22
- package/packages/pilothub/README.md +0 -57
- package/packages/pilothub/package.json +0 -41
- package/packages/pilothub/src/browserAuth.test.ts +0 -96
- package/packages/pilothub/src/browserAuth.ts +0 -174
- package/packages/pilothub/src/cli/buildInfo.ts +0 -94
- package/packages/pilothub/src/cli/commands/auth.ts +0 -97
- package/packages/pilothub/src/cli/commands/delete.test.ts +0 -73
- package/packages/pilothub/src/cli/commands/delete.ts +0 -83
- package/packages/pilothub/src/cli/commands/publish.test.ts +0 -122
- package/packages/pilothub/src/cli/commands/publish.ts +0 -108
- package/packages/pilothub/src/cli/commands/skills.test.ts +0 -191
- package/packages/pilothub/src/cli/commands/skills.ts +0 -380
- package/packages/pilothub/src/cli/commands/star.ts +0 -46
- package/packages/pilothub/src/cli/commands/sync.test.ts +0 -310
- package/packages/pilothub/src/cli/commands/sync.ts +0 -200
- package/packages/pilothub/src/cli/commands/syncHelpers.test.ts +0 -26
- package/packages/pilothub/src/cli/commands/syncHelpers.ts +0 -427
- package/packages/pilothub/src/cli/commands/syncTypes.ts +0 -27
- package/packages/pilothub/src/cli/commands/unstar.ts +0 -48
- package/packages/pilothub/src/cli/helpStyle.ts +0 -45
- package/packages/pilothub/src/cli/pilotbotConfig.test.ts +0 -159
- package/packages/pilothub/src/cli/pilotbotConfig.ts +0 -147
- package/packages/pilothub/src/cli/registry.test.ts +0 -63
- package/packages/pilothub/src/cli/registry.ts +0 -43
- package/packages/pilothub/src/cli/scanSkills.test.ts +0 -64
- package/packages/pilothub/src/cli/scanSkills.ts +0 -84
- package/packages/pilothub/src/cli/slug.ts +0 -16
- package/packages/pilothub/src/cli/types.ts +0 -12
- package/packages/pilothub/src/cli/ui.ts +0 -75
- package/packages/pilothub/src/cli.ts +0 -311
- package/packages/pilothub/src/config.ts +0 -36
- package/packages/pilothub/src/discovery.test.ts +0 -75
- package/packages/pilothub/src/discovery.ts +0 -19
- package/packages/pilothub/src/http.test.ts +0 -156
- package/packages/pilothub/src/http.ts +0 -301
- package/packages/pilothub/src/schema/ark.ts +0 -29
- package/packages/pilothub/src/schema/index.ts +0 -5
- package/packages/pilothub/src/schema/routes.ts +0 -22
- package/packages/pilothub/src/schema/schemas.ts +0 -260
- package/packages/pilothub/src/schema/textFiles.test.ts +0 -23
- package/packages/pilothub/src/schema/textFiles.ts +0 -66
- package/packages/pilothub/src/skills.test.ts +0 -191
- package/packages/pilothub/src/skills.ts +0 -172
- package/packages/pilothub/src/types.ts +0 -10
- package/packages/pilothub/tsconfig.json +0 -14
- package/packages/schema/README.md +0 -3
- package/packages/schema/dist/ark.js.map +0 -1
- package/packages/schema/dist/index.js.map +0 -1
- package/packages/schema/dist/routes.js.map +0 -1
- package/packages/schema/dist/schemas.js.map +0 -1
- package/packages/schema/dist/textFiles.js.map +0 -1
- package/packages/schema/package.json +0 -26
- package/packages/schema/src/ark.ts +0 -29
- package/packages/schema/src/index.ts +0 -5
- package/packages/schema/src/routes.ts +0 -22
- package/packages/schema/src/schemas.test.ts +0 -123
- package/packages/schema/src/schemas.ts +0 -287
- package/packages/schema/src/textFiles.test.ts +0 -23
- package/packages/schema/src/textFiles.ts +0 -66
- package/packages/schema/tsconfig.json +0 -15
- package/pilothub +0 -46
- package/playwright.config.ts +0 -33
- package/public/.well-known/pilothub.json +0 -6
- package/public/api/v1/openapi.json +0 -379
- package/public/favicon.ico +0 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +0 -25
- package/public/og.png +0 -0
- package/public/og.svg +0 -98
- package/public/pilot-logo.png +0 -0
- package/public/pilot-mark.png +0 -0
- package/public/robots.txt +0 -3
- package/public/tanstack-circle-logo.png +0 -0
- package/public/tanstack-word-logo-white.svg +0 -1
- package/scripts/check-peer-deps.ts +0 -56
- package/scripts/docs-list.ts +0 -148
- package/scripts/run-playwright-local.sh +0 -14
- package/server/og/fetchSkillOgMeta.ts +0 -27
- package/server/og/fetchSoulOgMeta.ts +0 -27
- package/server/og/ogAssets.ts +0 -80
- package/server/og/skillOgSvg.test.ts +0 -59
- package/server/og/skillOgSvg.ts +0 -258
- package/server/og/soulOgSvg.ts +0 -209
- package/server/routes/og/skill.png.ts +0 -103
- package/server/routes/og/soul.png.ts +0 -111
- package/src/__tests__/skill-detail-page.test.tsx +0 -86
- package/src/__tests__/skills-index.test.tsx +0 -145
- package/src/__tests__/upload.route.test.tsx +0 -228
- package/src/components/AppProviders.tsx +0 -19
- package/src/components/ClientOnly.tsx +0 -18
- package/src/components/Footer.tsx +0 -29
- package/src/components/Header.tsx +0 -295
- package/src/components/InstallSwitcher.tsx +0 -53
- package/src/components/SkillCard.tsx +0 -36
- package/src/components/SkillDetailPage.tsx +0 -817
- package/src/components/SkillDiffCard.tsx +0 -485
- package/src/components/SoulCard.tsx +0 -19
- package/src/components/SoulDetailPage.tsx +0 -263
- package/src/components/UserBootstrap.tsx +0 -18
- package/src/components/ui/dropdown-menu.tsx +0 -67
- package/src/components/ui/toggle-group.tsx +0 -35
- package/src/convex/client.ts +0 -3
- package/src/lib/badges.ts +0 -29
- package/src/lib/diffing.test.ts +0 -163
- package/src/lib/diffing.ts +0 -106
- package/src/lib/gravatar.test.ts +0 -9
- package/src/lib/gravatar.ts +0 -158
- package/src/lib/og.test.ts +0 -142
- package/src/lib/og.ts +0 -156
- package/src/lib/publicUser.ts +0 -39
- package/src/lib/roles.ts +0 -19
- package/src/lib/site.test.ts +0 -130
- package/src/lib/site.ts +0 -84
- package/src/lib/theme-transition.test.ts +0 -134
- package/src/lib/theme-transition.ts +0 -134
- package/src/lib/theme.test.tsx +0 -88
- package/src/lib/theme.ts +0 -43
- package/src/lib/uploadFiles.jsdom.test.ts +0 -33
- package/src/lib/uploadFiles.test.ts +0 -123
- package/src/lib/uploadFiles.ts +0 -245
- package/src/lib/uploadUtils.test.ts +0 -78
- package/src/lib/uploadUtils.ts +0 -93
- package/src/lib/useAuthStatus.ts +0 -12
- package/src/lib/utils.test.ts +0 -9
- package/src/lib/utils.ts +0 -6
- package/src/logo.svg +0 -12
- package/src/routeTree.gen.ts +0 -345
- package/src/router.tsx +0 -17
- package/src/routes/$owner/$slug.tsx +0 -55
- package/src/routes/__root.tsx +0 -136
- package/src/routes/admin.tsx +0 -11
- package/src/routes/cli/auth.tsx +0 -168
- package/src/routes/dashboard.tsx +0 -97
- package/src/routes/import.tsx +0 -415
- package/src/routes/index.tsx +0 -252
- package/src/routes/management.tsx +0 -529
- package/src/routes/settings.tsx +0 -203
- package/src/routes/skills/index.tsx +0 -422
- package/src/routes/souls/$slug.tsx +0 -55
- package/src/routes/souls/index.tsx +0 -243
- package/src/routes/stars.tsx +0 -68
- package/src/routes/u/$handle.tsx +0 -307
- package/src/routes/upload/utils.ts +0 -81
- package/src/routes/upload.tsx +0 -499
- package/src/styles.css +0 -2718
- package/tsconfig.json +0 -24
- package/tsconfig.oxlint.json +0 -16
- package/vercel.json +0 -8
- package/vite.config.ts +0 -48
- package/vitest.config.ts +0 -47
- package/vitest.e2e.config.ts +0 -11
- package/vitest.setup.ts +0 -1
- /package/{packages/pilothub/bin → bin}/pilothub.js +0 -0
- /package/{packages/schema/dist → dist/schema}/ark.d.ts +0 -0
- /package/{packages/schema/dist → dist/schema}/ark.js +0 -0
- /package/{packages/schema/dist → dist/schema}/index.d.ts +0 -0
- /package/{packages/schema/dist → dist/schema}/index.js +0 -0
- /package/{packages/schema/dist → dist/schema}/routes.d.ts +0 -0
- /package/{packages/schema/dist → dist/schema}/routes.js +0 -0
- /package/{packages/schema/dist → dist/schema}/textFiles.d.ts +0 -0
- /package/{packages/schema/dist → dist/schema}/textFiles.js +0 -0
package/src/lib/site.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
export type SiteMode = 'skills' | 'souls'
|
|
2
|
-
|
|
3
|
-
const DEFAULT_PILOTHUB_SITE_URL = 'https://pilothub.com'
|
|
4
|
-
const DEFAULT_ONLYCRABS_SITE_URL = 'https://onlycrabs.ai'
|
|
5
|
-
const DEFAULT_ONLYCRABS_HOST = 'onlycrabs.ai'
|
|
6
|
-
|
|
7
|
-
export function getPilotHubSiteUrl() {
|
|
8
|
-
return import.meta.env.VITE_SITE_URL ?? DEFAULT_PILOTHUB_SITE_URL
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function getOnlyCrabsSiteUrl() {
|
|
12
|
-
const explicit = import.meta.env.VITE_SOULHUB_SITE_URL
|
|
13
|
-
if (explicit) return explicit
|
|
14
|
-
|
|
15
|
-
const siteUrl = import.meta.env.VITE_SITE_URL
|
|
16
|
-
if (siteUrl) {
|
|
17
|
-
try {
|
|
18
|
-
const url = new URL(siteUrl)
|
|
19
|
-
if (
|
|
20
|
-
url.hostname === 'localhost' ||
|
|
21
|
-
url.hostname === '127.0.0.1' ||
|
|
22
|
-
url.hostname === '0.0.0.0'
|
|
23
|
-
) {
|
|
24
|
-
return url.origin
|
|
25
|
-
}
|
|
26
|
-
} catch {
|
|
27
|
-
// ignore invalid URLs, fall through to default
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return DEFAULT_ONLYCRABS_SITE_URL
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function getOnlyCrabsHost() {
|
|
35
|
-
return import.meta.env.VITE_SOULHUB_HOST ?? DEFAULT_ONLYCRABS_HOST
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function detectSiteMode(host?: string | null): SiteMode {
|
|
39
|
-
if (!host) return 'skills'
|
|
40
|
-
const onlyCrabsHost = getOnlyCrabsHost().toLowerCase()
|
|
41
|
-
const lower = host.toLowerCase()
|
|
42
|
-
if (lower === onlyCrabsHost || lower.endsWith(`.${onlyCrabsHost}`)) return 'souls'
|
|
43
|
-
return 'skills'
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function detectSiteModeFromUrl(value?: string | null): SiteMode {
|
|
47
|
-
if (!value) return 'skills'
|
|
48
|
-
try {
|
|
49
|
-
const host = new URL(value).hostname
|
|
50
|
-
return detectSiteMode(host)
|
|
51
|
-
} catch {
|
|
52
|
-
return detectSiteMode(value)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function getSiteMode(): SiteMode {
|
|
57
|
-
if (typeof window !== 'undefined') {
|
|
58
|
-
return detectSiteMode(window.location.hostname)
|
|
59
|
-
}
|
|
60
|
-
const forced = import.meta.env.VITE_SITE_MODE
|
|
61
|
-
if (forced === 'souls' || forced === 'skills') return forced
|
|
62
|
-
|
|
63
|
-
const onlyCrabsSite = import.meta.env.VITE_SOULHUB_SITE_URL
|
|
64
|
-
if (onlyCrabsSite) return detectSiteModeFromUrl(onlyCrabsSite)
|
|
65
|
-
|
|
66
|
-
const siteUrl = import.meta.env.VITE_SITE_URL ?? process.env.SITE_URL
|
|
67
|
-
if (siteUrl) return detectSiteModeFromUrl(siteUrl)
|
|
68
|
-
|
|
69
|
-
return 'skills'
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function getSiteName(mode: SiteMode = getSiteMode()) {
|
|
73
|
-
return mode === 'souls' ? 'SoulHub' : 'PilotHub'
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function getSiteDescription(mode: SiteMode = getSiteMode()) {
|
|
77
|
-
return mode === 'souls'
|
|
78
|
-
? 'SoulHub — the home for SOUL.md bundles and personal system lore.'
|
|
79
|
-
: 'PilotHub — a fast skill registry for agents, with vector search.'
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function getSiteUrlForMode(mode: SiteMode = getSiteMode()) {
|
|
83
|
-
return mode === 'souls' ? getOnlyCrabsSiteUrl() : getPilotHubSiteUrl()
|
|
84
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
-
import { startThemeTransition } from './theme-transition'
|
|
3
|
-
|
|
4
|
-
describe('startThemeTransition', () => {
|
|
5
|
-
it('no-ops when theme does not change', () => {
|
|
6
|
-
const setTheme = vi.fn()
|
|
7
|
-
startThemeTransition({
|
|
8
|
-
currentTheme: 'dark',
|
|
9
|
-
nextTheme: 'dark',
|
|
10
|
-
setTheme,
|
|
11
|
-
})
|
|
12
|
-
expect(setTheme).not.toHaveBeenCalled()
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it('applies theme without document (SSR)', () => {
|
|
16
|
-
const original = Object.getOwnPropertyDescriptor(globalThis, 'document')
|
|
17
|
-
Object.defineProperty(globalThis, 'document', { value: undefined, configurable: true })
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
const calls: string[] = []
|
|
21
|
-
const setTheme = vi.fn()
|
|
22
|
-
startThemeTransition({
|
|
23
|
-
currentTheme: 'light',
|
|
24
|
-
nextTheme: 'dark',
|
|
25
|
-
setTheme,
|
|
26
|
-
onBeforeThemeChange: () => calls.push('before'),
|
|
27
|
-
onAfterThemeChange: () => calls.push('after'),
|
|
28
|
-
})
|
|
29
|
-
expect(calls).toEqual(['before', 'after'])
|
|
30
|
-
expect(setTheme).toHaveBeenCalledWith('dark')
|
|
31
|
-
} finally {
|
|
32
|
-
if (original) Object.defineProperty(globalThis, 'document', original)
|
|
33
|
-
else delete (globalThis as unknown as { document?: unknown }).document
|
|
34
|
-
}
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('skips view-transition when prefers reduced motion', () => {
|
|
38
|
-
const setTheme = vi.fn()
|
|
39
|
-
const root = document.documentElement
|
|
40
|
-
|
|
41
|
-
window.matchMedia = vi.fn(() => ({ matches: true }) as unknown as MediaQueryList)
|
|
42
|
-
;(document as unknown as { startViewTransition?: unknown }).startViewTransition = vi.fn()
|
|
43
|
-
|
|
44
|
-
startThemeTransition({
|
|
45
|
-
currentTheme: 'light',
|
|
46
|
-
nextTheme: 'dark',
|
|
47
|
-
setTheme,
|
|
48
|
-
context: { pointerClientX: 10, pointerClientY: 10 },
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
expect(setTheme).toHaveBeenCalledWith('dark')
|
|
52
|
-
expect(root.classList.contains('theme-transition')).toBe(false)
|
|
53
|
-
expect(
|
|
54
|
-
(document as unknown as { startViewTransition?: unknown }).startViewTransition,
|
|
55
|
-
).not.toHaveBeenCalled()
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('uses view-transition when available', async () => {
|
|
59
|
-
const setTheme = vi.fn()
|
|
60
|
-
const root = document.documentElement
|
|
61
|
-
|
|
62
|
-
window.matchMedia = vi.fn(() => ({ matches: false }) as unknown as MediaQueryList)
|
|
63
|
-
|
|
64
|
-
;(
|
|
65
|
-
document as unknown as {
|
|
66
|
-
startViewTransition?: (callback: () => void) => { finished: Promise<void> }
|
|
67
|
-
}
|
|
68
|
-
).startViewTransition = (callback) => {
|
|
69
|
-
callback()
|
|
70
|
-
return { finished: Promise.resolve() }
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
startThemeTransition({
|
|
74
|
-
currentTheme: 'light',
|
|
75
|
-
nextTheme: 'dark',
|
|
76
|
-
setTheme,
|
|
77
|
-
context: { pointerClientX: 10, pointerClientY: 20 },
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
expect(setTheme).toHaveBeenCalledWith('dark')
|
|
81
|
-
expect(root.classList.contains('theme-transition')).toBe(true)
|
|
82
|
-
|
|
83
|
-
await new Promise((r) => setTimeout(r, 0))
|
|
84
|
-
expect(root.classList.contains('theme-transition')).toBe(false)
|
|
85
|
-
expect(root.style.getPropertyValue('--theme-switch-x')).toBe('')
|
|
86
|
-
expect(root.style.getPropertyValue('--theme-switch-y')).toBe('')
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('cleans up when view-transition does not provide finished', () => {
|
|
90
|
-
const setTheme = vi.fn()
|
|
91
|
-
const root = document.documentElement
|
|
92
|
-
|
|
93
|
-
window.matchMedia = vi.fn(() => ({ matches: false }) as unknown as MediaQueryList)
|
|
94
|
-
;(
|
|
95
|
-
document as unknown as { startViewTransition?: (callback: () => void) => unknown }
|
|
96
|
-
).startViewTransition = (callback) => {
|
|
97
|
-
callback()
|
|
98
|
-
return {}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
startThemeTransition({
|
|
102
|
-
currentTheme: 'light',
|
|
103
|
-
nextTheme: 'dark',
|
|
104
|
-
setTheme,
|
|
105
|
-
context: { element: document.body },
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
expect(setTheme).toHaveBeenCalledWith('dark')
|
|
109
|
-
expect(root.classList.contains('theme-transition')).toBe(false)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('falls back when view-transition throws', () => {
|
|
113
|
-
const setTheme = vi.fn()
|
|
114
|
-
const root = document.documentElement
|
|
115
|
-
|
|
116
|
-
window.matchMedia = vi.fn(() => ({ matches: false }) as unknown as MediaQueryList)
|
|
117
|
-
;(document as unknown as { startViewTransition?: () => never }).startViewTransition = () => {
|
|
118
|
-
throw new Error('nope')
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const element = document.createElement('button')
|
|
122
|
-
element.getBoundingClientRect = () => ({ left: 10, top: 10, width: 10, height: 10 }) as DOMRect
|
|
123
|
-
|
|
124
|
-
startThemeTransition({
|
|
125
|
-
currentTheme: 'light',
|
|
126
|
-
nextTheme: 'dark',
|
|
127
|
-
setTheme,
|
|
128
|
-
context: { element },
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
expect(setTheme).toHaveBeenCalledWith('dark')
|
|
132
|
-
expect(root.classList.contains('theme-transition')).toBe(false)
|
|
133
|
-
})
|
|
134
|
-
})
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { flushSync } from 'react-dom'
|
|
2
|
-
|
|
3
|
-
export type ThemeValue = 'light' | 'dark' | 'system' | (string & {})
|
|
4
|
-
|
|
5
|
-
export type ThemeTransitionContext = {
|
|
6
|
-
element?: HTMLElement | null
|
|
7
|
-
pointerClientX?: number
|
|
8
|
-
pointerClientY?: number
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export type ThemeTransitionOptions = {
|
|
12
|
-
nextTheme: ThemeValue
|
|
13
|
-
setTheme: (theme: ThemeValue) => void
|
|
14
|
-
context?: ThemeTransitionContext | undefined
|
|
15
|
-
onBeforeThemeChange?: () => void
|
|
16
|
-
onAfterThemeChange?: () => void
|
|
17
|
-
currentTheme?: ThemeValue | null
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
type DocumentWithViewTransition = Document & {
|
|
21
|
-
startViewTransition?: (callback: () => void) => {
|
|
22
|
-
finished: Promise<void>
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
type WindowWithMatchMedia = Window & {
|
|
27
|
-
matchMedia: (query: string) => MediaQueryList
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const clamp01 = (value: number) => {
|
|
31
|
-
if (Number.isNaN(value)) return 0.5
|
|
32
|
-
if (value <= 0) return 0
|
|
33
|
-
if (value >= 1) return 1
|
|
34
|
-
return value
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const resolveWindow = (): WindowWithMatchMedia | undefined =>
|
|
38
|
-
globalThis.window as WindowWithMatchMedia | undefined
|
|
39
|
-
|
|
40
|
-
const hasReducedMotionPreference = (): boolean => {
|
|
41
|
-
const currentWindow = resolveWindow()
|
|
42
|
-
if (!currentWindow || typeof currentWindow.matchMedia !== 'function') return false
|
|
43
|
-
return currentWindow.matchMedia('(prefers-reduced-motion: reduce)').matches ?? false
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const cleanupThemeTransition = (root: HTMLElement) => {
|
|
47
|
-
root.classList.remove('theme-transition')
|
|
48
|
-
root.style.removeProperty('--theme-switch-x')
|
|
49
|
-
root.style.removeProperty('--theme-switch-y')
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export const startThemeTransition = ({
|
|
53
|
-
nextTheme,
|
|
54
|
-
setTheme,
|
|
55
|
-
context,
|
|
56
|
-
onBeforeThemeChange,
|
|
57
|
-
onAfterThemeChange,
|
|
58
|
-
currentTheme,
|
|
59
|
-
}: ThemeTransitionOptions) => {
|
|
60
|
-
if (currentTheme === nextTheme) return
|
|
61
|
-
|
|
62
|
-
const documentReference = globalThis.document ?? null
|
|
63
|
-
if (!documentReference) {
|
|
64
|
-
onBeforeThemeChange?.()
|
|
65
|
-
setTheme(nextTheme)
|
|
66
|
-
onAfterThemeChange?.()
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const root = documentReference.documentElement
|
|
71
|
-
const document_ = documentReference as DocumentWithViewTransition
|
|
72
|
-
const prefersReducedMotion = hasReducedMotionPreference()
|
|
73
|
-
|
|
74
|
-
const applyTheme = () => {
|
|
75
|
-
onBeforeThemeChange?.()
|
|
76
|
-
flushSync(() => {
|
|
77
|
-
setTheme(nextTheme)
|
|
78
|
-
})
|
|
79
|
-
onAfterThemeChange?.()
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const canUseViewTransition = Boolean(document_.startViewTransition) && !prefersReducedMotion
|
|
83
|
-
if (canUseViewTransition) {
|
|
84
|
-
let xPercent = 0.5
|
|
85
|
-
let yPercent = 0.5
|
|
86
|
-
|
|
87
|
-
const currentWindow = resolveWindow()
|
|
88
|
-
if (
|
|
89
|
-
context?.pointerClientX !== undefined &&
|
|
90
|
-
context?.pointerClientY !== undefined &&
|
|
91
|
-
currentWindow
|
|
92
|
-
) {
|
|
93
|
-
xPercent = clamp01(context.pointerClientX / currentWindow.innerWidth)
|
|
94
|
-
yPercent = clamp01(context.pointerClientY / currentWindow.innerHeight)
|
|
95
|
-
} else if (context?.element) {
|
|
96
|
-
const rect = context.element.getBoundingClientRect()
|
|
97
|
-
if (rect.width > 0 && rect.height > 0 && currentWindow) {
|
|
98
|
-
xPercent = clamp01((rect.left + rect.width / 2) / currentWindow.innerWidth)
|
|
99
|
-
yPercent = clamp01((rect.top + rect.height / 2) / currentWindow.innerHeight)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
root.style.setProperty('--theme-switch-x', `${xPercent * 100}%`)
|
|
104
|
-
root.style.setProperty('--theme-switch-y', `${yPercent * 100}%`)
|
|
105
|
-
root.classList.add('theme-transition')
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
const transition = document_.startViewTransition?.(() => {
|
|
109
|
-
applyTheme()
|
|
110
|
-
})
|
|
111
|
-
if (transition?.finished === undefined) {
|
|
112
|
-
cleanupThemeTransition(root)
|
|
113
|
-
} else {
|
|
114
|
-
const handleTransitionFinish = async () => {
|
|
115
|
-
try {
|
|
116
|
-
await transition.finished
|
|
117
|
-
} catch {
|
|
118
|
-
// swallow transition cancellation errors
|
|
119
|
-
} finally {
|
|
120
|
-
cleanupThemeTransition(root)
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
void handleTransitionFinish()
|
|
124
|
-
}
|
|
125
|
-
} catch {
|
|
126
|
-
cleanupThemeTransition(root)
|
|
127
|
-
applyTheme()
|
|
128
|
-
}
|
|
129
|
-
return
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
applyTheme()
|
|
133
|
-
cleanupThemeTransition(root)
|
|
134
|
-
}
|
package/src/lib/theme.test.tsx
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
-
import { applyTheme, getStoredTheme, useThemeMode } from './theme'
|
|
4
|
-
|
|
5
|
-
describe('theme', () => {
|
|
6
|
-
let store: Record<string, string>
|
|
7
|
-
|
|
8
|
-
function Harness() {
|
|
9
|
-
const { mode, setMode } = useThemeMode()
|
|
10
|
-
return (
|
|
11
|
-
<div>
|
|
12
|
-
<div data-testid="mode">{mode}</div>
|
|
13
|
-
<button type="button" onClick={() => setMode('dark')}>
|
|
14
|
-
dark
|
|
15
|
-
</button>
|
|
16
|
-
</div>
|
|
17
|
-
)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
store = {}
|
|
22
|
-
Object.defineProperty(window, 'localStorage', {
|
|
23
|
-
value: {
|
|
24
|
-
getItem: (key: string) => (key in store ? store[key] : null),
|
|
25
|
-
setItem: (key: string, value: string) => {
|
|
26
|
-
store[key] = String(value)
|
|
27
|
-
},
|
|
28
|
-
removeItem: (key: string) => {
|
|
29
|
-
delete store[key]
|
|
30
|
-
},
|
|
31
|
-
clear: () => {
|
|
32
|
-
store = {}
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
configurable: true,
|
|
36
|
-
})
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
afterEach(() => {
|
|
40
|
-
document.documentElement.classList.remove('dark')
|
|
41
|
-
delete document.documentElement.dataset.theme
|
|
42
|
-
window.localStorage.clear()
|
|
43
|
-
vi.unstubAllGlobals()
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('reads stored theme with fallback', () => {
|
|
47
|
-
expect(getStoredTheme()).toBe('system')
|
|
48
|
-
window.localStorage.setItem('pilothub-theme', 'dark')
|
|
49
|
-
expect(getStoredTheme()).toBe('dark')
|
|
50
|
-
window.localStorage.setItem('pilothub-theme', 'nope')
|
|
51
|
-
expect(getStoredTheme()).toBe('system')
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('applies theme and toggles dark class', () => {
|
|
55
|
-
applyTheme('dark')
|
|
56
|
-
expect(document.documentElement.dataset.theme).toBe('dark')
|
|
57
|
-
expect(document.documentElement.classList.contains('dark')).toBe(true)
|
|
58
|
-
|
|
59
|
-
applyTheme('light')
|
|
60
|
-
expect(document.documentElement.dataset.theme).toBe('light')
|
|
61
|
-
expect(document.documentElement.classList.contains('dark')).toBe(false)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('resolves system theme via matchMedia', () => {
|
|
65
|
-
vi.stubGlobal('matchMedia', () => ({
|
|
66
|
-
matches: true,
|
|
67
|
-
addEventListener: vi.fn(),
|
|
68
|
-
removeEventListener: vi.fn(),
|
|
69
|
-
}))
|
|
70
|
-
applyTheme('system')
|
|
71
|
-
expect(document.documentElement.dataset.theme).toBe('dark')
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('useThemeMode persists and applies mode', async () => {
|
|
75
|
-
vi.stubGlobal('matchMedia', () => ({
|
|
76
|
-
matches: false,
|
|
77
|
-
addEventListener: vi.fn(),
|
|
78
|
-
removeEventListener: vi.fn(),
|
|
79
|
-
}))
|
|
80
|
-
render(<Harness />)
|
|
81
|
-
expect(screen.getByTestId('mode').textContent).toBe('system')
|
|
82
|
-
fireEvent.click(screen.getByRole('button', { name: 'dark' }))
|
|
83
|
-
await waitFor(() => {
|
|
84
|
-
expect(document.documentElement.dataset.theme).toBe('dark')
|
|
85
|
-
})
|
|
86
|
-
expect(window.localStorage.getItem('pilothub-theme')).toBe('dark')
|
|
87
|
-
})
|
|
88
|
-
})
|
package/src/lib/theme.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
|
|
3
|
-
export type ThemeMode = 'system' | 'light' | 'dark'
|
|
4
|
-
|
|
5
|
-
const THEME_KEY = 'pilothub-theme'
|
|
6
|
-
|
|
7
|
-
export function getStoredTheme(): ThemeMode {
|
|
8
|
-
if (typeof window === 'undefined') return 'system'
|
|
9
|
-
const stored = window.localStorage.getItem(THEME_KEY)
|
|
10
|
-
if (stored === 'light' || stored === 'dark' || stored === 'system') return stored
|
|
11
|
-
return 'system'
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function resolveTheme(mode: ThemeMode) {
|
|
15
|
-
if (mode !== 'system') return mode
|
|
16
|
-
if (typeof window === 'undefined') return 'light'
|
|
17
|
-
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function applyTheme(mode: ThemeMode) {
|
|
21
|
-
if (typeof document === 'undefined') return
|
|
22
|
-
const resolved = resolveTheme(mode)
|
|
23
|
-
document.documentElement.dataset.theme = resolved
|
|
24
|
-
document.documentElement.classList.toggle('dark', resolved === 'dark')
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function useThemeMode() {
|
|
28
|
-
const [mode, setMode] = useState<ThemeMode>(() => getStoredTheme())
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
applyTheme(mode)
|
|
32
|
-
if (typeof window !== 'undefined') {
|
|
33
|
-
window.localStorage.setItem(THEME_KEY, mode)
|
|
34
|
-
}
|
|
35
|
-
if (mode !== 'system' || typeof window === 'undefined') return
|
|
36
|
-
const media = window.matchMedia('(prefers-color-scheme: dark)')
|
|
37
|
-
const handler = () => applyTheme(mode)
|
|
38
|
-
media.addEventListener('change', handler)
|
|
39
|
-
return () => media.removeEventListener('change', handler)
|
|
40
|
-
}, [mode])
|
|
41
|
-
|
|
42
|
-
return { mode, setMode }
|
|
43
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { strToU8, unzipSync, zipSync } from 'fflate'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import { expandFiles } from './uploadFiles'
|
|
5
|
-
|
|
6
|
-
function readWithFileReader(blob: Blob) {
|
|
7
|
-
return new Promise<ArrayBuffer>((resolve, reject) => {
|
|
8
|
-
const reader = new FileReader()
|
|
9
|
-
reader.onerror = () => reject(reader.error ?? new Error('Could not read blob.'))
|
|
10
|
-
reader.onload = () => resolve(reader.result as ArrayBuffer)
|
|
11
|
-
reader.readAsArrayBuffer(blob)
|
|
12
|
-
})
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe('expandFiles (jsdom)', () => {
|
|
16
|
-
it('expands zip archives using FileReader fallback', async () => {
|
|
17
|
-
const zip = zipSync({
|
|
18
|
-
'hetzner-cloud-skill/SKILL.md': new Uint8Array(strToU8('hello')),
|
|
19
|
-
'hetzner-cloud-skill/notes.txt': new Uint8Array(strToU8('notes')),
|
|
20
|
-
})
|
|
21
|
-
const zipBytes = Uint8Array.from(zip).buffer
|
|
22
|
-
const zipFile = new File([zipBytes], 'bundle.zip', { type: 'application/zip' })
|
|
23
|
-
|
|
24
|
-
const readerBuffer = await readWithFileReader(zipFile)
|
|
25
|
-
const entries = unzipSync(new Uint8Array(readerBuffer))
|
|
26
|
-
expect(Object.keys(entries)).toEqual(
|
|
27
|
-
expect.arrayContaining(['hetzner-cloud-skill/SKILL.md', 'hetzner-cloud-skill/notes.txt']),
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
const expanded = await expandFiles([zipFile])
|
|
31
|
-
expect(expanded.map((file) => file.name)).toEqual(['SKILL.md', 'notes.txt'])
|
|
32
|
-
})
|
|
33
|
-
})
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
/* @vitest-environment node */
|
|
2
|
-
import { gzipSync, strToU8, zipSync } from 'fflate'
|
|
3
|
-
import { describe, expect, it } from 'vitest'
|
|
4
|
-
import { expandFiles } from './uploadFiles'
|
|
5
|
-
|
|
6
|
-
if (typeof File === 'undefined') {
|
|
7
|
-
class NodeFile extends Blob {
|
|
8
|
-
name: string
|
|
9
|
-
lastModified: number
|
|
10
|
-
|
|
11
|
-
constructor(parts: BlobPart[], name: string, options?: FilePropertyBag) {
|
|
12
|
-
super(parts, options)
|
|
13
|
-
this.name = name
|
|
14
|
-
this.lastModified = options?.lastModified ?? Date.now()
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
// @ts-expect-error Node test environment polyfill
|
|
18
|
-
globalThis.File = NodeFile
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function buildTar(entries: Array<{ name: string; content: string }>) {
|
|
22
|
-
const blocks: Uint8Array[] = []
|
|
23
|
-
for (const entry of entries) {
|
|
24
|
-
const content = strToU8(entry.content)
|
|
25
|
-
const header = new Uint8Array(512)
|
|
26
|
-
writeString(header, entry.name, 0, 100)
|
|
27
|
-
writeString(header, '0000777', 100, 8)
|
|
28
|
-
writeString(header, '0000000', 108, 8)
|
|
29
|
-
writeString(header, '0000000', 116, 8)
|
|
30
|
-
writeString(header, content.length.toString(8).padStart(11, '0'), 124, 12)
|
|
31
|
-
writeString(header, '00000000000', 136, 12)
|
|
32
|
-
header[156] = '0'.charCodeAt(0)
|
|
33
|
-
writeString(header, 'ustar', 257, 6)
|
|
34
|
-
for (let i = 148; i < 156; i += 1) {
|
|
35
|
-
header[i] = 32
|
|
36
|
-
}
|
|
37
|
-
let sum = 0
|
|
38
|
-
for (const byte of header) sum += byte
|
|
39
|
-
writeString(header, sum.toString(8).padStart(6, '0'), 148, 6)
|
|
40
|
-
header[154] = 0
|
|
41
|
-
header[155] = 32
|
|
42
|
-
blocks.push(header)
|
|
43
|
-
blocks.push(content)
|
|
44
|
-
const pad = (512 - (content.length % 512)) % 512
|
|
45
|
-
if (pad) blocks.push(new Uint8Array(pad))
|
|
46
|
-
}
|
|
47
|
-
blocks.push(new Uint8Array(1024))
|
|
48
|
-
const total = blocks.reduce((sum, block) => sum + block.length, 0)
|
|
49
|
-
const buffer = new Uint8Array(total)
|
|
50
|
-
let offset = 0
|
|
51
|
-
for (const block of blocks) {
|
|
52
|
-
buffer.set(block, offset)
|
|
53
|
-
offset += block.length
|
|
54
|
-
}
|
|
55
|
-
return buffer
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function writeString(target: Uint8Array, value: string, start: number, length: number) {
|
|
59
|
-
const bytes = strToU8(value)
|
|
60
|
-
target.set(bytes.subarray(0, length), start)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
describe('expandFiles', () => {
|
|
64
|
-
it('expands zip archives into files', async () => {
|
|
65
|
-
const zip = zipSync({
|
|
66
|
-
'SKILL.md': strToU8('hello'),
|
|
67
|
-
'docs/readme.txt': strToU8('doc'),
|
|
68
|
-
})
|
|
69
|
-
const zipFile = new File([Uint8Array.from(zip).buffer], 'pack.zip', { type: 'application/zip' })
|
|
70
|
-
const result = await expandFiles([zipFile])
|
|
71
|
-
expect(result.map((file) => file.name)).toEqual(['SKILL.md', 'docs/readme.txt'])
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('unwraps top-level folders in zip archives', async () => {
|
|
75
|
-
const zip = zipSync({
|
|
76
|
-
'hetzner-cloud-skill/SKILL.md': strToU8('hello'),
|
|
77
|
-
'hetzner-cloud-skill/docs/readme.txt': strToU8('doc'),
|
|
78
|
-
'__MACOSX/._SKILL.md': strToU8('junk'),
|
|
79
|
-
'hetzner-cloud-skill/.DS_Store': strToU8('junk2'),
|
|
80
|
-
'hetzner-cloud-skill/screenshot.png': strToU8('not-really-a-png'),
|
|
81
|
-
})
|
|
82
|
-
const zipFile = new File([Uint8Array.from(zip).buffer], 'pack.zip', { type: 'application/zip' })
|
|
83
|
-
const result = await expandFiles([zipFile])
|
|
84
|
-
expect(result.map((file) => file.name)).toEqual(['SKILL.md', 'docs/readme.txt'])
|
|
85
|
-
const png = result.find((file) => file.name.endsWith('.png'))
|
|
86
|
-
expect(png).toBeUndefined()
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('expands gzipped tar archives into files', async () => {
|
|
90
|
-
const tar = buildTar([
|
|
91
|
-
{ name: 'SKILL.md', content: 'hi' },
|
|
92
|
-
{ name: 'notes.txt', content: 'yo' },
|
|
93
|
-
])
|
|
94
|
-
const tgz = gzipSync(tar)
|
|
95
|
-
const tgzFile = new File([Uint8Array.from(tgz).buffer], 'bundle.tgz', {
|
|
96
|
-
type: 'application/gzip',
|
|
97
|
-
})
|
|
98
|
-
const result = await expandFiles([tgzFile])
|
|
99
|
-
expect(result.map((file) => file.name)).toEqual(['SKILL.md', 'notes.txt'])
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('unwraps top-level folders in tar.gz archives', async () => {
|
|
103
|
-
const tar = buildTar([
|
|
104
|
-
{ name: 'skill-folder/SKILL.md', content: 'hi' },
|
|
105
|
-
{ name: 'skill-folder/notes.txt', content: 'yo' },
|
|
106
|
-
])
|
|
107
|
-
const tgz = gzipSync(tar)
|
|
108
|
-
const tgzFile = new File([Uint8Array.from(tgz).buffer], 'bundle.tgz', {
|
|
109
|
-
type: 'application/gzip',
|
|
110
|
-
})
|
|
111
|
-
const result = await expandFiles([tgzFile])
|
|
112
|
-
expect(result.map((file) => file.name)).toEqual(['SKILL.md', 'notes.txt'])
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('expands .gz single files', async () => {
|
|
116
|
-
const gz = gzipSync(strToU8('content'))
|
|
117
|
-
const gzFile = new File([Uint8Array.from(gz).buffer], 'skill.md.gz', {
|
|
118
|
-
type: 'application/gzip',
|
|
119
|
-
})
|
|
120
|
-
const result = await expandFiles([gzFile])
|
|
121
|
-
expect(result.map((file) => file.name)).toEqual(['skill.md'])
|
|
122
|
-
})
|
|
123
|
-
})
|