@homeflare/alchemy 0.15.0 → 0.19.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 (512) hide show
  1. package/README.md +29 -0
  2. package/dist/linux/directory-lifecycle.d.ts +50 -0
  3. package/dist/linux/directory-lifecycle.d.ts.map +1 -0
  4. package/dist/linux/directory.d.ts +19 -0
  5. package/dist/linux/directory.d.ts.map +1 -0
  6. package/dist/linux/fake-linux-host.d.ts +35 -0
  7. package/dist/linux/fake-linux-host.d.ts.map +1 -0
  8. package/dist/linux/fake-systemd.d.ts +34 -0
  9. package/dist/linux/fake-systemd.d.ts.map +1 -0
  10. package/dist/linux/index.d.ts +27 -0
  11. package/dist/linux/index.d.ts.map +1 -0
  12. package/dist/linux/index.js +1554 -0
  13. package/dist/linux/index.js.map +37 -0
  14. package/dist/linux/providers.d.ts +17 -0
  15. package/dist/linux/providers.d.ts.map +1 -0
  16. package/dist/linux/region.d.ts +43 -0
  17. package/dist/linux/region.d.ts.map +1 -0
  18. package/dist/linux/remote-file-form.d.ts +47 -0
  19. package/dist/linux/remote-file-form.d.ts.map +1 -0
  20. package/dist/linux/remote-file-lifecycle.d.ts +36 -0
  21. package/dist/linux/remote-file-lifecycle.d.ts.map +1 -0
  22. package/dist/linux/remote-file-plan.d.ts +33 -0
  23. package/dist/linux/remote-file-plan.d.ts.map +1 -0
  24. package/dist/linux/remote-file.d.ts +28 -0
  25. package/dist/linux/remote-file.d.ts.map +1 -0
  26. package/dist/linux/ssh-command.d.ts +68 -0
  27. package/dist/linux/ssh-command.d.ts.map +1 -0
  28. package/dist/linux/ssh-runner.d.ts +22 -0
  29. package/dist/linux/ssh-runner.d.ts.map +1 -0
  30. package/dist/linux/ssh-scripts.d.ts +27 -0
  31. package/dist/linux/ssh-scripts.d.ts.map +1 -0
  32. package/dist/linux/systemctl.d.ts +67 -0
  33. package/dist/linux/systemctl.d.ts.map +1 -0
  34. package/dist/linux/unit-form.d.ts +78 -0
  35. package/dist/linux/unit-form.d.ts.map +1 -0
  36. package/dist/linux/unit-handlers.d.ts +15 -0
  37. package/dist/linux/unit-handlers.d.ts.map +1 -0
  38. package/dist/linux/unit-lifecycle.d.ts +44 -0
  39. package/dist/linux/unit-lifecycle.d.ts.map +1 -0
  40. package/dist/linux/unit-settle.d.ts +31 -0
  41. package/dist/linux/unit-settle.d.ts.map +1 -0
  42. package/dist/linux/unit.d.ts +35 -0
  43. package/dist/linux/unit.d.ts.map +1 -0
  44. package/dist/netbox/client.d.ts +52 -0
  45. package/dist/netbox/client.d.ts.map +1 -0
  46. package/dist/netbox/constraint-guard.d.ts +26 -0
  47. package/dist/netbox/constraint-guard.d.ts.map +1 -0
  48. package/dist/netbox/constraints.d.ts +76 -0
  49. package/dist/netbox/constraints.d.ts.map +1 -0
  50. package/dist/netbox/generated/constraints/index.d.ts +19 -0
  51. package/dist/netbox/generated/constraints/index.d.ts.map +1 -0
  52. package/dist/netbox/generated/constraints/netbox-ipam.d.ts +21 -0
  53. package/dist/netbox/generated/constraints/netbox-ipam.d.ts.map +1 -0
  54. package/dist/netbox/index.d.ts +17 -0
  55. package/dist/netbox/index.d.ts.map +1 -0
  56. package/dist/netbox/index.js +331 -0
  57. package/dist/netbox/index.js.map +18 -0
  58. package/dist/netbox/prefix-form.d.ts +54 -0
  59. package/dist/netbox/prefix-form.d.ts.map +1 -0
  60. package/dist/netbox/prefix.d.ts +80 -0
  61. package/dist/netbox/prefix.d.ts.map +1 -0
  62. package/dist/netbox/resource.d.ts +94 -0
  63. package/dist/netbox/resource.d.ts.map +1 -0
  64. package/dist/netbox/values.d.ts +24 -0
  65. package/dist/netbox/values.d.ts.map +1 -0
  66. package/dist/proxmox/api-token-form.d.ts.map +1 -1
  67. package/dist/proxmox/apply-endpoints.d.ts +30 -0
  68. package/dist/proxmox/apply-endpoints.d.ts.map +1 -0
  69. package/dist/proxmox/ceph-daemon-form.d.ts +29 -1
  70. package/dist/proxmox/ceph-daemon-form.d.ts.map +1 -1
  71. package/dist/proxmox/ceph-daemon.d.ts.map +1 -1
  72. package/dist/proxmox/ceph-flag.d.ts.map +1 -1
  73. package/dist/proxmox/ceph-fs.d.ts.map +1 -1
  74. package/dist/proxmox/ceph-osd-write.d.ts +19 -0
  75. package/dist/proxmox/ceph-osd-write.d.ts.map +1 -1
  76. package/dist/proxmox/ceph-pool.d.ts.map +1 -1
  77. package/dist/proxmox/constraints.d.ts +28 -5
  78. package/dist/proxmox/constraints.d.ts.map +1 -1
  79. package/dist/proxmox/generated/constraints/index.d.ts +1 -1
  80. package/dist/proxmox/generated/constraints/index.d.ts.map +1 -1
  81. package/dist/proxmox/generated/constraints/pbs-config.d.ts +6 -1
  82. package/dist/proxmox/generated/constraints/pbs-config.d.ts.map +1 -1
  83. package/dist/proxmox/generated/constraints/pve-access.d.ts +6 -1
  84. package/dist/proxmox/generated/constraints/pve-access.d.ts.map +1 -1
  85. package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts +23 -0
  86. package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts.map +1 -0
  87. package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts +23 -0
  88. package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts.map +1 -0
  89. package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts +23 -0
  90. package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts.map +1 -0
  91. package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts +23 -0
  92. package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts.map +1 -0
  93. package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts +23 -0
  94. package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts.map +1 -0
  95. package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts +23 -0
  96. package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts.map +1 -0
  97. package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts +23 -0
  98. package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts.map +1 -0
  99. package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts +23 -0
  100. package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts.map +1 -0
  101. package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts +23 -0
  102. package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts.map +1 -0
  103. package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts +23 -0
  104. package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts.map +1 -0
  105. package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts +23 -0
  106. package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts.map +1 -0
  107. package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts +23 -0
  108. package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts.map +1 -0
  109. package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts +23 -0
  110. package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts.map +1 -0
  111. package/dist/proxmox/generated/constraints/pve-pools.d.ts +6 -1
  112. package/dist/proxmox/generated/constraints/pve-pools.d.ts.map +1 -1
  113. package/dist/proxmox/generated/constraints/pve-storage.d.ts +6 -1
  114. package/dist/proxmox/generated/constraints/pve-storage.d.ts.map +1 -1
  115. package/dist/proxmox/generated/pbs/access-users.d.ts +137 -0
  116. package/dist/proxmox/generated/pbs/access-users.d.ts.map +1 -0
  117. package/dist/proxmox/generated/pbs/access.d.ts +171 -0
  118. package/dist/proxmox/generated/pbs/access.d.ts.map +1 -0
  119. package/dist/proxmox/generated/pbs/admin-datastore-store.d.ts +252 -0
  120. package/dist/proxmox/generated/pbs/admin-datastore-store.d.ts.map +1 -0
  121. package/dist/proxmox/generated/pbs/admin-metrics.d.ts +147 -0
  122. package/dist/proxmox/generated/pbs/admin-metrics.d.ts.map +1 -0
  123. package/dist/proxmox/generated/pbs/admin.d.ts +217 -0
  124. package/dist/proxmox/generated/pbs/admin.d.ts.map +1 -0
  125. package/dist/proxmox/generated/pbs/backup.d.ts +114 -0
  126. package/dist/proxmox/generated/pbs/backup.d.ts.map +1 -0
  127. package/dist/proxmox/generated/pbs/config-access-pam.d.ts +218 -0
  128. package/dist/proxmox/generated/pbs/config-access-pam.d.ts.map +1 -0
  129. package/dist/proxmox/generated/pbs/config-access.d.ts +203 -0
  130. package/dist/proxmox/generated/pbs/config-access.d.ts.map +1 -0
  131. package/dist/proxmox/generated/pbs/config-datastore.d.ts +195 -0
  132. package/dist/proxmox/generated/pbs/config-datastore.d.ts.map +1 -0
  133. package/dist/proxmox/generated/pbs/config-media-pool.d.ts +207 -0
  134. package/dist/proxmox/generated/pbs/config-media-pool.d.ts.map +1 -0
  135. package/dist/proxmox/generated/pbs/config-notifications-matchers.d.ts +159 -0
  136. package/dist/proxmox/generated/pbs/config-notifications-matchers.d.ts.map +1 -0
  137. package/dist/proxmox/generated/pbs/config-notifications.d.ts +199 -0
  138. package/dist/proxmox/generated/pbs/config-notifications.d.ts.map +1 -0
  139. package/dist/proxmox/generated/pbs/config-remote.d.ts +175 -0
  140. package/dist/proxmox/generated/pbs/config-remote.d.ts.map +1 -0
  141. package/dist/proxmox/generated/pbs/config-sync.d.ts +145 -0
  142. package/dist/proxmox/generated/pbs/config-sync.d.ts.map +1 -0
  143. package/dist/proxmox/generated/pbs/config-tape.d.ts +200 -0
  144. package/dist/proxmox/generated/pbs/config-tape.d.ts.map +1 -0
  145. package/dist/proxmox/generated/pbs/config-verify.d.ts +77 -0
  146. package/dist/proxmox/generated/pbs/config-verify.d.ts.map +1 -0
  147. package/dist/proxmox/generated/pbs/config.d.ts +119 -0
  148. package/dist/proxmox/generated/pbs/config.d.ts.map +1 -0
  149. package/dist/proxmox/generated/pbs/nodes-node-dns.d.ts +178 -0
  150. package/dist/proxmox/generated/pbs/nodes-node-dns.d.ts.map +1 -0
  151. package/dist/proxmox/generated/pbs/nodes-node-services.d.ts +205 -0
  152. package/dist/proxmox/generated/pbs/nodes-node-services.d.ts.map +1 -0
  153. package/dist/proxmox/generated/pbs/nodes-node.d.ts +166 -0
  154. package/dist/proxmox/generated/pbs/nodes-node.d.ts.map +1 -0
  155. package/dist/proxmox/generated/pbs/nodes.d.ts +171 -0
  156. package/dist/proxmox/generated/pbs/nodes.d.ts.map +1 -0
  157. package/dist/proxmox/generated/pbs/ping.d.ts +18 -0
  158. package/dist/proxmox/generated/pbs/ping.d.ts.map +1 -0
  159. package/dist/proxmox/generated/pbs/pull.d.ts +37 -0
  160. package/dist/proxmox/generated/pbs/pull.d.ts.map +1 -0
  161. package/dist/proxmox/generated/pbs/push.d.ts +36 -0
  162. package/dist/proxmox/generated/pbs/push.d.ts.map +1 -0
  163. package/dist/proxmox/generated/pbs/reader.d.ts +41 -0
  164. package/dist/proxmox/generated/pbs/reader.d.ts.map +1 -0
  165. package/dist/proxmox/generated/pbs/root.d.ts +16 -0
  166. package/dist/proxmox/generated/pbs/root.d.ts.map +1 -0
  167. package/dist/proxmox/generated/pbs/status.d.ts +54 -0
  168. package/dist/proxmox/generated/pbs/status.d.ts.map +1 -0
  169. package/dist/proxmox/generated/pbs/tape-drive.d.ts +181 -0
  170. package/dist/proxmox/generated/pbs/tape-drive.d.ts.map +1 -0
  171. package/dist/proxmox/generated/pbs/tape-media.d.ts +126 -0
  172. package/dist/proxmox/generated/pbs/tape-media.d.ts.map +1 -0
  173. package/dist/proxmox/generated/pbs/tape.d.ts +90 -0
  174. package/dist/proxmox/generated/pbs/tape.d.ts.map +1 -0
  175. package/dist/proxmox/generated/pbs/version.d.ts +20 -0
  176. package/dist/proxmox/generated/pbs/version.d.ts.map +1 -0
  177. package/dist/proxmox/generated/pbs.d.ts +44 -662
  178. package/dist/proxmox/generated/pbs.d.ts.map +1 -1
  179. package/dist/proxmox/generated/pve/access-roles.d.ts +164 -0
  180. package/dist/proxmox/generated/pve/access-roles.d.ts.map +1 -0
  181. package/dist/proxmox/generated/pve/access-users.d.ts +152 -0
  182. package/dist/proxmox/generated/pve/access-users.d.ts.map +1 -0
  183. package/dist/proxmox/generated/pve/access.d.ts +221 -0
  184. package/dist/proxmox/generated/pve/access.d.ts.map +1 -0
  185. package/dist/proxmox/generated/pve/cluster-acme-plugins-id.d.ts +45 -0
  186. package/dist/proxmox/generated/pve/cluster-acme-plugins-id.d.ts.map +1 -0
  187. package/dist/proxmox/generated/pve/cluster-acme-plugins.d.ts +47 -0
  188. package/dist/proxmox/generated/pve/cluster-acme-plugins.d.ts.map +1 -0
  189. package/dist/proxmox/generated/pve/cluster-acme-tos.d.ts +240 -0
  190. package/dist/proxmox/generated/pve/cluster-acme-tos.d.ts.map +1 -0
  191. package/dist/proxmox/generated/pve/cluster-bulk-action.d.ts +200 -0
  192. package/dist/proxmox/generated/pve/cluster-bulk-action.d.ts.map +1 -0
  193. package/dist/proxmox/generated/pve/cluster-firewall-refs.d.ts +109 -0
  194. package/dist/proxmox/generated/pve/cluster-firewall-refs.d.ts.map +1 -0
  195. package/dist/proxmox/generated/pve/cluster-firewall.d.ts +225 -0
  196. package/dist/proxmox/generated/pve/cluster-firewall.d.ts.map +1 -0
  197. package/dist/proxmox/generated/pve/cluster-ha.d.ts +199 -0
  198. package/dist/proxmox/generated/pve/cluster-ha.d.ts.map +1 -0
  199. package/dist/proxmox/generated/pve/cluster-jobs.d.ts +183 -0
  200. package/dist/proxmox/generated/pve/cluster-jobs.d.ts.map +1 -0
  201. package/dist/proxmox/generated/pve/cluster-metrics.d.ts +110 -0
  202. package/dist/proxmox/generated/pve/cluster-metrics.d.ts.map +1 -0
  203. package/dist/proxmox/generated/pve/cluster-notifications-endpoints.d.ts +224 -0
  204. package/dist/proxmox/generated/pve/cluster-notifications-endpoints.d.ts.map +1 -0
  205. package/dist/proxmox/generated/pve/cluster-notifications.d.ts +91 -0
  206. package/dist/proxmox/generated/pve/cluster-notifications.d.ts.map +1 -0
  207. package/dist/proxmox/generated/pve/cluster-options.d.ts +85 -0
  208. package/dist/proxmox/generated/pve/cluster-options.d.ts.map +1 -0
  209. package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models-cputype.d.ts +48 -0
  210. package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models-cputype.d.ts.map +1 -0
  211. package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models.d.ts +45 -0
  212. package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models.d.ts.map +1 -0
  213. package/dist/proxmox/generated/pve/cluster-replication.d.ts +130 -0
  214. package/dist/proxmox/generated/pve/cluster-replication.d.ts.map +1 -0
  215. package/dist/proxmox/generated/pve/cluster-sdn-fabrics.d.ts +220 -0
  216. package/dist/proxmox/generated/pve/cluster-sdn-fabrics.d.ts.map +1 -0
  217. package/dist/proxmox/generated/pve/cluster-sdn-ipams.d.ts +229 -0
  218. package/dist/proxmox/generated/pve/cluster-sdn-ipams.d.ts.map +1 -0
  219. package/dist/proxmox/generated/pve/cluster-sdn-vnets-vnet.d.ts +229 -0
  220. package/dist/proxmox/generated/pve/cluster-sdn-vnets-vnet.d.ts.map +1 -0
  221. package/dist/proxmox/generated/pve/cluster-sdn-vnets.d.ts +51 -0
  222. package/dist/proxmox/generated/pve/cluster-sdn-vnets.d.ts.map +1 -0
  223. package/dist/proxmox/generated/pve/cluster-sdn-zones.d.ts +225 -0
  224. package/dist/proxmox/generated/pve/cluster-sdn-zones.d.ts.map +1 -0
  225. package/dist/proxmox/generated/pve/cluster-sdn.d.ts +202 -0
  226. package/dist/proxmox/generated/pve/cluster-sdn.d.ts.map +1 -0
  227. package/dist/proxmox/generated/pve/cluster.d.ts +69 -0
  228. package/dist/proxmox/generated/pve/cluster.d.ts.map +1 -0
  229. package/dist/proxmox/generated/pve/nodes-node-ceph.d.ts +167 -0
  230. package/dist/proxmox/generated/pve/nodes-node-ceph.d.ts.map +1 -0
  231. package/dist/proxmox/generated/pve/nodes-node-config.d.ts +43 -0
  232. package/dist/proxmox/generated/pve/nodes-node-config.d.ts.map +1 -0
  233. package/dist/proxmox/generated/pve/nodes-node-disks.d.ts +225 -0
  234. package/dist/proxmox/generated/pve/nodes-node-disks.d.ts.map +1 -0
  235. package/dist/proxmox/generated/pve/nodes-node-firewall.d.ts +145 -0
  236. package/dist/proxmox/generated/pve/nodes-node-firewall.d.ts.map +1 -0
  237. package/dist/proxmox/generated/pve/nodes-node-hardware.d.ts +195 -0
  238. package/dist/proxmox/generated/pve/nodes-node-hardware.d.ts.map +1 -0
  239. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-firewall.d.ts +191 -0
  240. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-firewall.d.ts.map +1 -0
  241. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-move-volume.d.ts +33 -0
  242. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-move-volume.d.ts.map +1 -0
  243. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-mtunnel.d.ts +56 -0
  244. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-mtunnel.d.ts.map +1 -0
  245. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-resize.d.ts +28 -0
  246. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-resize.d.ts.map +1 -0
  247. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-rrd.d.ts +186 -0
  248. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-rrd.d.ts.map +1 -0
  249. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid.d.ts +191 -0
  250. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid.d.ts.map +1 -0
  251. package/dist/proxmox/generated/pve/nodes-node-network.d.ts +150 -0
  252. package/dist/proxmox/generated/pve/nodes-node-network.d.ts.map +1 -0
  253. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-config.d.ts +301 -0
  254. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-config.d.ts.map +1 -0
  255. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-dbus-vmstate.d.ts +170 -0
  256. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-dbus-vmstate.d.ts.map +1 -0
  257. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-firewall-rules.d.ts +144 -0
  258. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-firewall-rules.d.ts.map +1 -0
  259. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-move-disk.d.ts +34 -0
  260. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-move-disk.d.ts.map +1 -0
  261. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-mtunnel.d.ts +137 -0
  262. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-mtunnel.d.ts.map +1 -0
  263. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-status.d.ts +179 -0
  264. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-status.d.ts.map +1 -0
  265. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid.d.ts +143 -0
  266. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid.d.ts.map +1 -0
  267. package/dist/proxmox/generated/pve/nodes-node-qemu.d.ts +158 -0
  268. package/dist/proxmox/generated/pve/nodes-node-qemu.d.ts.map +1 -0
  269. package/dist/proxmox/generated/pve/nodes-node-replication.d.ts +187 -0
  270. package/dist/proxmox/generated/pve/nodes-node-replication.d.ts.map +1 -0
  271. package/dist/proxmox/generated/pve/nodes-node-services.d.ts +115 -0
  272. package/dist/proxmox/generated/pve/nodes-node-services.d.ts.map +1 -0
  273. package/dist/proxmox/generated/pve/nodes-node-storage-storage.d.ts +202 -0
  274. package/dist/proxmox/generated/pve/nodes-node-storage-storage.d.ts.map +1 -0
  275. package/dist/proxmox/generated/pve/nodes-node-storage.d.ts +40 -0
  276. package/dist/proxmox/generated/pve/nodes-node-storage.d.ts.map +1 -0
  277. package/dist/proxmox/generated/pve/nodes-node-subscription.d.ts +173 -0
  278. package/dist/proxmox/generated/pve/nodes-node-subscription.d.ts.map +1 -0
  279. package/dist/proxmox/generated/pve/nodes-node-vzdump.d.ts +97 -0
  280. package/dist/proxmox/generated/pve/nodes-node-vzdump.d.ts.map +1 -0
  281. package/dist/proxmox/generated/pve/nodes-node.d.ts +207 -0
  282. package/dist/proxmox/generated/pve/nodes-node.d.ts.map +1 -0
  283. package/dist/proxmox/generated/pve/nodes.d.ts +236 -0
  284. package/dist/proxmox/generated/pve/nodes.d.ts.map +1 -0
  285. package/dist/proxmox/generated/pve/pools.d.ts +82 -0
  286. package/dist/proxmox/generated/pve/pools.d.ts.map +1 -0
  287. package/dist/proxmox/generated/pve/storage.d.ts +159 -0
  288. package/dist/proxmox/generated/pve/storage.d.ts.map +1 -0
  289. package/dist/proxmox/generated/pve/version.d.ts +21 -0
  290. package/dist/proxmox/generated/pve/version.d.ts.map +1 -0
  291. package/dist/proxmox/generated/pve.d.ts +69 -4766
  292. package/dist/proxmox/generated/pve.d.ts.map +1 -1
  293. package/dist/proxmox/index.js +996 -372
  294. package/dist/proxmox/index.js.map +48 -32
  295. package/dist/proxmox/lxc-lifecycle.d.ts.map +1 -1
  296. package/dist/proxmox/metric-server-form.d.ts +11 -0
  297. package/dist/proxmox/metric-server-form.d.ts.map +1 -1
  298. package/dist/proxmox/metric-server.d.ts.map +1 -1
  299. package/dist/proxmox/network-apply.d.ts.map +1 -1
  300. package/dist/proxmox/node-network-form.d.ts +15 -0
  301. package/dist/proxmox/node-network-form.d.ts.map +1 -1
  302. package/dist/proxmox/node-network.d.ts.map +1 -1
  303. package/dist/proxmox/notification-target-endpoint.d.ts +26 -0
  304. package/dist/proxmox/notification-target-endpoint.d.ts.map +1 -0
  305. package/dist/proxmox/notification-target-form.d.ts +7 -0
  306. package/dist/proxmox/notification-target-form.d.ts.map +1 -1
  307. package/dist/proxmox/notification-target.d.ts.map +1 -1
  308. package/dist/proxmox/pbs-notification-target-endpoint.d.ts +29 -0
  309. package/dist/proxmox/pbs-notification-target-endpoint.d.ts.map +1 -0
  310. package/dist/proxmox/pbs-notification-target-lifecycle.d.ts.map +1 -1
  311. package/dist/proxmox/qemu.d.ts +10 -0
  312. package/dist/proxmox/qemu.d.ts.map +1 -1
  313. package/dist/proxmox/resource-guard.d.ts +15 -0
  314. package/dist/proxmox/resource-guard.d.ts.map +1 -0
  315. package/dist/proxmox/resource-spec.d.ts +15 -4
  316. package/dist/proxmox/resource-spec.d.ts.map +1 -1
  317. package/dist/proxmox/resource.d.ts +8 -0
  318. package/dist/proxmox/resource.d.ts.map +1 -1
  319. package/dist/proxmox/sdn-apply.d.ts.map +1 -1
  320. package/dist/proxmox/sdn-subnet.d.ts.map +1 -1
  321. package/dist/proxmox/zfs-pool-write.d.ts +11 -0
  322. package/dist/proxmox/zfs-pool-write.d.ts.map +1 -1
  323. package/dist/proxmox/zfs-pool.d.ts.map +1 -1
  324. package/docs/linux-host-measured.md +56 -0
  325. package/docs/linux-host.md +186 -0
  326. package/docs/netbox.md +103 -0
  327. package/package.json +9 -1
  328. package/src/linux/directory-lifecycle.ts +220 -0
  329. package/src/linux/directory.test.ts +132 -0
  330. package/src/linux/directory.ts +71 -0
  331. package/src/linux/fake-linux-host.ts +197 -0
  332. package/src/linux/fake-systemd.ts +103 -0
  333. package/src/linux/index.ts +32 -0
  334. package/src/linux/providers.ts +26 -0
  335. package/src/linux/region.test.ts +89 -0
  336. package/src/linux/region.ts +118 -0
  337. package/src/linux/remote-file-form.ts +96 -0
  338. package/src/linux/remote-file-lifecycle.test.ts +191 -0
  339. package/src/linux/remote-file-lifecycle.ts +161 -0
  340. package/src/linux/remote-file-plan.ts +154 -0
  341. package/src/linux/remote-file.ts +88 -0
  342. package/src/linux/ssh-command.test.ts +140 -0
  343. package/src/linux/ssh-command.ts +99 -0
  344. package/src/linux/ssh-runner.ts +188 -0
  345. package/src/linux/ssh-scripts.ts +105 -0
  346. package/src/linux/systemctl.ts +145 -0
  347. package/src/linux/unit-form.ts +183 -0
  348. package/src/linux/unit-handlers.ts +73 -0
  349. package/src/linux/unit-lifecycle.test.ts +220 -0
  350. package/src/linux/unit-lifecycle.ts +193 -0
  351. package/src/linux/unit-settle.ts +110 -0
  352. package/src/linux/unit.ts +81 -0
  353. package/src/netbox/client.test.ts +67 -0
  354. package/src/netbox/client.ts +146 -0
  355. package/src/netbox/constraint-guard.ts +57 -0
  356. package/src/netbox/constraints.test.ts +114 -0
  357. package/src/netbox/constraints.ts +152 -0
  358. package/src/netbox/generated/constraints/index.ts +23 -0
  359. package/src/netbox/generated/constraints/netbox-ipam.ts +32 -0
  360. package/src/netbox/index.ts +34 -0
  361. package/src/netbox/prefix-form.test.ts +127 -0
  362. package/src/netbox/prefix-form.ts +84 -0
  363. package/src/netbox/prefix.ts +136 -0
  364. package/src/netbox/resource.ts +208 -0
  365. package/src/netbox/values.test.ts +66 -0
  366. package/src/netbox/values.ts +40 -0
  367. package/src/proxmox/api-token-form.ts +5 -0
  368. package/src/proxmox/apply-endpoints.ts +34 -0
  369. package/src/proxmox/ceph-daemon-form.ts +56 -2
  370. package/src/proxmox/ceph-daemon.ts +10 -21
  371. package/src/proxmox/ceph-flag.ts +1 -0
  372. package/src/proxmox/ceph-fs.ts +1 -0
  373. package/src/proxmox/ceph-osd-write.ts +30 -4
  374. package/src/proxmox/ceph-pool.ts +10 -0
  375. package/src/proxmox/constraints-dialect.test.ts +165 -0
  376. package/src/proxmox/constraints-forms.test.ts +220 -0
  377. package/src/proxmox/constraints-host-forms.test.ts +132 -0
  378. package/src/proxmox/constraints-live.test.ts +155 -0
  379. package/src/proxmox/constraints-node-forms.test.ts +188 -0
  380. package/src/proxmox/constraints-required.test.ts +172 -0
  381. package/src/proxmox/constraints.test.ts +8 -63
  382. package/src/proxmox/constraints.ts +31 -6
  383. package/src/proxmox/generated/constraints/index.ts +27 -3
  384. package/src/proxmox/generated/constraints/pbs-config.ts +63 -1
  385. package/src/proxmox/generated/constraints/pve-access.ts +15 -3
  386. package/src/proxmox/generated/constraints/pve-cluster-backup.ts +74 -0
  387. package/src/proxmox/generated/constraints/pve-cluster-ceph.ts +27 -0
  388. package/src/proxmox/generated/constraints/pve-cluster-firewall.ts +35 -0
  389. package/src/proxmox/generated/constraints/pve-cluster-ha.ts +55 -0
  390. package/src/proxmox/generated/constraints/pve-cluster-metrics.ts +59 -0
  391. package/src/proxmox/generated/constraints/pve-cluster-notifications.ts +80 -0
  392. package/src/proxmox/generated/constraints/pve-cluster-replication.ts +43 -0
  393. package/src/proxmox/generated/constraints/pve-cluster-sdn.ts +90 -0
  394. package/src/proxmox/generated/constraints/pve-nodes-ceph.ts +57 -0
  395. package/src/proxmox/generated/constraints/pve-nodes-disks.ts +31 -0
  396. package/src/proxmox/generated/constraints/pve-nodes-lxc.ts +82 -0
  397. package/src/proxmox/generated/constraints/pve-nodes-network.ts +77 -0
  398. package/src/proxmox/generated/constraints/pve-nodes-qemu.ts +125 -0
  399. package/src/proxmox/generated/constraints/pve-pools.ts +6 -1
  400. package/src/proxmox/generated/constraints/pve-storage.ts +8 -3
  401. package/src/proxmox/generated/pbs/access-users.ts +137 -0
  402. package/src/proxmox/generated/pbs/access.ts +232 -0
  403. package/src/proxmox/generated/pbs/admin-datastore-store.ts +238 -0
  404. package/src/proxmox/generated/pbs/admin-metrics.ts +147 -0
  405. package/src/proxmox/generated/pbs/admin.ts +225 -0
  406. package/src/proxmox/generated/pbs/backup.ts +121 -0
  407. package/src/proxmox/generated/pbs/config-access-pam.ts +228 -0
  408. package/src/proxmox/generated/pbs/config-access.ts +228 -0
  409. package/src/proxmox/generated/pbs/config-datastore.ts +217 -0
  410. package/src/proxmox/generated/pbs/config-media-pool.ts +232 -0
  411. package/src/proxmox/generated/pbs/config-notifications-matchers.ts +187 -0
  412. package/src/proxmox/generated/pbs/config-notifications.ts +221 -0
  413. package/src/proxmox/generated/pbs/config-remote.ts +192 -0
  414. package/src/proxmox/generated/pbs/config-sync.ts +167 -0
  415. package/src/proxmox/generated/pbs/config-tape.ts +226 -0
  416. package/src/proxmox/generated/pbs/config-verify.ts +87 -0
  417. package/src/proxmox/generated/pbs/config.ts +135 -0
  418. package/src/proxmox/generated/pbs/nodes-node-dns.ts +229 -0
  419. package/src/proxmox/generated/pbs/nodes-node-services.ts +181 -0
  420. package/src/proxmox/generated/pbs/nodes-node.ts +244 -0
  421. package/src/proxmox/generated/pbs/nodes.ts +160 -0
  422. package/src/proxmox/generated/pbs/ping.ts +16 -0
  423. package/src/proxmox/generated/pbs/pull.ts +37 -0
  424. package/src/proxmox/generated/pbs/push.ts +36 -0
  425. package/src/proxmox/generated/pbs/reader.ts +41 -0
  426. package/src/proxmox/generated/pbs/root.ts +16 -0
  427. package/src/proxmox/generated/pbs/status.ts +50 -0
  428. package/src/proxmox/generated/pbs/tape-drive.ts +220 -0
  429. package/src/proxmox/generated/pbs/tape-media.ts +133 -0
  430. package/src/proxmox/generated/pbs/tape.ts +92 -0
  431. package/src/proxmox/generated/pbs/version.ts +16 -0
  432. package/src/proxmox/generated/pbs.ts +44 -787
  433. package/src/proxmox/generated/pve/access-roles.ts +168 -0
  434. package/src/proxmox/generated/pve/access-users.ts +155 -0
  435. package/src/proxmox/generated/pve/access.ts +217 -0
  436. package/src/proxmox/generated/pve/cluster-acme-plugins-id.ts +367 -0
  437. package/src/proxmox/generated/pve/cluster-acme-plugins.ts +366 -0
  438. package/src/proxmox/generated/pve/cluster-acme-tos.ts +232 -0
  439. package/src/proxmox/generated/pve/cluster-bulk-action.ts +227 -0
  440. package/src/proxmox/generated/pve/cluster-firewall-refs.ts +110 -0
  441. package/src/proxmox/generated/pve/cluster-firewall.ts +236 -0
  442. package/src/proxmox/generated/pve/cluster-ha.ts +203 -0
  443. package/src/proxmox/generated/pve/cluster-jobs.ts +190 -0
  444. package/src/proxmox/generated/pve/cluster-metrics.ts +116 -0
  445. package/src/proxmox/generated/pve/cluster-notifications-endpoints.ts +244 -0
  446. package/src/proxmox/generated/pve/cluster-notifications.ts +99 -0
  447. package/src/proxmox/generated/pve/cluster-options.ts +189 -0
  448. package/src/proxmox/generated/pve/cluster-qemu-custom-cpu-models-cputype.ts +272 -0
  449. package/src/proxmox/generated/pve/cluster-qemu-custom-cpu-models.ts +268 -0
  450. package/src/proxmox/generated/pve/cluster-replication.ts +127 -0
  451. package/src/proxmox/generated/pve/cluster-sdn-fabrics.ts +218 -0
  452. package/src/proxmox/generated/pve/cluster-sdn-ipams.ts +229 -0
  453. package/src/proxmox/generated/pve/cluster-sdn-vnets-vnet.ts +245 -0
  454. package/src/proxmox/generated/pve/cluster-sdn-vnets.ts +49 -0
  455. package/src/proxmox/generated/pve/cluster-sdn-zones.ts +224 -0
  456. package/src/proxmox/generated/pve/cluster-sdn.ts +201 -0
  457. package/src/proxmox/generated/pve/cluster.ts +71 -0
  458. package/src/proxmox/generated/pve/nodes-node-ceph.ts +167 -0
  459. package/src/proxmox/generated/pve/nodes-node-config.ts +56 -0
  460. package/src/proxmox/generated/pve/nodes-node-disks.ts +237 -0
  461. package/src/proxmox/generated/pve/nodes-node-firewall.ts +239 -0
  462. package/src/proxmox/generated/pve/nodes-node-hardware.ts +215 -0
  463. package/src/proxmox/generated/pve/nodes-node-lxc-vmid-firewall.ts +232 -0
  464. package/src/proxmox/generated/pve/nodes-node-lxc-vmid-move-volume.ts +1059 -0
  465. package/src/proxmox/generated/pve/nodes-node-lxc-vmid-mtunnel.ts +49 -0
  466. package/src/proxmox/generated/pve/nodes-node-lxc-vmid-resize.ts +285 -0
  467. package/src/proxmox/generated/pve/nodes-node-lxc-vmid-rrd.ts +184 -0
  468. package/src/proxmox/generated/pve/nodes-node-lxc-vmid.ts +240 -0
  469. package/src/proxmox/generated/pve/nodes-node-network.ts +234 -0
  470. package/src/proxmox/generated/pve/nodes-node-qemu-vmid-config.ts +441 -0
  471. package/src/proxmox/generated/pve/nodes-node-qemu-vmid-dbus-vmstate.ts +215 -0
  472. package/src/proxmox/generated/pve/nodes-node-qemu-vmid-firewall-rules.ts +145 -0
  473. package/src/proxmox/generated/pve/nodes-node-qemu-vmid-move-disk.ts +664 -0
  474. package/src/proxmox/generated/pve/nodes-node-qemu-vmid-mtunnel.ts +188 -0
  475. package/src/proxmox/generated/pve/nodes-node-qemu-vmid-status.ts +233 -0
  476. package/src/proxmox/generated/pve/nodes-node-qemu-vmid.ts +187 -0
  477. package/src/proxmox/generated/pve/nodes-node-qemu.ts +204 -0
  478. package/src/proxmox/generated/pve/nodes-node-replication.ts +187 -0
  479. package/src/proxmox/generated/pve/nodes-node-services.ts +221 -0
  480. package/src/proxmox/generated/pve/nodes-node-storage-storage.ts +226 -0
  481. package/src/proxmox/generated/pve/nodes-node-storage.ts +40 -0
  482. package/src/proxmox/generated/pve/nodes-node-subscription.ts +168 -0
  483. package/src/proxmox/generated/pve/nodes-node-vzdump.ts +96 -0
  484. package/src/proxmox/generated/pve/nodes-node.ts +211 -0
  485. package/src/proxmox/generated/pve/nodes.ts +235 -0
  486. package/src/proxmox/generated/pve/pools.ts +78 -0
  487. package/src/proxmox/generated/pve/storage.ts +213 -0
  488. package/src/proxmox/generated/pve/version.ts +21 -0
  489. package/src/proxmox/generated/pve.ts +69 -7403
  490. package/src/proxmox/lxc-interior.test.ts +17 -1
  491. package/src/proxmox/lxc-lifecycle.ts +48 -9
  492. package/src/proxmox/metric-server-form.ts +10 -0
  493. package/src/proxmox/metric-server.ts +11 -2
  494. package/src/proxmox/network-apply.ts +4 -0
  495. package/src/proxmox/node-network-form.ts +19 -0
  496. package/src/proxmox/node-network.ts +9 -3
  497. package/src/proxmox/notification-target-endpoint.ts +46 -0
  498. package/src/proxmox/notification-target-form.ts +11 -0
  499. package/src/proxmox/notification-target.ts +5 -2
  500. package/src/proxmox/pbs-notification-target-endpoint.ts +45 -0
  501. package/src/proxmox/pbs-notification-target-lifecycle.ts +16 -14
  502. package/src/proxmox/qemu.ts +9 -1
  503. package/src/proxmox/resource-guard.ts +61 -0
  504. package/src/proxmox/resource-spec.ts +16 -4
  505. package/src/proxmox/resource.ts +27 -27
  506. package/src/proxmox/sdn-apply.ts +4 -0
  507. package/src/proxmox/sdn-subnet.ts +5 -0
  508. package/src/proxmox/zfs-pool-write.ts +12 -0
  509. package/src/proxmox/zfs-pool.ts +2 -1
  510. package/dist/proxmox/generated/constraints/pve-cluster.d.ts +0 -18
  511. package/dist/proxmox/generated/constraints/pve-cluster.d.ts.map +0 -1
  512. package/src/proxmox/generated/constraints/pve-cluster.ts +0 -174
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Systemd.Unit's read / diff / reconcile / delete as plain async functions over a HostRunner, so
3
+ * the whole lifecycle runs against a fake host with no real systemd.
4
+ *
5
+ * ⛔ WHAT A DEPLOY DOES NOT DO — the rule this family was built around. It NEVER mass-restarts.
6
+ * A unit is restarted only when THIS resource's unit file changed, or when a digest the
7
+ * declaration itself listed in `restartOn` changed. A unit that is already running the content
8
+ * we declare is left strictly alone: no restart, no reload, not even a `start`. An estate rebuild
9
+ * must be able to reach a host that holds a vault, a UPS handler or a clock without bouncing
10
+ * them, and "the deploy ran" must never be a reason anything went down.
11
+ * ★ THE ORDER IS WRITE, RELOAD, ENABLE, THEN START OR RESTART. systemd reads the unit file at
12
+ * `daemon-reload`, so writing it while the old unit runs changes nothing yet, and a write that
13
+ * fails leaves the old unit running rather than leaving nothing running.
14
+ * ⚠️ A FAILED START LEAVES THE UNIT DOWN. On an update the new unit file stays and state keeps the
15
+ * previous digest, so the next deploy retries. On a CREATE the file this deploy wrote is removed
16
+ * and systemd reloaded again: left behind, the next plan's recovery read finds a unit with no
17
+ * state, reports it `Unowned`, and every later deploy demands `--adopt` for our own file.
18
+ * ⛔ A MASKED UNIT IS SOMEONE'S DECISION, NOT DRIFT. `systemctl mask` survives reboots and makes
19
+ * start fail; unmasking here would silently overrule whoever masked it.
20
+ */
21
+ import type { Diff } from 'alchemy/Diff';
22
+ import { type HostRunner, canActAsRoot } from '../launchd/runner.ts';
23
+ import {
24
+ type SystemdUnitAttributes,
25
+ type SystemdUnitProps,
26
+ UNIT_WRITE,
27
+ configDigest,
28
+ digestOf,
29
+ unitPathFor,
30
+ unitProblems,
31
+ unitText,
32
+ } from './unit-form.ts';
33
+ import { type UnitStatus, daemonReload, disableUnit, showUnit, stopUnit } from './systemctl.ts';
34
+ import { attributesOf, refuse, settle } from './unit-settle.ts';
35
+
36
+ const encoder = new TextEncoder();
37
+ const decoder = new TextDecoder();
38
+
39
+ /** Throw every refusal at once, so one plan shows the whole list. */
40
+ export const assertValid = (props: SystemdUnitProps, expect?: string): void => {
41
+ const found = unitProblems(props, expect);
42
+ if (found.length > 0) throw refuse(props.name, found.join('; '));
43
+ };
44
+
45
+ /** What is on the host now: the unit file's digest and systemd's view. `undefined` when neither. */
46
+ export const readUnit = async (
47
+ runner: HostRunner,
48
+ props: SystemdUnitProps,
49
+ ): Promise<SystemdUnitAttributes | undefined> => {
50
+ const bytes = await runner.readFile(unitPathFor(props));
51
+ const status = await showUnit(runner, props.name);
52
+ if (bytes === undefined && !status.known) return undefined;
53
+ return attributesOf(props, bytes === undefined ? '' : digestOf(decoder.decode(bytes)), status);
54
+ };
55
+
56
+ /** ⛔ Writing a unit file and driving systemd are root's, so refuse up front rather than mid-apply. */
57
+ export const assertMayWrite = (runner: HostRunner, name: string): void => {
58
+ if (canActAsRoot(runner)) return;
59
+ throw refuse(
60
+ name,
61
+ 'a systemd unit is root-owned and systemctl needs root. Point the runner at a destination ' +
62
+ 'whose ssh user is root, or provide a privileged HostRunner. This provider never calls sudo.',
63
+ );
64
+ };
65
+
66
+ const assertUsable = (props: SystemdUnitProps, status: UnitStatus): void => {
67
+ if (status.loadState === 'masked' || status.unitFileState === 'masked') {
68
+ throw refuse(
69
+ props.name,
70
+ 'is masked. If that is stale, `systemctl unmask` it deliberately, then redeploy. Nothing ' +
71
+ 'was written.',
72
+ );
73
+ }
74
+ };
75
+
76
+ export const diffUnit = async (
77
+ runner: HostRunner,
78
+ news: SystemdUnitProps,
79
+ output: SystemdUnitAttributes,
80
+ expect?: string,
81
+ ): Promise<Diff> => {
82
+ assertValid(news, expect);
83
+ // ★ The unit's name or its directory is its identity on the host: a change is a replace, and
84
+ // delete-first, because two unit files for one name cannot both be the one systemd reads.
85
+ if (news.name !== output.name || unitPathFor(news) !== output.unitPath) {
86
+ assertMayWrite(runner, news.name);
87
+ return { action: 'replace', deleteFirst: true };
88
+ }
89
+ const desired = digestOf(unitText(news));
90
+ const update = async (): Promise<Diff> => {
91
+ assertMayWrite(runner, news.name);
92
+ await runner.checkWrite?.(unitPathFor(news), UNIT_WRITE);
93
+ return { action: 'update' };
94
+ };
95
+ // ⛔ BOTH digests: the stored one catches a start that failed after the write landed, the live
96
+ // one catches a hand edit. Either alone reports `noop` over a unit that is not what we say.
97
+ if (desired !== output.unitSha256) return update();
98
+ if (configDigest(news.restartOn) !== output.configSha256) return update();
99
+ const live = await readUnit(runner, news);
100
+ if (live === undefined || live.unitSha256 !== desired) return update();
101
+ if (live.enabled !== (news.enabled !== false)) return update();
102
+ if (live.active !== (news.started !== false)) return update();
103
+ // ⚠️ systemd's own word for "the file on disk is newer than what I loaded" — a write that landed
104
+ // without its reload. It is drift that neither digest can see.
105
+ const status = await showUnit(runner, news.name);
106
+ return status.needDaemonReload ? update() : { action: 'noop' };
107
+ };
108
+
109
+ /**
110
+ * `adopt` is whether adoption is on AND this apply is a create or an unfinished generation of our
111
+ * own (ownership/adopt.ts): it lets such a generation take over a unit already on the host.
112
+ * ★ AN ADOPTED UNIT THAT ALREADY MATCHES IS NOT RESTARTED. That is the whole point of adopting one:
113
+ * the estate's live services come under declaration without a single bounce.
114
+ */
115
+ export const reconcileUnit = async (
116
+ runner: HostRunner,
117
+ props: SystemdUnitProps,
118
+ output: SystemdUnitAttributes | undefined,
119
+ adopt = false,
120
+ expect?: string,
121
+ ): Promise<SystemdUnitAttributes> => {
122
+ assertValid(props, expect);
123
+ assertMayWrite(runner, props.name);
124
+ const path = unitPathFor(props);
125
+ const status = await showUnit(runner, props.name);
126
+ assertUsable(props, status);
127
+ const text = unitText(props);
128
+ const desired = digestOf(text);
129
+ const before = await runner.readFile(path);
130
+ const liveSha = before === undefined ? undefined : digestOf(decoder.decode(before));
131
+ const moved = output !== undefined && (output.name !== props.name || output.unitPath !== path);
132
+ const prior = moved ? undefined : output;
133
+ /**
134
+ * ⛔ A UNIT THIS RESOURCE DOES NOT OWN IS NEVER OVERWRITTEN. Where the engine's probe never
135
+ * looked — a replace's new name, a create whose props were still an Output at plan time — a
136
+ * unit file already there with other content belongs to someone else, and writing ours over it
137
+ * would take their service. ★ A byte-identical file is this declaration's own leftover.
138
+ */
139
+ if (
140
+ prior === undefined &&
141
+ !(adopt && output === undefined) &&
142
+ liveSha !== undefined &&
143
+ liveSha !== desired
144
+ ) {
145
+ throw refuse(
146
+ props.name,
147
+ `${path} is already on this host and is not this resource. Remove it, or declare it as a ` +
148
+ 'new resource and deploy with --adopt.',
149
+ );
150
+ }
151
+ if (moved && output !== undefined) await deleteUnit(runner, output);
152
+ const wrote = liveSha !== desired;
153
+ /**
154
+ * ⛔ THE THREE THINGS THAT EARN A RESTART, and nothing else:
155
+ * - the unit file's bytes changed (`wrote`);
156
+ * - state holds an older digest than the file does — a previous deploy's write landed and its
157
+ * reload or restart never ran, so systemd is still running the old content;
158
+ * - systemd itself says its loaded unit is older than the file (`NeedDaemonReload`, measured);
159
+ * - a digest the declaration listed in `restartOn` changed.
160
+ * ★ An adopted, converged unit hits none of them, which is why adoption never bounces a service.
161
+ */
162
+ const stale = prior !== undefined && prior.unitSha256 !== desired;
163
+ const configChanged = prior !== undefined && prior.configSha256 !== configDigest(props.restartOn);
164
+ if (wrote) await runner.writeFileAtomic(path, encoder.encode(text), UNIT_WRITE);
165
+ if (wrote || status.needDaemonReload) await daemonReload(runner);
166
+ const changed = wrote || stale || status.needDaemonReload;
167
+ return settle(runner, props, { before, changed, configChanged, desired, path, status });
168
+ };
169
+
170
+ /**
171
+ * Stop, disable, remove the unit file, reload. Idempotent.
172
+ * ⛔ DELETING THE DECLARATION STOPS THE SERVICE. That is what removing it from the stack means, and
173
+ * it is why a unit that must outlive its declaration is adopted rather than deleted.
174
+ */
175
+ export const deleteUnit = async (
176
+ runner: HostRunner,
177
+ output: SystemdUnitAttributes,
178
+ ): Promise<void> => {
179
+ assertMayWrite(runner, output.name);
180
+ const status = await showUnit(runner, output.name);
181
+ if (status.activeState === 'active' || status.activeState === 'activating') {
182
+ await stopUnit(runner, output.name);
183
+ }
184
+ if (status.unitFileState === 'enabled' || status.unitFileState === 'enabled-runtime') {
185
+ await disableUnit(runner, output.name);
186
+ }
187
+ const stat = await runner.stat(output.unitPath);
188
+ if (stat !== undefined) {
189
+ if (stat.kind !== 'file') throw refuse(output.name, `${output.unitPath} is a ${stat.kind}`);
190
+ await runner.removeFile(output.unitPath);
191
+ }
192
+ await daemonReload(runner);
193
+ };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The step that decides whether anything on the host actually moves: enablement, running state and
3
+ * — only for a real change — a restart. Split from unit-lifecycle.ts because it is the rule the
4
+ * whole family exists to keep, and it should be readable on its own.
5
+ *
6
+ * ⛔ THE RESTART CONDITION IS THE WHOLE CONTRACT. `changed` is set by the caller from the four
7
+ * things that earn one (unit-lifecycle.ts reconcileUnit); `configChanged` is a digest the
8
+ * DECLARATION itself listed. Nothing else restarts a unit — not a re-run, not an unrelated
9
+ * resource in the same deploy, not `--adopt`.
10
+ */
11
+ import type { HostRunner } from '../launchd/runner.ts';
12
+ import {
13
+ type SystemdUnitAttributes,
14
+ type SystemdUnitProps,
15
+ configDigest,
16
+ unitPathFor,
17
+ } from './unit-form.ts';
18
+ import {
19
+ type UnitStatus,
20
+ daemonReload,
21
+ disableUnit,
22
+ enableUnit,
23
+ restartUnit,
24
+ showUnit,
25
+ startUnit,
26
+ stopUnit,
27
+ } from './systemctl.ts';
28
+
29
+ export const refuse = (name: string, message: string): Error =>
30
+ new Error(`Systemd.Unit ${name}: ${message}`);
31
+
32
+ export const attributesOf = (
33
+ props: SystemdUnitProps,
34
+ unitSha256: string,
35
+ status: UnitStatus,
36
+ ): SystemdUnitAttributes => ({
37
+ active: status.activeState === 'active' || status.activeState === 'activating',
38
+ activeState: status.activeState,
39
+ configSha256: configDigest(props.restartOn),
40
+ enabled: status.unitFileState === 'enabled' || status.unitFileState === 'enabled-runtime',
41
+ loadState: status.loadState,
42
+ name: props.name,
43
+ unitPath: unitPathFor(props),
44
+ unitSha256,
45
+ ...(status.unitFileState === undefined ? {} : { unitFileState: status.unitFileState }),
46
+ });
47
+
48
+ export type Settle = {
49
+ readonly before: Uint8Array | undefined;
50
+ /** The unit file or systemd's view of it changed in a way that earns a restart. */
51
+ readonly changed: boolean;
52
+ readonly configChanged: boolean;
53
+ readonly desired: string;
54
+ readonly path: string;
55
+ readonly status: UnitStatus;
56
+ };
57
+
58
+ /**
59
+ * Bring enablement and running state to the declaration, restarting ONLY for a real change.
60
+ * ⛔ THE RESTART CONDITION IS THE WHOLE CONTRACT: the unit file changed, or a digest the caller
61
+ * listed changed. Nothing else — not a re-run, not an unrelated resource, not `--adopt`.
62
+ */
63
+ export const settle = async (
64
+ runner: HostRunner,
65
+ props: SystemdUnitProps,
66
+ step: Settle,
67
+ ): Promise<SystemdUnitAttributes> => {
68
+ const wantEnabled = props.enabled !== false;
69
+ const wantStarted = props.started !== false;
70
+ const isEnabled =
71
+ step.status.unitFileState === 'enabled' || step.status.unitFileState === 'enabled-runtime';
72
+ const undo = async (detail: string): Promise<never> => {
73
+ // ⚠️ A create that never came up leaves nothing behind; an update keeps the new file and the
74
+ // old stored digest, so the next deploy retries.
75
+ if (step.before === undefined) {
76
+ await runner.removeFile(step.path).catch(() => undefined);
77
+ await daemonReload(runner).catch(() => undefined);
78
+ }
79
+ throw refuse(props.name, `${detail} The unit is NOT running.`);
80
+ };
81
+ try {
82
+ if (wantEnabled && (!isEnabled || step.changed)) await enableUnit(runner, props.name);
83
+ if (!wantEnabled && isEnabled) await disableUnit(runner, props.name);
84
+ const active = step.status.activeState === 'active' || step.status.activeState === 'activating';
85
+ if (!wantStarted) {
86
+ if (active) await stopUnit(runner, props.name);
87
+ } else if (!active) await startUnit(runner, props.name);
88
+ else if (step.changed || step.configChanged) await restartUnit(runner, props.name);
89
+ } catch (cause) {
90
+ return undo(cause instanceof Error ? cause.message : String(cause));
91
+ }
92
+ /**
93
+ * ⚠️ READ BACK, never echo the declaration. Returning `enabled: wantEnabled` because we called
94
+ * `enable` would put a CLAIM in state instead of a reading, and the next plan would compare
95
+ * the declaration against itself — so a link systemd did not actually make would never show up
96
+ * as drift, in either direction.
97
+ */
98
+ const after = await showUnit(runner, props.name);
99
+ if (wantStarted && after.activeState !== 'active' && after.activeState !== 'activating') {
100
+ return undo(`systemctl reported success but the unit is ${after.activeState}.`);
101
+ }
102
+ const settled = attributesOf(props, step.desired, after);
103
+ if (wantEnabled && !settled.enabled) {
104
+ return undo(
105
+ `systemctl enable reported success but systemd calls the unit ` +
106
+ `${JSON.stringify(after.unitFileState ?? '')}, not enabled.`,
107
+ );
108
+ }
109
+ return settled;
110
+ };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * `Systemd.Unit` and `Systemd.Timer` — one systemd unit file and systemd's view of it, declared.
3
+ *
4
+ * - create / update — write the unit file atomically, `daemon-reload`, enable or disable, then
5
+ * start, stop, or RESTART ONLY when something actually changed (unit-lifecycle.ts settle).
6
+ * - read — the unit file's SHA-256 plus `systemctl show` (load state, active state, enablement).
7
+ * - diff — the rendered digest against the stored AND the on-disk one, the declared enablement and
8
+ * running state against systemd's, and systemd's own `NeedDaemonReload`.
9
+ * - replace — only when the unit's name or directory changes, and DELETE FIRST: two unit files for
10
+ * one name cannot both be the one systemd reads.
11
+ * - delete — stop, disable, remove the file, reload.
12
+ *
13
+ * ⛔ A DEPLOY NEVER MASS-RESTARTS — see the ⛔ at the top of unit-lifecycle.ts. It is the property
14
+ * that lets this family reach a host holding a vault, a clock or a UPS handler.
15
+ * ⛔ THE UNIT FILE IS NEVER A SECRET (unit-form.ts): it is 0644 and its props sit in state.
16
+ * ⚠️ NOTHING IS ADOPTED WITHOUT `--adopt`. A unit already on the host reads as `Unowned`; adopting
17
+ * one that already matches the declaration does not restart it.
18
+ * ★ TWO RESOURCES, ONE LIFECYCLE. A timer is a unit whose file ends `.timer` and carries a
19
+ * `[Timer]` section; splitting the TYPE rather than the code means a plan says which of the two
20
+ * a row is, and a `.service` declared as a timer is a refusal instead of a puzzle.
21
+ */
22
+ import { Resource } from 'alchemy';
23
+ import * as Provider from 'alchemy/Provider';
24
+ import * as Effect from 'effect/Effect';
25
+ import { HostRunnerService } from '../launchd/runner.ts';
26
+ import type { SystemdUnitAttributes, SystemdUnitProps } from './unit-form.ts';
27
+ import { deleteHandler, diffHandler, readHandler, reconcileHandler } from './unit-handlers.ts';
28
+
29
+ export type { SystemdUnitAttributes, SystemdUnitProps, UnitSection } from './unit-form.ts';
30
+
31
+ export interface SystemdUnit extends Resource<
32
+ 'Systemd.Unit',
33
+ SystemdUnitProps,
34
+ SystemdUnitAttributes
35
+ > {}
36
+
37
+ export interface SystemdTimer extends Resource<
38
+ 'Systemd.Timer',
39
+ SystemdUnitProps,
40
+ SystemdUnitAttributes
41
+ > {}
42
+
43
+ export const SystemdUnit = Resource<SystemdUnit>('Systemd.Unit');
44
+ export const SystemdTimer = Resource<SystemdTimer>('Systemd.Timer');
45
+
46
+ export const SystemdUnitProvider = () =>
47
+ Provider.effect(
48
+ SystemdUnit,
49
+ Effect.gen(function* () {
50
+ const runner = yield* HostRunnerService;
51
+ return SystemdUnit.Provider.of({
52
+ /**
53
+ * ⛔ systemd's unit list is not a list of things this stack owns — it is every vendor,
54
+ * package and hand-written unit on the host. Returning them would invite a nuke to stop
55
+ * services nothing here declared.
56
+ */
57
+ list: () => Effect.succeed([]),
58
+ read: ({ olds, output }) => readHandler(runner, olds, output),
59
+ diff: ({ instanceId, news, output }) => diffHandler(runner, instanceId, news, output),
60
+ reconcile: (args) => reconcileHandler(runner, args),
61
+ delete: ({ output }) => deleteHandler(runner, output),
62
+ });
63
+ }),
64
+ );
65
+
66
+ export const SystemdTimerProvider = () =>
67
+ Provider.effect(
68
+ SystemdTimer,
69
+ Effect.gen(function* () {
70
+ const runner = yield* HostRunnerService;
71
+ return SystemdTimer.Provider.of({
72
+ list: () => Effect.succeed([]),
73
+ read: ({ olds, output }) => readHandler(runner, olds, output),
74
+ // ★ `'timer'` is the whole difference: the same lifecycle, refusing a name that is not one.
75
+ diff: ({ instanceId, news, output }) =>
76
+ diffHandler(runner, instanceId, news, output, 'timer'),
77
+ reconcile: (args) => reconcileHandler(runner, args, 'timer'),
78
+ delete: ({ output }) => deleteHandler(runner, output),
79
+ });
80
+ }),
81
+ );
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Identity resolution, and the two ways it must refuse.
3
+ *
4
+ * ⛔ THE FAILURE THIS GUARDS IS SILENT AND SELF-RENEWING. If an ambiguous match took the first
5
+ * row, adopt would bind to it, the next plan would bind to the other, and every plan after that
6
+ * would report drift that is not there — while "fixing" it PATCHed one object with the other's
7
+ * declaration. Failing loudly is the only outcome an operator can act on.
8
+ */
9
+ import { describe, expect, test } from 'bun:test';
10
+ import { NetboxError, apiBase, soleMatch } from './client.ts';
11
+
12
+ describe('soleMatch', () => {
13
+ test('no candidate is absent, not an error — that is what plans a create', () => {
14
+ expect(soleMatch([], 'ipam/prefixes 10.0.0.0/24')).toBeUndefined();
15
+ });
16
+
17
+ test('exactly one candidate is the object', () => {
18
+ expect(soleMatch([{ id: 7 }], 'ipam/prefixes 10.0.0.0/24')).toEqual({ id: 7 });
19
+ });
20
+
21
+ /** ⛔ Two rows means the identity rule is wrong, and the message says which object it was. */
22
+ test('more than one candidate throws and names the object', () => {
23
+ expect(() => soleMatch([{ id: 7 }, { id: 8 }], 'ipam/prefixes 10.0.0.0/24')).toThrow(
24
+ /ipam\/prefixes 10\.0\.0\.0\/24 matched 2 NetBox objects/,
25
+ );
26
+ });
27
+ });
28
+
29
+ describe('the instance origin', () => {
30
+ /**
31
+ * ⛔ NO DEFAULT HOSTNAME. A published package that fell back to a loopback address would let a
32
+ * consumer who forgot the variable watch every call fail against a machine that is not
33
+ * theirs, rather than being told which variable is missing.
34
+ */
35
+ test('an unset or blank NETBOX_URL names the variable', () => {
36
+ const before = process.env['NETBOX_URL'];
37
+ try {
38
+ delete process.env['NETBOX_URL'];
39
+ expect(() => apiBase()).toThrow(/NETBOX_URL is not set/);
40
+ process.env['NETBOX_URL'] = ' ';
41
+ expect(() => apiBase()).toThrow(/NETBOX_URL is not set/);
42
+ } finally {
43
+ if (before === undefined) delete process.env['NETBOX_URL'];
44
+ else process.env['NETBOX_URL'] = before;
45
+ }
46
+ });
47
+
48
+ test('a trailing slash is stripped so paths do not double up', () => {
49
+ const before = process.env['NETBOX_URL'];
50
+ try {
51
+ process.env['NETBOX_URL'] = 'https://netbox.example.com/';
52
+ expect(apiBase()).toBe('https://netbox.example.com');
53
+ } finally {
54
+ if (before === undefined) delete process.env['NETBOX_URL'];
55
+ else process.env['NETBOX_URL'] = before;
56
+ }
57
+ });
58
+ });
59
+
60
+ describe('NetboxError', () => {
61
+ /** ⚠️ The status is on the instance, because `absentOn404` folds ONLY 404 and nothing else. */
62
+ test('it carries the status a caller has to branch on', () => {
63
+ const error = new NetboxError(404, 'GET', 'ipam/prefixes/', 'Not found.');
64
+ expect(error.status).toBe(404);
65
+ expect(error.message).toContain('NetBox GET ipam/prefixes/ -> 404');
66
+ });
67
+ });
@@ -0,0 +1,146 @@
1
+ /**
2
+ * One authenticated call to the NetBox REST API at `/api`.
3
+ *
4
+ * ⚠️ `Authorization: Token <value>`, NOT `Bearer`. NetBox is Django REST Framework using DRF's own
5
+ * `TokenAuthentication`. MEASURED on this estate's own instance while it was up: `Bearer`
6
+ * returns 401 and `Token` returns 200. ⛔ A wrong scheme and a wrong credential are
7
+ * indistinguishable in the response, which is why this is written down rather than inferred.
8
+ *
9
+ * ⚠️ NETBOX DOES NOT WRAP A DETAIL RESPONSE THE WAY PVE DOES — a GET of one object returns the
10
+ * object. A LIST, however, IS wrapped: `{count, next, previous, results: [...]}`. A caller that
11
+ * treats a list like a detail sees `undefined` and calls a healthy object absent, which plans a
12
+ * create over something that already exists.
13
+ *
14
+ * ⛔ THE TOKEN IS NEVER A PROP. It is read from `NETBOX_TOKEN` at call time only. Alchemy persists
15
+ * attributes unencrypted; nothing stored here may be a credential.
16
+ */
17
+ import * as Effect from 'effect/Effect';
18
+ import * as HttpClient from 'effect/unstable/http/HttpClient';
19
+ import * as HttpClientRequest from 'effect/unstable/http/HttpClientRequest';
20
+
21
+ const PATH_PREFIX = '/api';
22
+
23
+ /**
24
+ * The instance, from `NETBOX_URL`. Trailing slash stripped.
25
+ *
26
+ * ⛔ NO DEFAULT HOSTNAME AND NO DEFAULT PORT. A published package that fell back to a loopback
27
+ * address would let a consumer who forgot the variable watch every call fail against a machine
28
+ * that is not theirs, instead of being told which variable is missing.
29
+ * ⚠️ Read at CALL time, never at module scope — a value captured at import is pinned to whatever
30
+ * the first isolate saw.
31
+ */
32
+ export const apiBase = (): string => {
33
+ const raw = process.env['NETBOX_URL'];
34
+ if (raw === undefined || raw.trim() === '') {
35
+ throw new Error(
36
+ 'NETBOX_URL is not set. Point it at your NetBox instance, e.g. https://netbox.example.com',
37
+ );
38
+ }
39
+ return raw.trim().replace(/\/$/, '');
40
+ };
41
+
42
+ export class NetboxError extends Error {
43
+ constructor(
44
+ readonly status: number,
45
+ readonly method: string,
46
+ readonly path: string,
47
+ detail: string,
48
+ ) {
49
+ super(`NetBox ${method} ${path} -> ${String(status)}: ${detail}`);
50
+ this.name = 'NetboxError';
51
+ }
52
+ }
53
+
54
+ const token = (): Effect.Effect<string, NetboxError> =>
55
+ Effect.gen(function* () {
56
+ const raw = process.env['NETBOX_TOKEN'];
57
+ if (raw === undefined || raw.trim() === '') {
58
+ return yield* Effect.fail(
59
+ new NetboxError(
60
+ 0,
61
+ 'GET',
62
+ '',
63
+ 'NETBOX_TOKEN is unset. Mint a scoped token and export it — never store it in Alchemy props.',
64
+ ),
65
+ );
66
+ }
67
+ return raw.trim();
68
+ });
69
+
70
+ /** A NetBox list response. ⚠️ Paginated: `next` is a URL, not a cursor to guess at. */
71
+ export interface NetboxList<T> {
72
+ readonly count: number;
73
+ readonly next: string | null;
74
+ readonly results: readonly T[];
75
+ }
76
+
77
+ export type NetboxRow = Record<string, unknown>;
78
+
79
+ /** One JSON call. Returns the parsed body, or `undefined` when the response is empty (DELETE). */
80
+ export const netbox = <T>(
81
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
82
+ path: string,
83
+ body?: Record<string, unknown>,
84
+ ) =>
85
+ Effect.gen(function* () {
86
+ const credential = yield* token();
87
+ const url = `${apiBase()}${PATH_PREFIX}/${path.replace(/^\//, '')}`;
88
+ /**
89
+ * ★ THROUGH `HttpClient`, NOT THE GLOBAL `fetch` — the idiom Alchemy's own providers follow.
90
+ * It buys a typed error channel, interruption when a plan is cancelled, the runtime's
91
+ * tracing, and a client a test can replace without monkey-patching a global.
92
+ */
93
+ const client = yield* HttpClient.HttpClient;
94
+ /**
95
+ * ⛔ THE CONTENT TYPE GOES ON `bodyText`, NOT IN THE HEADER MAP. `bodyText(s)` with no second
96
+ * argument sets `text/plain` and OVERWRITES a `Content-Type` set by `setHeaders`, because it
97
+ * is piped afterwards. That shipped `@homeflare/alchemy/forgejo` unable to write at all —
98
+ * every create and update in the package, broken identically, for as long as the only
99
+ * credential on the shelf was read-only and so never reached a request body.
100
+ */
101
+ const request = HttpClientRequest.make(method)(url).pipe(
102
+ HttpClientRequest.setHeaders({
103
+ Accept: 'application/json',
104
+ Authorization: `Token ${credential}`,
105
+ }),
106
+ body === undefined
107
+ ? (self) => self
108
+ : HttpClientRequest.bodyText(JSON.stringify(body), 'application/json'),
109
+ );
110
+ const response = yield* client
111
+ .execute(request)
112
+ .pipe(Effect.mapError((cause) => new NetboxError(0, method, path, String(cause))));
113
+ // ⛔ Status first, body once — a NetBox error body is JSON but a reverse proxy's 502 is not.
114
+ // ⚠️ AND A 502 IS THE EXPECTED SYMPTOM WHEN NETBOX IS DOWN BUT ITS FRONT DOOR IS UP, which
115
+ // is the state the reference instance has been in since 2026-09-14. Folding it to "absent"
116
+ // would plan a create for every object that already exists.
117
+ if (response.status < 200 || response.status >= 300) {
118
+ const detail = yield* response.text.pipe(Effect.orElseSucceed(() => ''));
119
+ return yield* Effect.fail(
120
+ new NetboxError(response.status, method, path, detail.slice(0, 300)),
121
+ );
122
+ }
123
+ // ⚠️ 204 AND AN EMPTY BODY ARE REAL: a DELETE answers no content, and parsing an empty body
124
+ // throws. Read the text once and parse only when there is something to parse.
125
+ const text = yield* response.text.pipe(
126
+ Effect.mapError((cause) => new NetboxError(0, method, path, String(cause))),
127
+ );
128
+ return (text === '' ? undefined : JSON.parse(text)) as T;
129
+ });
130
+
131
+ /**
132
+ * The single row a filtered list returns, or `undefined`.
133
+ *
134
+ * ⛔ MORE THAN ONE MATCH IS A DEFECT, NOT A REASON TO TAKE THE FIRST. A `locate` filter that is
135
+ * not unique would let adopt bind to whichever row NetBox happened to order first, and the next
136
+ * plan would bind to the other one and report drift that is not there.
137
+ */
138
+ export const soleMatch = <T>(rows: readonly T[], describe: string): T | undefined => {
139
+ if (rows.length > 1) {
140
+ throw new Error(
141
+ `${describe} matched ${String(rows.length)} NetBox objects. An object's identity must be ` +
142
+ 'unique; narrow it (the VRF, the site, the tenant) rather than taking the first row.',
143
+ );
144
+ }
145
+ return rows[0];
146
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The generated tables, looked up by endpoint, and the refusal that stops a plan.
3
+ *
4
+ * ★ ONE FILE BETWEEN THE PROVIDERS AND THE GENERATED DATA, so `resource.ts` gains three lines
5
+ * rather than an import of the generated module and a merge, and so a family that writes its
6
+ * own handlers reaches the same check by the same name.
7
+ *
8
+ * ⛔ AN UNKNOWN KEY IS A DEFECT, NOT A REASON TO SKIP THE CHECK. Silently passing would put us
9
+ * back where this started: a declaration nothing checked, and a plan that says so in no way at
10
+ * all. The generator resolves every key in its source scan against the vendor document, so a
11
+ * key that is absent here means the tables were not regenerated — which the message says.
12
+ */
13
+ import * as Effect from 'effect/Effect';
14
+ import {
15
+ type EndpointConstraints,
16
+ type EndpointKey,
17
+ type NetboxBody,
18
+ refusal,
19
+ violations,
20
+ } from './constraints.ts';
21
+ import { NETBOX_CONSTRAINTS } from './generated/constraints/index.ts';
22
+
23
+ export { NETBOX_CONSTRAINTS };
24
+
25
+ export const constraintsFor = (key: EndpointKey): EndpointConstraints => {
26
+ const table = NETBOX_CONSTRAINTS[key];
27
+ if (table === undefined) {
28
+ throw new Error(
29
+ `no vendor constraint table for '${key}'. It is named in this package but not generated: ` +
30
+ 'run `bun codegen/netbox.ts` (see codegen/README.md).',
31
+ );
32
+ }
33
+ return table;
34
+ };
35
+
36
+ /** Every vendor rule this body breaks. Pure — a test calls it with a literal body. */
37
+ export const bodyViolations = (
38
+ key: EndpointKey,
39
+ body: NetboxBody,
40
+ presence: boolean,
41
+ ): readonly string[] => violations(constraintsFor(key), body, { presence });
42
+
43
+ /**
44
+ * ⛔ `Effect.die`, NOT `Effect.fail`. A declaration the vendor would reject is a DEFECT in the
45
+ * stack file, not a recoverable condition: there is no retry and no fallback that makes a
46
+ * 201-character description legal.
47
+ */
48
+ export const guardBody = (
49
+ key: EndpointKey | undefined,
50
+ body: NetboxBody,
51
+ presence: boolean,
52
+ ): Effect.Effect<void> =>
53
+ Effect.suspend(() => {
54
+ if (key === undefined) return Effect.void;
55
+ const found = bodyViolations(key, body, presence);
56
+ return found.length === 0 ? Effect.void : Effect.die(new Error(refusal(key, found)));
57
+ });