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,170 @@
1
+ import { v } from 'convex/values'
2
+ import { internal } from './_generated/api'
3
+ import type { Doc, Id } from './_generated/dataModel'
4
+ import { action, internalMutation, internalQuery } from './_generated/server'
5
+ import { assertRole, requireUserFromAction } from './lib/access'
6
+
7
+ const DEFAULT_BATCH_SIZE = 50
8
+ const MAX_BATCH_SIZE = 200
9
+ const SYNC_STATE_KEY = 'souls'
10
+
11
+ type BackupPageItem =
12
+ | {
13
+ kind: 'ok'
14
+ soulId: Id<'souls'>
15
+ versionId: Id<'soulVersions'>
16
+ slug: string
17
+ displayName: string
18
+ version: string
19
+ ownerHandle: string
20
+ files: Doc<'soulVersions'>['files']
21
+ publishedAt: number
22
+ }
23
+ | { kind: 'missingLatestVersion'; soulId: Id<'souls'> }
24
+ | { kind: 'missingVersionDoc'; soulId: Id<'souls'>; versionId: Id<'soulVersions'> }
25
+ | { kind: 'missingOwner'; soulId: Id<'souls'>; ownerUserId: Id<'users'> }
26
+
27
+ type BackupPageResult = {
28
+ items: BackupPageItem[]
29
+ cursor: string | null
30
+ isDone: boolean
31
+ }
32
+
33
+ type BackupSyncState = {
34
+ cursor: string | null
35
+ }
36
+
37
+ export type SyncGitHubSoulBackupsResult = {
38
+ stats: {
39
+ soulsScanned: number
40
+ soulsSkipped: number
41
+ soulsBackedUp: number
42
+ soulsMissingVersion: number
43
+ soulsMissingOwner: number
44
+ errors: number
45
+ }
46
+ cursor: string | null
47
+ isDone: boolean
48
+ }
49
+
50
+ export const getGitHubSoulBackupPageInternal = internalQuery({
51
+ args: {
52
+ cursor: v.optional(v.string()),
53
+ batchSize: v.optional(v.number()),
54
+ },
55
+ handler: async (ctx, args): Promise<BackupPageResult> => {
56
+ const batchSize = clampInt(args.batchSize ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE)
57
+ const { page, isDone, continueCursor } = await ctx.db
58
+ .query('souls')
59
+ .order('asc')
60
+ .paginate({ cursor: args.cursor ?? null, numItems: batchSize })
61
+
62
+ const items: BackupPageItem[] = []
63
+ for (const soul of page) {
64
+ if (soul.softDeletedAt) continue
65
+ if (!soul.latestVersionId) {
66
+ items.push({ kind: 'missingLatestVersion', soulId: soul._id })
67
+ continue
68
+ }
69
+
70
+ const version = await ctx.db.get(soul.latestVersionId)
71
+ if (!version) {
72
+ items.push({
73
+ kind: 'missingVersionDoc',
74
+ soulId: soul._id,
75
+ versionId: soul.latestVersionId,
76
+ })
77
+ continue
78
+ }
79
+
80
+ const owner = await ctx.db.get(soul.ownerUserId)
81
+ if (!owner || owner.deletedAt) {
82
+ items.push({ kind: 'missingOwner', soulId: soul._id, ownerUserId: soul.ownerUserId })
83
+ continue
84
+ }
85
+
86
+ items.push({
87
+ kind: 'ok',
88
+ soulId: soul._id,
89
+ versionId: version._id,
90
+ slug: soul.slug,
91
+ displayName: soul.displayName,
92
+ version: version.version,
93
+ ownerHandle: owner.handle ?? owner._id,
94
+ files: version.files,
95
+ publishedAt: version.createdAt,
96
+ })
97
+ }
98
+
99
+ return { items, cursor: continueCursor, isDone }
100
+ },
101
+ })
102
+
103
+ export const getGitHubSoulBackupSyncStateInternal = internalQuery({
104
+ args: {},
105
+ handler: async (ctx): Promise<BackupSyncState> => {
106
+ const state = await ctx.db
107
+ .query('githubBackupSyncState')
108
+ .withIndex('by_key', (q) => q.eq('key', SYNC_STATE_KEY))
109
+ .unique()
110
+ return { cursor: state?.cursor ?? null }
111
+ },
112
+ })
113
+
114
+ export const setGitHubSoulBackupSyncStateInternal = internalMutation({
115
+ args: {
116
+ cursor: v.optional(v.string()),
117
+ },
118
+ handler: async (ctx, args) => {
119
+ const now = Date.now()
120
+ const state = await ctx.db
121
+ .query('githubBackupSyncState')
122
+ .withIndex('by_key', (q) => q.eq('key', SYNC_STATE_KEY))
123
+ .unique()
124
+
125
+ if (!state) {
126
+ await ctx.db.insert('githubBackupSyncState', {
127
+ key: SYNC_STATE_KEY,
128
+ cursor: args.cursor,
129
+ updatedAt: now,
130
+ })
131
+ return { ok: true as const }
132
+ }
133
+
134
+ await ctx.db.patch(state._id, {
135
+ cursor: args.cursor,
136
+ updatedAt: now,
137
+ })
138
+
139
+ return { ok: true as const }
140
+ },
141
+ })
142
+
143
+ export const syncGitHubSoulBackups: ReturnType<typeof action> = action({
144
+ args: {
145
+ dryRun: v.optional(v.boolean()),
146
+ batchSize: v.optional(v.number()),
147
+ maxBatches: v.optional(v.number()),
148
+ resetCursor: v.optional(v.boolean()),
149
+ },
150
+ handler: async (ctx, args): Promise<SyncGitHubSoulBackupsResult> => {
151
+ const { user } = await requireUserFromAction(ctx)
152
+ assertRole(user, ['admin'])
153
+
154
+ if (args.resetCursor && !args.dryRun) {
155
+ await ctx.runMutation(internal.githubSoulBackups.setGitHubSoulBackupSyncStateInternal, {
156
+ cursor: undefined,
157
+ })
158
+ }
159
+
160
+ return ctx.runAction(internal.githubSoulBackupsNode.syncGitHubSoulBackupsInternal, {
161
+ dryRun: args.dryRun,
162
+ batchSize: args.batchSize,
163
+ maxBatches: args.maxBatches,
164
+ }) as Promise<SyncGitHubSoulBackupsResult>
165
+ },
166
+ })
167
+
168
+ function clampInt(value: number, min: number, max: number) {
169
+ return Math.max(min, Math.min(max, Math.floor(value)))
170
+ }
@@ -0,0 +1,186 @@
1
+ 'use node'
2
+
3
+ import { v } from 'convex/values'
4
+ import { internal } from './_generated/api'
5
+ import type { Doc } from './_generated/dataModel'
6
+ import type { ActionCtx } from './_generated/server'
7
+ import { internalAction } from './_generated/server'
8
+ import {
9
+ backupSoulToGitHub,
10
+ fetchGitHubSoulMeta,
11
+ getGitHubSoulBackupContext,
12
+ isGitHubSoulBackupConfigured,
13
+ } from './lib/githubSoulBackup'
14
+
15
+ const DEFAULT_BATCH_SIZE = 50
16
+ const MAX_BATCH_SIZE = 200
17
+ const DEFAULT_MAX_BATCHES = 5
18
+ const MAX_MAX_BATCHES = 200
19
+
20
+ type BackupPageItem =
21
+ | {
22
+ kind: 'ok'
23
+ slug: string
24
+ version: string
25
+ displayName: string
26
+ ownerHandle: string
27
+ files: Doc<'soulVersions'>['files']
28
+ publishedAt: number
29
+ }
30
+ | { kind: 'missingLatestVersion' }
31
+ | { kind: 'missingVersionDoc' }
32
+ | { kind: 'missingOwner' }
33
+
34
+ export type GitHubSoulBackupSyncStats = {
35
+ soulsScanned: number
36
+ soulsSkipped: number
37
+ soulsBackedUp: number
38
+ soulsMissingVersion: number
39
+ soulsMissingOwner: number
40
+ errors: number
41
+ }
42
+
43
+ export type SyncGitHubSoulBackupsInternalArgs = {
44
+ dryRun?: boolean
45
+ batchSize?: number
46
+ maxBatches?: number
47
+ }
48
+
49
+ export type SyncGitHubSoulBackupsInternalResult = {
50
+ stats: GitHubSoulBackupSyncStats
51
+ cursor: string | null
52
+ isDone: boolean
53
+ }
54
+
55
+ export const backupSoulForPublishInternal = internalAction({
56
+ args: {
57
+ slug: v.string(),
58
+ version: v.string(),
59
+ displayName: v.string(),
60
+ ownerHandle: v.string(),
61
+ files: v.array(
62
+ v.object({
63
+ path: v.string(),
64
+ size: v.number(),
65
+ storageId: v.id('_storage'),
66
+ sha256: v.string(),
67
+ contentType: v.optional(v.string()),
68
+ }),
69
+ ),
70
+ publishedAt: v.number(),
71
+ },
72
+ handler: async (ctx, args) => {
73
+ if (!isGitHubSoulBackupConfigured()) {
74
+ return { skipped: true as const }
75
+ }
76
+ await backupSoulToGitHub(ctx, args)
77
+ return { skipped: false as const }
78
+ },
79
+ })
80
+
81
+ export async function syncGitHubSoulBackupsInternalHandler(
82
+ ctx: ActionCtx,
83
+ args: SyncGitHubSoulBackupsInternalArgs,
84
+ ): Promise<SyncGitHubSoulBackupsInternalResult> {
85
+ const dryRun = Boolean(args.dryRun)
86
+ const stats: GitHubSoulBackupSyncStats = {
87
+ soulsScanned: 0,
88
+ soulsSkipped: 0,
89
+ soulsBackedUp: 0,
90
+ soulsMissingVersion: 0,
91
+ soulsMissingOwner: 0,
92
+ errors: 0,
93
+ }
94
+
95
+ if (!isGitHubSoulBackupConfigured()) {
96
+ return { stats, cursor: null, isDone: true }
97
+ }
98
+
99
+ const batchSize = clampInt(args.batchSize ?? DEFAULT_BATCH_SIZE, 1, MAX_BATCH_SIZE)
100
+ const maxBatches = clampInt(args.maxBatches ?? DEFAULT_MAX_BATCHES, 1, MAX_MAX_BATCHES)
101
+ const context = await getGitHubSoulBackupContext()
102
+
103
+ const state = dryRun
104
+ ? { cursor: null as string | null }
105
+ : ((await ctx.runQuery(
106
+ internal.githubSoulBackups.getGitHubSoulBackupSyncStateInternal,
107
+ {},
108
+ )) as {
109
+ cursor: string | null
110
+ })
111
+
112
+ let cursor: string | null = state.cursor
113
+ let isDone = false
114
+
115
+ for (let batch = 0; batch < maxBatches; batch++) {
116
+ const page = (await ctx.runQuery(internal.githubSoulBackups.getGitHubSoulBackupPageInternal, {
117
+ cursor: cursor ?? undefined,
118
+ batchSize,
119
+ })) as { items: BackupPageItem[]; cursor: string | null; isDone: boolean }
120
+
121
+ cursor = page.cursor
122
+ isDone = page.isDone
123
+
124
+ for (const item of page.items) {
125
+ if (item.kind !== 'ok') {
126
+ if (item.kind === 'missingLatestVersion' || item.kind === 'missingVersionDoc') {
127
+ stats.soulsMissingVersion += 1
128
+ } else if (item.kind === 'missingOwner') {
129
+ stats.soulsMissingOwner += 1
130
+ }
131
+ continue
132
+ }
133
+
134
+ stats.soulsScanned += 1
135
+ try {
136
+ const meta = await fetchGitHubSoulMeta(context, item.ownerHandle, item.slug)
137
+ if (meta?.latest?.version === item.version) {
138
+ stats.soulsSkipped += 1
139
+ continue
140
+ }
141
+
142
+ if (!dryRun) {
143
+ await backupSoulToGitHub(
144
+ ctx,
145
+ {
146
+ slug: item.slug,
147
+ version: item.version,
148
+ displayName: item.displayName,
149
+ ownerHandle: item.ownerHandle,
150
+ files: item.files,
151
+ publishedAt: item.publishedAt,
152
+ },
153
+ context,
154
+ )
155
+ stats.soulsBackedUp += 1
156
+ }
157
+ } catch (error) {
158
+ console.error('GitHub soul backup sync failed', error)
159
+ stats.errors += 1
160
+ }
161
+ }
162
+
163
+ if (!dryRun) {
164
+ await ctx.runMutation(internal.githubSoulBackups.setGitHubSoulBackupSyncStateInternal, {
165
+ cursor: isDone ? undefined : (cursor ?? undefined),
166
+ })
167
+ }
168
+
169
+ if (isDone) break
170
+ }
171
+
172
+ return { stats, cursor, isDone }
173
+ }
174
+
175
+ export const syncGitHubSoulBackupsInternal = internalAction({
176
+ args: {
177
+ dryRun: v.optional(v.boolean()),
178
+ batchSize: v.optional(v.number()),
179
+ maxBatches: v.optional(v.number()),
180
+ },
181
+ handler: syncGitHubSoulBackupsInternalHandler,
182
+ })
183
+
184
+ function clampInt(value: number, min: number, max: number) {
185
+ return Math.max(min, Math.min(max, Math.floor(value)))
186
+ }
package/convex/http.ts ADDED
@@ -0,0 +1,194 @@
1
+ import { httpRouter } from 'convex/server'
2
+ import { ApiRoutes, LegacyApiRoutes } from 'pilothub-schema'
3
+ import { auth } from './auth'
4
+ import { downloadZip } from './downloads'
5
+ import {
6
+ cliPublishHttp,
7
+ cliSkillDeleteHttp,
8
+ cliSkillUndeleteHttp,
9
+ cliTelemetrySyncHttp,
10
+ cliUploadUrlHttp,
11
+ cliWhoamiHttp,
12
+ getSkillHttp,
13
+ resolveSkillVersionHttp,
14
+ searchSkillsHttp,
15
+ } from './httpApi'
16
+ import {
17
+ listSkillsV1Http,
18
+ listSoulsV1Http,
19
+ publishSkillV1Http,
20
+ publishSoulV1Http,
21
+ resolveSkillVersionV1Http,
22
+ searchSkillsV1Http,
23
+ skillsDeleteRouterV1Http,
24
+ skillsGetRouterV1Http,
25
+ skillsPostRouterV1Http,
26
+ soulsDeleteRouterV1Http,
27
+ soulsGetRouterV1Http,
28
+ soulsPostRouterV1Http,
29
+ starsDeleteRouterV1Http,
30
+ starsPostRouterV1Http,
31
+ whoamiV1Http,
32
+ } from './httpApiV1'
33
+
34
+ const http = httpRouter()
35
+
36
+ auth.addHttpRoutes(http)
37
+
38
+ http.route({
39
+ path: ApiRoutes.download,
40
+ method: 'GET',
41
+ handler: downloadZip,
42
+ })
43
+
44
+ http.route({
45
+ path: ApiRoutes.search,
46
+ method: 'GET',
47
+ handler: searchSkillsV1Http,
48
+ })
49
+
50
+ http.route({
51
+ path: ApiRoutes.resolve,
52
+ method: 'GET',
53
+ handler: resolveSkillVersionV1Http,
54
+ })
55
+
56
+ http.route({
57
+ path: ApiRoutes.skills,
58
+ method: 'GET',
59
+ handler: listSkillsV1Http,
60
+ })
61
+
62
+ http.route({
63
+ pathPrefix: `${ApiRoutes.skills}/`,
64
+ method: 'GET',
65
+ handler: skillsGetRouterV1Http,
66
+ })
67
+
68
+ http.route({
69
+ path: ApiRoutes.skills,
70
+ method: 'POST',
71
+ handler: publishSkillV1Http,
72
+ })
73
+
74
+ http.route({
75
+ pathPrefix: `${ApiRoutes.skills}/`,
76
+ method: 'POST',
77
+ handler: skillsPostRouterV1Http,
78
+ })
79
+
80
+ http.route({
81
+ pathPrefix: `${ApiRoutes.skills}/`,
82
+ method: 'DELETE',
83
+ handler: skillsDeleteRouterV1Http,
84
+ })
85
+
86
+ http.route({
87
+ pathPrefix: `${ApiRoutes.stars}/`,
88
+ method: 'POST',
89
+ handler: starsPostRouterV1Http,
90
+ })
91
+
92
+ http.route({
93
+ pathPrefix: `${ApiRoutes.stars}/`,
94
+ method: 'DELETE',
95
+ handler: starsDeleteRouterV1Http,
96
+ })
97
+
98
+ http.route({
99
+ path: ApiRoutes.whoami,
100
+ method: 'GET',
101
+ handler: whoamiV1Http,
102
+ })
103
+
104
+ http.route({
105
+ path: ApiRoutes.souls,
106
+ method: 'GET',
107
+ handler: listSoulsV1Http,
108
+ })
109
+
110
+ http.route({
111
+ pathPrefix: `${ApiRoutes.souls}/`,
112
+ method: 'GET',
113
+ handler: soulsGetRouterV1Http,
114
+ })
115
+
116
+ http.route({
117
+ path: ApiRoutes.souls,
118
+ method: 'POST',
119
+ handler: publishSoulV1Http,
120
+ })
121
+
122
+ http.route({
123
+ pathPrefix: `${ApiRoutes.souls}/`,
124
+ method: 'POST',
125
+ handler: soulsPostRouterV1Http,
126
+ })
127
+
128
+ http.route({
129
+ pathPrefix: `${ApiRoutes.souls}/`,
130
+ method: 'DELETE',
131
+ handler: soulsDeleteRouterV1Http,
132
+ })
133
+
134
+ // TODO: remove legacy /api routes after deprecation window.
135
+ http.route({
136
+ path: LegacyApiRoutes.download,
137
+ method: 'GET',
138
+ handler: downloadZip,
139
+ })
140
+ http.route({
141
+ path: LegacyApiRoutes.search,
142
+ method: 'GET',
143
+ handler: searchSkillsHttp,
144
+ })
145
+
146
+ http.route({
147
+ path: LegacyApiRoutes.skill,
148
+ method: 'GET',
149
+ handler: getSkillHttp,
150
+ })
151
+
152
+ http.route({
153
+ path: LegacyApiRoutes.skillResolve,
154
+ method: 'GET',
155
+ handler: resolveSkillVersionHttp,
156
+ })
157
+
158
+ http.route({
159
+ path: LegacyApiRoutes.cliWhoami,
160
+ method: 'GET',
161
+ handler: cliWhoamiHttp,
162
+ })
163
+
164
+ http.route({
165
+ path: LegacyApiRoutes.cliUploadUrl,
166
+ method: 'POST',
167
+ handler: cliUploadUrlHttp,
168
+ })
169
+
170
+ http.route({
171
+ path: LegacyApiRoutes.cliPublish,
172
+ method: 'POST',
173
+ handler: cliPublishHttp,
174
+ })
175
+
176
+ http.route({
177
+ path: LegacyApiRoutes.cliTelemetrySync,
178
+ method: 'POST',
179
+ handler: cliTelemetrySyncHttp,
180
+ })
181
+
182
+ http.route({
183
+ path: LegacyApiRoutes.cliSkillDelete,
184
+ method: 'POST',
185
+ handler: cliSkillDeleteHttp,
186
+ })
187
+
188
+ http.route({
189
+ path: LegacyApiRoutes.cliSkillUndelete,
190
+ method: 'POST',
191
+ handler: cliSkillUndeleteHttp,
192
+ })
193
+
194
+ export default http