kennel 0.2.2 → 0.2.5

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 (237) hide show
  1. package/README.md +23 -13
  2. package/dist/cli/dispatch.js +16 -3
  3. package/dist/cli/src/cli/dispatch.js +16 -3
  4. package/dist/cli/src/cli/usage.js +1 -1
  5. package/dist/cli/src/core/command.js +1 -0
  6. package/dist/cli/src/core/init.js +5 -5
  7. package/dist/cli/src/plugins/external.d.ts +29 -3
  8. package/dist/cli/src/plugins/external.js +78 -24
  9. package/dist/cli/src/site/server-plugins.d.ts +2 -7
  10. package/dist/cli/src/site/viewer.js +1 -0
  11. package/dist/cli/usage.js +1 -1
  12. package/dist/core/command.js +1 -0
  13. package/dist/core/init.js +5 -5
  14. package/dist/core/runtime.js +357 -3207
  15. package/dist/package/preview.js +1232 -5113
  16. package/dist/plugins/external.d.ts +29 -3
  17. package/dist/plugins/external.js +78 -24
  18. package/dist/site/server-plugins.d.ts +2 -7
  19. package/dist/site/server-plugins.js +552 -10447
  20. package/dist/site/viewer.js +1 -0
  21. package/kernel/apps/web/package.json +1 -5
  22. package/kernel/apps/web/scripts/dev.mjs +53 -0
  23. package/kernel/apps/web/src/app.css +0 -3
  24. package/kernel/apps/web/src/main.tsx +0 -1
  25. package/kernel/apps/web/src/plugins.ts +0 -30
  26. package/kernel/apps/web/tsconfig.json +1 -2
  27. package/kernel/apps/web/vite.config.ts +31 -13
  28. package/kernel/packages/api/package.json +0 -4
  29. package/kernel/packages/api/src/server/folder-metadata-plugin.ts +13 -38
  30. package/kernel/packages/api/src/server/index.ts +0 -1
  31. package/kernel/packages/api/src/server/plugins.ts +49 -16
  32. package/kernel/packages/api/test/core.test.ts +13 -25
  33. package/kernel/packages/markdown/README.md +1 -1
  34. package/package.json +1 -1
  35. package/web/assets/{abnfDiagram-VRR7QNED-Bgfb1nEB.js → abnfDiagram-VRR7QNED-xd_g1uK3.js} +1 -1
  36. package/web/assets/app-CX20nNIa.js +2 -0
  37. package/web/assets/{app-options-D-RJiQ-X.js → app-options-CkPJE5ep.js} +1 -1
  38. package/web/assets/{app-view-header-BLvLq02T.js → app-view-header-CNk5KnA-.js} +1 -1
  39. package/web/assets/{arc-Dk_KI36B.js → arc-B0u5yBJA.js} +1 -1
  40. package/web/assets/{architectureDiagram-ZJ3FMSHR-NR_KhRCf.js → architectureDiagram-ZJ3FMSHR-DfxOAo8P.js} +1 -1
  41. package/web/assets/{ask-tray-TzeTcO6-.js → ask-tray-SscJecDY.js} +1 -1
  42. package/web/assets/{blockDiagram-677ZJIJ3-CkLkwx9C.js → blockDiagram-677ZJIJ3-Ci3Usxh6.js} +1 -1
  43. package/web/assets/{c4Diagram-LMCZKHZV-CFCUH8e_.js → c4Diagram-LMCZKHZV-BqgwIVgh.js} +1 -1
  44. package/web/assets/channel-X7WHyxxT.js +1 -0
  45. package/web/assets/{chunk-2Q5K7J3B-B_BPLWsW.js → chunk-2Q5K7J3B-CfX69l2U.js} +1 -1
  46. package/web/assets/{chunk-32BRIVSS-B1VB1hOy.js → chunk-32BRIVSS-CuFlzZkl.js} +1 -1
  47. package/web/assets/{chunk-5VM5RSS4-KFcYr8Co.js → chunk-5VM5RSS4-DidCMlJa.js} +1 -1
  48. package/web/assets/{chunk-EX3LRPZG-CpBX3kuK.js → chunk-EX3LRPZG-B5CDm1dS.js} +1 -1
  49. package/web/assets/{chunk-JWPE2WC7-BPSl40wG.js → chunk-JWPE2WC7-D5fGV_sH.js} +1 -1
  50. package/web/assets/{chunk-MOJQB5TN-D2RcYmoU.js → chunk-MOJQB5TN-ayhH5Q-H.js} +1 -1
  51. package/web/assets/{chunk-RYQCIY6F-m7Gyn-t3.js → chunk-RYQCIY6F-C4BfOOZn.js} +1 -1
  52. package/web/assets/{chunk-V7JOEXUC-TDvdopxS.js → chunk-V7JOEXUC-DpUEIM9V.js} +1 -1
  53. package/web/assets/{chunk-VR4S4FIN-DZTsG51a.js → chunk-VR4S4FIN-DoQuFFRh.js} +1 -1
  54. package/web/assets/{chunk-XXDRQBXY-Dp0--RE2.js → chunk-XXDRQBXY-CI9eiLwn.js} +1 -1
  55. package/web/assets/classDiagram-OUVF2IWQ-BjsLQ3dz.js +1 -0
  56. package/web/assets/classDiagram-v2-EOCWNBFH-BjsLQ3dz.js +1 -0
  57. package/web/assets/client-7OWw5kW2.js +6 -0
  58. package/web/assets/client-DYj_td4Q.js +17 -0
  59. package/web/assets/{client-j2kQmxsO.js → client-Dk-NBvJR.js} +4 -4
  60. package/web/assets/command-zkzH4hVz.js +38 -0
  61. package/web/assets/{copy-context-C0Uqsp-3.js → copy-context-D5oiWrYC.js} +1 -1
  62. package/web/assets/{cose-bilkent-JH36ORCC-CPu3PI8h.js → cose-bilkent-JH36ORCC-BHRk1mO3.js} +1 -1
  63. package/web/assets/{cynefin-VYW2F7L2-DWxftYAm.js → cynefin-VYW2F7L2-C1B09TRE.js} +1 -1
  64. package/web/assets/{cynefinDiagram-TSTJHNR4-DsmN2V-p.js → cynefinDiagram-TSTJHNR4-B27qXQbv.js} +1 -1
  65. package/web/assets/{dagre-VKFMJZFB-COEbApyY.js → dagre-VKFMJZFB-B1qEi32g.js} +1 -1
  66. package/web/assets/{diagram-FQU43EPY-0O9WGWKB.js → diagram-FQU43EPY-BMkkgADD.js} +1 -1
  67. package/web/assets/{diagram-G47NLZAW-CutEdMVh.js → diagram-G47NLZAW-Cb8_g1A5.js} +1 -1
  68. package/web/assets/{diagram-NH7WQ7WH-C9KZc3B2.js → diagram-NH7WQ7WH-B51wuc5m.js} +1 -1
  69. package/web/assets/{diagram-OA4YK3LP-D8kkPXbY.js → diagram-OA4YK3LP-xnsecbbI.js} +1 -1
  70. package/web/assets/{diagram-WEI45ONY-_YqiTaGP.js → diagram-WEI45ONY-DSZdyaAV.js} +1 -1
  71. package/web/assets/{document-Dgt5eZFj.js → document-Dc2dftTW.js} +3 -3
  72. package/web/assets/{ebnfDiagram-CCIWWBDH-JO-zQh6g.js → ebnfDiagram-CCIWWBDH-aTHmcUGy.js} +1 -1
  73. package/web/assets/{entities-C3wz7FCx.js → entities-CCUtpskY.js} +1 -1
  74. package/web/assets/{erDiagram-Q63AITRT-9UjQwJ4P.js → erDiagram-Q63AITRT-CUUPBcvo.js} +1 -1
  75. package/web/assets/{file-viewer-DZJ8nYnl.js → file-viewer-DJ3rsmef.js} +1 -1
  76. package/web/assets/{flowDiagram-23GEKE2U-Ca08tnYM.js → flowDiagram-23GEKE2U-aTQXx2Mh.js} +1 -1
  77. package/web/assets/{folder-image-BfGgxo7i.js → folder-image-Bnv43tzP.js} +1 -1
  78. package/web/assets/{folder-landing-qUKBCnG-.js → folder-landing-DCnzYXXY.js} +1 -1
  79. package/web/assets/{folder-list-BfFjv7rR.js → folder-list-DvVkVjiR.js} +1 -1
  80. package/web/assets/{folder-metadata-DCw8xzHk.js → folder-metadata-C9hMMzIH.js} +1 -1
  81. package/web/assets/{folder-mobile-bar-Ubz4babf.js → folder-mobile-bar-VoTL1GS0.js} +1 -1
  82. package/web/assets/folder-plugin-view-Cvd804ZU.js +6 -0
  83. package/web/assets/{ganttDiagram-NO4QXBWP-KnHzX5W3.js → ganttDiagram-NO4QXBWP-DiwdUp7u.js} +1 -1
  84. package/web/assets/{gitGraphDiagram-IHSO6WYX-Cm8woNtK.js → gitGraphDiagram-IHSO6WYX-BL2gG_IS.js} +1 -1
  85. package/web/assets/{index-CNyy98F8.js → index-CUTtMSyJ.js} +2 -2
  86. package/web/assets/index-CegvVQ1C.js +1 -0
  87. package/web/assets/index-Dwhf8F_U.css +1 -0
  88. package/web/assets/{infoDiagram-FWYZ7A6U-yyqmUtIb.js → infoDiagram-FWYZ7A6U-B3n0zZpt.js} +1 -1
  89. package/web/assets/{ishikawaDiagram-FXEZZL3T-BfvR_Zy4.js → ishikawaDiagram-FXEZZL3T-D7YytbEv.js} +1 -1
  90. package/web/assets/{journeyDiagram-5HDEW3XC-BKuw06HM.js → journeyDiagram-5HDEW3XC-BYwqEl3C.js} +1 -1
  91. package/web/assets/{kanban-definition-HUTT4EX6-n0010rmQ.js → kanban-definition-HUTT4EX6-BQHNJgXZ.js} +1 -1
  92. package/web/assets/{linear-C0d0lT8O.js → linear-BPiQCLNk.js} +1 -1
  93. package/web/assets/{mermaid.core-BDVbShyO.js → mermaid.core-CD-KvBjV.js} +4 -4
  94. package/web/assets/{metadata-KV0dpp4G.js → metadata-COJ7fZul.js} +1 -1
  95. package/web/assets/{mindmap-definition-LN4V7U3C--_0ks3GF.js → mindmap-definition-LN4V7U3C-C1L_gyoj.js} +1 -1
  96. package/web/assets/{pegDiagram-2B236MQR-CBoNIpKR.js → pegDiagram-2B236MQR-BDAfItqS.js} +1 -1
  97. package/web/assets/{pieDiagram-ENE6RG2P-MA5tkYh5.js → pieDiagram-ENE6RG2P-CkZYH3Vz.js} +1 -1
  98. package/web/assets/{popover-DBD7FF-W.js → popover-0oA2lLUV.js} +2 -2
  99. package/web/assets/{project-search-dialog-DM1IkSZu.js → project-search-dialog-B2oqMT6h.js} +1 -1
  100. package/web/assets/{quadrantDiagram-ABIIQ3AL-CWT1ryqF.js → quadrantDiagram-ABIIQ3AL-Dy8Al3YZ.js} +1 -1
  101. package/web/assets/{railroadDiagram-RFXS5EU6-ZiLB5FGi.js → railroadDiagram-RFXS5EU6-Dd9_yo7B.js} +1 -1
  102. package/web/assets/{requirementDiagram-TGXJPOKE-CQq9uneq.js → requirementDiagram-TGXJPOKE-DPhIUsbs.js} +1 -1
  103. package/web/assets/{sankeyDiagram-HTMAVEWB-qDG-0JqI.js → sankeyDiagram-HTMAVEWB-B2QkYtub.js} +1 -1
  104. package/web/assets/{search-DNrQA5fF.js → search-C1J0-xiB.js} +1 -1
  105. package/web/assets/select-CtXC_PLC.js +30 -0
  106. package/web/assets/{selection-popup-DmtTeRs2.js → selection-popup-qhlUFzGq.js} +1 -1
  107. package/web/assets/{sequenceDiagram-DBY2YBRQ-CiApurLO.js → sequenceDiagram-DBY2YBRQ-Bx7fcZDx.js} +1 -1
  108. package/web/assets/{sizeCapture-X5ZJPWSS-DZSR5QXm.js → sizeCapture-X5ZJPWSS-CIXIhl_A.js} +1 -1
  109. package/web/assets/{stateDiagram-2N3HPSRC-CjJJR9_z.js → stateDiagram-2N3HPSRC-DCrdAflB.js} +1 -1
  110. package/web/assets/stateDiagram-v2-6OUMAXLB-0KCFLYeQ.js +1 -0
  111. package/web/assets/{swimlanes-5IMT3BWC-CKmo1sg6.js → swimlanes-5IMT3BWC-DA2vc7iZ.js} +2 -2
  112. package/web/assets/swimlanesDiagram-G3AALYLV-CAw3AV84.js +8 -0
  113. package/web/assets/{timeline-definition-FHXFAJF6-D-NMwS3y.js → timeline-definition-FHXFAJF6-lKfJiV9V.js} +1 -1
  114. package/web/assets/{vennDiagram-L72KCM5P-CfL6LBWw.js → vennDiagram-L72KCM5P-DO1q2KzG.js} +1 -1
  115. package/web/assets/{view-context-gyDl_p2V.js → view-context-DXKM3-l1.js} +4 -4
  116. package/web/assets/{viewer-capabilities-Bpp8Uvcg.js → viewer-capabilities-Cig_xM9u.js} +1 -1
  117. package/web/assets/{wardleyDiagram-EHGQE667-DgldMxaj.js → wardleyDiagram-EHGQE667-Dal9LkuZ.js} +1 -1
  118. package/web/assets/{xychartDiagram-FW5EYKEG-CIoAIFX8.js → xychartDiagram-FW5EYKEG-B3dPLPII.js} +1 -1
  119. package/web/index.html +2 -2
  120. package/kernel/packages/api/test/partial-loading.test.ts +0 -43
  121. package/kernel/plugins/calendar/SKILL.md +0 -173
  122. package/kernel/plugins/calendar/eslint.config.js +0 -10
  123. package/kernel/plugins/calendar/package.json +0 -73
  124. package/kernel/plugins/calendar/schema.json +0 -137
  125. package/kernel/plugins/calendar/src/calendar.tsx +0 -328
  126. package/kernel/plugins/calendar/src/client.tsx +0 -139
  127. package/kernel/plugins/calendar/src/context.ts +0 -48
  128. package/kernel/plugins/calendar/src/copy-button.tsx +0 -66
  129. package/kernel/plugins/calendar/src/event-dialog.tsx +0 -270
  130. package/kernel/plugins/calendar/src/index.ts +0 -4
  131. package/kernel/plugins/calendar/src/manifest.ts +0 -7
  132. package/kernel/plugins/calendar/src/parse.ts +0 -610
  133. package/kernel/plugins/calendar/src/server.ts +0 -160
  134. package/kernel/plugins/calendar/src/utils.ts +0 -80
  135. package/kernel/plugins/calendar/src/views.tsx +0 -418
  136. package/kernel/plugins/calendar/test/context.test.mjs +0 -60
  137. package/kernel/plugins/calendar/test/package.test.mjs +0 -43
  138. package/kernel/plugins/calendar/test/parse.test.mjs +0 -392
  139. package/kernel/plugins/calendar/test/server.test.mjs +0 -126
  140. package/kernel/plugins/calendar/tsconfig.json +0 -17
  141. package/kernel/plugins/mail/SKILL.md +0 -97
  142. package/kernel/plugins/mail/eslint.config.js +0 -10
  143. package/kernel/plugins/mail/package.json +0 -79
  144. package/kernel/plugins/mail/src/api.ts +0 -359
  145. package/kernel/plugins/mail/src/bun-ffi.d.ts +0 -14
  146. package/kernel/plugins/mail/src/client.tsx +0 -14
  147. package/kernel/plugins/mail/src/flock.ts +0 -137
  148. package/kernel/plugins/mail/src/folders.ts +0 -37
  149. package/kernel/plugins/mail/src/history.ts +0 -485
  150. package/kernel/plugins/mail/src/index.ts +0 -3
  151. package/kernel/plugins/mail/src/mail-card.tsx +0 -123
  152. package/kernel/plugins/mail/src/mail-conversation.tsx +0 -153
  153. package/kernel/plugins/mail/src/mail-detail-page.tsx +0 -467
  154. package/kernel/plugins/mail/src/mail-hooks.ts +0 -252
  155. package/kernel/plugins/mail/src/mail-page.tsx +0 -109
  156. package/kernel/plugins/mail/src/mail-request.ts +0 -78
  157. package/kernel/plugins/mail/src/mail-send-box.tsx +0 -175
  158. package/kernel/plugins/mail/src/mailos.ts +0 -723
  159. package/kernel/plugins/mail/src/manifest.ts +0 -7
  160. package/kernel/plugins/mail/src/node.ts +0 -171
  161. package/kernel/plugins/mail/src/parse.ts +0 -139
  162. package/kernel/plugins/mail/src/server.ts +0 -125
  163. package/kernel/plugins/mail/src/types.ts +0 -10
  164. package/kernel/plugins/mail/src/viewer.tsx +0 -147
  165. package/kernel/plugins/mail/test/flock.test.mjs +0 -156
  166. package/kernel/plugins/mail/test/folders.test.mjs +0 -138
  167. package/kernel/plugins/mail/test/history.test.mjs +0 -460
  168. package/kernel/plugins/mail/test/mailos.test.mjs +0 -752
  169. package/kernel/plugins/mail/test/node.test.mjs +0 -207
  170. package/kernel/plugins/mail/test/package.test.mjs +0 -54
  171. package/kernel/plugins/mail/test/parse.test.mjs +0 -32
  172. package/kernel/plugins/mail/test/server.test.mjs +0 -116
  173. package/kernel/plugins/mail/test/viewer.test.mjs +0 -192
  174. package/kernel/plugins/mail/tsconfig.json +0 -17
  175. package/kernel/plugins/projects/SKILL.md +0 -73
  176. package/kernel/plugins/projects/eslint.config.js +0 -10
  177. package/kernel/plugins/projects/package.json +0 -70
  178. package/kernel/plugins/projects/src/client.tsx +0 -17
  179. package/kernel/plugins/projects/src/data/model.ts +0 -129
  180. package/kernel/plugins/projects/src/index.ts +0 -17
  181. package/kernel/plugins/projects/src/manifest.ts +0 -7
  182. package/kernel/plugins/projects/src/project-context.ts +0 -41
  183. package/kernel/plugins/projects/src/server.ts +0 -276
  184. package/kernel/plugins/projects/src/ui/project-card.tsx +0 -61
  185. package/kernel/plugins/projects/src/ui/projects-page.tsx +0 -102
  186. package/kernel/plugins/projects/test/package.test.mjs +0 -51
  187. package/kernel/plugins/projects/tsconfig.json +0 -17
  188. package/kernel/plugins/tasks/SKILL.md +0 -341
  189. package/kernel/plugins/tasks/eslint.config.js +0 -10
  190. package/kernel/plugins/tasks/package.json +0 -74
  191. package/kernel/plugins/tasks/schema.json +0 -939
  192. package/kernel/plugins/tasks/src/api.ts +0 -212
  193. package/kernel/plugins/tasks/src/client.tsx +0 -23
  194. package/kernel/plugins/tasks/src/data/parse.ts +0 -544
  195. package/kernel/plugins/tasks/src/data/task-id.ts +0 -56
  196. package/kernel/plugins/tasks/src/data/todo-model.ts +0 -795
  197. package/kernel/plugins/tasks/src/index.ts +0 -34
  198. package/kernel/plugins/tasks/src/manifest.ts +0 -7
  199. package/kernel/plugins/tasks/src/pages/task-detail.tsx +0 -48
  200. package/kernel/plugins/tasks/src/pages/task-list.tsx +0 -88
  201. package/kernel/plugins/tasks/src/server.ts +0 -447
  202. package/kernel/plugins/tasks/src/styles.css +0 -9
  203. package/kernel/plugins/tasks/src/ui/compact-todo-list.tsx +0 -372
  204. package/kernel/plugins/tasks/src/ui/error-details.tsx +0 -78
  205. package/kernel/plugins/tasks/src/ui/task-page.tsx +0 -392
  206. package/kernel/plugins/tasks/src/ui/todo-agent-filter.tsx +0 -36
  207. package/kernel/plugins/tasks/src/ui/todo-filter-bar.tsx +0 -66
  208. package/kernel/plugins/tasks/src/ui/todo-item.tsx +0 -494
  209. package/kernel/plugins/tasks/src/ui/todo-status-filter.tsx +0 -60
  210. package/kernel/plugins/tasks/src/ui/todo-tag-filter.tsx +0 -48
  211. package/kernel/plugins/tasks/src/ui/todo-warnings.tsx +0 -106
  212. package/kernel/plugins/tasks/test/api.test.mjs +0 -99
  213. package/kernel/plugins/tasks/test/folder-sources.test.mjs +0 -46
  214. package/kernel/plugins/tasks/test/package.test.mjs +0 -54
  215. package/kernel/plugins/tasks/test/parse.test.mjs +0 -225
  216. package/kernel/plugins/tasks/test/server.test.mjs +0 -386
  217. package/kernel/plugins/tasks/tsconfig.json +0 -17
  218. package/web/assets/app-DoGr2Zi7.js +0 -2
  219. package/web/assets/channel-uMEjV8qc.js +0 -1
  220. package/web/assets/classDiagram-OUVF2IWQ-fbZocVor.js +0 -1
  221. package/web/assets/classDiagram-v2-EOCWNBFH-fbZocVor.js +0 -1
  222. package/web/assets/client-C6Nxdfif.js +0 -2
  223. package/web/assets/client-CXYfBko5.js +0 -17
  224. package/web/assets/client-CoKtOq-X.js +0 -7
  225. package/web/assets/client-DyRQz5RU.js +0 -53
  226. package/web/assets/client-OY6HUzuD.js +0 -98
  227. package/web/assets/client-YOYWZIFi.js +0 -34
  228. package/web/assets/command-BugZBGXE.js +0 -38
  229. package/web/assets/folder-plugin-view-DsWR1zmQ.js +0 -6
  230. package/web/assets/index-BaRnbYRw.js +0 -1
  231. package/web/assets/index-BexmLxIP.css +0 -1
  232. package/web/assets/index-DDirZMgx.js +0 -1
  233. package/web/assets/note-card-DBfX6gW1.js +0 -6
  234. package/web/assets/select-D8cqNgcp.js +0 -30
  235. package/web/assets/stateDiagram-v2-6OUMAXLB-BpeGSCu9.js +0 -1
  236. package/web/assets/swimlanesDiagram-G3AALYLV-D9RjLKb9.js +0 -8
  237. package/web/assets/use-folder-filter-LQyXns5Q.js +0 -1
@@ -1,795 +0,0 @@
1
- import { Array, Effect, Order, pipe, Schema } from "effect"
2
-
3
- /** Normalized task status shown in the viewer. */
4
- export type TodoItemStatus = "open" | "blocked" | "done"
5
-
6
- /** One normalized task, shared by the task list rows and the task page. */
7
- export type TodoItemData = {
8
- id: string
9
- key?: string
10
- title: string
11
- description?: string
12
- type?: string
13
- steps?: ReadonlyArray<TodoItemStep>
14
- acceptanceCriteria?: ReadonlyArray<TodoItemStep>
15
- links?: ReadonlyArray<TodoItemLink>
16
- /** Folder-relative note paths and https references that accompany the task. */
17
- notes?: ReadonlyArray<string>
18
- relations?: ReadonlyArray<TodoItemRelation>
19
- status: TodoItemStatus
20
- priority: 1 | 2 | 3 | 4 | 5
21
- dueDate?: string
22
- overdue?: boolean
23
- source: string
24
- tags: string[]
25
- }
26
-
27
- export type TodoItemStep = {
28
- id: string
29
- text: string
30
- done: boolean
31
- }
32
-
33
- export type TodoItemLink = {
34
- id: string
35
- provider: "github" | "linear" | "jira" | "web"
36
- kind:
37
- | "issue"
38
- | "pull_request"
39
- | "commit"
40
- | "repository"
41
- | "project"
42
- | "document"
43
- | "design"
44
- | "discussion"
45
- | "external"
46
- relation: string
47
- url: string
48
- repository?: string | null
49
- number?: number | null
50
- title?: string | null
51
- }
52
-
53
- export type TodoItemRelation = {
54
- type: string
55
- targetTaskId: string
56
- description?: string | null
57
- }
58
-
59
- export type TodoFilter = "all" | TodoItemStatus | "overdue"
60
-
61
- /** Multi-value filter selection; an empty array means "no filter". */
62
- export type TodoFilterSelection = readonly string[]
63
-
64
- export type TodoSource = Schema.Schema.Type<typeof TodoSourceSchema>
65
- export type JsonTodo = Schema.Schema.Type<typeof JsonTodoSchema>
66
- export type TodoDataset = Schema.Schema.Type<typeof TodoDatasetSchema>
67
-
68
- export type TodoState = {
69
- readonly dataset: TodoDataset | null
70
- readonly selectedSource: string
71
- readonly filter: TodoFilter
72
- /** Selected tag chips; empty shows tasks with any tags. */
73
- readonly tag: TodoFilterSelection
74
- /** Selected assignee ids; empty shows tasks for any agent. */
75
- readonly assignee: TodoFilterSelection
76
- readonly query: string
77
- }
78
-
79
- export type UiTodoItem = TodoItemData & {
80
- readonly jsonTodo: JsonTodo
81
- readonly overdue: boolean
82
- }
83
-
84
- export type TodoDatasetOptions = {
85
- readonly dataUrl?: string
86
- readonly schemaUrl?: string
87
- readonly path?: string
88
- }
89
-
90
- const DATASET_URL = "/data/todos.json"
91
- const LOCAL_SCHEMA_URL = "https://raggle.co/schemas/tasks.json"
92
-
93
- const TaskTypeSchema = Schema.Literal(
94
- "task",
95
- "bug",
96
- "feature",
97
- "chore",
98
- "spike",
99
- "epic",
100
- "milestone",
101
- )
102
- const TaskStatusSchema = Schema.Literal(
103
- "todo",
104
- "in_progress",
105
- "blocked",
106
- "review",
107
- "done",
108
- "canceled",
109
- )
110
- const TaskResolutionSchema = Schema.Literal(
111
- "completed",
112
- "duplicate",
113
- "wont_do",
114
- "not_reproducible",
115
- "moved",
116
- )
117
- const TaskRelationTypeSchema = Schema.Literal(
118
- "blocks",
119
- "blocked_by",
120
- "related_to",
121
- "duplicates",
122
- "duplicated_by",
123
- "parent_of",
124
- "child_of",
125
- )
126
- const TaskLinkProviderSchema = Schema.Literal("github", "linear", "jira", "web")
127
- const TaskLinkKindSchema = Schema.Literal(
128
- "issue",
129
- "pull_request",
130
- "commit",
131
- "repository",
132
- "project",
133
- "document",
134
- "design",
135
- "discussion",
136
- "external",
137
- )
138
- const TaskLinkRelationSchema = Schema.Literal(
139
- "references",
140
- "implemented_by",
141
- "fixes",
142
- "reported_by",
143
- "spec",
144
- "design",
145
- "review",
146
- "deployment",
147
- "blocked_by",
148
- )
149
-
150
- const JsonTodoSourceSchema = Schema.Struct({
151
- id: Schema.String,
152
- title: Schema.String,
153
- })
154
-
155
- const JsonTodoStepSchema = Schema.Struct({
156
- id: Schema.String,
157
- text: Schema.String,
158
- done: Schema.Boolean,
159
- })
160
-
161
- const AcceptanceCriterionSchema = Schema.Struct({
162
- id: Schema.String,
163
- text: Schema.String,
164
- done: Schema.optionalWith(Schema.Boolean, { default: () => false }),
165
- })
166
-
167
- const TaskEstimateSchema = Schema.Struct({
168
- points: Schema.optionalWith(Schema.NullOr(Schema.Number), {
169
- default: () => null,
170
- }),
171
- minutes: Schema.optionalWith(Schema.NullOr(Schema.Number), {
172
- default: () => null,
173
- }),
174
- confidence: Schema.optionalWith(
175
- Schema.NullOr(Schema.Literal("low", "medium", "high")),
176
- { default: () => null },
177
- ),
178
- })
179
-
180
- const TaskRelationSchema = Schema.Struct({
181
- type: TaskRelationTypeSchema,
182
- targetTaskId: Schema.String,
183
- description: Schema.optionalWith(Schema.NullOr(Schema.String), {
184
- default: () => null,
185
- }),
186
- })
187
-
188
- const TaskLinkSnapshotSchema = Schema.Struct({
189
- state: Schema.optionalWith(Schema.NullOr(Schema.String), {
190
- default: () => null,
191
- }),
192
- draft: Schema.optionalWith(Schema.NullOr(Schema.Boolean), {
193
- default: () => null,
194
- }),
195
- checks: Schema.optionalWith(Schema.NullOr(Schema.String), {
196
- default: () => null,
197
- }),
198
- updatedAt: Schema.optionalWith(Schema.NullOr(Schema.String), {
199
- default: () => null,
200
- }),
201
- })
202
-
203
- const TaskLinkSyncSchema = Schema.Struct({
204
- mode: Schema.optionalWith(Schema.Literal("manual", "read", "write"), {
205
- default: () => "manual" as const,
206
- }),
207
- checkedAt: Schema.optionalWith(Schema.NullOr(Schema.String), {
208
- default: () => null,
209
- }),
210
- })
211
-
212
- const TaskLinkSchema = Schema.Struct({
213
- id: Schema.String,
214
- provider: TaskLinkProviderSchema,
215
- kind: TaskLinkKindSchema,
216
- relation: TaskLinkRelationSchema,
217
- url: Schema.String,
218
- repository: Schema.optionalWith(Schema.NullOr(Schema.String), {
219
- default: () => null,
220
- }),
221
- number: Schema.optionalWith(Schema.NullOr(Schema.Number), {
222
- default: () => null,
223
- }),
224
- title: Schema.optionalWith(Schema.NullOr(Schema.String), {
225
- default: () => null,
226
- }),
227
- snapshot: Schema.optionalWith(Schema.NullOr(TaskLinkSnapshotSchema), {
228
- default: () => null,
229
- }),
230
- sync: Schema.optionalWith(Schema.NullOr(TaskLinkSyncSchema), {
231
- default: () => null,
232
- }),
233
- })
234
-
235
- const TaskCommentSchema = Schema.Struct({
236
- id: Schema.String,
237
- authorId: Schema.optionalWith(Schema.NullOr(Schema.String), {
238
- default: () => null,
239
- }),
240
- body: Schema.String,
241
- createdAt: Schema.String,
242
- })
243
-
244
- const TaskDecisionSchema = Schema.Struct({
245
- id: Schema.String,
246
- title: Schema.String,
247
- outcome: Schema.String,
248
- decidedAt: Schema.optionalWith(Schema.NullOr(Schema.String), {
249
- default: () => null,
250
- }),
251
- })
252
-
253
- const TaskCustomFieldSchema = Schema.Struct({
254
- id: Schema.String,
255
- value: Schema.Unknown,
256
- })
257
-
258
- const TodoSourceSchema = Schema.Struct({
259
- id: Schema.String,
260
- title: Schema.String,
261
- total: Schema.Number,
262
- open: Schema.Number,
263
- })
264
-
265
- const JsonTodoSchema = Schema.Struct({
266
- id: Schema.optionalWith(Schema.String, { default: () => "" }),
267
- key: Schema.optionalWith(Schema.String, { default: () => "" }),
268
- index: Schema.optionalWith(Schema.Number, { default: () => 0 }),
269
- rank: Schema.optionalWith(Schema.String, { default: () => "" }),
270
- name: Schema.String,
271
- description: Schema.optionalWith(Schema.NullOr(Schema.String), {
272
- default: () => "",
273
- }),
274
- type: Schema.optionalWith(TaskTypeSchema, { default: () => "task" as const }),
275
- status: Schema.optionalWith(TaskStatusSchema, {
276
- default: () => "todo" as const,
277
- }),
278
- resolution: Schema.optionalWith(Schema.NullOr(TaskResolutionSchema), {
279
- default: () => null,
280
- }),
281
- done: Schema.optionalWith(Schema.Boolean, { default: () => false }),
282
- priority: Schema.optionalWith(Schema.NullOr(Schema.Number), {
283
- default: () => 5,
284
- }),
285
- deadline: Schema.optionalWith(Schema.NullOr(Schema.String), {
286
- default: () => null,
287
- }),
288
- overdue: Schema.optionalWith(Schema.Boolean, { default: () => false }),
289
- reporterId: Schema.optionalWith(Schema.NullOr(Schema.String), {
290
- default: () => null,
291
- }),
292
- assigneeIds: Schema.optionalWith(Schema.Array(Schema.String), {
293
- default: () => [],
294
- }),
295
- estimate: Schema.optionalWith(Schema.NullOr(TaskEstimateSchema), {
296
- default: () => null,
297
- }),
298
- projectIds: Schema.optionalWith(Schema.Array(Schema.String), {
299
- default: () => [],
300
- }),
301
- cycleId: Schema.optionalWith(Schema.NullOr(Schema.String), {
302
- default: () => null,
303
- }),
304
- milestoneIds: Schema.optionalWith(Schema.Array(Schema.String), {
305
- default: () => [],
306
- }),
307
- componentIds: Schema.optionalWith(Schema.Array(Schema.String), {
308
- default: () => [],
309
- }),
310
- areaIds: Schema.optionalWith(Schema.Array(Schema.String), {
311
- default: () => [],
312
- }),
313
- parentId: Schema.optionalWith(Schema.NullOr(Schema.String), {
314
- default: () => null,
315
- }),
316
- relations: Schema.optionalWith(Schema.Array(TaskRelationSchema), {
317
- default: () => [],
318
- }),
319
- links: Schema.optionalWith(Schema.Array(TaskLinkSchema), {
320
- default: () => [],
321
- }),
322
- notes: Schema.optionalWith(Schema.Array(Schema.String), {
323
- default: () => [],
324
- }),
325
- acceptanceCriteria: Schema.optionalWith(
326
- Schema.Array(AcceptanceCriterionSchema),
327
- { default: () => [] },
328
- ),
329
- steps: Schema.optionalWith(Schema.Array(JsonTodoStepSchema), {
330
- default: () => [],
331
- }),
332
- comments: Schema.optionalWith(Schema.Array(TaskCommentSchema), {
333
- default: () => [],
334
- }),
335
- decisions: Schema.optionalWith(Schema.Array(TaskDecisionSchema), {
336
- default: () => [],
337
- }),
338
- createdAt: Schema.optionalWith(Schema.NullOr(Schema.String), {
339
- default: () => null,
340
- }),
341
- updatedAt: Schema.optionalWith(Schema.NullOr(Schema.String), {
342
- default: () => null,
343
- }),
344
- startedAt: Schema.optionalWith(Schema.NullOr(Schema.String), {
345
- default: () => null,
346
- }),
347
- completedAt: Schema.optionalWith(Schema.NullOr(Schema.String), {
348
- default: () => null,
349
- }),
350
- canceledAt: Schema.optionalWith(Schema.NullOr(Schema.String), {
351
- default: () => null,
352
- }),
353
- tags: Schema.optionalWith(Schema.Array(Schema.String), { default: () => [] }),
354
- customFields: Schema.optionalWith(Schema.Array(TaskCustomFieldSchema), {
355
- default: () => [],
356
- }),
357
- reminder: Schema.optionalWith(Schema.Unknown, { default: () => null }),
358
- source: Schema.optionalWith(JsonTodoSourceSchema, {
359
- default: () => ({ id: "todos", title: "Todos" }),
360
- }),
361
- })
362
-
363
- const PersonSchema = Schema.Struct({
364
- id: Schema.String,
365
- name: Schema.String,
366
- email: Schema.optionalWith(Schema.NullOr(Schema.String), {
367
- default: () => null,
368
- }),
369
- url: Schema.optionalWith(Schema.NullOr(Schema.String), {
370
- default: () => null,
371
- }),
372
- })
373
-
374
- const ProjectRefSchema = Schema.Struct({
375
- id: Schema.String,
376
- name: Schema.String,
377
- description: Schema.optionalWith(Schema.NullOr(Schema.String), {
378
- default: () => null,
379
- }),
380
- })
381
-
382
- const CycleSchema = Schema.Struct({
383
- id: Schema.String,
384
- name: Schema.String,
385
- startsAt: Schema.optionalWith(Schema.NullOr(Schema.String), {
386
- default: () => null,
387
- }),
388
- endsAt: Schema.optionalWith(Schema.NullOr(Schema.String), {
389
- default: () => null,
390
- }),
391
- })
392
-
393
- const NamedCatalogSchema = Schema.Struct({
394
- id: Schema.String,
395
- name: Schema.String,
396
- })
397
-
398
- const MilestoneSchema = Schema.Struct({
399
- id: Schema.String,
400
- name: Schema.String,
401
- dueAt: Schema.optionalWith(Schema.NullOr(Schema.String), {
402
- default: () => null,
403
- }),
404
- })
405
-
406
- const ComponentSchema = Schema.Struct({
407
- id: Schema.String,
408
- name: Schema.String,
409
- ownerId: Schema.optionalWith(Schema.NullOr(Schema.String), {
410
- default: () => null,
411
- }),
412
- })
413
-
414
- const IntegrationSchema = Schema.Struct({
415
- id: Schema.String,
416
- provider: TaskLinkProviderSchema,
417
- name: Schema.String,
418
- repository: Schema.optionalWith(Schema.NullOr(Schema.String), {
419
- default: () => null,
420
- }),
421
- baseUrl: Schema.optionalWith(Schema.NullOr(Schema.String), {
422
- default: () => null,
423
- }),
424
- syncMode: Schema.optionalWith(Schema.Literal("manual", "read", "write"), {
425
- default: () => "manual" as const,
426
- }),
427
- })
428
-
429
- const CustomFieldDefinitionSchema = Schema.Struct({
430
- id: Schema.String,
431
- name: Schema.String,
432
- type: Schema.Literal(
433
- "text",
434
- "number",
435
- "date",
436
- "select",
437
- "multi_select",
438
- "boolean",
439
- "url",
440
- ),
441
- options: Schema.optionalWith(Schema.Array(Schema.String), {
442
- default: () => [],
443
- }),
444
- })
445
-
446
- const AutomationSchema = Schema.Struct({
447
- id: Schema.String,
448
- name: Schema.String,
449
- trigger: Schema.Literal(
450
- "status_changed",
451
- "link_changed",
452
- "deadline_overdue",
453
- "task_created",
454
- "task_updated",
455
- ),
456
- enabled: Schema.optionalWith(Schema.Boolean, { default: () => true }),
457
- description: Schema.optionalWith(Schema.NullOr(Schema.String), {
458
- default: () => null,
459
- }),
460
- })
461
-
462
- export const TodoDatasetSchema = Schema.Struct({
463
- $schema: Schema.String,
464
- version: Schema.Literal(1, 2),
465
- project: Schema.Struct({
466
- name: Schema.String,
467
- }),
468
- sources: Schema.Array(TodoSourceSchema),
469
- people: Schema.optionalWith(Schema.Array(PersonSchema), {
470
- default: () => [],
471
- }),
472
- projects: Schema.optionalWith(Schema.Array(ProjectRefSchema), {
473
- default: () => [],
474
- }),
475
- cycles: Schema.optionalWith(Schema.Array(CycleSchema), { default: () => [] }),
476
- milestones: Schema.optionalWith(Schema.Array(MilestoneSchema), {
477
- default: () => [],
478
- }),
479
- components: Schema.optionalWith(Schema.Array(ComponentSchema), {
480
- default: () => [],
481
- }),
482
- areas: Schema.optionalWith(Schema.Array(NamedCatalogSchema), {
483
- default: () => [],
484
- }),
485
- integrations: Schema.optionalWith(Schema.Array(IntegrationSchema), {
486
- default: () => [],
487
- }),
488
- customFields: Schema.optionalWith(Schema.Array(CustomFieldDefinitionSchema), {
489
- default: () => [],
490
- }),
491
- automations: Schema.optionalWith(Schema.Array(AutomationSchema), {
492
- default: () => [],
493
- }),
494
- tasks: Schema.Array(JsonTodoSchema),
495
- summary: Schema.Struct({
496
- open: Schema.Number,
497
- completed: Schema.Number,
498
- sources: Schema.Number,
499
- }),
500
- })
501
-
502
- /** Overdue tasks first; overdue is the most urgent state. */
503
- const overdueOrder = Order.mapInput(
504
- Order.boolean,
505
- (item: UiTodoItem) => !item.overdue,
506
- )
507
- const priorityOrder = Order.mapInput(
508
- Order.number,
509
- (item: UiTodoItem) => item.priority,
510
- )
511
- const rankOrder = Order.mapInput(
512
- Order.string,
513
- (item: UiTodoItem) => item.jsonTodo.rank || "9999",
514
- )
515
- const deadlineOrder = Order.mapInput(
516
- Order.string,
517
- (item: UiTodoItem) => item.dueDate ?? "9999",
518
- )
519
- const titleOrder = Order.mapInput(Order.string, (item: UiTodoItem) =>
520
- item.title.toLowerCase(),
521
- )
522
-
523
- export const initialState: TodoState = {
524
- dataset: null,
525
- selectedSource: "*",
526
- filter: "open",
527
- tag: [],
528
- assignee: [],
529
- query: "",
530
- }
531
-
532
- export const decodeTodoDataset = (value: unknown) =>
533
- Schema.decodeUnknownSync(TodoDatasetSchema)(value)
534
-
535
- export const fetchTodoDataset = (options: TodoDatasetOptions = {}) =>
536
- Effect.tryPromise({
537
- try: async () => {
538
- const datasetUrl = datasetUrlFromOptions(options)
539
- const datasetResponse = await fetch(datasetUrl)
540
- if (!datasetResponse.ok) {
541
- throw new Error(`Could not load ${datasetUrl}.`)
542
- }
543
- const rawDataset: unknown = await datasetResponse.json()
544
- const dataset = Schema.decodeUnknownSync(TodoDatasetSchema)(rawDataset)
545
- validateDatasetLinks(dataset)
546
-
547
- const schemaUrl =
548
- options.schemaUrl ?? dataset["$schema"] ?? LOCAL_SCHEMA_URL
549
- const schemaResponse = await fetch(schemaUrl)
550
- if (!schemaResponse.ok) {
551
- throw new Error(`Could not load todo schema at ${schemaUrl}.`)
552
- }
553
- const jsonSchema = (await schemaResponse.json()) as {
554
- readonly $id?: string
555
- }
556
- if (
557
- jsonSchema["$id"] &&
558
- isRemoteUrl(schemaUrl) &&
559
- jsonSchema["$id"] !== schemaUrl
560
- ) {
561
- throw new Error(
562
- `Todo schema id mismatch: expected ${schemaUrl}, got ${jsonSchema["$id"]}.`,
563
- )
564
- }
565
-
566
- return dataset
567
- },
568
- catch: (error) =>
569
- error instanceof Error ? error : new Error(String(error)),
570
- })
571
-
572
- const datasetUrlFromOptions = (options: TodoDatasetOptions) => {
573
- const url = new URL(options.dataUrl ?? DATASET_URL, window.location.origin)
574
- if (options.path) {
575
- url.searchParams.set("path", options.path)
576
- }
577
- return `${url.pathname}${url.search}`
578
- }
579
-
580
- export const visibleTodos = (state: TodoState) =>
581
- Effect.succeed(
582
- pipe(
583
- todosForDataset(state.dataset),
584
- Array.filter(
585
- (item) =>
586
- state.selectedSource === "*" ||
587
- item.jsonTodo.source.id === state.selectedSource,
588
- ),
589
- Array.filter((item) => {
590
- if (state.filter === "all") return true
591
- if (state.filter === "overdue")
592
- return item.overdue && item.status !== "done"
593
- return item.status === state.filter
594
- }),
595
- Array.filter((item) =>
596
- state.tag.length
597
- ? item.tags.some((tag) => state.tag.includes(tag))
598
- : true,
599
- ),
600
- Array.filter((item) =>
601
- state.assignee.length
602
- ? item.jsonTodo.assigneeIds.some((id) => state.assignee.includes(id))
603
- : true,
604
- ),
605
- Array.filter((item) => {
606
- const query = state.query.trim().toLowerCase()
607
- if (!query) return true
608
- return [
609
- item.key,
610
- item.title,
611
- item.description,
612
- item.source,
613
- item.type,
614
- item.status,
615
- ...item.tags,
616
- ...(item.steps ?? []).map((step) => step.text),
617
- ...(item.acceptanceCriteria ?? []).map((criterion) => criterion.text),
618
- ...(item.links ?? []).flatMap(
619
- (link) =>
620
- [link.title, link.url, link.repository].filter(
621
- Boolean,
622
- ) as Array<string>,
623
- ),
624
- ...(item.notes ?? []),
625
- ]
626
- .filter(Boolean)
627
- .join(" ")
628
- .toLowerCase()
629
- .includes(query)
630
- }),
631
- Array.sort(
632
- Order.combineMany(overdueOrder, [
633
- priorityOrder,
634
- rankOrder,
635
- deadlineOrder,
636
- titleOrder,
637
- ]),
638
- ),
639
- ),
640
- )
641
-
642
- export const allTodos = (state: TodoState) =>
643
- Effect.succeed(todosForDataset(state.dataset))
644
-
645
- export type TodoAgent = {
646
- readonly id: string
647
- readonly name: string
648
- }
649
-
650
- /** Agents (assignees) used by a dataset's tasks, sorted by name. People
651
- * never assigned keep their name; ids without a person entry show raw. */
652
- export const datasetAgents = (dataset: TodoDataset | null): TodoAgent[] => {
653
- const tasks = dataset?.tasks ?? []
654
- const used = (id: string) =>
655
- tasks.some((task) => task.assigneeIds.includes(id))
656
- const names = new Map<string, string>()
657
- for (const person of dataset?.people ?? []) {
658
- if (used(person.id)) names.set(person.id, person.name)
659
- }
660
- for (const task of tasks) {
661
- for (const id of task.assigneeIds) {
662
- if (!names.has(id)) names.set(id, id)
663
- }
664
- }
665
- return [...names]
666
- .map(([id, name]) => ({ id, name }))
667
- .sort((a, b) => a.name.localeCompare(b.name))
668
- }
669
-
670
- /** UI-ready task items for a dataset, without filtering or sorting. */
671
- export const todoItems = (dataset: TodoDataset | null): UiTodoItem[] =>
672
- todosForDataset(dataset)
673
-
674
- export const datasetSummary = (dataset: TodoDataset | null) =>
675
- Effect.succeed({
676
- open: dataset?.tasks.filter(isOpenTask).length ?? 0,
677
- completed: dataset?.tasks.filter(isDone).length ?? 0,
678
- sources: dataset?.summary.sources ?? 0,
679
- overdue:
680
- dataset?.tasks.filter((task) => !isDone(task) && task.overdue).length ??
681
- 0,
682
- blocked:
683
- dataset?.tasks.filter((task) => !isDone(task) && isBlocked(task))
684
- .length ?? 0,
685
- })
686
-
687
- const toUiTodo = (task: JsonTodo): UiTodoItem => {
688
- const status: TodoItemStatus = isDone(task)
689
- ? "done"
690
- : isBlocked(task)
691
- ? "blocked"
692
- : "open"
693
- return {
694
- id: task.id,
695
- key: task.key || task.id,
696
- title: task.name,
697
- description: task.description ?? undefined,
698
- type: task.type,
699
- steps: [...(task.steps ?? [])],
700
- acceptanceCriteria: [...(task.acceptanceCriteria ?? [])],
701
- links: [...(task.links ?? [])],
702
- notes: [...(task.notes ?? [])],
703
- relations: [...(task.relations ?? [])],
704
- status,
705
- priority: normalizePriority(task.priority),
706
- dueDate: task.deadline ?? undefined,
707
- source: task.source.id,
708
- tags: [...task.tags],
709
- jsonTodo: task,
710
- overdue: !isDone(task) && (task.overdue || isOverdue(task.deadline)),
711
- }
712
- }
713
-
714
- const todosForDataset = (dataset: TodoDataset | null) =>
715
- pipe(dataset?.tasks ?? [], Array.map(toUiTodo))
716
-
717
- const isDone = (task: JsonTodo) => task.status === "done"
718
- const isOpenTask = (task: JsonTodo) =>
719
- !isDone(task) && task.status !== "canceled"
720
- const isBlocked = (task: JsonTodo) =>
721
- task.status === "blocked" ||
722
- task.relations.some((relation) => relation.type === "blocked_by")
723
-
724
- const isRemoteUrl = (value: string) => /^https?:\/\//i.test(value)
725
-
726
- const normalizePriority = (value: number | null): TodoItemData["priority"] => {
727
- if (value === 1 || value === 2 || value === 3 || value === 4 || value === 5) {
728
- return value
729
- }
730
- return 5
731
- }
732
-
733
- const isOverdue = (deadline: string | null) => {
734
- if (!deadline) return false
735
- const date = new Date(`${deadline}T12:00:00`)
736
- if (Number.isNaN(date.valueOf())) return false
737
- const today = new Date()
738
- today.setHours(0, 0, 0, 0)
739
- date.setHours(0, 0, 0, 0)
740
- return date < today
741
- }
742
-
743
- const validateDatasetLinks = (dataset: TodoDataset) => {
744
- const errors: Array<string> = []
745
- for (const task of dataset.tasks) {
746
- for (const link of task.links) {
747
- const error = validateLink(link)
748
- if (error) {
749
- errors.push(`${task.key || task.id} ${link.id}: ${error}`)
750
- }
751
- }
752
- }
753
- if (errors.length) {
754
- throw new Error(
755
- `Invalid todo links:\n${errors.map((error) => `- ${error}`).join("\n")}`,
756
- )
757
- }
758
- }
759
-
760
- const validateLink = (link: JsonTodo["links"][number]) => {
761
- let url: URL
762
- try {
763
- url = new URL(link.url)
764
- } catch {
765
- return "url is not a valid absolute URL"
766
- }
767
-
768
- const isLocalhost =
769
- url.hostname === "localhost" ||
770
- url.hostname === "127.0.0.1" ||
771
- url.hostname === "::1"
772
- if (url.username || url.password)
773
- return "url must not contain embedded credentials"
774
- if (url.protocol !== "https:" && !(url.protocol === "http:" && isLocalhost)) {
775
- return "url must use https, except localhost development links"
776
- }
777
- if (link.provider === "github" && url.hostname !== "github.com") {
778
- return "github links must use github.com"
779
- }
780
- if (
781
- link.provider === "linear" &&
782
- url.hostname !== "linear.app" &&
783
- !url.hostname.endsWith(".linear.app")
784
- ) {
785
- return "linear links must use linear.app"
786
- }
787
- if (
788
- link.provider === "jira" &&
789
- !url.hostname.endsWith(".atlassian.net") &&
790
- url.hostname !== "jira.atlassian.com"
791
- ) {
792
- return "jira links must use an Atlassian Jira host"
793
- }
794
- return null
795
- }