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.
Files changed (272) hide show
  1. package/.env.local.example +19 -0
  2. package/.github/workflows/ci.yml +40 -0
  3. package/.oxlintrc.json +3 -0
  4. package/AGENTS.md +45 -0
  5. package/CHANGELOG.md +138 -0
  6. package/DEPRECATIONS.md +7 -0
  7. package/LICENSE +21 -0
  8. package/README.md +150 -0
  9. package/biome.json +41 -0
  10. package/convex/_generated/api.d.ts +153 -0
  11. package/convex/_generated/api.js +23 -0
  12. package/convex/_generated/dataModel.d.ts +60 -0
  13. package/convex/_generated/server.d.ts +143 -0
  14. package/convex/_generated/server.js +93 -0
  15. package/convex/auth.config.ts +8 -0
  16. package/convex/auth.ts +19 -0
  17. package/convex/comments.ts +88 -0
  18. package/convex/crons.ts +34 -0
  19. package/convex/devSeed.ts +459 -0
  20. package/convex/devSeedExtra.ts +541 -0
  21. package/convex/downloads.ts +78 -0
  22. package/convex/githubBackups.ts +170 -0
  23. package/convex/githubBackupsNode.ts +183 -0
  24. package/convex/githubImport.ts +317 -0
  25. package/convex/githubSoulBackups.ts +170 -0
  26. package/convex/githubSoulBackupsNode.ts +186 -0
  27. package/convex/http.ts +194 -0
  28. package/convex/httpApi.handlers.test.ts +488 -0
  29. package/convex/httpApi.test.ts +70 -0
  30. package/convex/httpApi.ts +305 -0
  31. package/convex/httpApiV1.handlers.test.ts +584 -0
  32. package/convex/httpApiV1.ts +1172 -0
  33. package/convex/leaderboards.ts +39 -0
  34. package/convex/lib/access.ts +36 -0
  35. package/convex/lib/apiTokenAuth.ts +36 -0
  36. package/convex/lib/badges.ts +50 -0
  37. package/convex/lib/changelog.test.ts +34 -0
  38. package/convex/lib/changelog.ts +278 -0
  39. package/convex/lib/embeddings.ts +38 -0
  40. package/convex/lib/githubBackup.ts +443 -0
  41. package/convex/lib/githubImport.test.ts +247 -0
  42. package/convex/lib/githubImport.ts +425 -0
  43. package/convex/lib/githubSoulBackup.ts +443 -0
  44. package/convex/lib/leaderboards.ts +103 -0
  45. package/convex/lib/moderation.ts +42 -0
  46. package/convex/lib/public.ts +89 -0
  47. package/convex/lib/searchText.test.ts +46 -0
  48. package/convex/lib/searchText.ts +27 -0
  49. package/convex/lib/skillBackfill.test.ts +34 -0
  50. package/convex/lib/skillBackfill.ts +67 -0
  51. package/convex/lib/skillPublish.test.ts +28 -0
  52. package/convex/lib/skillPublish.ts +284 -0
  53. package/convex/lib/skillStats.ts +80 -0
  54. package/convex/lib/skills.test.ts +197 -0
  55. package/convex/lib/skills.ts +273 -0
  56. package/convex/lib/soulChangelog.ts +273 -0
  57. package/convex/lib/soulPublish.ts +236 -0
  58. package/convex/lib/tokens.test.ts +33 -0
  59. package/convex/lib/tokens.ts +51 -0
  60. package/convex/lib/webhooks.test.ts +91 -0
  61. package/convex/lib/webhooks.ts +112 -0
  62. package/convex/maintenance.test.ts +270 -0
  63. package/convex/maintenance.ts +840 -0
  64. package/convex/rateLimits.ts +50 -0
  65. package/convex/schema.ts +472 -0
  66. package/convex/search.test.ts +12 -0
  67. package/convex/search.ts +254 -0
  68. package/convex/seed.test.ts +37 -0
  69. package/convex/seed.ts +254 -0
  70. package/convex/seedSouls.ts +111 -0
  71. package/convex/skillStatEvents.ts +568 -0
  72. package/convex/skills.ts +1606 -0
  73. package/convex/soulComments.ts +88 -0
  74. package/convex/soulDownloads.ts +14 -0
  75. package/convex/soulStars.ts +71 -0
  76. package/convex/souls.ts +570 -0
  77. package/convex/stars.ts +108 -0
  78. package/convex/statsMaintenance.ts +205 -0
  79. package/convex/telemetry.ts +434 -0
  80. package/convex/tokens.ts +88 -0
  81. package/convex/tsconfig.json +7 -0
  82. package/convex/uploads.ts +20 -0
  83. package/convex/users.ts +122 -0
  84. package/convex/webhooks.ts +50 -0
  85. package/convex.json +3 -0
  86. package/docs/README.md +32 -0
  87. package/docs/api.md +51 -0
  88. package/docs/architecture.md +61 -0
  89. package/docs/auth.md +54 -0
  90. package/docs/cli.md +117 -0
  91. package/docs/deploy.md +78 -0
  92. package/docs/diffing.md +84 -0
  93. package/docs/github-import.md +171 -0
  94. package/docs/http-api.md +187 -0
  95. package/docs/manual-testing.md +64 -0
  96. package/docs/mintlify.md +43 -0
  97. package/docs/quickstart.md +120 -0
  98. package/docs/skill-format.md +58 -0
  99. package/docs/soul-format.md +37 -0
  100. package/docs/spec.md +177 -0
  101. package/docs/telemetry.md +91 -0
  102. package/docs/troubleshooting.md +49 -0
  103. package/docs/webhook.md +51 -0
  104. package/e2e/menu-smoke.pw.test.ts +49 -0
  105. package/e2e/pilothub.e2e.test.ts +494 -0
  106. package/e2e/search-exact.pw.test.ts +97 -0
  107. package/package.json +84 -0
  108. package/packages/pilothub/LICENSE +22 -0
  109. package/packages/pilothub/README.md +57 -0
  110. package/packages/pilothub/bin/pilothub.js +2 -0
  111. package/packages/pilothub/package.json +41 -0
  112. package/packages/pilothub/src/browserAuth.test.ts +96 -0
  113. package/packages/pilothub/src/browserAuth.ts +174 -0
  114. package/packages/pilothub/src/cli/buildInfo.ts +94 -0
  115. package/packages/pilothub/src/cli/commands/auth.ts +97 -0
  116. package/packages/pilothub/src/cli/commands/delete.test.ts +73 -0
  117. package/packages/pilothub/src/cli/commands/delete.ts +83 -0
  118. package/packages/pilothub/src/cli/commands/publish.test.ts +122 -0
  119. package/packages/pilothub/src/cli/commands/publish.ts +108 -0
  120. package/packages/pilothub/src/cli/commands/skills.test.ts +191 -0
  121. package/packages/pilothub/src/cli/commands/skills.ts +380 -0
  122. package/packages/pilothub/src/cli/commands/star.ts +46 -0
  123. package/packages/pilothub/src/cli/commands/sync.test.ts +310 -0
  124. package/packages/pilothub/src/cli/commands/sync.ts +200 -0
  125. package/packages/pilothub/src/cli/commands/syncHelpers.test.ts +26 -0
  126. package/packages/pilothub/src/cli/commands/syncHelpers.ts +427 -0
  127. package/packages/pilothub/src/cli/commands/syncTypes.ts +27 -0
  128. package/packages/pilothub/src/cli/commands/unstar.ts +48 -0
  129. package/packages/pilothub/src/cli/helpStyle.ts +45 -0
  130. package/packages/pilothub/src/cli/pilotbotConfig.test.ts +159 -0
  131. package/packages/pilothub/src/cli/pilotbotConfig.ts +147 -0
  132. package/packages/pilothub/src/cli/registry.test.ts +63 -0
  133. package/packages/pilothub/src/cli/registry.ts +43 -0
  134. package/packages/pilothub/src/cli/scanSkills.test.ts +64 -0
  135. package/packages/pilothub/src/cli/scanSkills.ts +84 -0
  136. package/packages/pilothub/src/cli/slug.ts +16 -0
  137. package/packages/pilothub/src/cli/types.ts +12 -0
  138. package/packages/pilothub/src/cli/ui.ts +75 -0
  139. package/packages/pilothub/src/cli.ts +311 -0
  140. package/packages/pilothub/src/config.ts +36 -0
  141. package/packages/pilothub/src/discovery.test.ts +75 -0
  142. package/packages/pilothub/src/discovery.ts +19 -0
  143. package/packages/pilothub/src/http.test.ts +156 -0
  144. package/packages/pilothub/src/http.ts +301 -0
  145. package/packages/pilothub/src/schema/ark.ts +29 -0
  146. package/packages/pilothub/src/schema/index.ts +5 -0
  147. package/packages/pilothub/src/schema/routes.ts +22 -0
  148. package/packages/pilothub/src/schema/schemas.ts +260 -0
  149. package/packages/pilothub/src/schema/textFiles.test.ts +23 -0
  150. package/packages/pilothub/src/schema/textFiles.ts +66 -0
  151. package/packages/pilothub/src/skills.test.ts +191 -0
  152. package/packages/pilothub/src/skills.ts +172 -0
  153. package/packages/pilothub/src/types.ts +10 -0
  154. package/packages/pilothub/tsconfig.json +14 -0
  155. package/packages/schema/README.md +3 -0
  156. package/packages/schema/dist/ark.d.ts +4 -0
  157. package/packages/schema/dist/ark.js +26 -0
  158. package/packages/schema/dist/ark.js.map +1 -0
  159. package/packages/schema/dist/index.d.ts +5 -0
  160. package/packages/schema/dist/index.js +5 -0
  161. package/packages/schema/dist/index.js.map +1 -0
  162. package/packages/schema/dist/routes.d.ts +21 -0
  163. package/packages/schema/dist/routes.js +22 -0
  164. package/packages/schema/dist/routes.js.map +1 -0
  165. package/packages/schema/dist/schemas.d.ts +297 -0
  166. package/packages/schema/dist/schemas.js +243 -0
  167. package/packages/schema/dist/schemas.js.map +1 -0
  168. package/packages/schema/dist/textFiles.d.ts +5 -0
  169. package/packages/schema/dist/textFiles.js +66 -0
  170. package/packages/schema/dist/textFiles.js.map +1 -0
  171. package/packages/schema/package.json +26 -0
  172. package/packages/schema/src/ark.ts +29 -0
  173. package/packages/schema/src/index.ts +5 -0
  174. package/packages/schema/src/routes.ts +22 -0
  175. package/packages/schema/src/schemas.test.ts +123 -0
  176. package/packages/schema/src/schemas.ts +287 -0
  177. package/packages/schema/src/textFiles.test.ts +23 -0
  178. package/packages/schema/src/textFiles.ts +66 -0
  179. package/packages/schema/tsconfig.json +15 -0
  180. package/pilothub +46 -0
  181. package/playwright.config.ts +33 -0
  182. package/public/.well-known/pilothub.json +6 -0
  183. package/public/api/v1/openapi.json +379 -0
  184. package/public/favicon.ico +0 -0
  185. package/public/logo192.png +0 -0
  186. package/public/logo512.png +0 -0
  187. package/public/manifest.json +25 -0
  188. package/public/og.png +0 -0
  189. package/public/og.svg +98 -0
  190. package/public/pilot-logo.png +0 -0
  191. package/public/pilot-mark.png +0 -0
  192. package/public/robots.txt +3 -0
  193. package/public/tanstack-circle-logo.png +0 -0
  194. package/public/tanstack-word-logo-white.svg +1 -0
  195. package/scripts/check-peer-deps.ts +56 -0
  196. package/scripts/docs-list.ts +148 -0
  197. package/scripts/run-playwright-local.sh +14 -0
  198. package/server/og/fetchSkillOgMeta.ts +27 -0
  199. package/server/og/fetchSoulOgMeta.ts +27 -0
  200. package/server/og/ogAssets.ts +80 -0
  201. package/server/og/skillOgSvg.test.ts +59 -0
  202. package/server/og/skillOgSvg.ts +258 -0
  203. package/server/og/soulOgSvg.ts +209 -0
  204. package/server/routes/og/skill.png.ts +103 -0
  205. package/server/routes/og/soul.png.ts +111 -0
  206. package/src/__tests__/skill-detail-page.test.tsx +86 -0
  207. package/src/__tests__/skills-index.test.tsx +145 -0
  208. package/src/__tests__/upload.route.test.tsx +228 -0
  209. package/src/components/AppProviders.tsx +19 -0
  210. package/src/components/ClientOnly.tsx +18 -0
  211. package/src/components/Footer.tsx +29 -0
  212. package/src/components/Header.tsx +295 -0
  213. package/src/components/InstallSwitcher.tsx +53 -0
  214. package/src/components/SkillCard.tsx +36 -0
  215. package/src/components/SkillDetailPage.tsx +817 -0
  216. package/src/components/SkillDiffCard.tsx +485 -0
  217. package/src/components/SoulCard.tsx +19 -0
  218. package/src/components/SoulDetailPage.tsx +263 -0
  219. package/src/components/UserBootstrap.tsx +18 -0
  220. package/src/components/ui/dropdown-menu.tsx +67 -0
  221. package/src/components/ui/toggle-group.tsx +35 -0
  222. package/src/convex/client.ts +3 -0
  223. package/src/lib/badges.ts +29 -0
  224. package/src/lib/diffing.test.ts +163 -0
  225. package/src/lib/diffing.ts +106 -0
  226. package/src/lib/gravatar.test.ts +9 -0
  227. package/src/lib/gravatar.ts +158 -0
  228. package/src/lib/og.test.ts +142 -0
  229. package/src/lib/og.ts +156 -0
  230. package/src/lib/publicUser.ts +39 -0
  231. package/src/lib/roles.ts +19 -0
  232. package/src/lib/site.test.ts +130 -0
  233. package/src/lib/site.ts +84 -0
  234. package/src/lib/theme-transition.test.ts +134 -0
  235. package/src/lib/theme-transition.ts +134 -0
  236. package/src/lib/theme.test.tsx +88 -0
  237. package/src/lib/theme.ts +43 -0
  238. package/src/lib/uploadFiles.jsdom.test.ts +33 -0
  239. package/src/lib/uploadFiles.test.ts +123 -0
  240. package/src/lib/uploadFiles.ts +245 -0
  241. package/src/lib/uploadUtils.test.ts +78 -0
  242. package/src/lib/uploadUtils.ts +93 -0
  243. package/src/lib/useAuthStatus.ts +12 -0
  244. package/src/lib/utils.test.ts +9 -0
  245. package/src/lib/utils.ts +6 -0
  246. package/src/logo.svg +12 -0
  247. package/src/routeTree.gen.ts +345 -0
  248. package/src/router.tsx +17 -0
  249. package/src/routes/$owner/$slug.tsx +55 -0
  250. package/src/routes/__root.tsx +136 -0
  251. package/src/routes/admin.tsx +11 -0
  252. package/src/routes/cli/auth.tsx +168 -0
  253. package/src/routes/dashboard.tsx +97 -0
  254. package/src/routes/import.tsx +415 -0
  255. package/src/routes/index.tsx +252 -0
  256. package/src/routes/management.tsx +529 -0
  257. package/src/routes/settings.tsx +203 -0
  258. package/src/routes/skills/index.tsx +422 -0
  259. package/src/routes/souls/$slug.tsx +55 -0
  260. package/src/routes/souls/index.tsx +243 -0
  261. package/src/routes/stars.tsx +68 -0
  262. package/src/routes/u/$handle.tsx +307 -0
  263. package/src/routes/upload/utils.ts +81 -0
  264. package/src/routes/upload.tsx +499 -0
  265. package/src/styles.css +2718 -0
  266. package/tsconfig.json +24 -0
  267. package/tsconfig.oxlint.json +16 -0
  268. package/vercel.json +8 -0
  269. package/vite.config.ts +48 -0
  270. package/vitest.config.ts +47 -0
  271. package/vitest.e2e.config.ts +11 -0
  272. package/vitest.setup.ts +1 -0
@@ -0,0 +1,66 @@
1
+ const RAW_TEXT_FILE_EXTENSIONS = [
2
+ 'md',
3
+ 'mdx',
4
+ 'txt',
5
+ 'json',
6
+ 'json5',
7
+ 'yaml',
8
+ 'yml',
9
+ 'toml',
10
+ 'js',
11
+ 'cjs',
12
+ 'mjs',
13
+ 'ts',
14
+ 'tsx',
15
+ 'jsx',
16
+ 'py',
17
+ 'sh',
18
+ 'rb',
19
+ 'go',
20
+ 'rs',
21
+ 'swift',
22
+ 'kt',
23
+ 'java',
24
+ 'cs',
25
+ 'cpp',
26
+ 'c',
27
+ 'h',
28
+ 'hpp',
29
+ 'sql',
30
+ 'csv',
31
+ 'ini',
32
+ 'cfg',
33
+ 'env',
34
+ 'xml',
35
+ 'html',
36
+ 'css',
37
+ 'scss',
38
+ 'sass',
39
+ 'svg',
40
+ ] as const
41
+
42
+ export const TEXT_FILE_EXTENSIONS = RAW_TEXT_FILE_EXTENSIONS
43
+ export const TEXT_FILE_EXTENSION_SET = new Set<string>(TEXT_FILE_EXTENSIONS)
44
+
45
+ const RAW_TEXT_CONTENT_TYPES = [
46
+ 'application/json',
47
+ 'application/xml',
48
+ 'application/yaml',
49
+ 'application/x-yaml',
50
+ 'application/toml',
51
+ 'application/javascript',
52
+ 'application/typescript',
53
+ 'application/markdown',
54
+ 'image/svg+xml',
55
+ ] as const
56
+
57
+ export const TEXT_CONTENT_TYPES = RAW_TEXT_CONTENT_TYPES
58
+ export const TEXT_CONTENT_TYPE_SET = new Set<string>(TEXT_CONTENT_TYPES)
59
+
60
+ export function isTextContentType(contentType: string) {
61
+ if (!contentType) return false
62
+ const normalized = contentType.split(';', 1)[0]?.trim().toLowerCase() ?? ''
63
+ if (!normalized) return false
64
+ if (normalized.startsWith('text/')) return true
65
+ return TEXT_CONTENT_TYPE_SET.has(normalized)
66
+ }
@@ -0,0 +1,191 @@
1
+ /* @vitest-environment node */
2
+ import { mkdir, mkdtemp, readFile, stat, writeFile } from 'node:fs/promises'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+ import { strToU8, zipSync } from 'fflate'
6
+ import { describe, expect, it } from 'vitest'
7
+ import type { SkillOrigin } from './skills'
8
+ import {
9
+ buildSkillFingerprint,
10
+ extractZipToDir,
11
+ hashSkillFiles,
12
+ hashSkillZip,
13
+ listTextFiles,
14
+ readLockfile,
15
+ readSkillOrigin,
16
+ sha256Hex,
17
+ writeLockfile,
18
+ writeSkillOrigin,
19
+ } from './skills'
20
+
21
+ describe('skills', () => {
22
+ it('extracts zip into directory and skips traversal', async () => {
23
+ const dir = await mkdtemp(join(tmpdir(), 'pilothub-'))
24
+ const zip = zipSync({
25
+ 'SKILL.md': strToU8('hello'),
26
+ '../evil.txt': strToU8('nope'),
27
+ })
28
+ await extractZipToDir(new Uint8Array(zip), dir)
29
+
30
+ expect((await readFile(join(dir, 'SKILL.md'), 'utf8')).trim()).toBe('hello')
31
+ await expect(stat(join(dir, '..', 'evil.txt'))).rejects.toBeTruthy()
32
+ })
33
+
34
+ it('writes and reads lockfile', async () => {
35
+ const workdir = await mkdtemp(join(tmpdir(), 'pilothub-work-'))
36
+ await writeLockfile(workdir, {
37
+ version: 1,
38
+ skills: { demo: { version: '1.0.0', installedAt: 1 } },
39
+ })
40
+ const read = await readLockfile(workdir)
41
+ expect(read.skills.demo?.version).toBe('1.0.0')
42
+ })
43
+
44
+ it('returns empty lockfile on invalid json', async () => {
45
+ const workdir = await mkdtemp(join(tmpdir(), 'pilothub-work-bad-'))
46
+ await mkdir(join(workdir, '.pilothub'), { recursive: true })
47
+ await writeFile(join(workdir, '.pilothub', 'lock.json'), '{', 'utf8')
48
+ const read = await readLockfile(workdir)
49
+ expect(read).toEqual({ version: 1, skills: {} })
50
+ })
51
+
52
+ it('returns empty lockfile on schema mismatch', async () => {
53
+ const workdir = await mkdtemp(join(tmpdir(), 'pilothub-work-schema-'))
54
+ await mkdir(join(workdir, '.pilothub'), { recursive: true })
55
+ await writeFile(
56
+ join(workdir, '.pilothub', 'lock.json'),
57
+ JSON.stringify({ version: 1, skills: 'nope' }),
58
+ 'utf8',
59
+ )
60
+ const read = await readLockfile(workdir)
61
+ expect(read).toEqual({ version: 1, skills: {} })
62
+ })
63
+
64
+ it('skips dotfiles and node_modules when listing text files', async () => {
65
+ const workdir = await mkdtemp(join(tmpdir(), 'pilothub-files-'))
66
+ await writeFile(join(workdir, 'SKILL.md'), 'hi', 'utf8')
67
+ await writeFile(join(workdir, '.secret.txt'), 'no', 'utf8')
68
+ await mkdir(join(workdir, 'node_modules'), { recursive: true })
69
+ await writeFile(join(workdir, 'node_modules', 'a.txt'), 'no', 'utf8')
70
+ const files = await listTextFiles(workdir)
71
+ expect(files.map((file) => file.relPath)).toEqual(['SKILL.md'])
72
+ })
73
+
74
+ it('respects .gitignore and .pilothubignore', async () => {
75
+ const workdir = await mkdtemp(join(tmpdir(), 'pilothub-ignore-'))
76
+ await writeFile(join(workdir, '.gitignore'), 'ignored.md\n', 'utf8')
77
+ await writeFile(join(workdir, '.pilothubignore'), 'private.md\n', 'utf8')
78
+ await writeFile(join(workdir, 'SKILL.md'), 'hi', 'utf8')
79
+ await writeFile(join(workdir, 'ignored.md'), 'no', 'utf8')
80
+ await writeFile(join(workdir, 'private.md'), 'no', 'utf8')
81
+ await writeFile(join(workdir, 'public.json'), '{}', 'utf8')
82
+
83
+ const files = await listTextFiles(workdir)
84
+ const paths = files.map((file) => file.relPath).sort()
85
+ expect(paths).toEqual(['SKILL.md', 'public.json'])
86
+ expect(files.find((file) => file.relPath === 'SKILL.md')?.contentType).toMatch(/^text\//)
87
+ expect(files.find((file) => file.relPath === 'public.json')?.contentType).toBe(
88
+ 'application/json',
89
+ )
90
+ })
91
+
92
+ it('falls back to text/plain for unknown text extensions', async () => {
93
+ const workdir = await mkdtemp(join(tmpdir(), 'pilothub-env-'))
94
+ await writeFile(join(workdir, 'SKILL.md'), 'hi', 'utf8')
95
+ await writeFile(join(workdir, 'config.env'), 'TOKEN=demo', 'utf8')
96
+ const files = await listTextFiles(workdir)
97
+ expect(files.find((file) => file.relPath === 'config.env')?.contentType).toBe('text/plain')
98
+ })
99
+
100
+ it('hashes skill files deterministically', async () => {
101
+ const { fingerprint } = hashSkillFiles([
102
+ { relPath: 'b.txt', bytes: strToU8('b') },
103
+ { relPath: 'a.txt', bytes: strToU8('a') },
104
+ ])
105
+ const expected = buildSkillFingerprint([
106
+ { path: 'a.txt', sha256: sha256Hex(strToU8('a')) },
107
+ { path: 'b.txt', sha256: sha256Hex(strToU8('b')) },
108
+ ])
109
+ expect(fingerprint).toBe(expected)
110
+ })
111
+
112
+ it('hashes text files inside a downloaded zip deterministically', () => {
113
+ const zip = zipSync({
114
+ 'SKILL.md': strToU8('hello'),
115
+ 'notes.md': strToU8('world'),
116
+ 'image.png': strToU8('nope'),
117
+ })
118
+ const { fingerprint } = hashSkillZip(new Uint8Array(zip))
119
+ const expected = buildSkillFingerprint([
120
+ { path: 'SKILL.md', sha256: sha256Hex(strToU8('hello')) },
121
+ { path: 'notes.md', sha256: sha256Hex(strToU8('world')) },
122
+ ])
123
+ expect(fingerprint).toBe(expected)
124
+ })
125
+
126
+ it('ignores unsafe or non-text entries when hashing zips', () => {
127
+ const zip = zipSync({
128
+ 'SKILL.md': strToU8('hello'),
129
+ 'folder/': strToU8(''),
130
+ '../evil.txt': strToU8('nope'),
131
+ 'bad\\path.txt': strToU8('nope'),
132
+ 'image.png': strToU8('nope'),
133
+ })
134
+ const { files } = hashSkillZip(new Uint8Array(zip))
135
+ expect(files).toEqual([{ path: 'SKILL.md', sha256: sha256Hex(strToU8('hello')), size: 5 }])
136
+ })
137
+
138
+ it('builds fingerprints from valid entries only', () => {
139
+ const fingerprint = buildSkillFingerprint([
140
+ { path: '', sha256: '' },
141
+ { path: 'valid.txt', sha256: sha256Hex(strToU8('ok')) },
142
+ ])
143
+ const expected = buildSkillFingerprint([
144
+ { path: 'valid.txt', sha256: sha256Hex(strToU8('ok')) },
145
+ ])
146
+ expect(fingerprint).toBe(expected)
147
+ })
148
+
149
+ it('returns null for invalid skill origin metadata', async () => {
150
+ const workdir = await mkdtemp(join(tmpdir(), 'pilothub-origin-'))
151
+ expect(await readSkillOrigin(workdir)).toBeNull()
152
+
153
+ await mkdir(join(workdir, '.pilothub'), { recursive: true })
154
+ await writeFile(
155
+ join(workdir, '.pilothub', 'origin.json'),
156
+ JSON.stringify({ version: 2 }),
157
+ 'utf8',
158
+ )
159
+ expect(await readSkillOrigin(workdir)).toBeNull()
160
+
161
+ await writeFile(
162
+ join(workdir, '.pilothub', 'origin.json'),
163
+ JSON.stringify({ version: 1, registry: 'demo', slug: 'x', installedAt: 1 }),
164
+ 'utf8',
165
+ )
166
+ expect(await readSkillOrigin(workdir)).toBeNull()
167
+
168
+ await writeFile(
169
+ join(workdir, '.pilothub', 'origin.json'),
170
+ JSON.stringify({
171
+ version: 1,
172
+ registry: 'demo',
173
+ slug: 'x',
174
+ installedVersion: '0.1.0',
175
+ installedAt: 'nope',
176
+ }),
177
+ 'utf8',
178
+ )
179
+ expect(await readSkillOrigin(workdir)).toBeNull()
180
+
181
+ const origin: SkillOrigin = {
182
+ version: 1,
183
+ registry: 'https://example.com',
184
+ slug: 'demo',
185
+ installedVersion: '1.2.3',
186
+ installedAt: 123,
187
+ }
188
+ await writeSkillOrigin(workdir, origin)
189
+ expect(await readSkillOrigin(workdir)).toEqual(origin)
190
+ })
191
+ })
@@ -0,0 +1,172 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
3
+ import { dirname, join, relative, resolve, sep } from 'node:path'
4
+ import { unzipSync } from 'fflate'
5
+ import ignore from 'ignore'
6
+ import mime from 'mime'
7
+ import { type Lockfile, LockfileSchema, parseArk, TEXT_FILE_EXTENSION_SET } from './schema/index.js'
8
+
9
+ export type SkillOrigin = {
10
+ version: 1
11
+ registry: string
12
+ slug: string
13
+ installedVersion: string
14
+ installedAt: number
15
+ }
16
+
17
+ export async function extractZipToDir(zipBytes: Uint8Array, targetDir: string) {
18
+ const entries = unzipSync(zipBytes)
19
+ await mkdir(targetDir, { recursive: true })
20
+ for (const [rawPath, data] of Object.entries(entries)) {
21
+ const safePath = sanitizeRelPath(rawPath)
22
+ if (!safePath) continue
23
+ const outPath = join(targetDir, safePath)
24
+ await mkdir(dirname(outPath), { recursive: true })
25
+ await writeFile(outPath, data)
26
+ }
27
+ }
28
+
29
+ export async function listTextFiles(root: string) {
30
+ const files: Array<{ relPath: string; bytes: Uint8Array; contentType?: string }> = []
31
+ const absRoot = resolve(root)
32
+ const ig = ignore()
33
+ ig.add(['.git/', 'node_modules/', '.pilothub/'])
34
+ await addIgnoreFile(ig, join(absRoot, '.gitignore'))
35
+ await addIgnoreFile(ig, join(absRoot, '.pilothubignore'))
36
+
37
+ await walk(absRoot, async (absPath) => {
38
+ const relPath = normalizePath(relative(absRoot, absPath))
39
+ if (!relPath) return
40
+ if (ig.ignores(relPath)) return
41
+ const ext = relPath.split('.').at(-1)?.toLowerCase() ?? ''
42
+ if (!ext || !TEXT_FILE_EXTENSION_SET.has(ext)) return
43
+ const buffer = await readFile(absPath)
44
+ const contentType = mime.getType(relPath) ?? 'text/plain'
45
+ files.push({ relPath, bytes: new Uint8Array(buffer), contentType })
46
+ })
47
+ return files
48
+ }
49
+
50
+ export type SkillFileHash = { path: string; sha256: string; size: number }
51
+
52
+ export function sha256Hex(bytes: Uint8Array) {
53
+ return createHash('sha256').update(bytes).digest('hex')
54
+ }
55
+
56
+ export function buildSkillFingerprint(files: Array<{ path: string; sha256: string }>) {
57
+ const normalized = files
58
+ .filter((file) => Boolean(file.path) && Boolean(file.sha256))
59
+ .map((file) => ({ path: file.path, sha256: file.sha256 }))
60
+ .sort((a, b) => a.path.localeCompare(b.path))
61
+ const payload = normalized.map((file) => `${file.path}:${file.sha256}`).join('\n')
62
+ return createHash('sha256').update(payload).digest('hex')
63
+ }
64
+
65
+ export function hashSkillFiles(files: Array<{ relPath: string; bytes: Uint8Array }>) {
66
+ const hashed = files.map((file) => ({
67
+ path: file.relPath,
68
+ sha256: sha256Hex(file.bytes),
69
+ size: file.bytes.byteLength,
70
+ }))
71
+ return { files: hashed, fingerprint: buildSkillFingerprint(hashed) }
72
+ }
73
+
74
+ export function hashSkillZip(zipBytes: Uint8Array) {
75
+ const entries = unzipSync(zipBytes)
76
+ const hashed = Object.entries(entries)
77
+ .map(([rawPath, bytes]) => {
78
+ const safePath = sanitizeZipPath(rawPath)
79
+ if (!safePath) return null
80
+ const ext = safePath.split('.').at(-1)?.toLowerCase() ?? ''
81
+ if (!ext || !TEXT_FILE_EXTENSION_SET.has(ext)) return null
82
+ return { path: safePath, sha256: sha256Hex(bytes), size: bytes.byteLength }
83
+ })
84
+ .filter(Boolean) as SkillFileHash[]
85
+
86
+ return { files: hashed, fingerprint: buildSkillFingerprint(hashed) }
87
+ }
88
+
89
+ export async function readLockfile(workdir: string): Promise<Lockfile> {
90
+ const path = join(workdir, '.pilothub', 'lock.json')
91
+ try {
92
+ const raw = await readFile(path, 'utf8')
93
+ const parsed = JSON.parse(raw) as unknown
94
+ return parseArk(LockfileSchema, parsed, 'Lockfile')
95
+ } catch {
96
+ return { version: 1, skills: {} }
97
+ }
98
+ }
99
+
100
+ export async function writeLockfile(workdir: string, lock: Lockfile) {
101
+ const path = join(workdir, '.pilothub', 'lock.json')
102
+ await mkdir(dirname(path), { recursive: true })
103
+ await writeFile(path, `${JSON.stringify(lock, null, 2)}\n`, 'utf8')
104
+ }
105
+
106
+ export async function readSkillOrigin(skillFolder: string): Promise<SkillOrigin | null> {
107
+ const path = join(skillFolder, '.pilothub', 'origin.json')
108
+ try {
109
+ const raw = await readFile(path, 'utf8')
110
+ const parsed = JSON.parse(raw) as Partial<SkillOrigin>
111
+ if (parsed.version !== 1) return null
112
+ if (!parsed.registry || !parsed.slug || !parsed.installedVersion) return null
113
+ if (typeof parsed.installedAt !== 'number' || !Number.isFinite(parsed.installedAt)) return null
114
+ return {
115
+ version: 1,
116
+ registry: String(parsed.registry),
117
+ slug: String(parsed.slug),
118
+ installedVersion: String(parsed.installedVersion),
119
+ installedAt: parsed.installedAt,
120
+ }
121
+ } catch {
122
+ return null
123
+ }
124
+ }
125
+
126
+ export async function writeSkillOrigin(skillFolder: string, origin: SkillOrigin) {
127
+ const path = join(skillFolder, '.pilothub', 'origin.json')
128
+ await mkdir(dirname(path), { recursive: true })
129
+ await writeFile(path, `${JSON.stringify(origin, null, 2)}\n`, 'utf8')
130
+ }
131
+
132
+ function normalizePath(path: string) {
133
+ return path
134
+ .split(sep)
135
+ .join('/')
136
+ .replace(/^\.\/+/, '')
137
+ }
138
+
139
+ function sanitizeRelPath(path: string) {
140
+ const normalized = path.replace(/^\.\/+/, '').replace(/^\/+/, '')
141
+ if (!normalized || normalized.endsWith('/')) return null
142
+ if (normalized.includes('..') || normalized.includes('\\')) return null
143
+ return normalized
144
+ }
145
+
146
+ function sanitizeZipPath(path: string) {
147
+ return sanitizeRelPath(path)
148
+ }
149
+
150
+ async function walk(dir: string, onFile: (path: string) => Promise<void>) {
151
+ const entries = await readdir(dir, { withFileTypes: true })
152
+ for (const entry of entries) {
153
+ if (entry.name.startsWith('.')) continue
154
+ if (entry.name === 'node_modules') continue
155
+ const full = join(dir, entry.name)
156
+ if (entry.isDirectory()) {
157
+ await walk(full, onFile)
158
+ continue
159
+ }
160
+ if (!entry.isFile()) continue
161
+ await onFile(full)
162
+ }
163
+ }
164
+
165
+ async function addIgnoreFile(ig: ReturnType<typeof ignore>, path: string) {
166
+ try {
167
+ const raw = await readFile(path, 'utf8')
168
+ ig.add(raw.split(/\r?\n/))
169
+ } catch {
170
+ // optional
171
+ }
172
+ }
@@ -0,0 +1,10 @@
1
+ export type Lockfile = {
2
+ version: 1
3
+ skills: Record<
4
+ string,
5
+ {
6
+ version: string | null
7
+ installedAt: number
8
+ }
9
+ >
10
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "Bundler",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "declaration": true,
10
+ "sourceMap": true,
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src/**/*.ts"]
14
+ }
@@ -0,0 +1,3 @@
1
+ # pilothub-schema
2
+
3
+ Shared runtime schemas (ArkType) for PilotHub.
@@ -0,0 +1,4 @@
1
+ import { ArkErrors } from 'arktype';
2
+ export type ArkValidator<T> = (data: unknown) => T | ArkErrors;
3
+ export declare function parseArk<T>(schema: ArkValidator<T>, data: unknown, label: string): T;
4
+ export declare function formatArkErrors(errors: ArkErrors): string;
@@ -0,0 +1,26 @@
1
+ import { ArkErrors } from 'arktype';
2
+ export function parseArk(schema, data, label) {
3
+ const result = schema(data);
4
+ if (result instanceof ArkErrors) {
5
+ throw new Error(`${label}: ${formatArkErrors(result)}`);
6
+ }
7
+ return result;
8
+ }
9
+ export function formatArkErrors(errors) {
10
+ const parts = [];
11
+ for (const error of errors) {
12
+ if (parts.length >= 3)
13
+ break;
14
+ const path = Array.isArray(error.path) ? error.path.join('.') : '';
15
+ const location = path ? `${path}: ` : '';
16
+ const description = typeof error.description === 'string'
17
+ ? error.description
18
+ : 'invalid value';
19
+ parts.push(`${location}${description}`);
20
+ }
21
+ if (errors.count > parts.length) {
22
+ parts.push(`+${errors.count - parts.length} more`);
23
+ }
24
+ return parts.join('; ');
25
+ }
26
+ //# sourceMappingURL=ark.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ark.js","sourceRoot":"","sources":["../src/ark.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAInC,MAAM,UAAU,QAAQ,CAAI,MAAuB,EAAE,IAAa,EAAE,KAAa;IAC/E,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,CAAA;IAC3B,IAAI,MAAM,YAAY,SAAS,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,KAAK,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IACzD,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAiB;IAC/C,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC;YAAE,MAAK;QAC5B,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QAClE,MAAM,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;QACxC,MAAM,WAAW,GACf,OAAQ,KAAmC,CAAC,WAAW,KAAK,QAAQ;YAClE,CAAC,CAAG,KAAiC,CAAC,WAAsB;YAC5D,CAAC,CAAC,eAAe,CAAA;QACrB,KAAK,CAAC,IAAI,CAAC,GAAG,QAAQ,GAAG,WAAW,EAAE,CAAC,CAAA;IACzC,CAAC;IACD,IAAI,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QAChC,KAAK,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,MAAM,OAAO,CAAC,CAAA;IACpD,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC"}
@@ -0,0 +1,5 @@
1
+ export type { ArkValidator } from './ark.js';
2
+ export { formatArkErrors, parseArk } from './ark.js';
3
+ export { ApiRoutes, LegacyApiRoutes } from './routes.js';
4
+ export * from './schemas.js';
5
+ export * from './textFiles.js';
@@ -0,0 +1,5 @@
1
+ export { formatArkErrors, parseArk } from './ark.js';
2
+ export { ApiRoutes, LegacyApiRoutes } from './routes.js';
3
+ export * from './schemas.js';
4
+ export * from './textFiles.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AACpD,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AACxD,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA"}
@@ -0,0 +1,21 @@
1
+ export declare const LegacyApiRoutes: {
2
+ readonly download: "/api/download";
3
+ readonly search: "/api/search";
4
+ readonly skill: "/api/skill";
5
+ readonly skillResolve: "/api/skill/resolve";
6
+ readonly cliWhoami: "/api/cli/whoami";
7
+ readonly cliUploadUrl: "/api/cli/upload-url";
8
+ readonly cliPublish: "/api/cli/publish";
9
+ readonly cliTelemetrySync: "/api/cli/telemetry/sync";
10
+ readonly cliSkillDelete: "/api/cli/skill/delete";
11
+ readonly cliSkillUndelete: "/api/cli/skill/undelete";
12
+ };
13
+ export declare const ApiRoutes: {
14
+ readonly search: "/api/v1/search";
15
+ readonly resolve: "/api/v1/resolve";
16
+ readonly download: "/api/v1/download";
17
+ readonly skills: "/api/v1/skills";
18
+ readonly stars: "/api/v1/stars";
19
+ readonly souls: "/api/v1/souls";
20
+ readonly whoami: "/api/v1/whoami";
21
+ };
@@ -0,0 +1,22 @@
1
+ export const LegacyApiRoutes = {
2
+ download: '/api/download',
3
+ search: '/api/search',
4
+ skill: '/api/skill',
5
+ skillResolve: '/api/skill/resolve',
6
+ cliWhoami: '/api/cli/whoami',
7
+ cliUploadUrl: '/api/cli/upload-url',
8
+ cliPublish: '/api/cli/publish',
9
+ cliTelemetrySync: '/api/cli/telemetry/sync',
10
+ cliSkillDelete: '/api/cli/skill/delete',
11
+ cliSkillUndelete: '/api/cli/skill/undelete',
12
+ };
13
+ export const ApiRoutes = {
14
+ search: '/api/v1/search',
15
+ resolve: '/api/v1/resolve',
16
+ download: '/api/v1/download',
17
+ skills: '/api/v1/skills',
18
+ stars: '/api/v1/stars',
19
+ souls: '/api/v1/souls',
20
+ whoami: '/api/v1/whoami',
21
+ };
22
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,QAAQ,EAAE,eAAe;IACzB,MAAM,EAAE,aAAa;IACrB,KAAK,EAAE,YAAY;IACnB,YAAY,EAAE,oBAAoB;IAClC,SAAS,EAAE,iBAAiB;IAC5B,YAAY,EAAE,qBAAqB;IACnC,UAAU,EAAE,kBAAkB;IAC9B,gBAAgB,EAAE,yBAAyB;IAC3C,cAAc,EAAE,uBAAuB;IACvC,gBAAgB,EAAE,yBAAyB;CACnC,CAAA;AAEV,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,MAAM,EAAE,gBAAgB;IACxB,OAAO,EAAE,iBAAiB;IAC1B,QAAQ,EAAE,kBAAkB;IAC5B,MAAM,EAAE,gBAAgB;IACxB,KAAK,EAAE,eAAe;IACtB,KAAK,EAAE,eAAe;IACtB,MAAM,EAAE,gBAAgB;CAChB,CAAA"}