@homeflare/alchemy 0.1.0

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 (417) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/dist/cloudflare/client.d.ts +45 -0
  4. package/dist/cloudflare/client.d.ts.map +1 -0
  5. package/dist/cloudflare/index.d.ts +11 -0
  6. package/dist/cloudflare/index.d.ts.map +1 -0
  7. package/dist/cloudflare/index.js +129 -0
  8. package/dist/cloudflare/index.js.map +13 -0
  9. package/dist/cloudflare/lock-rules.d.ts +50 -0
  10. package/dist/cloudflare/lock-rules.d.ts.map +1 -0
  11. package/dist/cloudflare/providers.d.ts +19 -0
  12. package/dist/cloudflare/providers.d.ts.map +1 -0
  13. package/dist/cloudflare/r2-bucket-lock.d.ts +100 -0
  14. package/dist/cloudflare/r2-bucket-lock.d.ts.map +1 -0
  15. package/dist/forgejo/branch-protection-form.d.ts +19 -0
  16. package/dist/forgejo/branch-protection-form.d.ts.map +1 -0
  17. package/dist/forgejo/branch-protection.d.ts +47 -0
  18. package/dist/forgejo/branch-protection.d.ts.map +1 -0
  19. package/dist/forgejo/client.d.ts +37 -0
  20. package/dist/forgejo/client.d.ts.map +1 -0
  21. package/dist/forgejo/index.d.ts +18 -0
  22. package/dist/forgejo/index.d.ts.map +1 -0
  23. package/dist/forgejo/index.js +479 -0
  24. package/dist/forgejo/index.js.map +21 -0
  25. package/dist/forgejo/org-actions-secrets.d.ts +45 -0
  26. package/dist/forgejo/org-actions-secrets.d.ts.map +1 -0
  27. package/dist/forgejo/org-label.d.ts +46 -0
  28. package/dist/forgejo/org-label.d.ts.map +1 -0
  29. package/dist/forgejo/org-team.d.ts +42 -0
  30. package/dist/forgejo/org-team.d.ts.map +1 -0
  31. package/dist/forgejo/repo-webhook-form.d.ts +27 -0
  32. package/dist/forgejo/repo-webhook-form.d.ts.map +1 -0
  33. package/dist/forgejo/repo-webhook.d.ts +46 -0
  34. package/dist/forgejo/repo-webhook.d.ts.map +1 -0
  35. package/dist/forgejo/repository.d.ts +45 -0
  36. package/dist/forgejo/repository.d.ts.map +1 -0
  37. package/dist/forgejo/resource.d.ts +83 -0
  38. package/dist/forgejo/resource.d.ts.map +1 -0
  39. package/dist/forgejo/team-member.d.ts +39 -0
  40. package/dist/forgejo/team-member.d.ts.map +1 -0
  41. package/dist/forgejo/values.d.ts +24 -0
  42. package/dist/forgejo/values.d.ts.map +1 -0
  43. package/dist/index.d.ts +18 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +3 -0
  46. package/dist/index.js.map +9 -0
  47. package/dist/openbao/auth-role-form.d.ts +38 -0
  48. package/dist/openbao/auth-role-form.d.ts.map +1 -0
  49. package/dist/openbao/auth-role.d.ts +20 -0
  50. package/dist/openbao/auth-role.d.ts.map +1 -0
  51. package/dist/openbao/bao-address.d.ts +72 -0
  52. package/dist/openbao/bao-address.d.ts.map +1 -0
  53. package/dist/openbao/bao-http.d.ts +71 -0
  54. package/dist/openbao/bao-http.d.ts.map +1 -0
  55. package/dist/openbao/bao-status.d.ts +74 -0
  56. package/dist/openbao/bao-status.d.ts.map +1 -0
  57. package/dist/openbao/cloudflare-group-scope.d.ts +73 -0
  58. package/dist/openbao/cloudflare-group-scope.d.ts.map +1 -0
  59. package/dist/openbao/cloudflare-parity-catalog.d.ts +28 -0
  60. package/dist/openbao/cloudflare-parity-catalog.d.ts.map +1 -0
  61. package/dist/openbao/cloudflare-parity-snapshot.d.ts +31 -0
  62. package/dist/openbao/cloudflare-parity-snapshot.d.ts.map +1 -0
  63. package/dist/openbao/cloudflare-parity.d.ts +14 -0
  64. package/dist/openbao/cloudflare-parity.d.ts.map +1 -0
  65. package/dist/openbao/cloudflare-permission-groups-solve.d.ts +85 -0
  66. package/dist/openbao/cloudflare-permission-groups-solve.d.ts.map +1 -0
  67. package/dist/openbao/cloudflare-permission-groups.d.ts +87 -0
  68. package/dist/openbao/cloudflare-permission-groups.d.ts.map +1 -0
  69. package/dist/openbao/cloudflare-policy.d.ts +65 -0
  70. package/dist/openbao/cloudflare-policy.d.ts.map +1 -0
  71. package/dist/openbao/cloudflare-role-form.d.ts +110 -0
  72. package/dist/openbao/cloudflare-role-form.d.ts.map +1 -0
  73. package/dist/openbao/cloudflare-role-wire.d.ts +27 -0
  74. package/dist/openbao/cloudflare-role-wire.d.ts.map +1 -0
  75. package/dist/openbao/cloudflare-role.d.ts +35 -0
  76. package/dist/openbao/cloudflare-role.d.ts.map +1 -0
  77. package/dist/openbao/cloudflare-roles-config.d.ts +67 -0
  78. package/dist/openbao/cloudflare-roles-config.d.ts.map +1 -0
  79. package/dist/openbao/cloudflare-roles-expand.d.ts +76 -0
  80. package/dist/openbao/cloudflare-roles-expand.d.ts.map +1 -0
  81. package/dist/openbao/digest.d.ts +24 -0
  82. package/dist/openbao/digest.d.ts.map +1 -0
  83. package/dist/openbao/fake-bao.d.ts +40 -0
  84. package/dist/openbao/fake-bao.d.ts.map +1 -0
  85. package/dist/openbao/forgejo-bootstrap-parse.d.ts +46 -0
  86. package/dist/openbao/forgejo-bootstrap-parse.d.ts.map +1 -0
  87. package/dist/openbao/forgejo-bootstrap.d.ts +29 -0
  88. package/dist/openbao/forgejo-bootstrap.d.ts.map +1 -0
  89. package/dist/openbao/index.d.ts +18 -0
  90. package/dist/openbao/index.d.ts.map +1 -0
  91. package/dist/openbao/index.js +1158 -0
  92. package/dist/openbao/index.js.map +33 -0
  93. package/dist/openbao/mount-form.d.ts +60 -0
  94. package/dist/openbao/mount-form.d.ts.map +1 -0
  95. package/dist/openbao/mount-wire.d.ts +24 -0
  96. package/dist/openbao/mount-wire.d.ts.map +1 -0
  97. package/dist/openbao/mount.d.ts +20 -0
  98. package/dist/openbao/mount.d.ts.map +1 -0
  99. package/dist/openbao/pki-role-form.d.ts +118 -0
  100. package/dist/openbao/pki-role-form.d.ts.map +1 -0
  101. package/dist/openbao/pki-role.d.ts +50 -0
  102. package/dist/openbao/pki-role.d.ts.map +1 -0
  103. package/dist/openbao/policy-assembly.d.ts +13 -0
  104. package/dist/openbao/policy-assembly.d.ts.map +1 -0
  105. package/dist/openbao/policy-wire.d.ts +41 -0
  106. package/dist/openbao/policy-wire.d.ts.map +1 -0
  107. package/dist/openbao/policy.d.ts +83 -0
  108. package/dist/openbao/policy.d.ts.map +1 -0
  109. package/dist/openbao/proxmox-role-form.d.ts +92 -0
  110. package/dist/openbao/proxmox-role-form.d.ts.map +1 -0
  111. package/dist/openbao/proxmox-role.d.ts +47 -0
  112. package/dist/openbao/proxmox-role.d.ts.map +1 -0
  113. package/dist/openbao/ssh-role-form.d.ts +99 -0
  114. package/dist/openbao/ssh-role-form.d.ts.map +1 -0
  115. package/dist/openbao/ssh-role.d.ts +43 -0
  116. package/dist/openbao/ssh-role.d.ts.map +1 -0
  117. package/dist/proxmox/acl.d.ts +72 -0
  118. package/dist/proxmox/acl.d.ts.map +1 -0
  119. package/dist/proxmox/api-token-form.d.ts +59 -0
  120. package/dist/proxmox/api-token-form.d.ts.map +1 -0
  121. package/dist/proxmox/api-token.d.ts +159 -0
  122. package/dist/proxmox/api-token.d.ts.map +1 -0
  123. package/dist/proxmox/backup-job.d.ts +112 -0
  124. package/dist/proxmox/backup-job.d.ts.map +1 -0
  125. package/dist/proxmox/ceph-daemon-form.d.ts +70 -0
  126. package/dist/proxmox/ceph-daemon-form.d.ts.map +1 -0
  127. package/dist/proxmox/ceph-daemon.d.ts +162 -0
  128. package/dist/proxmox/ceph-daemon.d.ts.map +1 -0
  129. package/dist/proxmox/ceph-flag.d.ts +131 -0
  130. package/dist/proxmox/ceph-flag.d.ts.map +1 -0
  131. package/dist/proxmox/ceph-fs-wire.d.ts +106 -0
  132. package/dist/proxmox/ceph-fs-wire.d.ts.map +1 -0
  133. package/dist/proxmox/ceph-fs.d.ts +124 -0
  134. package/dist/proxmox/ceph-fs.d.ts.map +1 -0
  135. package/dist/proxmox/ceph-osd-tree.d.ts +154 -0
  136. package/dist/proxmox/ceph-osd-tree.d.ts.map +1 -0
  137. package/dist/proxmox/ceph-osd-write.d.ts +36 -0
  138. package/dist/proxmox/ceph-osd-write.d.ts.map +1 -0
  139. package/dist/proxmox/ceph-osd.d.ts +102 -0
  140. package/dist/proxmox/ceph-osd.d.ts.map +1 -0
  141. package/dist/proxmox/ceph-pool-form.d.ts +217 -0
  142. package/dist/proxmox/ceph-pool-form.d.ts.map +1 -0
  143. package/dist/proxmox/ceph-pool-settle.d.ts +39 -0
  144. package/dist/proxmox/ceph-pool-settle.d.ts.map +1 -0
  145. package/dist/proxmox/ceph-pool.d.ts +48 -0
  146. package/dist/proxmox/ceph-pool.d.ts.map +1 -0
  147. package/dist/proxmox/client.d.ts +64 -0
  148. package/dist/proxmox/client.d.ts.map +1 -0
  149. package/dist/proxmox/credentials.d.ts +153 -0
  150. package/dist/proxmox/credentials.d.ts.map +1 -0
  151. package/dist/proxmox/firewall-alias-form.d.ts +98 -0
  152. package/dist/proxmox/firewall-alias-form.d.ts.map +1 -0
  153. package/dist/proxmox/firewall-alias.d.ts +141 -0
  154. package/dist/proxmox/firewall-alias.d.ts.map +1 -0
  155. package/dist/proxmox/generated/pbs.d.ts +388 -0
  156. package/dist/proxmox/generated/pbs.d.ts.map +1 -0
  157. package/dist/proxmox/generated/pve.d.ts +4713 -0
  158. package/dist/proxmox/generated/pve.d.ts.map +1 -0
  159. package/dist/proxmox/group.d.ts +104 -0
  160. package/dist/proxmox/group.d.ts.map +1 -0
  161. package/dist/proxmox/ha-resource.d.ts +104 -0
  162. package/dist/proxmox/ha-resource.d.ts.map +1 -0
  163. package/dist/proxmox/ha-rule-form.d.ts +103 -0
  164. package/dist/proxmox/ha-rule-form.d.ts.map +1 -0
  165. package/dist/proxmox/ha-rule.d.ts +156 -0
  166. package/dist/proxmox/ha-rule.d.ts.map +1 -0
  167. package/dist/proxmox/index.d.ts +43 -0
  168. package/dist/proxmox/index.d.ts.map +1 -0
  169. package/dist/proxmox/index.js +2322 -0
  170. package/dist/proxmox/index.js.map +70 -0
  171. package/dist/proxmox/lease-cache.d.ts +98 -0
  172. package/dist/proxmox/lease-cache.d.ts.map +1 -0
  173. package/dist/proxmox/lxc.d.ts +59 -0
  174. package/dist/proxmox/lxc.d.ts.map +1 -0
  175. package/dist/proxmox/members.d.ts +56 -0
  176. package/dist/proxmox/members.d.ts.map +1 -0
  177. package/dist/proxmox/metric-server-form.d.ts +53 -0
  178. package/dist/proxmox/metric-server-form.d.ts.map +1 -0
  179. package/dist/proxmox/metric-server-otel.d.ts +49 -0
  180. package/dist/proxmox/metric-server-otel.d.ts.map +1 -0
  181. package/dist/proxmox/metric-server.d.ts +123 -0
  182. package/dist/proxmox/metric-server.d.ts.map +1 -0
  183. package/dist/proxmox/network-apply-read.d.ts +93 -0
  184. package/dist/proxmox/network-apply-read.d.ts.map +1 -0
  185. package/dist/proxmox/network-apply.d.ts +102 -0
  186. package/dist/proxmox/network-apply.d.ts.map +1 -0
  187. package/dist/proxmox/node-network-form.d.ts +128 -0
  188. package/dist/proxmox/node-network-form.d.ts.map +1 -0
  189. package/dist/proxmox/node-network.d.ts +173 -0
  190. package/dist/proxmox/node-network.d.ts.map +1 -0
  191. package/dist/proxmox/notification-target-form.d.ts +44 -0
  192. package/dist/proxmox/notification-target-form.d.ts.map +1 -0
  193. package/dist/proxmox/notification-target.d.ts +146 -0
  194. package/dist/proxmox/notification-target.d.ts.map +1 -0
  195. package/dist/proxmox/pbs-datastore-form.d.ts +147 -0
  196. package/dist/proxmox/pbs-datastore-form.d.ts.map +1 -0
  197. package/dist/proxmox/pbs-datastore-guard.d.ts +55 -0
  198. package/dist/proxmox/pbs-datastore-guard.d.ts.map +1 -0
  199. package/dist/proxmox/pbs-datastore.d.ts +140 -0
  200. package/dist/proxmox/pbs-datastore.d.ts.map +1 -0
  201. package/dist/proxmox/pbs-prune-job-form.d.ts +97 -0
  202. package/dist/proxmox/pbs-prune-job-form.d.ts.map +1 -0
  203. package/dist/proxmox/pbs-prune-job.d.ts +168 -0
  204. package/dist/proxmox/pbs-prune-job.d.ts.map +1 -0
  205. package/dist/proxmox/pbs-sync-job-form.d.ts +83 -0
  206. package/dist/proxmox/pbs-sync-job-form.d.ts.map +1 -0
  207. package/dist/proxmox/pbs-sync-job.d.ts +246 -0
  208. package/dist/proxmox/pbs-sync-job.d.ts.map +1 -0
  209. package/dist/proxmox/pbs-verify-job-form.d.ts +114 -0
  210. package/dist/proxmox/pbs-verify-job-form.d.ts.map +1 -0
  211. package/dist/proxmox/pbs-verify-job.d.ts +145 -0
  212. package/dist/proxmox/pbs-verify-job.d.ts.map +1 -0
  213. package/dist/proxmox/pool.d.ts +42 -0
  214. package/dist/proxmox/pool.d.ts.map +1 -0
  215. package/dist/proxmox/qemu.d.ts +46 -0
  216. package/dist/proxmox/qemu.d.ts.map +1 -0
  217. package/dist/proxmox/replication-job-form.d.ts +71 -0
  218. package/dist/proxmox/replication-job-form.d.ts.map +1 -0
  219. package/dist/proxmox/replication-job.d.ts +162 -0
  220. package/dist/proxmox/replication-job.d.ts.map +1 -0
  221. package/dist/proxmox/resource.d.ts +157 -0
  222. package/dist/proxmox/resource.d.ts.map +1 -0
  223. package/dist/proxmox/role.d.ts +97 -0
  224. package/dist/proxmox/role.d.ts.map +1 -0
  225. package/dist/proxmox/sdn-apply-read.d.ts +80 -0
  226. package/dist/proxmox/sdn-apply-read.d.ts.map +1 -0
  227. package/dist/proxmox/sdn-apply.d.ts +63 -0
  228. package/dist/proxmox/sdn-apply.d.ts.map +1 -0
  229. package/dist/proxmox/sdn-subnet-form.d.ts +111 -0
  230. package/dist/proxmox/sdn-subnet-form.d.ts.map +1 -0
  231. package/dist/proxmox/sdn-subnet.d.ts +161 -0
  232. package/dist/proxmox/sdn-subnet.d.ts.map +1 -0
  233. package/dist/proxmox/sdn-vnet.d.ts +113 -0
  234. package/dist/proxmox/sdn-vnet.d.ts.map +1 -0
  235. package/dist/proxmox/sdn-zone.d.ts +126 -0
  236. package/dist/proxmox/sdn-zone.d.ts.map +1 -0
  237. package/dist/proxmox/storage.d.ts +120 -0
  238. package/dist/proxmox/storage.d.ts.map +1 -0
  239. package/dist/proxmox/user.d.ts +107 -0
  240. package/dist/proxmox/user.d.ts.map +1 -0
  241. package/dist/proxmox/values.d.ts +134 -0
  242. package/dist/proxmox/values.d.ts.map +1 -0
  243. package/dist/proxmox/zfs-pool-write.d.ts +76 -0
  244. package/dist/proxmox/zfs-pool-write.d.ts.map +1 -0
  245. package/dist/proxmox/zfs-pool.d.ts +146 -0
  246. package/dist/proxmox/zfs-pool.d.ts.map +1 -0
  247. package/dist/talos/credentials.d.ts +30 -0
  248. package/dist/talos/credentials.d.ts.map +1 -0
  249. package/dist/talos/index.d.ts +15 -0
  250. package/dist/talos/index.d.ts.map +1 -0
  251. package/dist/talos/index.js +392 -0
  252. package/dist/talos/index.js.map +16 -0
  253. package/dist/talos/kubeconfig.d.ts +53 -0
  254. package/dist/talos/kubeconfig.d.ts.map +1 -0
  255. package/dist/talos/resource.d.ts +32 -0
  256. package/dist/talos/resource.d.ts.map +1 -0
  257. package/dist/talos/talos-bootstrap.d.ts +27 -0
  258. package/dist/talos/talos-bootstrap.d.ts.map +1 -0
  259. package/dist/talos/talos-cluster-health.d.ts +36 -0
  260. package/dist/talos/talos-cluster-health.d.ts.map +1 -0
  261. package/dist/talos/talos-machine-config.d.ts +41 -0
  262. package/dist/talos/talos-machine-config.d.ts.map +1 -0
  263. package/dist/talos/talosctl.d.ts +29 -0
  264. package/dist/talos/talosctl.d.ts.map +1 -0
  265. package/dist/talos/values.d.ts +20 -0
  266. package/dist/talos/values.d.ts.map +1 -0
  267. package/package.json +57 -0
  268. package/src/cloudflare/client.ts +76 -0
  269. package/src/cloudflare/index.ts +10 -0
  270. package/src/cloudflare/lock-rules.ts +60 -0
  271. package/src/cloudflare/providers.ts +23 -0
  272. package/src/cloudflare/r2-bucket-lock.ts +220 -0
  273. package/src/forgejo/branch-protection-form.ts +29 -0
  274. package/src/forgejo/branch-protection.ts +101 -0
  275. package/src/forgejo/client.ts +136 -0
  276. package/src/forgejo/index.ts +17 -0
  277. package/src/forgejo/org-actions-secrets.ts +99 -0
  278. package/src/forgejo/org-label.ts +100 -0
  279. package/src/forgejo/org-team.ts +105 -0
  280. package/src/forgejo/repo-webhook-form.ts +47 -0
  281. package/src/forgejo/repo-webhook.ts +118 -0
  282. package/src/forgejo/repository.ts +126 -0
  283. package/src/forgejo/resource.ts +171 -0
  284. package/src/forgejo/team-member.ts +97 -0
  285. package/src/forgejo/values.test.ts +48 -0
  286. package/src/forgejo/values.ts +61 -0
  287. package/src/index.ts +18 -0
  288. package/src/openbao/auth-role-form.ts +112 -0
  289. package/src/openbao/auth-role.ts +101 -0
  290. package/src/openbao/bao-address.test.ts +89 -0
  291. package/src/openbao/bao-address.ts +97 -0
  292. package/src/openbao/bao-http-gate.test.ts +88 -0
  293. package/src/openbao/bao-http-unix.test.ts +70 -0
  294. package/src/openbao/bao-http.test.ts +161 -0
  295. package/src/openbao/bao-http.ts +185 -0
  296. package/src/openbao/bao-status.test.ts +114 -0
  297. package/src/openbao/bao-status.ts +137 -0
  298. package/src/openbao/cloudflare-group-scope.test.ts +103 -0
  299. package/src/openbao/cloudflare-group-scope.ts +83 -0
  300. package/src/openbao/cloudflare-parity-catalog.ts +104 -0
  301. package/src/openbao/cloudflare-parity-snapshot.ts +84 -0
  302. package/src/openbao/cloudflare-parity.ts +152 -0
  303. package/src/openbao/cloudflare-permission-groups-solve.test.ts +171 -0
  304. package/src/openbao/cloudflare-permission-groups-solve.ts +197 -0
  305. package/src/openbao/cloudflare-permission-groups.test.ts +369 -0
  306. package/src/openbao/cloudflare-permission-groups.ts +299 -0
  307. package/src/openbao/cloudflare-policy.ts +180 -0
  308. package/src/openbao/cloudflare-role-form.test.ts +177 -0
  309. package/src/openbao/cloudflare-role-form.ts +183 -0
  310. package/src/openbao/cloudflare-role-wire.test.ts +132 -0
  311. package/src/openbao/cloudflare-role-wire.ts +41 -0
  312. package/src/openbao/cloudflare-role.ts +189 -0
  313. package/src/openbao/cloudflare-roles-config.ts +230 -0
  314. package/src/openbao/cloudflare-roles-expand-bucket.test.ts +134 -0
  315. package/src/openbao/cloudflare-roles-expand.ts +231 -0
  316. package/src/openbao/digest.ts +26 -0
  317. package/src/openbao/fake-bao.ts +84 -0
  318. package/src/openbao/forgejo-bootstrap-parse.test.ts +76 -0
  319. package/src/openbao/forgejo-bootstrap-parse.ts +90 -0
  320. package/src/openbao/forgejo-bootstrap.ts +181 -0
  321. package/src/openbao/index.ts +17 -0
  322. package/src/openbao/mount-form.ts +147 -0
  323. package/src/openbao/mount-wire.test.ts +108 -0
  324. package/src/openbao/mount-wire.ts +79 -0
  325. package/src/openbao/mount.ts +132 -0
  326. package/src/openbao/pki-role-form.ts +248 -0
  327. package/src/openbao/pki-role.ts +202 -0
  328. package/src/openbao/policy-assembly.test.ts +28 -0
  329. package/src/openbao/policy-assembly.ts +13 -0
  330. package/src/openbao/policy-wire.test.ts +90 -0
  331. package/src/openbao/policy-wire.ts +53 -0
  332. package/src/openbao/policy.ts +212 -0
  333. package/src/openbao/proxmox-role-form.ts +174 -0
  334. package/src/openbao/proxmox-role.ts +214 -0
  335. package/src/openbao/ssh-role-form.ts +248 -0
  336. package/src/openbao/ssh-role.ts +240 -0
  337. package/src/proxmox/acl.ts +267 -0
  338. package/src/proxmox/api-token-form.ts +122 -0
  339. package/src/proxmox/api-token.ts +222 -0
  340. package/src/proxmox/backup-job.ts +213 -0
  341. package/src/proxmox/ceph-daemon-form.ts +84 -0
  342. package/src/proxmox/ceph-daemon.ts +243 -0
  343. package/src/proxmox/ceph-flag.ts +249 -0
  344. package/src/proxmox/ceph-fs-wire.ts +246 -0
  345. package/src/proxmox/ceph-fs.ts +249 -0
  346. package/src/proxmox/ceph-osd-tree.ts +193 -0
  347. package/src/proxmox/ceph-osd-write.ts +76 -0
  348. package/src/proxmox/ceph-osd.ts +244 -0
  349. package/src/proxmox/ceph-pool-form.ts +249 -0
  350. package/src/proxmox/ceph-pool-settle.ts +70 -0
  351. package/src/proxmox/ceph-pool.ts +212 -0
  352. package/src/proxmox/client.test.ts +185 -0
  353. package/src/proxmox/client.ts +240 -0
  354. package/src/proxmox/credentials.test.ts +144 -0
  355. package/src/proxmox/credentials.ts +254 -0
  356. package/src/proxmox/firewall-alias-form.ts +112 -0
  357. package/src/proxmox/firewall-alias.ts +208 -0
  358. package/src/proxmox/generated/pbs.ts +461 -0
  359. package/src/proxmox/generated/pve.ts +7344 -0
  360. package/src/proxmox/group.ts +199 -0
  361. package/src/proxmox/ha-resource.ts +169 -0
  362. package/src/proxmox/ha-rule-form.ts +185 -0
  363. package/src/proxmox/ha-rule.ts +240 -0
  364. package/src/proxmox/index.ts +42 -0
  365. package/src/proxmox/lease-cache.test.ts +163 -0
  366. package/src/proxmox/lease-cache.ts +131 -0
  367. package/src/proxmox/lxc.ts +117 -0
  368. package/src/proxmox/members.test.ts +54 -0
  369. package/src/proxmox/members.ts +174 -0
  370. package/src/proxmox/metric-server-form.ts +128 -0
  371. package/src/proxmox/metric-server-otel.ts +99 -0
  372. package/src/proxmox/metric-server.ts +214 -0
  373. package/src/proxmox/network-apply-read.ts +228 -0
  374. package/src/proxmox/network-apply.ts +244 -0
  375. package/src/proxmox/node-network-form.ts +213 -0
  376. package/src/proxmox/node-network.ts +243 -0
  377. package/src/proxmox/notification-target-form.ts +104 -0
  378. package/src/proxmox/notification-target.ts +222 -0
  379. package/src/proxmox/pbs-datastore-form.ts +248 -0
  380. package/src/proxmox/pbs-datastore-guard.ts +105 -0
  381. package/src/proxmox/pbs-datastore.ts +271 -0
  382. package/src/proxmox/pbs-prune-job-form.ts +208 -0
  383. package/src/proxmox/pbs-prune-job.ts +227 -0
  384. package/src/proxmox/pbs-sync-job-form.ts +234 -0
  385. package/src/proxmox/pbs-sync-job.ts +258 -0
  386. package/src/proxmox/pbs-verify-job-form.ts +131 -0
  387. package/src/proxmox/pbs-verify-job.ts +227 -0
  388. package/src/proxmox/pool.test.ts +42 -0
  389. package/src/proxmox/pool.ts +80 -0
  390. package/src/proxmox/qemu.ts +89 -0
  391. package/src/proxmox/replication-job-form.ts +89 -0
  392. package/src/proxmox/replication-job.ts +248 -0
  393. package/src/proxmox/resource.ts +257 -0
  394. package/src/proxmox/role.ts +161 -0
  395. package/src/proxmox/sdn-apply-read.test.ts +152 -0
  396. package/src/proxmox/sdn-apply-read.ts +189 -0
  397. package/src/proxmox/sdn-apply.ts +154 -0
  398. package/src/proxmox/sdn-subnet-form.ts +156 -0
  399. package/src/proxmox/sdn-subnet.ts +238 -0
  400. package/src/proxmox/sdn-vnet.ts +228 -0
  401. package/src/proxmox/sdn-zone.ts +246 -0
  402. package/src/proxmox/storage.ts +245 -0
  403. package/src/proxmox/user.ts +214 -0
  404. package/src/proxmox/values.test.ts +193 -0
  405. package/src/proxmox/values.ts +203 -0
  406. package/src/proxmox/zfs-pool-write.ts +130 -0
  407. package/src/proxmox/zfs-pool.ts +253 -0
  408. package/src/talos/credentials.ts +136 -0
  409. package/src/talos/index.ts +14 -0
  410. package/src/talos/kubeconfig.ts +170 -0
  411. package/src/talos/resource.ts +31 -0
  412. package/src/talos/talos-bootstrap.ts +109 -0
  413. package/src/talos/talos-cluster-health.ts +119 -0
  414. package/src/talos/talos-machine-config.ts +146 -0
  415. package/src/talos/talosctl.ts +94 -0
  416. package/src/talos/values.test.ts +80 -0
  417. package/src/talos/values.ts +70 -0
@@ -0,0 +1,100 @@
1
+ /**
2
+ * `Forgejo.OrgLabel` — one org-level issue label shared across every repository in the org.
3
+ *
4
+ * ★ RETIRES `house/nix/homeflare-config/hosts/macmini/forgejo/labels-reconcile.sh`, which
5
+ * converged the whole `labels.yaml` file in one shot. That script CREATED and PATCHED labels
6
+ * from the spec and REPORTED `EXTRA` labels present live but absent from the file — it never
7
+ * deleted them, because removing a label strips it from every issue that carries it.
8
+ *
9
+ * ★ THIS RESOURCE DOES NOT REMOVE EXTRA LABELS EITHER. Alchemy manages only labels you declare;
10
+ * labels on the org that nobody declared stay untouched — the same boundary as the script's
11
+ * `EXTRA … NOT deleted` lines. Removing a declaration defaults to `retain`, so dropping a label
12
+ * from the stack does not DELETE it unless the caller opts into `.pipe(RemovalPolicy.destroy())`.
13
+ * That matches the script's never-delete posture while still allowing a deliberate destroy.
14
+ *
15
+ * ⚠️ THE WIRE PATH USES NUMERIC ID, NOT NAME — measured on live org labels: PATCH and DELETE are
16
+ * `/orgs/{org}/labels/{id}`. `locate` lists by name; `wirePath` uses the id from that row.
17
+ *
18
+ * ⛔ TOKEN NEEDS `write:organization` FOR RECONCILE — the mcp-read token lacks it (POST 403
19
+ * measured 2026-09-13). Plan/deploy needs a provision-scoped credential in `FORGEJO_TOKEN`.
20
+ */
21
+ import { Resource } from 'alchemy';
22
+ import * as Provider from 'alchemy/Provider';
23
+ import * as Effect from 'effect/Effect';
24
+ import { forgejo } from './client.ts';
25
+ import { type ForgejoRequirements, forgejoHandlers } from './resource.ts';
26
+ import { bool, color, text } from './values.ts';
27
+
28
+ export interface OrgLabelProps {
29
+ /** Organization login — `homeflare` / `HomeFlare` must match the live org. */
30
+ org: string;
31
+ /** Label name — primary key for locate; immutable on update. */
32
+ name: string;
33
+ /** Six-digit hex, with or without `#`. */
34
+ color: string;
35
+ description?: string;
36
+ exclusive?: boolean;
37
+ }
38
+
39
+ export interface OrgLabelAttributes {
40
+ org: string;
41
+ name: string;
42
+ labelId: number;
43
+ color: string;
44
+ description: string;
45
+ exclusive: boolean;
46
+ }
47
+
48
+ export interface ForgejoOrgLabel extends Resource<
49
+ 'Forgejo.OrgLabel',
50
+ OrgLabelProps,
51
+ OrgLabelAttributes,
52
+ never,
53
+ ForgejoRequirements
54
+ > {}
55
+
56
+ export const ForgejoOrgLabel = Resource<ForgejoOrgLabel>('Forgejo.OrgLabel', {
57
+ defaultRemovalPolicy: 'retain',
58
+ });
59
+
60
+ const handlers = forgejoHandlers<OrgLabelProps, OrgLabelAttributes>({
61
+ attributes: (live, props) => {
62
+ const id = live['id'];
63
+ if (typeof id !== 'number') return undefined;
64
+ return {
65
+ color: color(live['color']),
66
+ description: text(live['description']),
67
+ exclusive: bool(live['exclusive']),
68
+ labelId: id,
69
+ name: props.name,
70
+ org: props.org,
71
+ };
72
+ },
73
+ collection: (props) => `orgs/${props.org}/labels`,
74
+ createForm: (props) => ({
75
+ color: color(props.color),
76
+ description: props.description ?? '',
77
+ exclusive: props.exclusive ?? false,
78
+ name: props.name,
79
+ }),
80
+ locate: (props) =>
81
+ forgejo<Record<string, unknown>[]>('GET', `orgs/${props.org}/labels?limit=200`).pipe(
82
+ Effect.map((rows) =>
83
+ Array.isArray(rows) ? rows.find((row) => row['name'] === props.name) : undefined,
84
+ ),
85
+ ),
86
+ matches: (attributes, props) =>
87
+ attributes.color === color(props.color) &&
88
+ attributes.description === (props.description ?? '') &&
89
+ attributes.exclusive === (props.exclusive ?? false),
90
+ path: (props) => `orgs/${props.org}/labels/${props.name}`,
91
+ updateForm: (props) => ({
92
+ color: color(props.color),
93
+ description: props.description ?? '',
94
+ exclusive: props.exclusive ?? false,
95
+ }),
96
+ wirePath: (props, live) => `orgs/${props.org}/labels/${String(live['id'])}`,
97
+ });
98
+
99
+ export const ForgejoOrgLabelProvider = () =>
100
+ Provider.effect(ForgejoOrgLabel, Effect.succeed(ForgejoOrgLabel.Provider.of(handlers)));
@@ -0,0 +1,105 @@
1
+ /**
2
+ * `Forgejo.OrgTeam` — one organization team under `/orgs/{org}/teams`.
3
+ *
4
+ * ★ READ OFF `house/mcp-servers/docs/api/upstream/forgejo.json` (Forgejo 16.x OpenAPI): create is
5
+ * `POST /orgs/{org}/teams`; read/update/delete use numeric id at `/teams/{id}` — not the name.
6
+ *
7
+ * ⚠️ PATCH AND DELETE USE `/teams/{id}` — same seam as org labels. `locate` lists by name;
8
+ * `wirePath` carries the id from that row.
9
+ *
10
+ * ⛔ TOKEN NEEDS `write:organization` TO CREATE OR PATCH — read list/get needs `read:organization`.
11
+ */
12
+ import { Resource } from 'alchemy';
13
+ import * as Provider from 'alchemy/Provider';
14
+ import * as Effect from 'effect/Effect';
15
+ import { forgejo } from './client.ts';
16
+ import { type ForgejoRequirements, forgejoHandlers } from './resource.ts';
17
+ import { bool, recordEqual, stringArray, stringRecord, text } from './values.ts';
18
+
19
+ export type TeamPermission = 'read' | 'write' | 'admin';
20
+
21
+ export interface OrgTeamProps {
22
+ org: string;
23
+ /** Team name — primary locate key; sent on every PATCH body. */
24
+ name: string;
25
+ description?: string;
26
+ permission?: TeamPermission;
27
+ units?: string[];
28
+ unitsMap?: Record<string, string>;
29
+ includesAllRepositories?: boolean;
30
+ canCreateOrgRepo?: boolean;
31
+ }
32
+
33
+ export interface OrgTeamAttributes {
34
+ org: string;
35
+ name: string;
36
+ teamId: number;
37
+ description: string;
38
+ permission: TeamPermission;
39
+ units: string[];
40
+ unitsMap: Record<string, string>;
41
+ includesAllRepositories: boolean;
42
+ canCreateOrgRepo: boolean;
43
+ }
44
+
45
+ export interface ForgejoOrgTeam extends Resource<
46
+ 'Forgejo.OrgTeam',
47
+ OrgTeamProps,
48
+ OrgTeamAttributes,
49
+ never,
50
+ ForgejoRequirements
51
+ > {}
52
+
53
+ export const ForgejoOrgTeam = Resource<ForgejoOrgTeam>('Forgejo.OrgTeam');
54
+
55
+ const teamForm = (props: OrgTeamProps) => ({
56
+ can_create_org_repo: props.canCreateOrgRepo ?? false,
57
+ description: props.description ?? '',
58
+ includes_all_repositories: props.includesAllRepositories ?? false,
59
+ name: props.name,
60
+ ...(props.permission === undefined ? {} : { permission: props.permission }),
61
+ ...(props.units === undefined ? {} : { units: props.units }),
62
+ ...(props.unitsMap === undefined ? {} : { units_map: props.unitsMap }),
63
+ });
64
+
65
+ const handlers = forgejoHandlers<OrgTeamProps, OrgTeamAttributes>({
66
+ attributes: (live, props) => {
67
+ const id = live['id'];
68
+ if (typeof id !== 'number') return undefined;
69
+ const permission = text(live['permission'], 'read');
70
+ if (permission !== 'read' && permission !== 'write' && permission !== 'admin') return undefined;
71
+ return {
72
+ canCreateOrgRepo: bool(live['can_create_org_repo']),
73
+ description: text(live['description']),
74
+ includesAllRepositories: bool(live['includes_all_repositories']),
75
+ name: props.name,
76
+ org: props.org,
77
+ permission,
78
+ teamId: id,
79
+ units: stringArray(live['units']),
80
+ unitsMap: stringRecord(live['units_map']),
81
+ };
82
+ },
83
+ collection: (props) => `orgs/${props.org}/teams`,
84
+ createForm: teamForm,
85
+ locate: (props) =>
86
+ forgejo<Record<string, unknown>[]>('GET', `orgs/${props.org}/teams?limit=200`).pipe(
87
+ Effect.map((rows) =>
88
+ Array.isArray(rows) ? rows.find((row) => row['name'] === props.name) : undefined,
89
+ ),
90
+ ),
91
+ matches: (attributes, props) =>
92
+ attributes.description === (props.description ?? '') &&
93
+ attributes.permission === (props.permission ?? 'read') &&
94
+ attributes.includesAllRepositories === (props.includesAllRepositories ?? false) &&
95
+ attributes.canCreateOrgRepo === (props.canCreateOrgRepo ?? false) &&
96
+ (props.units === undefined ||
97
+ stringArray(props.units).join('\0') === attributes.units.join('\0')) &&
98
+ (props.unitsMap === undefined || recordEqual(attributes.unitsMap, props.unitsMap)),
99
+ path: (props) => `orgs/${props.org}/teams/${props.name}`,
100
+ updateForm: teamForm,
101
+ wirePath: (_props, live) => `teams/${String(live['id'])}`,
102
+ });
103
+
104
+ export const ForgejoOrgTeamProvider = () =>
105
+ Provider.effect(ForgejoOrgTeam, Effect.succeed(ForgejoOrgTeam.Provider.of(handlers)));
@@ -0,0 +1,47 @@
1
+ /**
2
+ * JSON bodies for `Forgejo.RepoWebhook` — split to keep repo-webhook.ts under the line cap.
3
+ *
4
+ * ⛔ `config.secret` IS NEVER A PROP. When present it is read from `FORGEJO_HOOK_SECRET_*` at
5
+ * create/update time only (see hookSecretEnvKey in values.ts). Alchemy attributes are Postgres
6
+ * plaintext — a webhook HMAC secret must not be stored there.
7
+ */
8
+ import type { RepoWebhookProps } from './repo-webhook.ts';
9
+ import { hookSecretEnvKey, stringRecord, text } from './values.ts';
10
+
11
+ export const hookUrl = (live: Record<string, unknown>) =>
12
+ text(live['url']) || text(stringRecord(live['config'])['url']);
13
+
14
+ export const hookConfigForm = (props: RepoWebhookProps, includeSecret: boolean) => {
15
+ const config: Record<string, string> = {
16
+ content_type: props.contentType ?? 'json',
17
+ url: props.url,
18
+ };
19
+ if (includeSecret) {
20
+ const key = hookSecretEnvKey(props.owner, props.repo, props.name);
21
+ const secret = process.env[key]?.trim();
22
+ if (secret !== undefined && secret !== '') config['secret'] = secret;
23
+ }
24
+ return config;
25
+ };
26
+
27
+ export const createHookForm = (props: RepoWebhookProps) => ({
28
+ active: props.active ?? true,
29
+ ...(props.authorizationHeader === undefined
30
+ ? {}
31
+ : { authorization_header: props.authorizationHeader }),
32
+ ...(props.branchFilter === undefined ? {} : { branch_filter: props.branchFilter }),
33
+ config: hookConfigForm(props, true),
34
+ events: props.events,
35
+ name: props.name,
36
+ type: props.type,
37
+ });
38
+
39
+ export const updateHookForm = (props: RepoWebhookProps) => ({
40
+ active: props.active ?? true,
41
+ ...(props.authorizationHeader === undefined
42
+ ? {}
43
+ : { authorization_header: props.authorizationHeader }),
44
+ ...(props.branchFilter === undefined ? {} : { branch_filter: props.branchFilter }),
45
+ config: hookConfigForm(props, false),
46
+ events: props.events,
47
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * `Forgejo.RepoWebhook` — one repository webhook under `/repos/{owner}/{repo}/hooks`.
3
+ *
4
+ * ★ READ OFF `house/mcp-servers/docs/api/upstream/forgejo.json`: create is POST collection; read/update/delete
5
+ * use numeric hook id at `/repos/{owner}/{repo}/hooks/{id}`. `locate` lists and matches on `name`.
6
+ *
7
+ * ⛔ NO WEBHOOK `secret` IN PROPS OR ATTRIBUTES. Optional HMAC material is env-only at write time
8
+ * (`FORGEJO_HOOK_SECRET_*` — see repo-webhook-form.ts). `hookConfigPublic` strips `secret` on read.
9
+ *
10
+ * ⛔ TOKEN NEEDS `write:repository` TO CREATE OR PATCH — list/get needs `read:repository`.
11
+ */
12
+ import { Resource } from 'alchemy';
13
+ import * as Provider from 'alchemy/Provider';
14
+ import * as Effect from 'effect/Effect';
15
+ import { forgejo } from './client.ts';
16
+ import { createHookForm, hookUrl, updateHookForm } from './repo-webhook-form.ts';
17
+ import { type ForgejoRequirements, forgejoHandlers } from './resource.ts';
18
+ import { bool, hookConfigPublic, stringArray, text } from './values.ts';
19
+
20
+ export type RepoWebhookType =
21
+ | 'dingtalk'
22
+ | 'discord'
23
+ | 'feishu'
24
+ | 'forgejo'
25
+ | 'gitea'
26
+ | 'gogs'
27
+ | 'msteams'
28
+ | 'packagist'
29
+ | 'slack'
30
+ | 'telegram'
31
+ | 'wechatwork';
32
+
33
+ export interface RepoWebhookProps {
34
+ owner: string;
35
+ repo: string;
36
+ /** Stable locate key — also sent as Hook.name on create. */
37
+ name: string;
38
+ type: RepoWebhookType;
39
+ url: string;
40
+ events: string[];
41
+ contentType?: 'json' | 'form';
42
+ active?: boolean;
43
+ branchFilter?: string;
44
+ authorizationHeader?: string;
45
+ }
46
+
47
+ export interface RepoWebhookAttributes {
48
+ owner: string;
49
+ repo: string;
50
+ name: string;
51
+ hookId: number;
52
+ type: RepoWebhookType;
53
+ url: string;
54
+ contentType: string;
55
+ events: string[];
56
+ active: boolean;
57
+ branchFilter: string;
58
+ authorizationHeader: string;
59
+ }
60
+
61
+ export interface ForgejoRepoWebhook extends Resource<
62
+ 'Forgejo.RepoWebhook',
63
+ RepoWebhookProps,
64
+ RepoWebhookAttributes,
65
+ never,
66
+ ForgejoRequirements
67
+ > {}
68
+
69
+ export const ForgejoRepoWebhook = Resource<ForgejoRepoWebhook>('Forgejo.RepoWebhook');
70
+
71
+ const handlers = forgejoHandlers<RepoWebhookProps, RepoWebhookAttributes>({
72
+ attributes: (live, props) => {
73
+ const id = live['id'];
74
+ if (typeof id !== 'number') return undefined;
75
+ const config = hookConfigPublic(live['config']);
76
+ const type = text(live['type']);
77
+ return {
78
+ active: bool(live['active'], true),
79
+ authorizationHeader: text(live['authorization_header']),
80
+ branchFilter: text(live['branch_filter']),
81
+ contentType: text(config['content_type'], text(live['content_type'], 'json')),
82
+ events: stringArray(live['events']),
83
+ hookId: id,
84
+ name: props.name,
85
+ owner: props.owner,
86
+ repo: props.repo,
87
+ type: type as RepoWebhookType,
88
+ url: hookUrl(live),
89
+ };
90
+ },
91
+ collection: (props) => `repos/${props.owner}/${props.repo}/hooks`,
92
+ createForm: createHookForm,
93
+ locate: (props) =>
94
+ forgejo<Record<string, unknown>[]>(
95
+ 'GET',
96
+ `repos/${props.owner}/${props.repo}/hooks?limit=200`,
97
+ ).pipe(
98
+ Effect.map((rows) =>
99
+ Array.isArray(rows)
100
+ ? rows.find((row) => text(row['name']) === props.name || hookUrl(row) === props.url)
101
+ : undefined,
102
+ ),
103
+ ),
104
+ matches: (attributes, props) =>
105
+ attributes.type === props.type &&
106
+ attributes.url === props.url &&
107
+ attributes.active === (props.active ?? true) &&
108
+ attributes.contentType === (props.contentType ?? 'json') &&
109
+ attributes.branchFilter === (props.branchFilter ?? '') &&
110
+ attributes.authorizationHeader === (props.authorizationHeader ?? '') &&
111
+ stringArray(props.events).join('\0') === attributes.events.join('\0'),
112
+ path: (props) => `repos/${props.owner}/${props.repo}/hooks/${props.name}`,
113
+ updateForm: updateHookForm,
114
+ wirePath: (props, live) => `repos/${props.owner}/${props.repo}/hooks/${String(live['id'])}`,
115
+ });
116
+
117
+ export const ForgejoRepoWebhookProvider = () =>
118
+ Provider.effect(ForgejoRepoWebhook, Effect.succeed(ForgejoRepoWebhook.Provider.of(handlers)));
@@ -0,0 +1,126 @@
1
+ /**
2
+ * `Forgejo.Repository` — an organization-owned git repository.
3
+ *
4
+ * ★ MEASURED 2026-09-13 against the live daemon (`GET /api/v1/repos/HomeFlare/homeflare-config`):
5
+ * responses are flat JSON with `private`, `has_issues`, `default_branch`, etc. Create is
6
+ * `POST /orgs/{org}/repos`; read/update/delete use `/repos/{owner}/{repo}`.
7
+ *
8
+ * ⛔ `defaultRemovalPolicy: 'retain'` — history, issues and packages in a repo are irreplaceable.
9
+ * Opt into deletion with `.pipe(RemovalPolicy.destroy())`; `delete` below is fully implemented.
10
+ */
11
+ import { Resource } from 'alchemy';
12
+ import * as Provider from 'alchemy/Provider';
13
+ import * as Effect from 'effect/Effect';
14
+ import { type ForgejoRequirements, forgejoHandlers } from './resource.ts';
15
+ import { bool, text } from './values.ts';
16
+
17
+ export interface RepositoryProps {
18
+ /** Organization login — the owner segment in `HomeFlare/homeflare-config`. */
19
+ org: string;
20
+ /** Repository name. Changing it is a replace, not an update. */
21
+ name: string;
22
+ description?: string;
23
+ private?: boolean;
24
+ hasIssues?: boolean;
25
+ hasWiki?: boolean;
26
+ hasProjects?: boolean;
27
+ /** Only applied when the repository already has at least one branch. */
28
+ defaultBranch?: string;
29
+ /** Create an initial commit with a README when the repository is new. */
30
+ autoInit?: boolean;
31
+ }
32
+
33
+ export interface RepositoryAttributes {
34
+ org: string;
35
+ name: string;
36
+ repoId: number;
37
+ description: string;
38
+ private: boolean;
39
+ hasIssues: boolean;
40
+ hasWiki: boolean;
41
+ hasProjects: boolean;
42
+ defaultBranch: string;
43
+ empty: boolean;
44
+ }
45
+
46
+ export interface ForgejoRepository extends Resource<
47
+ 'Forgejo.Repository',
48
+ RepositoryProps,
49
+ RepositoryAttributes,
50
+ never,
51
+ ForgejoRequirements
52
+ > {}
53
+
54
+ export const ForgejoRepository = Resource<ForgejoRepository>('Forgejo.Repository', {
55
+ defaultRemovalPolicy: 'retain',
56
+ });
57
+
58
+ const handlers = forgejoHandlers<RepositoryProps, RepositoryAttributes>({
59
+ attributes: (live, props) => {
60
+ const id = live['id'];
61
+ if (typeof id !== 'number') return undefined;
62
+ return {
63
+ defaultBranch: text(live['default_branch'], 'main'),
64
+ description: text(live['description']),
65
+ empty: bool(live['empty']),
66
+ hasIssues: bool(live['has_issues'], true),
67
+ hasProjects: bool(live['has_projects'], true),
68
+ hasWiki: bool(live['has_wiki'], true),
69
+ name: props.name,
70
+ org: props.org,
71
+ private: bool(live['private'], true),
72
+ repoId: id,
73
+ };
74
+ },
75
+ collection: (props) => `orgs/${props.org}/repos`,
76
+ createForm: (props) => ({
77
+ auto_init: props.autoInit ?? false,
78
+ description: props.description ?? '',
79
+ has_issues: props.hasIssues ?? true,
80
+ has_projects: props.hasProjects ?? true,
81
+ has_wiki: props.hasWiki ?? true,
82
+ name: props.name,
83
+ private: props.private ?? true,
84
+ }),
85
+ /**
86
+ * ⛔ AN UNDECLARED FIELD IS NEITHER COMPARED NOR SENT — the rule every other family in this
87
+ * estate follows (storage.ts, backup-job.ts, and all four Pbs.* families say it in those
88
+ * words), and this file was the exception.
89
+ *
90
+ * 🔴 WHAT THE EXCEPTION DID. Five fields were compared against a DEFAULT rather than skipped:
91
+ * `description ?? ''`, and `?? true` for private, hasIssues, hasWiki and hasProjects. So a
92
+ * declaration that simply did not mention `hasWiki` asserted "the wiki is ON", and one that
93
+ * did not mention `description` asserted "the description is EMPTY" — and `updateForm` sent
94
+ * both. Declaring a repo you only wanted to rename would have wiped its description and
95
+ * switched features on, reported as a plain `update`. Only `defaultBranch` was written
96
+ * correctly, on the line below, which is what the other five now copy.
97
+ *
98
+ * ⚠️ THE PRICE, STATED: dropping a line no longer resets a field. Clear one out of band in the
99
+ * Forgejo UI or with a direct PATCH. It costs no forever-diff, because an undeclared field is
100
+ * not compared either.
101
+ *
102
+ * ⚠️ `private` IS THE ONE WORTH DECLARING ANYWAY ON EVERY REPO. Leaving it undeclared now means
103
+ * Alchemy will not notice a repository being made public. That is the correct trade for a
104
+ * generic family — undeclared is unmanaged, not assumed — but it is a real gap, so say it
105
+ * explicitly in each declaration rather than relying on a default that no longer exists.
106
+ */
107
+ matches: (attributes, props) =>
108
+ (props.description === undefined || attributes.description === props.description) &&
109
+ (props.private === undefined || attributes.private === props.private) &&
110
+ (props.hasIssues === undefined || attributes.hasIssues === props.hasIssues) &&
111
+ (props.hasWiki === undefined || attributes.hasWiki === props.hasWiki) &&
112
+ (props.hasProjects === undefined || attributes.hasProjects === props.hasProjects) &&
113
+ (props.defaultBranch === undefined || attributes.defaultBranch === props.defaultBranch),
114
+ path: (props) => `repos/${props.org}/${props.name}`,
115
+ updateForm: (props) => ({
116
+ ...(props.defaultBranch === undefined ? {} : { default_branch: props.defaultBranch }),
117
+ ...(props.description === undefined ? {} : { description: props.description }),
118
+ ...(props.hasIssues === undefined ? {} : { has_issues: props.hasIssues }),
119
+ ...(props.hasProjects === undefined ? {} : { has_projects: props.hasProjects }),
120
+ ...(props.hasWiki === undefined ? {} : { has_wiki: props.hasWiki }),
121
+ ...(props.private === undefined ? {} : { private: props.private }),
122
+ }),
123
+ });
124
+
125
+ export const ForgejoRepositoryProvider = () =>
126
+ Provider.effect(ForgejoRepository, Effect.succeed(ForgejoRepository.Provider.of(handlers)));
@@ -0,0 +1,171 @@
1
+ /**
2
+ * One shape for every Forgejo object — the same four operations, different path and JSON body.
3
+ *
4
+ * ⛔ THE ALTERNATIVE IS COPYING THE SAME HANDLERS PER RESOURCE. Repositories, org labels, teams,
5
+ * runners, branch protection and package settings are all create / read / update / delete over
6
+ * `/api/v1` with JSON bodies. Hand-rolling the fourth copy is where someone drops the read-back
7
+ * or the isResolved guard and the plan lies.
8
+ *
9
+ * ★ SO THE FOUR OPERATIONS LIVE HERE, ONCE. A new Forgejo object should be a spec plus ~forty
10
+ * lines, not a hundred and thirty.
11
+ *
12
+ * ⚠️ GITEA ANSWERS WITH THE OBJECT DIRECTLY — NOT `{"data": ...}`. See client.ts. reconcile still
13
+ * READS BACK after every write rather than trusting the status code alone.
14
+ *
15
+ * ★ WHY `Forgejo.Repository` DECLARES `defaultRemovalPolicy: 'retain'`, WRITTEN ONCE HERE.
16
+ * Alchemy's own `GitHub.Repository` defaults to retain "because their contents are
17
+ * irreplaceable". A git repository is the most irreplaceable object on this estate. `retain`
18
+ * means the engine skips `provider.delete` when a resource is orphaned unless the caller opted
19
+ * into `.pipe(RemovalPolicy.destroy())`. The `delete` handler is FULLY IMPLEMENTED anyway — a
20
+ * stub that silently does nothing lies to whoever reads the plan.
21
+ */
22
+ import { isResolved } from 'alchemy/Diff';
23
+ import type { Input } from 'alchemy/Input';
24
+ import * as Effect from 'effect/Effect';
25
+ import type * as HttpClient from 'effect/unstable/http/HttpClient';
26
+ import { ForgejoError, forgejo } from './client.ts';
27
+
28
+ /**
29
+ * ★ WHAT EVERY OPERATION HERE NEEDS FROM THE RUNTIME — and it is NOT what the first draft said.
30
+ *
31
+ * These resources declared `ChildProcessSpawner`, copied from house/proxmox, where it is there
32
+ * because a PVE credential is minted by shelling out to `bao`. Nothing in this package shells
33
+ * out: the token comes from `FORGEJO_TOKEN` in the environment. Declaring a service you never
34
+ * use is not harmless — it makes a stack provide a layer for nothing, and it hides the one
35
+ * service you DO need.
36
+ *
37
+ * What is actually required is `HttpClient`, for the reason in client.ts: Alchemy's own
38
+ * providers call through Effect's client rather than the global `fetch`.
39
+ */
40
+ export type ForgejoRequirements = HttpClient.HttpClient;
41
+
42
+ export type ForgejoSpec<Props extends object, Attributes> = {
43
+ /** `repos/HomeFlare/homeflare`, `orgs/homeflare/labels/40` — one object. */
44
+ readonly path: (props: Props) => string;
45
+ /** `orgs/HomeFlare/repos`, `orgs/homeflare/labels` — where a new one is POSTed. */
46
+ readonly collection: (props: Props) => string;
47
+ readonly attributes: (live: Record<string, unknown>, props: Props) => Attributes | undefined;
48
+ /** JSON body on create. ⚠️ Gitea takes JSON, not form encoding. */
49
+ readonly createForm: (props: Props) => Record<string, unknown>;
50
+ readonly updateForm?: (props: Props) => Record<string, unknown>;
51
+ readonly matches: (attributes: Attributes, props: Props) => boolean;
52
+ /**
53
+ * When the stable key is not enough for the wire path — org labels are addressed by numeric id.
54
+ * `locate` lists or searches; `wirePath` builds update/delete paths from the live row.
55
+ */
56
+ readonly locate?: (
57
+ props: Props,
58
+ ) => Effect.Effect<Record<string, unknown> | undefined, ForgejoError, ForgejoRequirements>;
59
+ readonly wirePath?: (props: Props, live: Record<string, unknown>) => string;
60
+ /**
61
+ * When create is PUT upsert rather than POST — org actions secrets have no POST and no GET-by-name.
62
+ * `before === undefined` means absent; the callback decides whether to write. Existence-only families
63
+ * noop when `before` is set because the API never returns the value for comparison.
64
+ */
65
+ readonly upsert?: (
66
+ props: Props,
67
+ before: Record<string, unknown> | undefined,
68
+ ) => Effect.Effect<void, ForgejoError, ForgejoRequirements>;
69
+ };
70
+
71
+ /** ⚠️ Only 404 folds to "absent". A 403 is a real failure — folding it would plan a create over an existing object. */
72
+ const absentOn404 = <A>(io: Effect.Effect<A, ForgejoError, ForgejoRequirements>) =>
73
+ io.pipe(
74
+ Effect.catchIf(
75
+ (cause): cause is ForgejoError => cause instanceof ForgejoError && cause.status === 404,
76
+ () => Effect.succeed(undefined as A),
77
+ ),
78
+ );
79
+
80
+ export const forgejoOperations = <Props extends object, Attributes>(
81
+ spec: ForgejoSpec<Props, Attributes>,
82
+ ) => {
83
+ const fetchLive = (props: Props) =>
84
+ absentOn404(
85
+ spec.locate !== undefined
86
+ ? spec.locate(props)
87
+ : forgejo<Record<string, unknown>>('GET', spec.path(props)),
88
+ );
89
+
90
+ const objectPath = (props: Props, live: Record<string, unknown>) =>
91
+ spec.wirePath === undefined ? spec.path(props) : spec.wirePath(props, live);
92
+
93
+ const read = (props: Props) =>
94
+ fetchLive(props).pipe(
95
+ Effect.map((data) => (data === undefined ? undefined : spec.attributes(data, props))),
96
+ );
97
+
98
+ return {
99
+ read,
100
+
101
+ diff: (news: Input<Props>, output: Attributes | undefined) =>
102
+ Effect.gen(function* () {
103
+ if (output === undefined || !isResolved(news)) return undefined;
104
+ const live = yield* read(news);
105
+ if (live === undefined) return { action: 'update' } as const;
106
+ if (spec.matches(live, news)) return { action: 'noop' } as const;
107
+ return spec.updateForm === undefined
108
+ ? ({ action: 'replace' } as const)
109
+ : ({ action: 'update' } as const);
110
+ }),
111
+
112
+ reconcile: (news: Props) =>
113
+ Effect.gen(function* () {
114
+ const before = yield* fetchLive(news);
115
+ if (spec.upsert !== undefined) {
116
+ yield* spec.upsert(news, before);
117
+ } else if (before === undefined) {
118
+ yield* forgejo('POST', spec.collection(news), spec.createForm(news));
119
+ } else if (spec.updateForm !== undefined) {
120
+ const mapped = spec.attributes(before, news);
121
+ if (mapped === undefined || spec.matches(mapped, news)) {
122
+ /* adoption or already converged — no PATCH */
123
+ } else {
124
+ const form = spec.updateForm(news);
125
+ if (Object.keys(form).length > 0) {
126
+ yield* forgejo('PATCH', objectPath(news, before), form);
127
+ }
128
+ }
129
+ }
130
+ const after = yield* read(news);
131
+ if (after === undefined) {
132
+ return yield* Effect.die(
133
+ new Error(
134
+ `${spec.path(news)}: the write returned no error but the object is still absent. ` +
135
+ 'Gitea returns the object directly — read back rather than trusting the status code.',
136
+ ),
137
+ );
138
+ }
139
+ return after;
140
+ }),
141
+
142
+ destroy: (olds: Props) =>
143
+ Effect.gen(function* () {
144
+ const live = yield* fetchLive(olds);
145
+ if (live === undefined) return;
146
+ yield* forgejo('DELETE', objectPath(olds, live));
147
+ }),
148
+ };
149
+ };
150
+
151
+ /**
152
+ * The five provider handlers for a spec'd Forgejo object, wired once.
153
+ *
154
+ * ⛔ IT STOPS AT THE HANDLERS AND DOES NOT RETURN THE LAYER — same reasoning as pveHandlers in
155
+ * house/proxmox: wrapping `Provider.effect` here needs casts against Alchemy's `Props<R>`.
156
+ *
157
+ * ⚠️ `list` answers empty — Forgejo index endpoints return the whole org; adoption stays explicit.
158
+ */
159
+ export const forgejoHandlers = <Props extends object, Attributes extends object>(
160
+ spec: ForgejoSpec<Props, Attributes>,
161
+ ) => {
162
+ const ops = forgejoOperations(spec);
163
+ return {
164
+ list: () => Effect.succeed([]),
165
+ read: ({ olds }: { olds: Props }) => ops.read(olds),
166
+ diff: ({ news, output }: { news: Input<Props>; output: Attributes | undefined }) =>
167
+ ops.diff(news, output),
168
+ reconcile: ({ news }: { news: Props }) => ops.reconcile(news),
169
+ delete: ({ olds }: { olds: Props }) => ops.destroy(olds),
170
+ };
171
+ };