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.
Files changed (388) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +36 -129
  3. package/dist/browserAuth.d.ts +20 -0
  4. package/dist/browserAuth.js +156 -0
  5. package/dist/browserAuth.js.map +1 -0
  6. package/dist/browserAuth.test.d.ts +1 -0
  7. package/dist/browserAuth.test.js +83 -0
  8. package/dist/browserAuth.test.js.map +1 -0
  9. package/dist/cli/buildInfo.d.ts +3 -0
  10. package/dist/cli/buildInfo.js +103 -0
  11. package/dist/cli/buildInfo.js.map +1 -0
  12. package/dist/cli/commands/auth.d.ts +9 -0
  13. package/dist/cli/commands/auth.js +75 -0
  14. package/dist/cli/commands/auth.js.map +1 -0
  15. package/dist/cli/commands/delete.d.ts +11 -0
  16. package/dist/cli/commands/delete.js +67 -0
  17. package/dist/cli/commands/delete.js.map +1 -0
  18. package/dist/cli/commands/delete.test.d.ts +1 -0
  19. package/dist/cli/commands/delete.test.js +52 -0
  20. package/dist/cli/commands/delete.test.js.map +1 -0
  21. package/dist/cli/commands/publish.d.ts +9 -0
  22. package/dist/cli/commands/publish.js +87 -0
  23. package/dist/cli/commands/publish.js.map +1 -0
  24. package/dist/cli/commands/publish.test.d.ts +1 -0
  25. package/dist/cli/commands/publish.test.js +104 -0
  26. package/dist/cli/commands/publish.test.js.map +1 -0
  27. package/dist/cli/commands/skills.d.ts +23 -0
  28. package/dist/cli/commands/skills.js +298 -0
  29. package/dist/cli/commands/skills.js.map +1 -0
  30. package/dist/cli/commands/skills.test.d.ts +1 -0
  31. package/dist/cli/commands/skills.test.js +156 -0
  32. package/dist/cli/commands/skills.test.js.map +1 -0
  33. package/dist/cli/commands/star.d.ts +8 -0
  34. package/dist/cli/commands/star.js +38 -0
  35. package/dist/cli/commands/star.js.map +1 -0
  36. package/dist/cli/commands/sync.d.ts +3 -0
  37. package/dist/cli/commands/sync.js +160 -0
  38. package/dist/cli/commands/sync.js.map +1 -0
  39. package/dist/cli/commands/sync.test.d.ts +1 -0
  40. package/dist/cli/commands/sync.test.js +277 -0
  41. package/dist/cli/commands/sync.test.js.map +1 -0
  42. package/dist/cli/commands/syncHelpers.d.ts +76 -0
  43. package/dist/cli/commands/syncHelpers.js +349 -0
  44. package/dist/cli/commands/syncHelpers.js.map +1 -0
  45. package/dist/cli/commands/syncHelpers.test.d.ts +1 -0
  46. package/dist/cli/commands/syncHelpers.test.js +22 -0
  47. package/dist/cli/commands/syncHelpers.test.js.map +1 -0
  48. package/dist/cli/commands/syncTypes.d.ts +24 -0
  49. package/dist/cli/commands/syncTypes.js +2 -0
  50. package/dist/cli/commands/syncTypes.js.map +1 -0
  51. package/dist/cli/commands/unstar.d.ts +8 -0
  52. package/dist/cli/commands/unstar.js +38 -0
  53. package/dist/cli/commands/unstar.js.map +1 -0
  54. package/dist/cli/helpStyle.d.ts +13 -0
  55. package/dist/cli/helpStyle.js +38 -0
  56. package/dist/cli/helpStyle.js.map +1 -0
  57. package/dist/cli/pilotbotConfig.d.ts +6 -0
  58. package/dist/cli/pilotbotConfig.js +110 -0
  59. package/dist/cli/pilotbotConfig.js.map +1 -0
  60. package/dist/cli/pilotbotConfig.test.d.ts +1 -0
  61. package/dist/cli/pilotbotConfig.test.js +133 -0
  62. package/dist/cli/pilotbotConfig.test.js.map +1 -0
  63. package/dist/cli/registry.d.ts +7 -0
  64. package/dist/cli/registry.js +42 -0
  65. package/dist/cli/registry.js.map +1 -0
  66. package/dist/cli/registry.test.d.ts +1 -0
  67. package/dist/cli/registry.test.js +48 -0
  68. package/dist/cli/registry.test.js.map +1 -0
  69. package/dist/cli/scanSkills.d.ts +7 -0
  70. package/dist/cli/scanSkills.js +75 -0
  71. package/dist/cli/scanSkills.js.map +1 -0
  72. package/dist/cli/scanSkills.test.d.ts +1 -0
  73. package/dist/cli/scanSkills.test.js +60 -0
  74. package/dist/cli/scanSkills.test.js.map +1 -0
  75. package/dist/cli/slug.d.ts +2 -0
  76. package/dist/cli/slug.js +16 -0
  77. package/dist/cli/slug.js.map +1 -0
  78. package/dist/cli/types.d.ts +15 -0
  79. package/dist/cli/types.js +2 -0
  80. package/dist/cli/types.js.map +1 -0
  81. package/dist/cli/ui.d.ts +7 -0
  82. package/dist/cli/ui.js +72 -0
  83. package/dist/cli/ui.js.map +1 -0
  84. package/dist/cli.d.ts +2 -0
  85. package/dist/cli.js +268 -0
  86. package/dist/cli.js.map +1 -0
  87. package/dist/config.d.ts +4 -0
  88. package/dist/config.js +38 -0
  89. package/dist/config.js.map +1 -0
  90. package/dist/discovery.d.ts +5 -0
  91. package/dist/discovery.js +21 -0
  92. package/dist/discovery.js.map +1 -0
  93. package/dist/discovery.test.d.ts +1 -0
  94. package/dist/discovery.test.js +46 -0
  95. package/dist/discovery.test.js.map +1 -0
  96. package/dist/http.d.ts +32 -0
  97. package/dist/http.js +261 -0
  98. package/dist/http.js.map +1 -0
  99. package/dist/http.test.d.ts +1 -0
  100. package/dist/http.test.js +135 -0
  101. package/dist/http.test.js.map +1 -0
  102. package/dist/schema/ark.js.map +1 -0
  103. package/dist/schema/index.js.map +1 -0
  104. package/dist/schema/routes.js.map +1 -0
  105. package/{packages/schema/dist → dist/schema}/schemas.d.ts +0 -39
  106. package/{packages/schema/dist → dist/schema}/schemas.js +0 -22
  107. package/dist/schema/schemas.js.map +1 -0
  108. package/dist/schema/textFiles.js.map +1 -0
  109. package/dist/schema/textFiles.test.d.ts +1 -0
  110. package/dist/schema/textFiles.test.js +20 -0
  111. package/dist/schema/textFiles.test.js.map +1 -0
  112. package/dist/skills.d.ts +43 -0
  113. package/dist/skills.js +163 -0
  114. package/dist/skills.js.map +1 -0
  115. package/dist/skills.test.d.ts +1 -0
  116. package/dist/skills.test.js +144 -0
  117. package/dist/skills.test.js.map +1 -0
  118. package/dist/types.d.ts +7 -0
  119. package/dist/types.js +2 -0
  120. package/dist/types.js.map +1 -0
  121. package/package.json +27 -70
  122. package/.env.local.example +0 -19
  123. package/.github/workflows/ci.yml +0 -40
  124. package/.oxlintrc.json +0 -3
  125. package/AGENTS.md +0 -45
  126. package/CHANGELOG.md +0 -138
  127. package/DEPRECATIONS.md +0 -7
  128. package/biome.json +0 -41
  129. package/convex/_generated/api.d.ts +0 -153
  130. package/convex/_generated/api.js +0 -23
  131. package/convex/_generated/dataModel.d.ts +0 -60
  132. package/convex/_generated/server.d.ts +0 -143
  133. package/convex/_generated/server.js +0 -93
  134. package/convex/auth.config.ts +0 -8
  135. package/convex/auth.ts +0 -19
  136. package/convex/comments.ts +0 -88
  137. package/convex/crons.ts +0 -34
  138. package/convex/devSeed.ts +0 -459
  139. package/convex/devSeedExtra.ts +0 -541
  140. package/convex/downloads.ts +0 -78
  141. package/convex/githubBackups.ts +0 -170
  142. package/convex/githubBackupsNode.ts +0 -183
  143. package/convex/githubImport.ts +0 -317
  144. package/convex/githubSoulBackups.ts +0 -170
  145. package/convex/githubSoulBackupsNode.ts +0 -186
  146. package/convex/http.ts +0 -194
  147. package/convex/httpApi.handlers.test.ts +0 -488
  148. package/convex/httpApi.test.ts +0 -70
  149. package/convex/httpApi.ts +0 -305
  150. package/convex/httpApiV1.handlers.test.ts +0 -584
  151. package/convex/httpApiV1.ts +0 -1172
  152. package/convex/leaderboards.ts +0 -39
  153. package/convex/lib/access.ts +0 -36
  154. package/convex/lib/apiTokenAuth.ts +0 -36
  155. package/convex/lib/badges.ts +0 -50
  156. package/convex/lib/changelog.test.ts +0 -34
  157. package/convex/lib/changelog.ts +0 -278
  158. package/convex/lib/embeddings.ts +0 -38
  159. package/convex/lib/githubBackup.ts +0 -443
  160. package/convex/lib/githubImport.test.ts +0 -247
  161. package/convex/lib/githubImport.ts +0 -425
  162. package/convex/lib/githubSoulBackup.ts +0 -443
  163. package/convex/lib/leaderboards.ts +0 -103
  164. package/convex/lib/moderation.ts +0 -42
  165. package/convex/lib/public.ts +0 -89
  166. package/convex/lib/searchText.test.ts +0 -46
  167. package/convex/lib/searchText.ts +0 -27
  168. package/convex/lib/skillBackfill.test.ts +0 -34
  169. package/convex/lib/skillBackfill.ts +0 -67
  170. package/convex/lib/skillPublish.test.ts +0 -28
  171. package/convex/lib/skillPublish.ts +0 -284
  172. package/convex/lib/skillStats.ts +0 -80
  173. package/convex/lib/skills.test.ts +0 -197
  174. package/convex/lib/skills.ts +0 -273
  175. package/convex/lib/soulChangelog.ts +0 -273
  176. package/convex/lib/soulPublish.ts +0 -236
  177. package/convex/lib/tokens.test.ts +0 -33
  178. package/convex/lib/tokens.ts +0 -51
  179. package/convex/lib/webhooks.test.ts +0 -91
  180. package/convex/lib/webhooks.ts +0 -112
  181. package/convex/maintenance.test.ts +0 -270
  182. package/convex/maintenance.ts +0 -840
  183. package/convex/rateLimits.ts +0 -50
  184. package/convex/schema.ts +0 -472
  185. package/convex/search.test.ts +0 -12
  186. package/convex/search.ts +0 -254
  187. package/convex/seed.test.ts +0 -37
  188. package/convex/seed.ts +0 -254
  189. package/convex/seedSouls.ts +0 -111
  190. package/convex/skillStatEvents.ts +0 -568
  191. package/convex/skills.ts +0 -1606
  192. package/convex/soulComments.ts +0 -88
  193. package/convex/soulDownloads.ts +0 -14
  194. package/convex/soulStars.ts +0 -71
  195. package/convex/souls.ts +0 -570
  196. package/convex/stars.ts +0 -108
  197. package/convex/statsMaintenance.ts +0 -205
  198. package/convex/telemetry.ts +0 -434
  199. package/convex/tokens.ts +0 -88
  200. package/convex/tsconfig.json +0 -7
  201. package/convex/uploads.ts +0 -20
  202. package/convex/users.ts +0 -122
  203. package/convex/webhooks.ts +0 -50
  204. package/convex.json +0 -3
  205. package/docs/README.md +0 -32
  206. package/docs/api.md +0 -51
  207. package/docs/architecture.md +0 -61
  208. package/docs/auth.md +0 -54
  209. package/docs/cli.md +0 -117
  210. package/docs/deploy.md +0 -78
  211. package/docs/diffing.md +0 -84
  212. package/docs/github-import.md +0 -171
  213. package/docs/http-api.md +0 -187
  214. package/docs/manual-testing.md +0 -64
  215. package/docs/mintlify.md +0 -43
  216. package/docs/quickstart.md +0 -120
  217. package/docs/skill-format.md +0 -58
  218. package/docs/soul-format.md +0 -37
  219. package/docs/spec.md +0 -177
  220. package/docs/telemetry.md +0 -91
  221. package/docs/troubleshooting.md +0 -49
  222. package/docs/webhook.md +0 -51
  223. package/e2e/menu-smoke.pw.test.ts +0 -49
  224. package/e2e/pilothub.e2e.test.ts +0 -494
  225. package/e2e/search-exact.pw.test.ts +0 -97
  226. package/packages/pilothub/LICENSE +0 -22
  227. package/packages/pilothub/README.md +0 -57
  228. package/packages/pilothub/package.json +0 -41
  229. package/packages/pilothub/src/browserAuth.test.ts +0 -96
  230. package/packages/pilothub/src/browserAuth.ts +0 -174
  231. package/packages/pilothub/src/cli/buildInfo.ts +0 -94
  232. package/packages/pilothub/src/cli/commands/auth.ts +0 -97
  233. package/packages/pilothub/src/cli/commands/delete.test.ts +0 -73
  234. package/packages/pilothub/src/cli/commands/delete.ts +0 -83
  235. package/packages/pilothub/src/cli/commands/publish.test.ts +0 -122
  236. package/packages/pilothub/src/cli/commands/publish.ts +0 -108
  237. package/packages/pilothub/src/cli/commands/skills.test.ts +0 -191
  238. package/packages/pilothub/src/cli/commands/skills.ts +0 -380
  239. package/packages/pilothub/src/cli/commands/star.ts +0 -46
  240. package/packages/pilothub/src/cli/commands/sync.test.ts +0 -310
  241. package/packages/pilothub/src/cli/commands/sync.ts +0 -200
  242. package/packages/pilothub/src/cli/commands/syncHelpers.test.ts +0 -26
  243. package/packages/pilothub/src/cli/commands/syncHelpers.ts +0 -427
  244. package/packages/pilothub/src/cli/commands/syncTypes.ts +0 -27
  245. package/packages/pilothub/src/cli/commands/unstar.ts +0 -48
  246. package/packages/pilothub/src/cli/helpStyle.ts +0 -45
  247. package/packages/pilothub/src/cli/pilotbotConfig.test.ts +0 -159
  248. package/packages/pilothub/src/cli/pilotbotConfig.ts +0 -147
  249. package/packages/pilothub/src/cli/registry.test.ts +0 -63
  250. package/packages/pilothub/src/cli/registry.ts +0 -43
  251. package/packages/pilothub/src/cli/scanSkills.test.ts +0 -64
  252. package/packages/pilothub/src/cli/scanSkills.ts +0 -84
  253. package/packages/pilothub/src/cli/slug.ts +0 -16
  254. package/packages/pilothub/src/cli/types.ts +0 -12
  255. package/packages/pilothub/src/cli/ui.ts +0 -75
  256. package/packages/pilothub/src/cli.ts +0 -311
  257. package/packages/pilothub/src/config.ts +0 -36
  258. package/packages/pilothub/src/discovery.test.ts +0 -75
  259. package/packages/pilothub/src/discovery.ts +0 -19
  260. package/packages/pilothub/src/http.test.ts +0 -156
  261. package/packages/pilothub/src/http.ts +0 -301
  262. package/packages/pilothub/src/schema/ark.ts +0 -29
  263. package/packages/pilothub/src/schema/index.ts +0 -5
  264. package/packages/pilothub/src/schema/routes.ts +0 -22
  265. package/packages/pilothub/src/schema/schemas.ts +0 -260
  266. package/packages/pilothub/src/schema/textFiles.test.ts +0 -23
  267. package/packages/pilothub/src/schema/textFiles.ts +0 -66
  268. package/packages/pilothub/src/skills.test.ts +0 -191
  269. package/packages/pilothub/src/skills.ts +0 -172
  270. package/packages/pilothub/src/types.ts +0 -10
  271. package/packages/pilothub/tsconfig.json +0 -14
  272. package/packages/schema/README.md +0 -3
  273. package/packages/schema/dist/ark.js.map +0 -1
  274. package/packages/schema/dist/index.js.map +0 -1
  275. package/packages/schema/dist/routes.js.map +0 -1
  276. package/packages/schema/dist/schemas.js.map +0 -1
  277. package/packages/schema/dist/textFiles.js.map +0 -1
  278. package/packages/schema/package.json +0 -26
  279. package/packages/schema/src/ark.ts +0 -29
  280. package/packages/schema/src/index.ts +0 -5
  281. package/packages/schema/src/routes.ts +0 -22
  282. package/packages/schema/src/schemas.test.ts +0 -123
  283. package/packages/schema/src/schemas.ts +0 -287
  284. package/packages/schema/src/textFiles.test.ts +0 -23
  285. package/packages/schema/src/textFiles.ts +0 -66
  286. package/packages/schema/tsconfig.json +0 -15
  287. package/pilothub +0 -46
  288. package/playwright.config.ts +0 -33
  289. package/public/.well-known/pilothub.json +0 -6
  290. package/public/api/v1/openapi.json +0 -379
  291. package/public/favicon.ico +0 -0
  292. package/public/logo192.png +0 -0
  293. package/public/logo512.png +0 -0
  294. package/public/manifest.json +0 -25
  295. package/public/og.png +0 -0
  296. package/public/og.svg +0 -98
  297. package/public/pilot-logo.png +0 -0
  298. package/public/pilot-mark.png +0 -0
  299. package/public/robots.txt +0 -3
  300. package/public/tanstack-circle-logo.png +0 -0
  301. package/public/tanstack-word-logo-white.svg +0 -1
  302. package/scripts/check-peer-deps.ts +0 -56
  303. package/scripts/docs-list.ts +0 -148
  304. package/scripts/run-playwright-local.sh +0 -14
  305. package/server/og/fetchSkillOgMeta.ts +0 -27
  306. package/server/og/fetchSoulOgMeta.ts +0 -27
  307. package/server/og/ogAssets.ts +0 -80
  308. package/server/og/skillOgSvg.test.ts +0 -59
  309. package/server/og/skillOgSvg.ts +0 -258
  310. package/server/og/soulOgSvg.ts +0 -209
  311. package/server/routes/og/skill.png.ts +0 -103
  312. package/server/routes/og/soul.png.ts +0 -111
  313. package/src/__tests__/skill-detail-page.test.tsx +0 -86
  314. package/src/__tests__/skills-index.test.tsx +0 -145
  315. package/src/__tests__/upload.route.test.tsx +0 -228
  316. package/src/components/AppProviders.tsx +0 -19
  317. package/src/components/ClientOnly.tsx +0 -18
  318. package/src/components/Footer.tsx +0 -29
  319. package/src/components/Header.tsx +0 -295
  320. package/src/components/InstallSwitcher.tsx +0 -53
  321. package/src/components/SkillCard.tsx +0 -36
  322. package/src/components/SkillDetailPage.tsx +0 -817
  323. package/src/components/SkillDiffCard.tsx +0 -485
  324. package/src/components/SoulCard.tsx +0 -19
  325. package/src/components/SoulDetailPage.tsx +0 -263
  326. package/src/components/UserBootstrap.tsx +0 -18
  327. package/src/components/ui/dropdown-menu.tsx +0 -67
  328. package/src/components/ui/toggle-group.tsx +0 -35
  329. package/src/convex/client.ts +0 -3
  330. package/src/lib/badges.ts +0 -29
  331. package/src/lib/diffing.test.ts +0 -163
  332. package/src/lib/diffing.ts +0 -106
  333. package/src/lib/gravatar.test.ts +0 -9
  334. package/src/lib/gravatar.ts +0 -158
  335. package/src/lib/og.test.ts +0 -142
  336. package/src/lib/og.ts +0 -156
  337. package/src/lib/publicUser.ts +0 -39
  338. package/src/lib/roles.ts +0 -19
  339. package/src/lib/site.test.ts +0 -130
  340. package/src/lib/site.ts +0 -84
  341. package/src/lib/theme-transition.test.ts +0 -134
  342. package/src/lib/theme-transition.ts +0 -134
  343. package/src/lib/theme.test.tsx +0 -88
  344. package/src/lib/theme.ts +0 -43
  345. package/src/lib/uploadFiles.jsdom.test.ts +0 -33
  346. package/src/lib/uploadFiles.test.ts +0 -123
  347. package/src/lib/uploadFiles.ts +0 -245
  348. package/src/lib/uploadUtils.test.ts +0 -78
  349. package/src/lib/uploadUtils.ts +0 -93
  350. package/src/lib/useAuthStatus.ts +0 -12
  351. package/src/lib/utils.test.ts +0 -9
  352. package/src/lib/utils.ts +0 -6
  353. package/src/logo.svg +0 -12
  354. package/src/routeTree.gen.ts +0 -345
  355. package/src/router.tsx +0 -17
  356. package/src/routes/$owner/$slug.tsx +0 -55
  357. package/src/routes/__root.tsx +0 -136
  358. package/src/routes/admin.tsx +0 -11
  359. package/src/routes/cli/auth.tsx +0 -168
  360. package/src/routes/dashboard.tsx +0 -97
  361. package/src/routes/import.tsx +0 -415
  362. package/src/routes/index.tsx +0 -252
  363. package/src/routes/management.tsx +0 -529
  364. package/src/routes/settings.tsx +0 -203
  365. package/src/routes/skills/index.tsx +0 -422
  366. package/src/routes/souls/$slug.tsx +0 -55
  367. package/src/routes/souls/index.tsx +0 -243
  368. package/src/routes/stars.tsx +0 -68
  369. package/src/routes/u/$handle.tsx +0 -307
  370. package/src/routes/upload/utils.ts +0 -81
  371. package/src/routes/upload.tsx +0 -499
  372. package/src/styles.css +0 -2718
  373. package/tsconfig.json +0 -24
  374. package/tsconfig.oxlint.json +0 -16
  375. package/vercel.json +0 -8
  376. package/vite.config.ts +0 -48
  377. package/vitest.config.ts +0 -47
  378. package/vitest.e2e.config.ts +0 -11
  379. package/vitest.setup.ts +0 -1
  380. /package/{packages/pilothub/bin → bin}/pilothub.js +0 -0
  381. /package/{packages/schema/dist → dist/schema}/ark.d.ts +0 -0
  382. /package/{packages/schema/dist → dist/schema}/ark.js +0 -0
  383. /package/{packages/schema/dist → dist/schema}/index.d.ts +0 -0
  384. /package/{packages/schema/dist → dist/schema}/index.js +0 -0
  385. /package/{packages/schema/dist → dist/schema}/routes.d.ts +0 -0
  386. /package/{packages/schema/dist → dist/schema}/routes.js +0 -0
  387. /package/{packages/schema/dist → dist/schema}/textFiles.d.ts +0 -0
  388. /package/{packages/schema/dist → dist/schema}/textFiles.js +0 -0
@@ -1,817 +0,0 @@
1
- import { Link, useNavigate } from '@tanstack/react-router'
2
- import { useAction, useMutation, useQuery } from 'convex/react'
3
- import type { PilotbotSkillMetadata, SkillInstallSpec } from 'pilothub-schema'
4
- import { useEffect, useMemo, useState } from 'react'
5
- import ReactMarkdown from 'react-markdown'
6
- import remarkGfm from 'remark-gfm'
7
- import { api } from '../../convex/_generated/api'
8
- import type { Doc, Id } from '../../convex/_generated/dataModel'
9
- import { getSkillBadges } from '../lib/badges'
10
- import type { PublicSkill, PublicUser } from '../lib/publicUser'
11
- import { canManageSkill, isModerator } from '../lib/roles'
12
- import { useAuthStatus } from '../lib/useAuthStatus'
13
- import { SkillDiffCard } from './SkillDiffCard'
14
-
15
- type SkillDetailPageProps = {
16
- slug: string
17
- canonicalOwner?: string
18
- redirectToCanonical?: boolean
19
- }
20
-
21
- type SkillBySlugResult = {
22
- skill: PublicSkill
23
- latestVersion: Doc<'skillVersions'> | null
24
- owner: PublicUser | null
25
- forkOf: {
26
- kind: 'fork' | 'duplicate'
27
- version: string | null
28
- skill: { slug: string; displayName: string }
29
- owner: { handle: string | null; userId: Id<'users'> | null }
30
- } | null
31
- canonical: {
32
- skill: { slug: string; displayName: string }
33
- owner: { handle: string | null; userId: Id<'users'> | null }
34
- } | null
35
- } | null
36
-
37
- type SkillFile = Doc<'skillVersions'>['files'][number]
38
-
39
- export function SkillDetailPage({
40
- slug,
41
- canonicalOwner,
42
- redirectToCanonical,
43
- }: SkillDetailPageProps) {
44
- const navigate = useNavigate()
45
- const { isAuthenticated, me } = useAuthStatus()
46
- const result = useQuery(api.skills.getBySlug, { slug }) as SkillBySlugResult | undefined
47
- const toggleStar = useMutation(api.stars.toggle)
48
- const reportSkill = useMutation(api.skills.report)
49
- const addComment = useMutation(api.comments.add)
50
- const removeComment = useMutation(api.comments.remove)
51
- const updateTags = useMutation(api.skills.updateTags)
52
- const getReadme = useAction(api.skills.getReadme)
53
- const [readme, setReadme] = useState<string | null>(null)
54
- const [readmeError, setReadmeError] = useState<string | null>(null)
55
- const [comment, setComment] = useState('')
56
- const [tagName, setTagName] = useState('latest')
57
- const [tagVersionId, setTagVersionId] = useState<Id<'skillVersions'> | ''>('')
58
- const [activeTab, setActiveTab] = useState<'files' | 'compare' | 'versions'>('files')
59
-
60
- const isLoadingSkill = result === undefined
61
- const skill = result?.skill
62
- const owner = result?.owner
63
- const latestVersion = result?.latestVersion
64
- const versions = useQuery(
65
- api.skills.listVersions,
66
- skill ? { skillId: skill._id, limit: 50 } : 'skip',
67
- ) as Doc<'skillVersions'>[] | undefined
68
- const diffVersions = useQuery(
69
- api.skills.listVersions,
70
- skill ? { skillId: skill._id, limit: 200 } : 'skip',
71
- ) as Doc<'skillVersions'>[] | undefined
72
-
73
- const isStarred = useQuery(
74
- api.stars.isStarred,
75
- isAuthenticated && skill ? { skillId: skill._id } : 'skip',
76
- )
77
- const comments = useQuery(
78
- api.comments.listBySkill,
79
- skill ? { skillId: skill._id, limit: 50 } : 'skip',
80
- ) as Array<{ comment: Doc<'comments'>; user: PublicUser | null }> | undefined
81
-
82
- const canManage = canManageSkill(me, skill)
83
- const isStaff = isModerator(me)
84
-
85
- const ownerHandle = owner?.handle ?? owner?.name ?? null
86
- const ownerParam = ownerHandle ?? (owner?._id ? String(owner._id) : null)
87
- const wantsCanonicalRedirect = Boolean(
88
- ownerParam &&
89
- (redirectToCanonical ||
90
- (typeof canonicalOwner === 'string' && canonicalOwner && canonicalOwner !== ownerParam)),
91
- )
92
-
93
- const forkOf = result?.forkOf ?? null
94
- const canonical = result?.canonical ?? null
95
- const forkOfLabel = forkOf?.kind === 'duplicate' ? 'duplicate of' : 'fork of'
96
- const forkOfOwnerHandle = forkOf?.owner?.handle ?? null
97
- const forkOfOwnerId = forkOf?.owner?.userId ?? null
98
- const canonicalOwnerHandle = canonical?.owner?.handle ?? null
99
- const canonicalOwnerId = canonical?.owner?.userId ?? null
100
- const forkOfHref = forkOf?.skill?.slug
101
- ? buildSkillHref(forkOfOwnerHandle, forkOfOwnerId, forkOf.skill.slug)
102
- : null
103
- const canonicalHref =
104
- canonical?.skill?.slug && canonical.skill.slug !== forkOf?.skill?.slug
105
- ? buildSkillHref(canonicalOwnerHandle, canonicalOwnerId, canonical.skill.slug)
106
- : null
107
-
108
- useEffect(() => {
109
- if (!wantsCanonicalRedirect || !ownerParam) return
110
- void navigate({
111
- to: '/$owner/$slug',
112
- params: { owner: ownerParam, slug },
113
- replace: true,
114
- })
115
- }, [navigate, ownerParam, slug, wantsCanonicalRedirect])
116
-
117
- const versionById = new Map<Id<'skillVersions'>, Doc<'skillVersions'>>(
118
- (diffVersions ?? versions ?? []).map((version) => [version._id, version]),
119
- )
120
- const pilotbot = (latestVersion?.parsed as { pilotbot?: PilotbotSkillMetadata } | undefined)?.pilotbot
121
- const osLabels = useMemo(() => formatOsList(pilotbot?.os), [pilotbot?.os])
122
- const requirements = pilotbot?.requires
123
- const installSpecs = pilotbot?.install ?? []
124
- const nixPlugin = pilotbot?.nix?.plugin
125
- const nixSystems = pilotbot?.nix?.systems ?? []
126
- const nixSnippet = nixPlugin ? formatNixInstallSnippet(nixPlugin) : null
127
- const configRequirements = pilotbot?.config
128
- const configExample = configRequirements?.example
129
- ? formatConfigSnippet(configRequirements.example)
130
- : null
131
- const cliHelp = pilotbot?.cliHelp
132
- const hasRuntimeRequirements = Boolean(
133
- pilotbot?.emoji ||
134
- osLabels.length ||
135
- requirements?.bins?.length ||
136
- requirements?.anyBins?.length ||
137
- requirements?.env?.length ||
138
- requirements?.config?.length ||
139
- pilotbot?.primaryEnv,
140
- )
141
- const hasInstallSpecs = installSpecs.length > 0
142
- const hasPluginBundle = Boolean(nixSnippet || configRequirements || cliHelp)
143
- const readmeContent = useMemo(() => {
144
- if (!readme) return null
145
- return stripFrontmatter(readme)
146
- }, [readme])
147
- const latestFiles: SkillFile[] = latestVersion?.files ?? []
148
-
149
- useEffect(() => {
150
- if (!latestVersion) return
151
- setReadme(null)
152
- setReadmeError(null)
153
- let cancelled = false
154
- void getReadme({ versionId: latestVersion._id })
155
- .then((data) => {
156
- if (cancelled) return
157
- setReadme(data.text)
158
- })
159
- .catch((error) => {
160
- if (cancelled) return
161
- setReadmeError(error instanceof Error ? error.message : 'Failed to load README')
162
- setReadme(null)
163
- })
164
- return () => {
165
- cancelled = true
166
- }
167
- }, [latestVersion, getReadme])
168
-
169
- useEffect(() => {
170
- if (!tagVersionId && latestVersion) {
171
- setTagVersionId(latestVersion._id)
172
- }
173
- }, [latestVersion, tagVersionId])
174
-
175
- if (isLoadingSkill || wantsCanonicalRedirect) {
176
- return (
177
- <main className="section">
178
- <div className="card">
179
- <div className="loading-indicator">Loading skill…</div>
180
- </div>
181
- </main>
182
- )
183
- }
184
-
185
- if (result === null || !skill) {
186
- return (
187
- <main className="section">
188
- <div className="card">Skill not found.</div>
189
- </main>
190
- )
191
- }
192
-
193
- const tagEntries = Object.entries(skill.tags ?? {}) as Array<[string, Id<'skillVersions'>]>
194
-
195
- return (
196
- <main className="section">
197
- <div className="skill-detail-stack">
198
- <div className="card skill-hero">
199
- <div className={`skill-hero-top${hasPluginBundle ? ' has-plugin' : ''}`}>
200
- <div className="skill-hero-header">
201
- <div className="skill-hero-title">
202
- <div className="skill-hero-title-row">
203
- <h1 className="section-title" style={{ margin: 0 }}>
204
- {skill.displayName}
205
- </h1>
206
- {nixPlugin ? <span className="tag tag-accent">Plugin bundle (nix)</span> : null}
207
- </div>
208
- <p className="section-subtitle">{skill.summary ?? 'No summary provided.'}</p>
209
-
210
- {nixPlugin ? (
211
- <div className="skill-hero-note">
212
- Bundles the skill pack, CLI binary, and config requirements in one Nix install.
213
- </div>
214
- ) : null}
215
- <div className="stat">
216
- ⭐ {skill.stats.stars} · ⤓ {skill.stats.downloads} · ⤒{' '}
217
- {skill.stats.installsCurrent ?? 0} current · {skill.stats.installsAllTime ?? 0}{' '}
218
- all-time
219
- </div>
220
- {owner?.handle ? (
221
- <div className="stat">
222
- by <a href={`/u/${owner.handle}`}>@{owner.handle}</a>
223
- </div>
224
- ) : null}
225
- {forkOf && forkOfHref ? (
226
- <div className="stat">
227
- {forkOfLabel}{' '}
228
- <a href={forkOfHref}>
229
- {forkOfOwnerHandle ? `@${forkOfOwnerHandle}/` : ''}
230
- {forkOf.skill.slug}
231
- </a>
232
- {forkOf.version ? ` (based on ${forkOf.version})` : null}
233
- </div>
234
- ) : null}
235
- {canonicalHref ? (
236
- <div className="stat">
237
- canonical:{' '}
238
- <a href={canonicalHref}>
239
- {canonicalOwnerHandle ? `@${canonicalOwnerHandle}/` : ''}
240
- {canonical?.skill?.slug}
241
- </a>
242
- </div>
243
- ) : null}
244
- {getSkillBadges(skill).map((badge) => (
245
- <div key={badge} className="tag">
246
- {badge}
247
- </div>
248
- ))}
249
- <div className="skill-actions">
250
- {isAuthenticated ? (
251
- <button
252
- className={`star-toggle${isStarred ? ' is-active' : ''}`}
253
- type="button"
254
- onClick={() => void toggleStar({ skillId: skill._id })}
255
- aria-label={isStarred ? 'Unstar skill' : 'Star skill'}
256
- >
257
- <span aria-hidden="true">★</span>
258
- </button>
259
- ) : null}
260
- {isAuthenticated ? (
261
- <button
262
- className="btn btn-ghost"
263
- type="button"
264
- onClick={async () => {
265
- const reason = window.prompt('Report this skill? Add a reason if you want.')
266
- if (reason === null) return
267
- try {
268
- const result = await reportSkill({
269
- skillId: skill._id,
270
- reason: reason.trim() || undefined,
271
- })
272
- if (result.reported) {
273
- window.alert('Thanks — your report has been submitted.')
274
- } else {
275
- window.alert('You have already reported this skill.')
276
- }
277
- } catch (error) {
278
- console.error('Failed to report skill', error)
279
- window.alert('Unable to submit report. Please try again.')
280
- }
281
- }}
282
- >
283
- Report
284
- </button>
285
- ) : null}
286
- {isStaff ? (
287
- <Link className="btn" to="/management" search={{ skill: skill.slug }}>
288
- Manage
289
- </Link>
290
- ) : null}
291
- </div>
292
- </div>
293
- <div className="skill-hero-cta">
294
- <div className="skill-version-pill">
295
- <span className="skill-version-label">Current version</span>
296
- <strong>v{latestVersion?.version ?? '—'}</strong>
297
- </div>
298
- {!nixPlugin ? (
299
- <a
300
- className="btn btn-primary"
301
- href={`${import.meta.env.VITE_CONVEX_SITE_URL}/api/v1/download?slug=${skill.slug}`}
302
- >
303
- Download zip
304
- </a>
305
- ) : null}
306
- </div>
307
- </div>
308
- {hasPluginBundle ? (
309
- <div className="skill-panel bundle-card">
310
- <div className="bundle-header">
311
- <div className="bundle-title">Plugin bundle (nix)</div>
312
- <div className="bundle-subtitle">Skill pack · CLI binary · Config</div>
313
- </div>
314
- <div className="bundle-includes">
315
- <span>SKILL.md</span>
316
- <span>CLI</span>
317
- <span>Config</span>
318
- </div>
319
- {configRequirements ? (
320
- <div className="bundle-section">
321
- <div className="bundle-section-title">Config requirements</div>
322
- <div className="bundle-meta">
323
- {configRequirements.requiredEnv?.length ? (
324
- <div className="stat">
325
- <strong>Required env</strong>
326
- <span>{configRequirements.requiredEnv.join(', ')}</span>
327
- </div>
328
- ) : null}
329
- {configRequirements.stateDirs?.length ? (
330
- <div className="stat">
331
- <strong>State dirs</strong>
332
- <span>{configRequirements.stateDirs.join(', ')}</span>
333
- </div>
334
- ) : null}
335
- </div>
336
- </div>
337
- ) : null}
338
- {cliHelp ? (
339
- <details className="bundle-section bundle-details">
340
- <summary>CLI help (from plugin)</summary>
341
- <pre className="hero-install-code mono">{cliHelp}</pre>
342
- </details>
343
- ) : null}
344
- </div>
345
- ) : null}
346
- </div>
347
- <div className="skill-tag-row">
348
- {tagEntries.length === 0 ? (
349
- <span className="section-subtitle" style={{ margin: 0 }}>
350
- No tags yet.
351
- </span>
352
- ) : (
353
- tagEntries.map(([tag, versionId]) => (
354
- <span key={tag} className="tag">
355
- {tag}
356
- <span className="tag-meta">
357
- v{versionById.get(versionId)?.version ?? versionId}
358
- </span>
359
- </span>
360
- ))
361
- )}
362
- </div>
363
- {canManage ? (
364
- <form
365
- onSubmit={(event) => {
366
- event.preventDefault()
367
- if (!tagName.trim() || !tagVersionId) return
368
- void updateTags({
369
- skillId: skill._id,
370
- tags: [{ tag: tagName.trim(), versionId: tagVersionId }],
371
- })
372
- }}
373
- className="tag-form"
374
- >
375
- <input
376
- className="search-input"
377
- value={tagName}
378
- onChange={(event) => setTagName(event.target.value)}
379
- placeholder="latest"
380
- />
381
- <select
382
- className="search-input"
383
- value={tagVersionId ?? ''}
384
- onChange={(event) => setTagVersionId(event.target.value as Id<'skillVersions'>)}
385
- >
386
- {(diffVersions ?? []).map((version) => (
387
- <option key={version._id} value={version._id}>
388
- v{version.version}
389
- </option>
390
- ))}
391
- </select>
392
- <button className="btn" type="submit">
393
- Update tag
394
- </button>
395
- </form>
396
- ) : null}
397
- {hasRuntimeRequirements || hasInstallSpecs ? (
398
- <div className="skill-hero-content">
399
- <div className="skill-hero-panels">
400
- {hasRuntimeRequirements ? (
401
- <div className="skill-panel">
402
- <h3 className="section-title" style={{ fontSize: '1rem', margin: 0 }}>
403
- Runtime requirements
404
- </h3>
405
- <div className="skill-panel-body">
406
- {pilotbot?.emoji ? <div className="tag">{pilotbot.emoji} Pilotbot</div> : null}
407
- {osLabels.length ? (
408
- <div className="stat">
409
- <strong>OS</strong>
410
- <span>{osLabels.join(' · ')}</span>
411
- </div>
412
- ) : null}
413
- {requirements?.bins?.length ? (
414
- <div className="stat">
415
- <strong>Bins</strong>
416
- <span>{requirements.bins.join(', ')}</span>
417
- </div>
418
- ) : null}
419
- {requirements?.anyBins?.length ? (
420
- <div className="stat">
421
- <strong>Any bin</strong>
422
- <span>{requirements.anyBins.join(', ')}</span>
423
- </div>
424
- ) : null}
425
- {requirements?.env?.length ? (
426
- <div className="stat">
427
- <strong>Env</strong>
428
- <span>{requirements.env.join(', ')}</span>
429
- </div>
430
- ) : null}
431
- {requirements?.config?.length ? (
432
- <div className="stat">
433
- <strong>Config</strong>
434
- <span>{requirements.config.join(', ')}</span>
435
- </div>
436
- ) : null}
437
- {pilotbot?.primaryEnv ? (
438
- <div className="stat">
439
- <strong>Primary env</strong>
440
- <span>{pilotbot.primaryEnv}</span>
441
- </div>
442
- ) : null}
443
- </div>
444
- </div>
445
- ) : null}
446
- {hasInstallSpecs ? (
447
- <div className="skill-panel">
448
- <h3 className="section-title" style={{ fontSize: '1rem', margin: 0 }}>
449
- Install
450
- </h3>
451
- <div className="skill-panel-body">
452
- {installSpecs.map((spec, index) => {
453
- const command = formatInstallCommand(spec)
454
- return (
455
- <div key={`${spec.id ?? spec.kind}-${index}`} className="stat">
456
- <div>
457
- <strong>{spec.label ?? formatInstallLabel(spec)}</strong>
458
- {spec.bins?.length ? (
459
- <div style={{ color: 'var(--ink-soft)', fontSize: '0.85rem' }}>
460
- Bins: {spec.bins.join(', ')}
461
- </div>
462
- ) : null}
463
- {command ? <code>{command}</code> : null}
464
- </div>
465
- </div>
466
- )
467
- })}
468
- </div>
469
- </div>
470
- ) : null}
471
- </div>
472
- </div>
473
- ) : null}
474
- </div>
475
- {nixSnippet ? (
476
- <div className="card">
477
- <h2 className="section-title" style={{ fontSize: '1.2rem', margin: 0 }}>
478
- Install via Nix
479
- </h2>
480
- <p className="section-subtitle" style={{ margin: 0 }}>
481
- {nixSystems.length ? `Systems: ${nixSystems.join(', ')}` : 'nix-pilotbot'}
482
- </p>
483
- <pre className="hero-install-code" style={{ marginTop: 12 }}>
484
- {nixSnippet}
485
- </pre>
486
- </div>
487
- ) : null}
488
- {configExample ? (
489
- <div className="card">
490
- <h2 className="section-title" style={{ fontSize: '1.2rem', margin: 0 }}>
491
- Config example
492
- </h2>
493
- <p className="section-subtitle" style={{ margin: 0 }}>
494
- Starter config for this plugin bundle.
495
- </p>
496
- <pre className="hero-install-code" style={{ marginTop: 12 }}>
497
- {configExample}
498
- </pre>
499
- </div>
500
- ) : null}
501
- <div className="card tab-card">
502
- <div className="tab-header">
503
- <button
504
- className={`tab-button${activeTab === 'files' ? ' is-active' : ''}`}
505
- type="button"
506
- onClick={() => setActiveTab('files')}
507
- >
508
- Files
509
- </button>
510
- <button
511
- className={`tab-button${activeTab === 'compare' ? ' is-active' : ''}`}
512
- type="button"
513
- onClick={() => setActiveTab('compare')}
514
- >
515
- Compare
516
- </button>
517
- <button
518
- className={`tab-button${activeTab === 'versions' ? ' is-active' : ''}`}
519
- type="button"
520
- onClick={() => setActiveTab('versions')}
521
- >
522
- Versions
523
- </button>
524
- </div>
525
- {activeTab === 'files' ? (
526
- <div className="tab-body">
527
- <div>
528
- <h2 className="section-title" style={{ fontSize: '1.2rem', margin: 0 }}>
529
- SKILL.md
530
- </h2>
531
- <div className="markdown">
532
- {readmeContent ? (
533
- <ReactMarkdown remarkPlugins={[remarkGfm]}>{readmeContent}</ReactMarkdown>
534
- ) : readmeError ? (
535
- <div className="stat">Failed to load SKILL.md: {readmeError}</div>
536
- ) : (
537
- <div>Loading…</div>
538
- )}
539
- </div>
540
- </div>
541
- <div className="file-list">
542
- <div className="file-list-header">
543
- <h3 className="section-title" style={{ fontSize: '1.05rem', margin: 0 }}>
544
- Files
545
- </h3>
546
- <span className="section-subtitle" style={{ margin: 0 }}>
547
- {latestFiles.length} total
548
- </span>
549
- </div>
550
- <div className="file-list-body">
551
- {latestFiles.length === 0 ? (
552
- <div className="stat">No files available.</div>
553
- ) : (
554
- latestFiles.map((file) => (
555
- <div key={file.path} className="file-row">
556
- <span className="file-path">{file.path}</span>
557
- <span className="file-meta">{formatBytes(file.size)}</span>
558
- </div>
559
- ))
560
- )}
561
- </div>
562
- </div>
563
- </div>
564
- ) : null}
565
- {activeTab === 'compare' && skill ? (
566
- <div className="tab-body">
567
- <SkillDiffCard skill={skill} versions={diffVersions ?? []} variant="embedded" />
568
- </div>
569
- ) : null}
570
- {activeTab === 'versions' ? (
571
- <div className="tab-body">
572
- <div>
573
- <h2 className="section-title" style={{ fontSize: '1.2rem', margin: 0 }}>
574
- Versions
575
- </h2>
576
- <p className="section-subtitle" style={{ margin: 0 }}>
577
- {nixPlugin
578
- ? 'Review release history and changelog.'
579
- : 'Download older releases or scan the changelog.'}
580
- </p>
581
- </div>
582
- <div className="version-scroll">
583
- <div className="version-list">
584
- {(versions ?? []).map((version) => (
585
- <div key={version._id} className="version-row">
586
- <div className="version-info">
587
- <div>
588
- v{version.version} · {new Date(version.createdAt).toLocaleDateString()}
589
- {version.changelogSource === 'auto' ? (
590
- <span style={{ color: 'var(--ink-soft)' }}> · auto</span>
591
- ) : null}
592
- </div>
593
- <div style={{ color: '#5c554e', whiteSpace: 'pre-wrap' }}>
594
- {version.changelog}
595
- </div>
596
- </div>
597
- {!nixPlugin ? (
598
- <div className="version-actions">
599
- <a
600
- className="btn version-zip"
601
- href={`${import.meta.env.VITE_CONVEX_SITE_URL}/api/v1/download?slug=${skill.slug}&version=${version.version}`}
602
- >
603
- Zip
604
- </a>
605
- </div>
606
- ) : null}
607
- </div>
608
- ))}
609
- </div>
610
- </div>
611
- </div>
612
- ) : null}
613
- </div>
614
- <div className="card">
615
- <h2 className="section-title" style={{ fontSize: '1.2rem', margin: 0 }}>
616
- Comments
617
- </h2>
618
- {isAuthenticated ? (
619
- <form
620
- onSubmit={(event) => {
621
- event.preventDefault()
622
- if (!comment.trim()) return
623
- void addComment({ skillId: skill._id, body: comment.trim() }).then(() =>
624
- setComment(''),
625
- )
626
- }}
627
- className="comment-form"
628
- >
629
- <textarea
630
- className="comment-input"
631
- rows={4}
632
- value={comment}
633
- onChange={(event) => setComment(event.target.value)}
634
- placeholder="Leave a note…"
635
- />
636
- <button className="btn comment-submit" type="submit">
637
- Post comment
638
- </button>
639
- </form>
640
- ) : (
641
- <p className="section-subtitle">Sign in to comment.</p>
642
- )}
643
- <div style={{ display: 'grid', gap: 12, marginTop: 16 }}>
644
- {(comments ?? []).length === 0 ? (
645
- <div className="stat">No comments yet.</div>
646
- ) : (
647
- (comments ?? []).map((entry) => (
648
- <div key={entry.comment._id} className="stat" style={{ alignItems: 'flex-start' }}>
649
- <div>
650
- <strong>@{entry.user?.handle ?? entry.user?.name ?? 'user'}</strong>
651
- <div style={{ color: '#5c554e' }}>{entry.comment.body}</div>
652
- </div>
653
- {isAuthenticated && me && (me._id === entry.comment.userId || isModerator(me)) ? (
654
- <button
655
- className="btn"
656
- type="button"
657
- onClick={() => void removeComment({ commentId: entry.comment._id })}
658
- >
659
- Delete
660
- </button>
661
- ) : null}
662
- </div>
663
- ))
664
- )}
665
- </div>
666
- </div>
667
- </div>
668
- </main>
669
- )
670
- }
671
-
672
- function buildSkillHref(ownerHandle: string | null, ownerId: Id<'users'> | null, slug: string) {
673
- const owner = ownerHandle?.trim() || (ownerId ? String(ownerId) : 'unknown')
674
- return `/${owner}/${slug}`
675
- }
676
-
677
- function formatConfigSnippet(raw: string) {
678
- const trimmed = raw.trim()
679
- if (!trimmed || raw.includes('\n')) return raw
680
- try {
681
- const parsed = JSON.parse(raw)
682
- return JSON.stringify(parsed, null, 2)
683
- } catch {
684
- // fall through
685
- }
686
-
687
- let out = ''
688
- let indent = 0
689
- let inString = false
690
- let isEscaped = false
691
-
692
- const newline = () => {
693
- out = out.replace(/[ \t]+$/u, '')
694
- out += `\n${' '.repeat(indent * 2)}`
695
- }
696
-
697
- for (let i = 0; i < raw.length; i += 1) {
698
- const ch = raw[i]
699
- if (inString) {
700
- out += ch
701
- if (isEscaped) {
702
- isEscaped = false
703
- } else if (ch === '\\') {
704
- isEscaped = true
705
- } else if (ch === '"') {
706
- inString = false
707
- }
708
- continue
709
- }
710
-
711
- if (ch === '"') {
712
- inString = true
713
- out += ch
714
- continue
715
- }
716
-
717
- if (ch === '{' || ch === '[') {
718
- out += ch
719
- indent += 1
720
- newline()
721
- continue
722
- }
723
-
724
- if (ch === '}' || ch === ']') {
725
- indent = Math.max(0, indent - 1)
726
- newline()
727
- out += ch
728
- continue
729
- }
730
-
731
- if (ch === ';' || ch === ',') {
732
- out += ch
733
- newline()
734
- continue
735
- }
736
-
737
- if (ch === '\n' || ch === '\r' || ch === '\t') {
738
- continue
739
- }
740
-
741
- if (ch === ' ') {
742
- if (out.endsWith(' ') || out.endsWith('\n')) {
743
- continue
744
- }
745
- out += ' '
746
- continue
747
- }
748
-
749
- out += ch
750
- }
751
-
752
- return out.trim()
753
- }
754
-
755
- function stripFrontmatter(content: string) {
756
- const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
757
- if (!normalized.startsWith('---')) return content
758
- const endIndex = normalized.indexOf('\n---', 3)
759
- if (endIndex === -1) return content
760
- return normalized.slice(endIndex + 4).replace(/^\n+/, '')
761
- }
762
-
763
- function formatOsList(os?: string[]) {
764
- if (!os?.length) return []
765
- return os.map((entry) => {
766
- const key = entry.trim().toLowerCase()
767
- if (key === 'darwin' || key === 'macos' || key === 'mac') return 'macOS'
768
- if (key === 'linux') return 'Linux'
769
- if (key === 'windows' || key === 'win32') return 'Windows'
770
- return entry
771
- })
772
- }
773
-
774
- function formatInstallLabel(spec: SkillInstallSpec) {
775
- if (spec.kind === 'brew') return 'Homebrew'
776
- if (spec.kind === 'node') return 'Node'
777
- if (spec.kind === 'go') return 'Go'
778
- if (spec.kind === 'uv') return 'uv'
779
- return 'Install'
780
- }
781
-
782
- function formatInstallCommand(spec: SkillInstallSpec) {
783
- if (spec.kind === 'brew' && spec.formula) {
784
- if (spec.tap && !spec.formula.includes('/')) {
785
- return `brew install ${spec.tap}/${spec.formula}`
786
- }
787
- return `brew install ${spec.formula}`
788
- }
789
- if (spec.kind === 'node' && spec.package) {
790
- return `npm i -g ${spec.package}`
791
- }
792
- if (spec.kind === 'go' && spec.module) {
793
- return `go install ${spec.module}`
794
- }
795
- if (spec.kind === 'uv' && spec.package) {
796
- return `uv tool install ${spec.package}`
797
- }
798
- return null
799
- }
800
-
801
- function formatBytes(bytes: number) {
802
- if (!Number.isFinite(bytes)) return '—'
803
- if (bytes < 1024) return `${bytes} B`
804
- const units = ['KB', 'MB', 'GB']
805
- let value = bytes / 1024
806
- let unitIndex = 0
807
- while (value >= 1024 && unitIndex < units.length - 1) {
808
- value /= 1024
809
- unitIndex += 1
810
- }
811
- return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unitIndex]}`
812
- }
813
-
814
- function formatNixInstallSnippet(plugin: string) {
815
- const snippet = `programs.pilotbot.plugins = [ { source = "${plugin}"; } ];`
816
- return formatConfigSnippet(snippet)
817
- }