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
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
'use node'
|
|
2
|
-
|
|
3
|
-
import { createPrivateKey, createSign } from 'node:crypto'
|
|
4
|
-
import type { Id } from '../_generated/dataModel'
|
|
5
|
-
import type { ActionCtx } from '../_generated/server'
|
|
6
|
-
|
|
7
|
-
const GITHUB_API = 'https://api.github.com'
|
|
8
|
-
const DEFAULT_REPO = 'pilotbot/skills'
|
|
9
|
-
const DEFAULT_ROOT = 'skills'
|
|
10
|
-
const META_FILENAME = '_meta.json'
|
|
11
|
-
const USER_AGENT = 'pilothub/skills-backup'
|
|
12
|
-
|
|
13
|
-
type BackupFile = {
|
|
14
|
-
path: string
|
|
15
|
-
size: number
|
|
16
|
-
storageId: Id<'_storage'>
|
|
17
|
-
sha256: string
|
|
18
|
-
contentType?: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
type BackupParams = {
|
|
22
|
-
slug: string
|
|
23
|
-
version: string
|
|
24
|
-
displayName: string
|
|
25
|
-
ownerHandle: string
|
|
26
|
-
files: BackupFile[]
|
|
27
|
-
publishedAt: number
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
type RepoInfo = {
|
|
31
|
-
default_branch?: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
type GitRef = {
|
|
35
|
-
object: { sha: string }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
type GitCommit = {
|
|
39
|
-
sha: string
|
|
40
|
-
tree: { sha: string }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
type GitTreeEntry = {
|
|
44
|
-
path?: string
|
|
45
|
-
type?: string
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
type GitTree = {
|
|
49
|
-
tree?: GitTreeEntry[]
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
type MetaFile = {
|
|
53
|
-
owner: string
|
|
54
|
-
slug: string
|
|
55
|
-
displayName: string
|
|
56
|
-
latest: {
|
|
57
|
-
version: string
|
|
58
|
-
publishedAt: number
|
|
59
|
-
commit: string | null
|
|
60
|
-
}
|
|
61
|
-
history: Array<{
|
|
62
|
-
version: string
|
|
63
|
-
publishedAt: number
|
|
64
|
-
commit: string
|
|
65
|
-
}>
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export type GitHubBackupContext = {
|
|
69
|
-
token: string
|
|
70
|
-
repo: string
|
|
71
|
-
repoOwner: string
|
|
72
|
-
repoName: string
|
|
73
|
-
branch: string
|
|
74
|
-
root: string
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function isGitHubBackupConfigured() {
|
|
78
|
-
return Boolean(
|
|
79
|
-
process.env.GITHUB_APP_ID &&
|
|
80
|
-
process.env.GITHUB_APP_PRIVATE_KEY &&
|
|
81
|
-
process.env.GITHUB_APP_INSTALLATION_ID,
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export async function getGitHubBackupContext(): Promise<GitHubBackupContext> {
|
|
86
|
-
const repo = process.env.GITHUB_SKILLS_REPO ?? DEFAULT_REPO
|
|
87
|
-
const root = process.env.GITHUB_SKILLS_ROOT ?? DEFAULT_ROOT
|
|
88
|
-
const [repoOwner, repoName] = parseRepo(repo)
|
|
89
|
-
const token = await createInstallationToken()
|
|
90
|
-
const repoInfo = await githubGet<RepoInfo>(token, `/repos/${repoOwner}/${repoName}`)
|
|
91
|
-
const branch = repoInfo.default_branch ?? 'main'
|
|
92
|
-
|
|
93
|
-
return { token, repo, repoOwner, repoName, branch, root }
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export async function fetchGitHubSkillMeta(
|
|
97
|
-
context: GitHubBackupContext,
|
|
98
|
-
ownerHandle: string,
|
|
99
|
-
slug: string,
|
|
100
|
-
): Promise<MetaFile | null> {
|
|
101
|
-
const skillRoot = buildSkillRoot(context.root, ownerHandle, slug)
|
|
102
|
-
return fetchMetaFile(
|
|
103
|
-
context.token,
|
|
104
|
-
context.repoOwner,
|
|
105
|
-
context.repoName,
|
|
106
|
-
`${skillRoot}/${META_FILENAME}`,
|
|
107
|
-
context.branch,
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export async function backupSkillToGitHub(
|
|
112
|
-
ctx: ActionCtx,
|
|
113
|
-
params: BackupParams,
|
|
114
|
-
context?: GitHubBackupContext,
|
|
115
|
-
) {
|
|
116
|
-
if (!isGitHubBackupConfigured()) return
|
|
117
|
-
|
|
118
|
-
const resolved = context ?? (await getGitHubBackupContext())
|
|
119
|
-
const skillRoot = buildSkillRoot(resolved.root, params.ownerHandle, params.slug)
|
|
120
|
-
const ref = await githubGet<GitRef>(
|
|
121
|
-
resolved.token,
|
|
122
|
-
`/repos/${resolved.repoOwner}/${resolved.repoName}/git/ref/heads/${resolved.branch}`,
|
|
123
|
-
)
|
|
124
|
-
const baseCommitSha = ref.object.sha
|
|
125
|
-
const baseCommit = await githubGet<GitCommit>(
|
|
126
|
-
resolved.token,
|
|
127
|
-
`/repos/${resolved.repoOwner}/${resolved.repoName}/git/commits/${baseCommitSha}`,
|
|
128
|
-
)
|
|
129
|
-
const baseTreeSha = baseCommit.tree.sha
|
|
130
|
-
const existingTree = await githubGet<GitTree>(
|
|
131
|
-
resolved.token,
|
|
132
|
-
`/repos/${resolved.repoOwner}/${resolved.repoName}/git/trees/${baseTreeSha}?recursive=1`,
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
const prefix = `${skillRoot}/`
|
|
136
|
-
const existingPaths = new Set(
|
|
137
|
-
(existingTree.tree ?? [])
|
|
138
|
-
.filter((entry) => entry.type === 'blob' && entry.path?.startsWith(prefix))
|
|
139
|
-
.map((entry) => entry.path ?? ''),
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
const newPaths = new Set<string>()
|
|
143
|
-
const treeEntries: Array<{
|
|
144
|
-
path: string
|
|
145
|
-
mode: '100644'
|
|
146
|
-
type: 'blob'
|
|
147
|
-
sha: string | null
|
|
148
|
-
}> = []
|
|
149
|
-
|
|
150
|
-
for (const file of params.files) {
|
|
151
|
-
const content = await fetchStorageBase64(ctx, file.storageId)
|
|
152
|
-
const blobSha = await createBlob(resolved.token, resolved.repoOwner, resolved.repoName, content)
|
|
153
|
-
const path = `${skillRoot}/${file.path}`
|
|
154
|
-
newPaths.add(path)
|
|
155
|
-
treeEntries.push({ path, mode: '100644', type: 'blob', sha: blobSha })
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const existingMeta = await fetchMetaFile(
|
|
159
|
-
resolved.token,
|
|
160
|
-
resolved.repoOwner,
|
|
161
|
-
resolved.repoName,
|
|
162
|
-
`${skillRoot}/${META_FILENAME}`,
|
|
163
|
-
resolved.branch,
|
|
164
|
-
)
|
|
165
|
-
const metaPath = `${skillRoot}/${META_FILENAME}`
|
|
166
|
-
const metaDraft = buildMetaFile(params, existingMeta, resolved.repo, baseCommitSha, null)
|
|
167
|
-
const metaDraftContent = `${JSON.stringify(metaDraft, null, 2)}\n`
|
|
168
|
-
const metaDraftSha = await createBlob(
|
|
169
|
-
resolved.token,
|
|
170
|
-
resolved.repoOwner,
|
|
171
|
-
resolved.repoName,
|
|
172
|
-
toBase64(metaDraftContent),
|
|
173
|
-
)
|
|
174
|
-
newPaths.add(metaPath)
|
|
175
|
-
treeEntries.push({ path: metaPath, mode: '100644', type: 'blob', sha: metaDraftSha })
|
|
176
|
-
|
|
177
|
-
for (const path of existingPaths) {
|
|
178
|
-
if (newPaths.has(path)) continue
|
|
179
|
-
treeEntries.push({ path, mode: '100644', type: 'blob', sha: null })
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const newTree = await githubPost<{ sha: string }>(
|
|
183
|
-
resolved.token,
|
|
184
|
-
`/repos/${resolved.repoOwner}/${resolved.repoName}/git/trees`,
|
|
185
|
-
{
|
|
186
|
-
base_tree: baseTreeSha,
|
|
187
|
-
tree: treeEntries,
|
|
188
|
-
},
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
const commit = await githubPost<GitCommit>(
|
|
192
|
-
resolved.token,
|
|
193
|
-
`/repos/${resolved.repoOwner}/${resolved.repoName}/git/commits`,
|
|
194
|
-
{
|
|
195
|
-
message: `skill: ${params.slug} v${params.version}`,
|
|
196
|
-
tree: newTree.sha,
|
|
197
|
-
parents: [baseCommitSha],
|
|
198
|
-
},
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
const metaFinal = buildMetaFile(params, existingMeta, resolved.repo, baseCommitSha, commit.sha)
|
|
202
|
-
const metaFinalContent = `${JSON.stringify(metaFinal, null, 2)}\n`
|
|
203
|
-
const metaFinalSha = await createBlob(
|
|
204
|
-
resolved.token,
|
|
205
|
-
resolved.repoOwner,
|
|
206
|
-
resolved.repoName,
|
|
207
|
-
toBase64(metaFinalContent),
|
|
208
|
-
)
|
|
209
|
-
const metaTree = await githubPost<{ sha: string }>(
|
|
210
|
-
resolved.token,
|
|
211
|
-
`/repos/${resolved.repoOwner}/${resolved.repoName}/git/trees`,
|
|
212
|
-
{
|
|
213
|
-
base_tree: commit.tree.sha,
|
|
214
|
-
tree: [{ path: metaPath, mode: '100644', type: 'blob', sha: metaFinalSha }],
|
|
215
|
-
},
|
|
216
|
-
)
|
|
217
|
-
const metaCommit = await githubPost<GitCommit>(
|
|
218
|
-
resolved.token,
|
|
219
|
-
`/repos/${resolved.repoOwner}/${resolved.repoName}/git/commits`,
|
|
220
|
-
{
|
|
221
|
-
message: `meta: ${params.slug} v${params.version}`,
|
|
222
|
-
tree: metaTree.sha,
|
|
223
|
-
parents: [commit.sha],
|
|
224
|
-
},
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
await githubPatch(
|
|
228
|
-
resolved.token,
|
|
229
|
-
`/repos/${resolved.repoOwner}/${resolved.repoName}/git/refs/heads/${resolved.branch}`,
|
|
230
|
-
{
|
|
231
|
-
sha: metaCommit.sha,
|
|
232
|
-
},
|
|
233
|
-
)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function buildMetaFile(
|
|
237
|
-
params: BackupParams,
|
|
238
|
-
existing: MetaFile | null,
|
|
239
|
-
repo: string,
|
|
240
|
-
baseCommitSha: string,
|
|
241
|
-
latestCommitSha: string | null,
|
|
242
|
-
): MetaFile {
|
|
243
|
-
let history = [...(existing?.history ?? [])]
|
|
244
|
-
if (existing?.latest?.version) {
|
|
245
|
-
const previousCommit = existing.latest.commit ?? commitUrl(repo, baseCommitSha)
|
|
246
|
-
const previous = {
|
|
247
|
-
version: existing.latest.version,
|
|
248
|
-
publishedAt: existing.latest.publishedAt,
|
|
249
|
-
commit: previousCommit,
|
|
250
|
-
}
|
|
251
|
-
history = [previous, ...history.filter((entry) => entry.version !== previous.version)]
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
owner: normalizeOwner(params.ownerHandle),
|
|
256
|
-
slug: params.slug,
|
|
257
|
-
displayName: params.displayName,
|
|
258
|
-
latest: {
|
|
259
|
-
version: params.version,
|
|
260
|
-
publishedAt: params.publishedAt,
|
|
261
|
-
commit: latestCommitSha ? commitUrl(repo, latestCommitSha) : null,
|
|
262
|
-
},
|
|
263
|
-
history: history.slice(0, 200),
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
async function fetchMetaFile(
|
|
268
|
-
token: string,
|
|
269
|
-
repoOwner: string,
|
|
270
|
-
repoName: string,
|
|
271
|
-
path: string,
|
|
272
|
-
branch: string,
|
|
273
|
-
): Promise<MetaFile | null> {
|
|
274
|
-
try {
|
|
275
|
-
const response = await githubGet<{ content?: string }>(
|
|
276
|
-
token,
|
|
277
|
-
`/repos/${repoOwner}/${repoName}/contents/${encodePath(path)}?ref=${branch}`,
|
|
278
|
-
)
|
|
279
|
-
if (!response.content) return null
|
|
280
|
-
const raw = fromBase64(response.content)
|
|
281
|
-
return JSON.parse(raw) as MetaFile
|
|
282
|
-
} catch (error) {
|
|
283
|
-
if (isNotFoundError(error)) return null
|
|
284
|
-
throw error
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
async function fetchStorageBase64(ctx: ActionCtx, storageId: Id<'_storage'>) {
|
|
289
|
-
const blob = await ctx.storage.get(storageId)
|
|
290
|
-
if (!blob) throw new Error('File missing in storage')
|
|
291
|
-
const buffer = Buffer.from(await blob.arrayBuffer())
|
|
292
|
-
return buffer.toString('base64')
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async function createInstallationToken() {
|
|
296
|
-
const appId = process.env.GITHUB_APP_ID
|
|
297
|
-
const installationId = process.env.GITHUB_APP_INSTALLATION_ID
|
|
298
|
-
if (!appId || !installationId) {
|
|
299
|
-
throw new Error('GitHub App credentials missing')
|
|
300
|
-
}
|
|
301
|
-
const jwt = createAppJwt(appId)
|
|
302
|
-
const response = await fetch(`${GITHUB_API}/app/installations/${installationId}/access_tokens`, {
|
|
303
|
-
method: 'POST',
|
|
304
|
-
headers: buildHeaders(jwt, true),
|
|
305
|
-
})
|
|
306
|
-
if (!response.ok) {
|
|
307
|
-
const message = await response.text()
|
|
308
|
-
throw new Error(`GitHub App token failed: ${message}`)
|
|
309
|
-
}
|
|
310
|
-
const payload = (await response.json()) as { token?: string }
|
|
311
|
-
if (!payload.token) throw new Error('GitHub App token missing')
|
|
312
|
-
return payload.token
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function createAppJwt(appId: string) {
|
|
316
|
-
const privateKey = loadPrivateKey()
|
|
317
|
-
const now = Math.floor(Date.now() / 1000)
|
|
318
|
-
const header = { alg: 'RS256', typ: 'JWT' }
|
|
319
|
-
const payload = { iat: now - 60, exp: now + 9 * 60, iss: appId }
|
|
320
|
-
const encodedHeader = base64Url(JSON.stringify(header))
|
|
321
|
-
const encodedPayload = base64Url(JSON.stringify(payload))
|
|
322
|
-
const signingInput = `${encodedHeader}.${encodedPayload}`
|
|
323
|
-
const sign = createSign('RSA-SHA256')
|
|
324
|
-
sign.update(signingInput)
|
|
325
|
-
sign.end()
|
|
326
|
-
const signature = sign.sign(privateKey)
|
|
327
|
-
return `${signingInput}.${base64Url(signature)}`
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function loadPrivateKey() {
|
|
331
|
-
const raw = process.env.GITHUB_APP_PRIVATE_KEY
|
|
332
|
-
if (!raw) throw new Error('GITHUB_APP_PRIVATE_KEY is not configured')
|
|
333
|
-
const normalized = raw.replace(/\\n/g, '\n')
|
|
334
|
-
return createPrivateKey(normalized)
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
async function createBlob(token: string, repoOwner: string, repoName: string, content: string) {
|
|
338
|
-
const result = await githubPost<{ sha: string }>(
|
|
339
|
-
token,
|
|
340
|
-
`/repos/${repoOwner}/${repoName}/git/blobs`,
|
|
341
|
-
{
|
|
342
|
-
content,
|
|
343
|
-
encoding: 'base64',
|
|
344
|
-
},
|
|
345
|
-
)
|
|
346
|
-
if (!result.sha) throw new Error('GitHub blob missing sha')
|
|
347
|
-
return result.sha
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
async function githubGet<T>(token: string, path: string): Promise<T> {
|
|
351
|
-
const response = await fetch(`${GITHUB_API}${path}`, {
|
|
352
|
-
headers: buildHeaders(token),
|
|
353
|
-
})
|
|
354
|
-
if (!response.ok) {
|
|
355
|
-
const message = await response.text()
|
|
356
|
-
throw new Error(`GitHub GET ${path} failed: ${message}`)
|
|
357
|
-
}
|
|
358
|
-
return (await response.json()) as T
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
async function githubPost<T>(token: string, path: string, body: unknown): Promise<T> {
|
|
362
|
-
const response = await fetch(`${GITHUB_API}${path}`, {
|
|
363
|
-
method: 'POST',
|
|
364
|
-
headers: buildHeaders(token),
|
|
365
|
-
body: JSON.stringify(body),
|
|
366
|
-
})
|
|
367
|
-
if (!response.ok) {
|
|
368
|
-
const message = await response.text()
|
|
369
|
-
throw new Error(`GitHub POST ${path} failed: ${message}`)
|
|
370
|
-
}
|
|
371
|
-
return (await response.json()) as T
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
async function githubPatch(token: string, path: string, body: unknown) {
|
|
375
|
-
const response = await fetch(`${GITHUB_API}${path}`, {
|
|
376
|
-
method: 'PATCH',
|
|
377
|
-
headers: buildHeaders(token),
|
|
378
|
-
body: JSON.stringify(body),
|
|
379
|
-
})
|
|
380
|
-
if (!response.ok) {
|
|
381
|
-
const message = await response.text()
|
|
382
|
-
throw new Error(`GitHub PATCH ${path} failed: ${message}`)
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function buildHeaders(token: string, isAppJwt = false) {
|
|
387
|
-
return {
|
|
388
|
-
Authorization: `${isAppJwt ? 'Bearer' : 'token'} ${token}`,
|
|
389
|
-
Accept: 'application/vnd.github+json',
|
|
390
|
-
'User-Agent': USER_AGENT,
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function parseRepo(repo: string) {
|
|
395
|
-
const [owner, name] = repo.split('/')
|
|
396
|
-
if (!owner || !name) throw new Error('GITHUB_SKILLS_REPO must be owner/repo')
|
|
397
|
-
return [owner, name] as const
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function normalizeOwner(value: string) {
|
|
401
|
-
const normalized = value
|
|
402
|
-
.trim()
|
|
403
|
-
.toLowerCase()
|
|
404
|
-
.replace(/[^a-z0-9-]/g, '-')
|
|
405
|
-
.replace(/-+/g, '-')
|
|
406
|
-
.replace(/^-+|-+$/g, '')
|
|
407
|
-
return normalized || 'unknown'
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function commitUrl(repo: string, sha: string) {
|
|
411
|
-
return `https://github.com/${repo}/commit/${sha}`
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function buildSkillRoot(root: string, ownerHandle: string, slug: string) {
|
|
415
|
-
const ownerSegment = normalizeOwner(ownerHandle)
|
|
416
|
-
return `${root}/${ownerSegment}/${slug}`
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function encodePath(path: string) {
|
|
420
|
-
return path
|
|
421
|
-
.split('/')
|
|
422
|
-
.map((segment) => encodeURIComponent(segment))
|
|
423
|
-
.join('/')
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function base64Url(value: string | Buffer) {
|
|
427
|
-
const buffer = typeof value === 'string' ? Buffer.from(value) : value
|
|
428
|
-
return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function toBase64(value: string) {
|
|
432
|
-
return Buffer.from(value).toString('base64')
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function fromBase64(value: string) {
|
|
436
|
-
return Buffer.from(value, 'base64').toString('utf8')
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function isNotFoundError(error: unknown) {
|
|
440
|
-
return (
|
|
441
|
-
error instanceof Error && (error.message.includes('404') || error.message.includes('Not Found'))
|
|
442
|
-
)
|
|
443
|
-
}
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
/* @vitest-environment node */
|
|
2
|
-
|
|
3
|
-
import { unzipSync } from 'fflate'
|
|
4
|
-
import { describe, expect, it } from 'vitest'
|
|
5
|
-
import {
|
|
6
|
-
buildGitHubZipForTests,
|
|
7
|
-
computeDefaultSelectedPaths,
|
|
8
|
-
detectGitHubImportCandidates,
|
|
9
|
-
extractMarkdownRelativeTargets,
|
|
10
|
-
fetchGitHubZipBytes,
|
|
11
|
-
parseGitHubImportUrl,
|
|
12
|
-
resolveGitHubCommit,
|
|
13
|
-
resolveMarkdownTarget,
|
|
14
|
-
stripGitHubZipRoot,
|
|
15
|
-
} from './githubImport'
|
|
16
|
-
|
|
17
|
-
function requestInfoToUrlString(input: RequestInfo | URL): string {
|
|
18
|
-
if (typeof input === 'string') return input
|
|
19
|
-
if (input instanceof URL) return input.toString()
|
|
20
|
-
if (input instanceof Request) return input.url
|
|
21
|
-
|
|
22
|
-
throw new Error('Unexpected fetch input type')
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe('github import', () => {
|
|
26
|
-
it('parses repo root urls', () => {
|
|
27
|
-
expect(parseGitHubImportUrl('https://github.com/visionik/ouracli')).toEqual({
|
|
28
|
-
owner: 'visionik',
|
|
29
|
-
repo: 'ouracli',
|
|
30
|
-
originalUrl: 'https://github.com/visionik/ouracli',
|
|
31
|
-
})
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('rejects non-https and non-github urls', () => {
|
|
35
|
-
expect(() => parseGitHubImportUrl('http://github.com/a/b')).toThrow(/https/i)
|
|
36
|
-
expect(() => parseGitHubImportUrl('https://example.com/a/b')).toThrow(/github\.com/i)
|
|
37
|
-
expect(() => parseGitHubImportUrl('not-a-url')).toThrow(/Invalid URL/i)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('rejects malformed tree/blob urls', () => {
|
|
41
|
-
expect(() => parseGitHubImportUrl('https://github.com/a/b/tree/')).toThrow(/Missing ref/i)
|
|
42
|
-
expect(() => parseGitHubImportUrl('https://github.com/a/b/blob/main')).toThrow(/Missing path/i)
|
|
43
|
-
expect(() => parseGitHubImportUrl('https://github.com/a/b/tree/main/bad%5cpath')).toThrow()
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('parses tree urls with ref and path', () => {
|
|
47
|
-
expect(parseGitHubImportUrl('https://github.com/a/b/tree/main/skills/foo')).toEqual({
|
|
48
|
-
owner: 'a',
|
|
49
|
-
repo: 'b',
|
|
50
|
-
ref: 'main',
|
|
51
|
-
path: 'skills/foo',
|
|
52
|
-
originalUrl: 'https://github.com/a/b/tree/main/skills/foo',
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('parses blob urls and derives folder path', () => {
|
|
57
|
-
expect(parseGitHubImportUrl('https://github.com/a/b/blob/main/skills/foo/SKILL.md')).toEqual({
|
|
58
|
-
owner: 'a',
|
|
59
|
-
repo: 'b',
|
|
60
|
-
ref: 'main',
|
|
61
|
-
path: 'skills/foo',
|
|
62
|
-
originalUrl: 'https://github.com/a/b/blob/main/skills/foo/SKILL.md',
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('strips single top-level folder from GitHub zip entries', () => {
|
|
67
|
-
const zip = buildGitHubZipForTests({
|
|
68
|
-
'repo-1/skill/SKILL.md': 'Body',
|
|
69
|
-
'repo-1/skill/a.txt': 'a',
|
|
70
|
-
})
|
|
71
|
-
const stripped = stripGitHubZipRoot(unzipSync(zip))
|
|
72
|
-
expect(Object.keys(stripped).sort()).toEqual(['skill/SKILL.md', 'skill/a.txt'])
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('keeps paths when zip has multiple top-level roots', () => {
|
|
76
|
-
const zip = buildGitHubZipForTests({
|
|
77
|
-
'a/SKILL.md': 'Body',
|
|
78
|
-
'b/SKILL.md': 'Body',
|
|
79
|
-
})
|
|
80
|
-
const stripped = stripGitHubZipRoot(unzipSync(zip))
|
|
81
|
-
expect(Object.keys(stripped).sort()).toEqual(['a/SKILL.md', 'b/SKILL.md'])
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('detects candidates in a GitHub zip and strips the root folder', () => {
|
|
85
|
-
const zip = buildGitHubZipForTests({
|
|
86
|
-
'ouracli-123/SKILL.md': `---\nname: demo\ndescription: Hello\n---\nBody`,
|
|
87
|
-
'ouracli-123/src/index.ts': 'export {}',
|
|
88
|
-
})
|
|
89
|
-
const stripped = stripGitHubZipRoot(unzipSync(zip))
|
|
90
|
-
const candidates = detectGitHubImportCandidates(stripped)
|
|
91
|
-
expect(candidates.map((c) => c.path)).toEqual([''])
|
|
92
|
-
expect(candidates[0]?.name).toBe('demo')
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('detects multiple candidates and supports skills.md', () => {
|
|
96
|
-
const zip = buildGitHubZipForTests({
|
|
97
|
-
'repo-1/alpha/SKILL.md': `---\nname: Alpha\n---\nBody`,
|
|
98
|
-
'repo-1/beta/skills.md': `---\nname: Beta\n---\nBody`,
|
|
99
|
-
'repo-1/readme.md': 'x',
|
|
100
|
-
})
|
|
101
|
-
const stripped = stripGitHubZipRoot(unzipSync(zip))
|
|
102
|
-
const candidates = detectGitHubImportCandidates(stripped)
|
|
103
|
-
expect(candidates.map((c) => c.path)).toEqual(['alpha', 'beta'])
|
|
104
|
-
expect(candidates.map((c) => c.name)).toEqual(['Alpha', 'Beta'])
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('computes default selection via markdown references', () => {
|
|
108
|
-
const entries = {
|
|
109
|
-
'skill/SKILL.md': `---\nname: demo\n---\nSee [usage](docs/usage.md) and .\nIgnore [web](https://example.com).`,
|
|
110
|
-
'skill/docs/usage.md': `See [more](more.md)`,
|
|
111
|
-
'skill/docs/more.md': `Ok`,
|
|
112
|
-
'skill/img/logo.svg': `<svg/>`,
|
|
113
|
-
'skill/extra.txt': 'not referenced',
|
|
114
|
-
}
|
|
115
|
-
const zip = buildGitHubZipForTests(
|
|
116
|
-
Object.fromEntries(Object.entries(entries).map(([k, v]) => [`repo-1/${k}`, v])),
|
|
117
|
-
)
|
|
118
|
-
const raw = unzipSync(zip)
|
|
119
|
-
const stripped = stripGitHubZipRoot(raw)
|
|
120
|
-
const candidates = detectGitHubImportCandidates(stripped)
|
|
121
|
-
const candidate = candidates.find((c) => c.path === 'skill')
|
|
122
|
-
expect(candidate).toBeTruthy()
|
|
123
|
-
if (!candidate) throw new Error('candidate not found')
|
|
124
|
-
|
|
125
|
-
const files = Object.entries(stripped)
|
|
126
|
-
.filter(([path]) => path.startsWith('skill/'))
|
|
127
|
-
.map(([path, bytes]) => ({ path, bytes }))
|
|
128
|
-
const selected = computeDefaultSelectedPaths({ candidate, files })
|
|
129
|
-
expect(selected).toContain('skill/SKILL.md')
|
|
130
|
-
expect(selected).toContain('skill/docs/usage.md')
|
|
131
|
-
expect(selected).toContain('skill/docs/more.md')
|
|
132
|
-
expect(selected).toContain('skill/img/logo.svg')
|
|
133
|
-
expect(selected).not.toContain('skill/extra.txt')
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('does not select files outside skill folder (even when referenced)', () => {
|
|
137
|
-
const entries = {
|
|
138
|
-
'skill/SKILL.md': `See [outside](../outside.md) and [abs](/abs.md) and [mail](mailto:test@example.com).`,
|
|
139
|
-
'outside.md': `secret`,
|
|
140
|
-
'skill/docs/usage.md': `Ok`,
|
|
141
|
-
}
|
|
142
|
-
const zip = buildGitHubZipForTests(
|
|
143
|
-
Object.fromEntries(Object.entries(entries).map(([k, v]) => [`repo-1/${k}`, v])),
|
|
144
|
-
)
|
|
145
|
-
const stripped = stripGitHubZipRoot(unzipSync(zip))
|
|
146
|
-
const candidate = detectGitHubImportCandidates(stripped).find((c) => c.path === 'skill')
|
|
147
|
-
expect(candidate).toBeTruthy()
|
|
148
|
-
if (!candidate) throw new Error('candidate not found')
|
|
149
|
-
const files = Object.entries(stripped).map(([path, bytes]) => ({ path, bytes }))
|
|
150
|
-
const selected = computeDefaultSelectedPaths({ candidate, files })
|
|
151
|
-
expect(selected).toContain('skill/SKILL.md')
|
|
152
|
-
expect(selected).not.toContain('outside.md')
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
it('extracts markdown targets with titles and angle brackets', () => {
|
|
156
|
-
const targets = extractMarkdownRelativeTargets(
|
|
157
|
-
`See [a](docs/usage.md "Title") and [b](<docs/my file.md>) and `,
|
|
158
|
-
)
|
|
159
|
-
expect(targets).toEqual(['docs/usage.md', 'docs/my file.md', 'img/logo.svg'])
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it('resolves markdown targets safely', () => {
|
|
163
|
-
expect(resolveMarkdownTarget('a/SKILL.md', 'docs/usage.md')).toBe('a/docs/usage.md')
|
|
164
|
-
expect(resolveMarkdownTarget('a/SKILL.md', '../oops.md')).toBeNull()
|
|
165
|
-
expect(resolveMarkdownTarget('a/SKILL.md', '/abs.md')).toBeNull()
|
|
166
|
-
expect(resolveMarkdownTarget('a/SKILL.md', 'docs/usage.md#section')).toBe('a/docs/usage.md')
|
|
167
|
-
expect(resolveMarkdownTarget('a/SKILL.md', 'docs/usage.md?x=1')).toBe('a/docs/usage.md')
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
it('resolves HEAD commit via redirect chain and refuses unexpected redirect hosts', async () => {
|
|
171
|
-
const fetcher: typeof fetch = async (input) => {
|
|
172
|
-
const url = requestInfoToUrlString(input)
|
|
173
|
-
if (url.includes('/archive/HEAD.zip')) {
|
|
174
|
-
return new Response(null, {
|
|
175
|
-
status: 302,
|
|
176
|
-
headers: {
|
|
177
|
-
location:
|
|
178
|
-
'https://codeload.github.com/a/b/zip/0123456789012345678901234567890123456789',
|
|
179
|
-
},
|
|
180
|
-
})
|
|
181
|
-
}
|
|
182
|
-
if (url.startsWith('https://codeload.github.com/a/b/zip/')) {
|
|
183
|
-
return new Response(null, { status: 200 })
|
|
184
|
-
}
|
|
185
|
-
throw new Error(`Unexpected fetch: ${url}`)
|
|
186
|
-
}
|
|
187
|
-
const resolved = await resolveGitHubCommit(
|
|
188
|
-
{ owner: 'a', repo: 'b', originalUrl: 'https://github.com/a/b' },
|
|
189
|
-
fetcher,
|
|
190
|
-
)
|
|
191
|
-
expect(resolved.commit).toBe('0123456789012345678901234567890123456789')
|
|
192
|
-
|
|
193
|
-
const badFetcher: typeof fetch = async (input) => {
|
|
194
|
-
const url = requestInfoToUrlString(input)
|
|
195
|
-
if (url.includes('/archive/HEAD.zip')) {
|
|
196
|
-
return new Response(null, {
|
|
197
|
-
status: 302,
|
|
198
|
-
headers: { location: 'https://evil.example/zip/abc' },
|
|
199
|
-
})
|
|
200
|
-
}
|
|
201
|
-
throw new Error(`Unexpected fetch: ${url}`)
|
|
202
|
-
}
|
|
203
|
-
await expect(
|
|
204
|
-
resolveGitHubCommit(
|
|
205
|
-
{ owner: 'a', repo: 'b', originalUrl: 'https://github.com/a/b' },
|
|
206
|
-
badFetcher,
|
|
207
|
-
),
|
|
208
|
-
).rejects.toThrow(/redirect/i)
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
it('resolves explicit ref commit via GitHub API', async () => {
|
|
212
|
-
const fetcher: typeof fetch = async (input) => {
|
|
213
|
-
const url = requestInfoToUrlString(input)
|
|
214
|
-
if (url.startsWith('https://api.github.com/repos/a/b/commits/')) {
|
|
215
|
-
return new Response(JSON.stringify({ sha: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' }), {
|
|
216
|
-
status: 200,
|
|
217
|
-
})
|
|
218
|
-
}
|
|
219
|
-
throw new Error(`Unexpected fetch: ${url}`)
|
|
220
|
-
}
|
|
221
|
-
const resolved = await resolveGitHubCommit(
|
|
222
|
-
{ owner: 'a', repo: 'b', ref: 'main', originalUrl: 'https://github.com/a/b' },
|
|
223
|
-
fetcher,
|
|
224
|
-
)
|
|
225
|
-
expect(resolved.commit).toBe('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
it('enforces zip byte cap when content-length is too large', async () => {
|
|
229
|
-
const resolved = {
|
|
230
|
-
owner: 'a',
|
|
231
|
-
repo: 'b',
|
|
232
|
-
ref: 'main',
|
|
233
|
-
commit: '0123456789012345678901234567890123456789',
|
|
234
|
-
path: '',
|
|
235
|
-
repoUrl: 'https://github.com/a/b',
|
|
236
|
-
originalUrl: 'https://github.com/a/b',
|
|
237
|
-
} as const
|
|
238
|
-
const fetcher: typeof fetch = async () =>
|
|
239
|
-
new Response(new Blob([new Uint8Array([1, 2, 3])]), {
|
|
240
|
-
status: 200,
|
|
241
|
-
headers: { 'content-length': String(999_999_999) },
|
|
242
|
-
})
|
|
243
|
-
await expect(fetchGitHubZipBytes(resolved, fetcher, { maxZipBytes: 10 })).rejects.toThrow(
|
|
244
|
-
/too large/i,
|
|
245
|
-
)
|
|
246
|
-
})
|
|
247
|
-
})
|