@homeflare/alchemy 0.14.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 (567) 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/openbao/index.d.ts +10 -3
  67. package/dist/openbao/index.d.ts.map +1 -1
  68. package/dist/proxmox/acl.d.ts.map +1 -1
  69. package/dist/proxmox/api-token-form.d.ts.map +1 -1
  70. package/dist/proxmox/apply-endpoints.d.ts +30 -0
  71. package/dist/proxmox/apply-endpoints.d.ts.map +1 -0
  72. package/dist/proxmox/backup-job.d.ts.map +1 -1
  73. package/dist/proxmox/ceph-daemon-form.d.ts +29 -1
  74. package/dist/proxmox/ceph-daemon-form.d.ts.map +1 -1
  75. package/dist/proxmox/ceph-daemon.d.ts.map +1 -1
  76. package/dist/proxmox/ceph-flag.d.ts.map +1 -1
  77. package/dist/proxmox/ceph-fs.d.ts.map +1 -1
  78. package/dist/proxmox/ceph-osd-write.d.ts +19 -0
  79. package/dist/proxmox/ceph-osd-write.d.ts.map +1 -1
  80. package/dist/proxmox/ceph-pool.d.ts.map +1 -1
  81. package/dist/proxmox/constraint-guard.d.ts +28 -0
  82. package/dist/proxmox/constraint-guard.d.ts.map +1 -0
  83. package/dist/proxmox/constraints.d.ts +94 -0
  84. package/dist/proxmox/constraints.d.ts.map +1 -0
  85. package/dist/proxmox/firewall-alias.d.ts.map +1 -1
  86. package/dist/proxmox/generated/constraints/index.d.ts +19 -0
  87. package/dist/proxmox/generated/constraints/index.d.ts.map +1 -0
  88. package/dist/proxmox/generated/constraints/pbs-config.d.ts +23 -0
  89. package/dist/proxmox/generated/constraints/pbs-config.d.ts.map +1 -0
  90. package/dist/proxmox/generated/constraints/pve-access.d.ts +23 -0
  91. package/dist/proxmox/generated/constraints/pve-access.d.ts.map +1 -0
  92. package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts +23 -0
  93. package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts.map +1 -0
  94. package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts +23 -0
  95. package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts.map +1 -0
  96. package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts +23 -0
  97. package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts.map +1 -0
  98. package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts +23 -0
  99. package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts.map +1 -0
  100. package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts +23 -0
  101. package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts.map +1 -0
  102. package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts +23 -0
  103. package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts.map +1 -0
  104. package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts +23 -0
  105. package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts.map +1 -0
  106. package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts +23 -0
  107. package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts.map +1 -0
  108. package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts +23 -0
  109. package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts.map +1 -0
  110. package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts +23 -0
  111. package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts.map +1 -0
  112. package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts +23 -0
  113. package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts.map +1 -0
  114. package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts +23 -0
  115. package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts.map +1 -0
  116. package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts +23 -0
  117. package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts.map +1 -0
  118. package/dist/proxmox/generated/constraints/pve-pools.d.ts +23 -0
  119. package/dist/proxmox/generated/constraints/pve-pools.d.ts.map +1 -0
  120. package/dist/proxmox/generated/constraints/pve-storage.d.ts +23 -0
  121. package/dist/proxmox/generated/constraints/pve-storage.d.ts.map +1 -0
  122. package/dist/proxmox/generated/pbs/access-users.d.ts +137 -0
  123. package/dist/proxmox/generated/pbs/access-users.d.ts.map +1 -0
  124. package/dist/proxmox/generated/pbs/access.d.ts +171 -0
  125. package/dist/proxmox/generated/pbs/access.d.ts.map +1 -0
  126. package/dist/proxmox/generated/pbs/admin-datastore-store.d.ts +252 -0
  127. package/dist/proxmox/generated/pbs/admin-datastore-store.d.ts.map +1 -0
  128. package/dist/proxmox/generated/pbs/admin-metrics.d.ts +147 -0
  129. package/dist/proxmox/generated/pbs/admin-metrics.d.ts.map +1 -0
  130. package/dist/proxmox/generated/pbs/admin.d.ts +217 -0
  131. package/dist/proxmox/generated/pbs/admin.d.ts.map +1 -0
  132. package/dist/proxmox/generated/pbs/backup.d.ts +114 -0
  133. package/dist/proxmox/generated/pbs/backup.d.ts.map +1 -0
  134. package/dist/proxmox/generated/pbs/config-access-pam.d.ts +218 -0
  135. package/dist/proxmox/generated/pbs/config-access-pam.d.ts.map +1 -0
  136. package/dist/proxmox/generated/pbs/config-access.d.ts +203 -0
  137. package/dist/proxmox/generated/pbs/config-access.d.ts.map +1 -0
  138. package/dist/proxmox/generated/pbs/config-datastore.d.ts +195 -0
  139. package/dist/proxmox/generated/pbs/config-datastore.d.ts.map +1 -0
  140. package/dist/proxmox/generated/pbs/config-media-pool.d.ts +207 -0
  141. package/dist/proxmox/generated/pbs/config-media-pool.d.ts.map +1 -0
  142. package/dist/proxmox/generated/pbs/config-notifications-matchers.d.ts +159 -0
  143. package/dist/proxmox/generated/pbs/config-notifications-matchers.d.ts.map +1 -0
  144. package/dist/proxmox/generated/pbs/config-notifications.d.ts +199 -0
  145. package/dist/proxmox/generated/pbs/config-notifications.d.ts.map +1 -0
  146. package/dist/proxmox/generated/pbs/config-remote.d.ts +175 -0
  147. package/dist/proxmox/generated/pbs/config-remote.d.ts.map +1 -0
  148. package/dist/proxmox/generated/pbs/config-sync.d.ts +145 -0
  149. package/dist/proxmox/generated/pbs/config-sync.d.ts.map +1 -0
  150. package/dist/proxmox/generated/pbs/config-tape.d.ts +200 -0
  151. package/dist/proxmox/generated/pbs/config-tape.d.ts.map +1 -0
  152. package/dist/proxmox/generated/pbs/config-verify.d.ts +77 -0
  153. package/dist/proxmox/generated/pbs/config-verify.d.ts.map +1 -0
  154. package/dist/proxmox/generated/pbs/config.d.ts +119 -0
  155. package/dist/proxmox/generated/pbs/config.d.ts.map +1 -0
  156. package/dist/proxmox/generated/pbs/nodes-node-dns.d.ts +178 -0
  157. package/dist/proxmox/generated/pbs/nodes-node-dns.d.ts.map +1 -0
  158. package/dist/proxmox/generated/pbs/nodes-node-services.d.ts +205 -0
  159. package/dist/proxmox/generated/pbs/nodes-node-services.d.ts.map +1 -0
  160. package/dist/proxmox/generated/pbs/nodes-node.d.ts +166 -0
  161. package/dist/proxmox/generated/pbs/nodes-node.d.ts.map +1 -0
  162. package/dist/proxmox/generated/pbs/nodes.d.ts +171 -0
  163. package/dist/proxmox/generated/pbs/nodes.d.ts.map +1 -0
  164. package/dist/proxmox/generated/pbs/ping.d.ts +18 -0
  165. package/dist/proxmox/generated/pbs/ping.d.ts.map +1 -0
  166. package/dist/proxmox/generated/pbs/pull.d.ts +37 -0
  167. package/dist/proxmox/generated/pbs/pull.d.ts.map +1 -0
  168. package/dist/proxmox/generated/pbs/push.d.ts +36 -0
  169. package/dist/proxmox/generated/pbs/push.d.ts.map +1 -0
  170. package/dist/proxmox/generated/pbs/reader.d.ts +41 -0
  171. package/dist/proxmox/generated/pbs/reader.d.ts.map +1 -0
  172. package/dist/proxmox/generated/pbs/root.d.ts +16 -0
  173. package/dist/proxmox/generated/pbs/root.d.ts.map +1 -0
  174. package/dist/proxmox/generated/pbs/status.d.ts +54 -0
  175. package/dist/proxmox/generated/pbs/status.d.ts.map +1 -0
  176. package/dist/proxmox/generated/pbs/tape-drive.d.ts +181 -0
  177. package/dist/proxmox/generated/pbs/tape-drive.d.ts.map +1 -0
  178. package/dist/proxmox/generated/pbs/tape-media.d.ts +126 -0
  179. package/dist/proxmox/generated/pbs/tape-media.d.ts.map +1 -0
  180. package/dist/proxmox/generated/pbs/tape.d.ts +90 -0
  181. package/dist/proxmox/generated/pbs/tape.d.ts.map +1 -0
  182. package/dist/proxmox/generated/pbs/version.d.ts +20 -0
  183. package/dist/proxmox/generated/pbs/version.d.ts.map +1 -0
  184. package/dist/proxmox/generated/pbs.d.ts +44 -662
  185. package/dist/proxmox/generated/pbs.d.ts.map +1 -1
  186. package/dist/proxmox/generated/pve/access-roles.d.ts +164 -0
  187. package/dist/proxmox/generated/pve/access-roles.d.ts.map +1 -0
  188. package/dist/proxmox/generated/pve/access-users.d.ts +152 -0
  189. package/dist/proxmox/generated/pve/access-users.d.ts.map +1 -0
  190. package/dist/proxmox/generated/pve/access.d.ts +221 -0
  191. package/dist/proxmox/generated/pve/access.d.ts.map +1 -0
  192. package/dist/proxmox/generated/pve/cluster-acme-plugins-id.d.ts +45 -0
  193. package/dist/proxmox/generated/pve/cluster-acme-plugins-id.d.ts.map +1 -0
  194. package/dist/proxmox/generated/pve/cluster-acme-plugins.d.ts +47 -0
  195. package/dist/proxmox/generated/pve/cluster-acme-plugins.d.ts.map +1 -0
  196. package/dist/proxmox/generated/pve/cluster-acme-tos.d.ts +240 -0
  197. package/dist/proxmox/generated/pve/cluster-acme-tos.d.ts.map +1 -0
  198. package/dist/proxmox/generated/pve/cluster-bulk-action.d.ts +200 -0
  199. package/dist/proxmox/generated/pve/cluster-bulk-action.d.ts.map +1 -0
  200. package/dist/proxmox/generated/pve/cluster-firewall-refs.d.ts +109 -0
  201. package/dist/proxmox/generated/pve/cluster-firewall-refs.d.ts.map +1 -0
  202. package/dist/proxmox/generated/pve/cluster-firewall.d.ts +225 -0
  203. package/dist/proxmox/generated/pve/cluster-firewall.d.ts.map +1 -0
  204. package/dist/proxmox/generated/pve/cluster-ha.d.ts +199 -0
  205. package/dist/proxmox/generated/pve/cluster-ha.d.ts.map +1 -0
  206. package/dist/proxmox/generated/pve/cluster-jobs.d.ts +183 -0
  207. package/dist/proxmox/generated/pve/cluster-jobs.d.ts.map +1 -0
  208. package/dist/proxmox/generated/pve/cluster-metrics.d.ts +110 -0
  209. package/dist/proxmox/generated/pve/cluster-metrics.d.ts.map +1 -0
  210. package/dist/proxmox/generated/pve/cluster-notifications-endpoints.d.ts +224 -0
  211. package/dist/proxmox/generated/pve/cluster-notifications-endpoints.d.ts.map +1 -0
  212. package/dist/proxmox/generated/pve/cluster-notifications.d.ts +91 -0
  213. package/dist/proxmox/generated/pve/cluster-notifications.d.ts.map +1 -0
  214. package/dist/proxmox/generated/pve/cluster-options.d.ts +85 -0
  215. package/dist/proxmox/generated/pve/cluster-options.d.ts.map +1 -0
  216. package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models-cputype.d.ts +48 -0
  217. package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models-cputype.d.ts.map +1 -0
  218. package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models.d.ts +45 -0
  219. package/dist/proxmox/generated/pve/cluster-qemu-custom-cpu-models.d.ts.map +1 -0
  220. package/dist/proxmox/generated/pve/cluster-replication.d.ts +130 -0
  221. package/dist/proxmox/generated/pve/cluster-replication.d.ts.map +1 -0
  222. package/dist/proxmox/generated/pve/cluster-sdn-fabrics.d.ts +220 -0
  223. package/dist/proxmox/generated/pve/cluster-sdn-fabrics.d.ts.map +1 -0
  224. package/dist/proxmox/generated/pve/cluster-sdn-ipams.d.ts +229 -0
  225. package/dist/proxmox/generated/pve/cluster-sdn-ipams.d.ts.map +1 -0
  226. package/dist/proxmox/generated/pve/cluster-sdn-vnets-vnet.d.ts +229 -0
  227. package/dist/proxmox/generated/pve/cluster-sdn-vnets-vnet.d.ts.map +1 -0
  228. package/dist/proxmox/generated/pve/cluster-sdn-vnets.d.ts +51 -0
  229. package/dist/proxmox/generated/pve/cluster-sdn-vnets.d.ts.map +1 -0
  230. package/dist/proxmox/generated/pve/cluster-sdn-zones.d.ts +225 -0
  231. package/dist/proxmox/generated/pve/cluster-sdn-zones.d.ts.map +1 -0
  232. package/dist/proxmox/generated/pve/cluster-sdn.d.ts +202 -0
  233. package/dist/proxmox/generated/pve/cluster-sdn.d.ts.map +1 -0
  234. package/dist/proxmox/generated/pve/cluster.d.ts +69 -0
  235. package/dist/proxmox/generated/pve/cluster.d.ts.map +1 -0
  236. package/dist/proxmox/generated/pve/nodes-node-ceph.d.ts +167 -0
  237. package/dist/proxmox/generated/pve/nodes-node-ceph.d.ts.map +1 -0
  238. package/dist/proxmox/generated/pve/nodes-node-config.d.ts +43 -0
  239. package/dist/proxmox/generated/pve/nodes-node-config.d.ts.map +1 -0
  240. package/dist/proxmox/generated/pve/nodes-node-disks.d.ts +225 -0
  241. package/dist/proxmox/generated/pve/nodes-node-disks.d.ts.map +1 -0
  242. package/dist/proxmox/generated/pve/nodes-node-firewall.d.ts +145 -0
  243. package/dist/proxmox/generated/pve/nodes-node-firewall.d.ts.map +1 -0
  244. package/dist/proxmox/generated/pve/nodes-node-hardware.d.ts +195 -0
  245. package/dist/proxmox/generated/pve/nodes-node-hardware.d.ts.map +1 -0
  246. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-firewall.d.ts +191 -0
  247. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-firewall.d.ts.map +1 -0
  248. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-move-volume.d.ts +33 -0
  249. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-move-volume.d.ts.map +1 -0
  250. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-mtunnel.d.ts +56 -0
  251. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-mtunnel.d.ts.map +1 -0
  252. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-resize.d.ts +28 -0
  253. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-resize.d.ts.map +1 -0
  254. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-rrd.d.ts +186 -0
  255. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid-rrd.d.ts.map +1 -0
  256. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid.d.ts +191 -0
  257. package/dist/proxmox/generated/pve/nodes-node-lxc-vmid.d.ts.map +1 -0
  258. package/dist/proxmox/generated/pve/nodes-node-network.d.ts +150 -0
  259. package/dist/proxmox/generated/pve/nodes-node-network.d.ts.map +1 -0
  260. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-config.d.ts +301 -0
  261. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-config.d.ts.map +1 -0
  262. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-dbus-vmstate.d.ts +170 -0
  263. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-dbus-vmstate.d.ts.map +1 -0
  264. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-firewall-rules.d.ts +144 -0
  265. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-firewall-rules.d.ts.map +1 -0
  266. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-move-disk.d.ts +34 -0
  267. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-move-disk.d.ts.map +1 -0
  268. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-mtunnel.d.ts +137 -0
  269. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-mtunnel.d.ts.map +1 -0
  270. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-status.d.ts +179 -0
  271. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid-status.d.ts.map +1 -0
  272. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid.d.ts +143 -0
  273. package/dist/proxmox/generated/pve/nodes-node-qemu-vmid.d.ts.map +1 -0
  274. package/dist/proxmox/generated/pve/nodes-node-qemu.d.ts +158 -0
  275. package/dist/proxmox/generated/pve/nodes-node-qemu.d.ts.map +1 -0
  276. package/dist/proxmox/generated/pve/nodes-node-replication.d.ts +187 -0
  277. package/dist/proxmox/generated/pve/nodes-node-replication.d.ts.map +1 -0
  278. package/dist/proxmox/generated/pve/nodes-node-services.d.ts +115 -0
  279. package/dist/proxmox/generated/pve/nodes-node-services.d.ts.map +1 -0
  280. package/dist/proxmox/generated/pve/nodes-node-storage-storage.d.ts +202 -0
  281. package/dist/proxmox/generated/pve/nodes-node-storage-storage.d.ts.map +1 -0
  282. package/dist/proxmox/generated/pve/nodes-node-storage.d.ts +40 -0
  283. package/dist/proxmox/generated/pve/nodes-node-storage.d.ts.map +1 -0
  284. package/dist/proxmox/generated/pve/nodes-node-subscription.d.ts +173 -0
  285. package/dist/proxmox/generated/pve/nodes-node-subscription.d.ts.map +1 -0
  286. package/dist/proxmox/generated/pve/nodes-node-vzdump.d.ts +97 -0
  287. package/dist/proxmox/generated/pve/nodes-node-vzdump.d.ts.map +1 -0
  288. package/dist/proxmox/generated/pve/nodes-node.d.ts +207 -0
  289. package/dist/proxmox/generated/pve/nodes-node.d.ts.map +1 -0
  290. package/dist/proxmox/generated/pve/nodes.d.ts +236 -0
  291. package/dist/proxmox/generated/pve/nodes.d.ts.map +1 -0
  292. package/dist/proxmox/generated/pve/pools.d.ts +82 -0
  293. package/dist/proxmox/generated/pve/pools.d.ts.map +1 -0
  294. package/dist/proxmox/generated/pve/storage.d.ts +159 -0
  295. package/dist/proxmox/generated/pve/storage.d.ts.map +1 -0
  296. package/dist/proxmox/generated/pve/version.d.ts +21 -0
  297. package/dist/proxmox/generated/pve/version.d.ts.map +1 -0
  298. package/dist/proxmox/generated/pve.d.ts +69 -4766
  299. package/dist/proxmox/generated/pve.d.ts.map +1 -1
  300. package/dist/proxmox/group.d.ts.map +1 -1
  301. package/dist/proxmox/ha-resource.d.ts.map +1 -1
  302. package/dist/proxmox/ha-rule.d.ts.map +1 -1
  303. package/dist/proxmox/index.d.ts +6 -1
  304. package/dist/proxmox/index.d.ts.map +1 -1
  305. package/dist/proxmox/index.js +1483 -300
  306. package/dist/proxmox/index.js.map +70 -45
  307. package/dist/proxmox/lxc-lifecycle.d.ts.map +1 -1
  308. package/dist/proxmox/metric-server-form.d.ts +11 -0
  309. package/dist/proxmox/metric-server-form.d.ts.map +1 -1
  310. package/dist/proxmox/metric-server.d.ts.map +1 -1
  311. package/dist/proxmox/network-apply.d.ts.map +1 -1
  312. package/dist/proxmox/node-network-form.d.ts +15 -0
  313. package/dist/proxmox/node-network-form.d.ts.map +1 -1
  314. package/dist/proxmox/node-network.d.ts.map +1 -1
  315. package/dist/proxmox/notification-matcher.d.ts.map +1 -1
  316. package/dist/proxmox/notification-target-endpoint.d.ts +26 -0
  317. package/dist/proxmox/notification-target-endpoint.d.ts.map +1 -0
  318. package/dist/proxmox/notification-target-form.d.ts +7 -0
  319. package/dist/proxmox/notification-target-form.d.ts.map +1 -1
  320. package/dist/proxmox/notification-target.d.ts.map +1 -1
  321. package/dist/proxmox/pbs-datastore-endpoint.d.ts +22 -0
  322. package/dist/proxmox/pbs-datastore-endpoint.d.ts.map +1 -0
  323. package/dist/proxmox/pbs-datastore.d.ts.map +1 -1
  324. package/dist/proxmox/pbs-notification-matcher.d.ts.map +1 -1
  325. package/dist/proxmox/pbs-notification-target-endpoint.d.ts +29 -0
  326. package/dist/proxmox/pbs-notification-target-endpoint.d.ts.map +1 -0
  327. package/dist/proxmox/pbs-notification-target-lifecycle.d.ts.map +1 -1
  328. package/dist/proxmox/pbs-prune-job.d.ts.map +1 -1
  329. package/dist/proxmox/pbs-sync-job-form.d.ts.map +1 -1
  330. package/dist/proxmox/pbs-verify-job.d.ts.map +1 -1
  331. package/dist/proxmox/pool.d.ts.map +1 -1
  332. package/dist/proxmox/provision-baseline.d.ts +30 -5
  333. package/dist/proxmox/provision-baseline.d.ts.map +1 -1
  334. package/dist/proxmox/qemu.d.ts +10 -0
  335. package/dist/proxmox/qemu.d.ts.map +1 -1
  336. package/dist/proxmox/replication-job.d.ts.map +1 -1
  337. package/dist/proxmox/resource-guard.d.ts +15 -0
  338. package/dist/proxmox/resource-guard.d.ts.map +1 -0
  339. package/dist/proxmox/resource-spec.d.ts +131 -0
  340. package/dist/proxmox/resource-spec.d.ts.map +1 -0
  341. package/dist/proxmox/resource.d.ts +17 -90
  342. package/dist/proxmox/resource.d.ts.map +1 -1
  343. package/dist/proxmox/role.d.ts.map +1 -1
  344. package/dist/proxmox/sdn-apply.d.ts.map +1 -1
  345. package/dist/proxmox/sdn-subnet.d.ts.map +1 -1
  346. package/dist/proxmox/sdn-vnet.d.ts.map +1 -1
  347. package/dist/proxmox/sdn-zone.d.ts.map +1 -1
  348. package/dist/proxmox/storage.d.ts.map +1 -1
  349. package/dist/proxmox/user.d.ts.map +1 -1
  350. package/dist/proxmox/zfs-pool-write.d.ts +11 -0
  351. package/dist/proxmox/zfs-pool-write.d.ts.map +1 -1
  352. package/dist/proxmox/zfs-pool.d.ts.map +1 -1
  353. package/docs/linux-host-measured.md +56 -0
  354. package/docs/linux-host.md +186 -0
  355. package/docs/netbox.md +103 -0
  356. package/docs/provision-baseline.md +34 -7
  357. package/docs/proxmox-lxc.md +40 -0
  358. package/package.json +9 -1
  359. package/src/linux/directory-lifecycle.ts +220 -0
  360. package/src/linux/directory.test.ts +132 -0
  361. package/src/linux/directory.ts +71 -0
  362. package/src/linux/fake-linux-host.ts +197 -0
  363. package/src/linux/fake-systemd.ts +103 -0
  364. package/src/linux/index.ts +32 -0
  365. package/src/linux/providers.ts +26 -0
  366. package/src/linux/region.test.ts +89 -0
  367. package/src/linux/region.ts +118 -0
  368. package/src/linux/remote-file-form.ts +96 -0
  369. package/src/linux/remote-file-lifecycle.test.ts +191 -0
  370. package/src/linux/remote-file-lifecycle.ts +161 -0
  371. package/src/linux/remote-file-plan.ts +154 -0
  372. package/src/linux/remote-file.ts +88 -0
  373. package/src/linux/ssh-command.test.ts +140 -0
  374. package/src/linux/ssh-command.ts +99 -0
  375. package/src/linux/ssh-runner.ts +188 -0
  376. package/src/linux/ssh-scripts.ts +105 -0
  377. package/src/linux/systemctl.ts +145 -0
  378. package/src/linux/unit-form.ts +183 -0
  379. package/src/linux/unit-handlers.ts +73 -0
  380. package/src/linux/unit-lifecycle.test.ts +220 -0
  381. package/src/linux/unit-lifecycle.ts +193 -0
  382. package/src/linux/unit-settle.ts +110 -0
  383. package/src/linux/unit.ts +81 -0
  384. package/src/netbox/client.test.ts +67 -0
  385. package/src/netbox/client.ts +146 -0
  386. package/src/netbox/constraint-guard.ts +57 -0
  387. package/src/netbox/constraints.test.ts +114 -0
  388. package/src/netbox/constraints.ts +152 -0
  389. package/src/netbox/generated/constraints/index.ts +23 -0
  390. package/src/netbox/generated/constraints/netbox-ipam.ts +32 -0
  391. package/src/netbox/index.ts +34 -0
  392. package/src/netbox/prefix-form.test.ts +127 -0
  393. package/src/netbox/prefix-form.ts +84 -0
  394. package/src/netbox/prefix.ts +136 -0
  395. package/src/netbox/resource.ts +208 -0
  396. package/src/netbox/values.test.ts +66 -0
  397. package/src/netbox/values.ts +40 -0
  398. package/src/openbao/index-types.test.ts +87 -0
  399. package/src/openbao/index.ts +15 -3
  400. package/src/proxmox/acl.ts +2 -0
  401. package/src/proxmox/api-token-form.ts +5 -0
  402. package/src/proxmox/apply-endpoints.ts +34 -0
  403. package/src/proxmox/backup-job.ts +2 -0
  404. package/src/proxmox/ceph-daemon-form.ts +56 -2
  405. package/src/proxmox/ceph-daemon.ts +10 -21
  406. package/src/proxmox/ceph-flag.ts +1 -0
  407. package/src/proxmox/ceph-fs.ts +1 -0
  408. package/src/proxmox/ceph-osd-write.ts +30 -4
  409. package/src/proxmox/ceph-pool.ts +10 -0
  410. package/src/proxmox/constraint-guard.ts +53 -0
  411. package/src/proxmox/constraints-dialect.test.ts +165 -0
  412. package/src/proxmox/constraints-forms.test.ts +220 -0
  413. package/src/proxmox/constraints-host-forms.test.ts +132 -0
  414. package/src/proxmox/constraints-live.test.ts +155 -0
  415. package/src/proxmox/constraints-node-forms.test.ts +188 -0
  416. package/src/proxmox/constraints-required.test.ts +172 -0
  417. package/src/proxmox/constraints.test.ts +205 -0
  418. package/src/proxmox/constraints.ts +160 -0
  419. package/src/proxmox/firewall-alias.ts +5 -0
  420. package/src/proxmox/generated/constraints/index.ts +55 -0
  421. package/src/proxmox/generated/constraints/pbs-config.ts +203 -0
  422. package/src/proxmox/generated/constraints/pve-access.ts +69 -0
  423. package/src/proxmox/generated/constraints/pve-cluster-backup.ts +74 -0
  424. package/src/proxmox/generated/constraints/pve-cluster-ceph.ts +27 -0
  425. package/src/proxmox/generated/constraints/pve-cluster-firewall.ts +35 -0
  426. package/src/proxmox/generated/constraints/pve-cluster-ha.ts +55 -0
  427. package/src/proxmox/generated/constraints/pve-cluster-metrics.ts +59 -0
  428. package/src/proxmox/generated/constraints/pve-cluster-notifications.ts +80 -0
  429. package/src/proxmox/generated/constraints/pve-cluster-replication.ts +43 -0
  430. package/src/proxmox/generated/constraints/pve-cluster-sdn.ts +90 -0
  431. package/src/proxmox/generated/constraints/pve-nodes-ceph.ts +57 -0
  432. package/src/proxmox/generated/constraints/pve-nodes-disks.ts +31 -0
  433. package/src/proxmox/generated/constraints/pve-nodes-lxc.ts +82 -0
  434. package/src/proxmox/generated/constraints/pve-nodes-network.ts +77 -0
  435. package/src/proxmox/generated/constraints/pve-nodes-qemu.ts +125 -0
  436. package/src/proxmox/generated/constraints/pve-pools.ts +31 -0
  437. package/src/proxmox/generated/constraints/pve-storage.ts +80 -0
  438. package/src/proxmox/generated/pbs/access-users.ts +137 -0
  439. package/src/proxmox/generated/pbs/access.ts +232 -0
  440. package/src/proxmox/generated/pbs/admin-datastore-store.ts +238 -0
  441. package/src/proxmox/generated/pbs/admin-metrics.ts +147 -0
  442. package/src/proxmox/generated/pbs/admin.ts +225 -0
  443. package/src/proxmox/generated/pbs/backup.ts +121 -0
  444. package/src/proxmox/generated/pbs/config-access-pam.ts +228 -0
  445. package/src/proxmox/generated/pbs/config-access.ts +228 -0
  446. package/src/proxmox/generated/pbs/config-datastore.ts +217 -0
  447. package/src/proxmox/generated/pbs/config-media-pool.ts +232 -0
  448. package/src/proxmox/generated/pbs/config-notifications-matchers.ts +187 -0
  449. package/src/proxmox/generated/pbs/config-notifications.ts +221 -0
  450. package/src/proxmox/generated/pbs/config-remote.ts +192 -0
  451. package/src/proxmox/generated/pbs/config-sync.ts +167 -0
  452. package/src/proxmox/generated/pbs/config-tape.ts +226 -0
  453. package/src/proxmox/generated/pbs/config-verify.ts +87 -0
  454. package/src/proxmox/generated/pbs/config.ts +135 -0
  455. package/src/proxmox/generated/pbs/nodes-node-dns.ts +229 -0
  456. package/src/proxmox/generated/pbs/nodes-node-services.ts +181 -0
  457. package/src/proxmox/generated/pbs/nodes-node.ts +244 -0
  458. package/src/proxmox/generated/pbs/nodes.ts +160 -0
  459. package/src/proxmox/generated/pbs/ping.ts +16 -0
  460. package/src/proxmox/generated/pbs/pull.ts +37 -0
  461. package/src/proxmox/generated/pbs/push.ts +36 -0
  462. package/src/proxmox/generated/pbs/reader.ts +41 -0
  463. package/src/proxmox/generated/pbs/root.ts +16 -0
  464. package/src/proxmox/generated/pbs/status.ts +50 -0
  465. package/src/proxmox/generated/pbs/tape-drive.ts +220 -0
  466. package/src/proxmox/generated/pbs/tape-media.ts +133 -0
  467. package/src/proxmox/generated/pbs/tape.ts +92 -0
  468. package/src/proxmox/generated/pbs/version.ts +16 -0
  469. package/src/proxmox/generated/pbs.ts +44 -787
  470. package/src/proxmox/generated/pve/access-roles.ts +168 -0
  471. package/src/proxmox/generated/pve/access-users.ts +155 -0
  472. package/src/proxmox/generated/pve/access.ts +217 -0
  473. package/src/proxmox/generated/pve/cluster-acme-plugins-id.ts +367 -0
  474. package/src/proxmox/generated/pve/cluster-acme-plugins.ts +366 -0
  475. package/src/proxmox/generated/pve/cluster-acme-tos.ts +232 -0
  476. package/src/proxmox/generated/pve/cluster-bulk-action.ts +227 -0
  477. package/src/proxmox/generated/pve/cluster-firewall-refs.ts +110 -0
  478. package/src/proxmox/generated/pve/cluster-firewall.ts +236 -0
  479. package/src/proxmox/generated/pve/cluster-ha.ts +203 -0
  480. package/src/proxmox/generated/pve/cluster-jobs.ts +190 -0
  481. package/src/proxmox/generated/pve/cluster-metrics.ts +116 -0
  482. package/src/proxmox/generated/pve/cluster-notifications-endpoints.ts +244 -0
  483. package/src/proxmox/generated/pve/cluster-notifications.ts +99 -0
  484. package/src/proxmox/generated/pve/cluster-options.ts +189 -0
  485. package/src/proxmox/generated/pve/cluster-qemu-custom-cpu-models-cputype.ts +272 -0
  486. package/src/proxmox/generated/pve/cluster-qemu-custom-cpu-models.ts +268 -0
  487. package/src/proxmox/generated/pve/cluster-replication.ts +127 -0
  488. package/src/proxmox/generated/pve/cluster-sdn-fabrics.ts +218 -0
  489. package/src/proxmox/generated/pve/cluster-sdn-ipams.ts +229 -0
  490. package/src/proxmox/generated/pve/cluster-sdn-vnets-vnet.ts +245 -0
  491. package/src/proxmox/generated/pve/cluster-sdn-vnets.ts +49 -0
  492. package/src/proxmox/generated/pve/cluster-sdn-zones.ts +224 -0
  493. package/src/proxmox/generated/pve/cluster-sdn.ts +201 -0
  494. package/src/proxmox/generated/pve/cluster.ts +71 -0
  495. package/src/proxmox/generated/pve/nodes-node-ceph.ts +167 -0
  496. package/src/proxmox/generated/pve/nodes-node-config.ts +56 -0
  497. package/src/proxmox/generated/pve/nodes-node-disks.ts +237 -0
  498. package/src/proxmox/generated/pve/nodes-node-firewall.ts +239 -0
  499. package/src/proxmox/generated/pve/nodes-node-hardware.ts +215 -0
  500. package/src/proxmox/generated/pve/nodes-node-lxc-vmid-firewall.ts +232 -0
  501. package/src/proxmox/generated/pve/nodes-node-lxc-vmid-move-volume.ts +1059 -0
  502. package/src/proxmox/generated/pve/nodes-node-lxc-vmid-mtunnel.ts +49 -0
  503. package/src/proxmox/generated/pve/nodes-node-lxc-vmid-resize.ts +285 -0
  504. package/src/proxmox/generated/pve/nodes-node-lxc-vmid-rrd.ts +184 -0
  505. package/src/proxmox/generated/pve/nodes-node-lxc-vmid.ts +240 -0
  506. package/src/proxmox/generated/pve/nodes-node-network.ts +234 -0
  507. package/src/proxmox/generated/pve/nodes-node-qemu-vmid-config.ts +441 -0
  508. package/src/proxmox/generated/pve/nodes-node-qemu-vmid-dbus-vmstate.ts +215 -0
  509. package/src/proxmox/generated/pve/nodes-node-qemu-vmid-firewall-rules.ts +145 -0
  510. package/src/proxmox/generated/pve/nodes-node-qemu-vmid-move-disk.ts +664 -0
  511. package/src/proxmox/generated/pve/nodes-node-qemu-vmid-mtunnel.ts +188 -0
  512. package/src/proxmox/generated/pve/nodes-node-qemu-vmid-status.ts +233 -0
  513. package/src/proxmox/generated/pve/nodes-node-qemu-vmid.ts +187 -0
  514. package/src/proxmox/generated/pve/nodes-node-qemu.ts +204 -0
  515. package/src/proxmox/generated/pve/nodes-node-replication.ts +187 -0
  516. package/src/proxmox/generated/pve/nodes-node-services.ts +221 -0
  517. package/src/proxmox/generated/pve/nodes-node-storage-storage.ts +226 -0
  518. package/src/proxmox/generated/pve/nodes-node-storage.ts +40 -0
  519. package/src/proxmox/generated/pve/nodes-node-subscription.ts +168 -0
  520. package/src/proxmox/generated/pve/nodes-node-vzdump.ts +96 -0
  521. package/src/proxmox/generated/pve/nodes-node.ts +211 -0
  522. package/src/proxmox/generated/pve/nodes.ts +235 -0
  523. package/src/proxmox/generated/pve/pools.ts +78 -0
  524. package/src/proxmox/generated/pve/storage.ts +213 -0
  525. package/src/proxmox/generated/pve/version.ts +21 -0
  526. package/src/proxmox/generated/pve.ts +69 -7403
  527. package/src/proxmox/group.ts +2 -0
  528. package/src/proxmox/ha-resource.ts +5 -0
  529. package/src/proxmox/ha-rule.ts +2 -0
  530. package/src/proxmox/index.ts +12 -0
  531. package/src/proxmox/lxc-interior.test.ts +77 -0
  532. package/src/proxmox/lxc-lifecycle.ts +48 -9
  533. package/src/proxmox/metric-server-form.ts +10 -0
  534. package/src/proxmox/metric-server.ts +11 -2
  535. package/src/proxmox/network-apply.ts +4 -0
  536. package/src/proxmox/node-network-form.ts +19 -0
  537. package/src/proxmox/node-network.ts +9 -3
  538. package/src/proxmox/notification-matcher.ts +5 -0
  539. package/src/proxmox/notification-target-endpoint.ts +46 -0
  540. package/src/proxmox/notification-target-form.ts +11 -0
  541. package/src/proxmox/notification-target.ts +5 -2
  542. package/src/proxmox/pbs-datastore-endpoint.ts +29 -0
  543. package/src/proxmox/pbs-datastore.ts +8 -1
  544. package/src/proxmox/pbs-notification-matcher.ts +5 -0
  545. package/src/proxmox/pbs-notification-target-endpoint.ts +45 -0
  546. package/src/proxmox/pbs-notification-target-lifecycle.ts +16 -14
  547. package/src/proxmox/pbs-prune-job.ts +2 -0
  548. package/src/proxmox/pbs-sync-job-form.ts +2 -0
  549. package/src/proxmox/pbs-verify-job.ts +2 -0
  550. package/src/proxmox/pool.ts +2 -0
  551. package/src/proxmox/provision-baseline.test.ts +38 -0
  552. package/src/proxmox/provision-baseline.ts +73 -14
  553. package/src/proxmox/provision-bootstrap.test.ts +51 -0
  554. package/src/proxmox/qemu.ts +9 -1
  555. package/src/proxmox/replication-job.ts +5 -0
  556. package/src/proxmox/resource-guard.ts +61 -0
  557. package/src/proxmox/resource-spec.ts +129 -0
  558. package/src/proxmox/resource.ts +52 -93
  559. package/src/proxmox/role.ts +2 -0
  560. package/src/proxmox/sdn-apply.ts +4 -0
  561. package/src/proxmox/sdn-subnet.ts +5 -0
  562. package/src/proxmox/sdn-vnet.ts +2 -0
  563. package/src/proxmox/sdn-zone.ts +2 -0
  564. package/src/proxmox/storage.ts +2 -0
  565. package/src/proxmox/user.ts +2 -0
  566. package/src/proxmox/zfs-pool-write.ts +12 -0
  567. package/src/proxmox/zfs-pool.ts +2 -1
@@ -0,0 +1,197 @@
1
+ /**
2
+ * An in-memory Linux host for the tests beside it: a filesystem in a Map, the four directory
3
+ * programs, and a systemd that behaves the way systemctl.ts measured the real one behaving.
4
+ *
5
+ * ⛔ TEST-ONLY. No provider imports this file, it is not on the barrel, and it never touches the
6
+ * real filesystem or runs a real program — which is the only way the lifecycle tests can prove
7
+ * "an adopted unit is not restarted" without stopping something on a live host.
8
+ * ★ IT MODELS THE BEHAVIOURS THE PROVIDERS DEPEND ON: `show` of an unknown unit EXITS 0 with
9
+ * `LoadState=not-found`; `enable` fails for a unit file with no `[Install]`; `NeedDaemonReload`
10
+ * turns on when a unit file changes under systemd and off at `daemon-reload`; every call is
11
+ * recorded in order, so a test can assert that a restart did NOT happen.
12
+ */
13
+ import { sha256Hex } from '../launchd/job-form.ts';
14
+ import { type UnitState, fakeSystemd } from './fake-systemd.ts';
15
+ import type {
16
+ ExecResult,
17
+ FileStat,
18
+ HostRunner,
19
+ HostUser,
20
+ WriteOptions,
21
+ } from '../launchd/runner.ts';
22
+
23
+ type Entry = { bytes: Uint8Array; mode: number; uid: number; gid: number; kind: FileStat['kind'] };
24
+
25
+ export type FakeLinuxOptions = {
26
+ euid?: number;
27
+ privileged?: boolean;
28
+ /** Directories that exist, and their owner uid. Every level above one of these exists as root. */
29
+ dirs?: Record<string, number>;
30
+ users?: Record<string, HostUser>;
31
+ groups?: Record<string, number>;
32
+ /** ⛔ Make every `systemctl show` fail, to prove a failed READ refuses instead of writing. */
33
+ failShow?: boolean;
34
+ };
35
+
36
+ const ok = (stdout = ''): ExecResult => ({ exitCode: 0, stderr: '', stdout });
37
+ const fail = (exitCode: number, stderr: string): ExecResult => ({ exitCode, stderr, stdout: '' });
38
+ const errno = (code: string, path: string) =>
39
+ Object.assign(new Error(`${code}: ${path}`), { code });
40
+ const parent = (path: string) => path.slice(0, path.lastIndexOf('/')) || '/';
41
+ const decode = (bytes: Uint8Array | undefined) =>
42
+ bytes === undefined ? undefined : new TextDecoder().decode(bytes);
43
+
44
+ export const fakeLinuxHost = (options: FakeLinuxOptions = {}) => {
45
+ const euid = options.euid ?? 0;
46
+ const privileged = options.privileged ?? false;
47
+ const root = privileged || euid === 0;
48
+ const dirs = new Map(Object.entries(options.dirs ?? {}));
49
+ const files = new Map<string, Entry>();
50
+ const calls: string[][] = [];
51
+ const units = new Map<string, UnitState>();
52
+ const above = new Set<string>(['/']);
53
+ for (const dir of dirs.keys()) {
54
+ const parts = dir.split('/').slice(1, -1);
55
+ for (const index of parts.keys()) above.add(`/${parts.slice(0, index + 1).join('/')}`);
56
+ }
57
+ for (const dir of dirs.keys()) above.delete(dir);
58
+ const implied = (path: string) => above.has(path);
59
+ /** Unit name → the path its file was last written at, so `show` can find it again. */
60
+ const unitPaths = new Map<string, string>();
61
+ /** Declared directories keep their own mode and owner; implied ones are root's 0755. */
62
+ const modes = new Map<string, { mode: number; uid: number; gid: number }>();
63
+ for (const [dir, owner] of dirs) modes.set(dir, { gid: 0, mode: 0o755, uid: owner });
64
+
65
+ const digest = (text: string) => sha256Hex(new TextEncoder().encode(text));
66
+
67
+ /** ★ The default unit directory needs no registration, exactly as it needs none on a real host. */
68
+ const pathOf = (name: string) => unitPaths.get(name) ?? `/etc/systemd/system/${name}`;
69
+ const fileFor = (name: string) => decode(files.get(pathOf(name))?.bytes);
70
+
71
+ const systemctl = fakeSystemd({
72
+ digest,
73
+ failShow: options.failShow === true,
74
+ fileAt: (path) => decode(files.get(path)?.bytes),
75
+ fileFor,
76
+ files,
77
+ pathOf,
78
+ units,
79
+ });
80
+
81
+ const directoryProgram = (argv: readonly string[]): ExecResult => {
82
+ const [program, ...rest] = argv;
83
+ const path = rest.at(-1) ?? '';
84
+ if (program === 'mkdir') {
85
+ const mode = rest[0] === '-m' ? Number.parseInt(rest[1] ?? '755', 8) : 0o755;
86
+ if (!dirs.has(parent(path)) && !implied(parent(path))) {
87
+ return fail(1, `mkdir: cannot create directory '${path}': No such file or directory`);
88
+ }
89
+ if (dirs.has(path) || implied(path)) return fail(1, `mkdir: '${path}': File exists`);
90
+ dirs.set(path, euid);
91
+ modes.set(path, { gid: 0, mode, uid: euid });
92
+ return ok();
93
+ }
94
+ if (program === 'chmod' || program === 'chown') {
95
+ const live = modes.get(path);
96
+ if (!dirs.has(path) || live === undefined) return fail(1, `${String(program)}: ${path}`);
97
+ if (program === 'chmod') live.mode = Number.parseInt(rest[0] ?? '755', 8);
98
+ else {
99
+ if (!root) return fail(1, `chown: ${path}: Operation not permitted`);
100
+ const [uid, gid] = (rest[0] ?? '').split(':');
101
+ if (uid !== undefined && uid !== '') live.uid = Number(uid);
102
+ if (gid !== undefined && gid !== '') live.gid = Number(gid);
103
+ }
104
+ return ok();
105
+ }
106
+ if (program === 'rmdir') {
107
+ if (!dirs.has(path)) return fail(1, `rmdir: '${path}': No such file or directory`);
108
+ if ([...files.keys()].some((file) => parent(file) === path)) {
109
+ return fail(1, `rmdir: failed to remove '${path}': Directory not empty`);
110
+ }
111
+ dirs.delete(path);
112
+ modes.delete(path);
113
+ return ok();
114
+ }
115
+ return fail(127, `fake: no program ${String(program)}`);
116
+ };
117
+
118
+ const runner: HostRunner = {
119
+ effectiveUid: () => euid,
120
+ exec: async (argv) => {
121
+ calls.push([...argv]);
122
+ if (argv[0] === 'systemctl') return systemctl(argv.slice(1).filter((arg) => arg !== '--'));
123
+ if (argv[0] === 'getent') return fail(2, 'getent: not modelled');
124
+ return directoryProgram(argv.filter((arg) => arg !== '--'));
125
+ },
126
+ lookupGroup: async (nameOrId) => options.groups?.[nameOrId],
127
+ lookupUser: async (nameOrId) =>
128
+ options.users?.[nameOrId] ??
129
+ Object.values(options.users ?? {}).find((user) => String(user.uid) === nameOrId),
130
+ privileged,
131
+ readFile: async (path) => files.get(path)?.bytes,
132
+ removeFile: async (path) => {
133
+ if (files.has(path) && (dirs.get(parent(path)) ?? 0) !== euid && !root) {
134
+ throw errno('EACCES', path);
135
+ }
136
+ files.delete(path);
137
+ },
138
+ sleep: async () => undefined,
139
+ stat: async (path) => {
140
+ const entry = files.get(path);
141
+ if (entry !== undefined) {
142
+ return {
143
+ gid: entry.gid,
144
+ kind: entry.kind,
145
+ mode: entry.mode,
146
+ size: entry.bytes.length,
147
+ uid: entry.uid,
148
+ };
149
+ }
150
+ const live = modes.get(path);
151
+ if (live !== undefined) return { ...live, kind: 'directory', size: 0 };
152
+ if (implied(path)) return { gid: 0, kind: 'directory', mode: 0o755, size: 0, uid: 0 };
153
+ return undefined;
154
+ },
155
+ writeFileAtomic: async (path: string, bytes: Uint8Array, write: WriteOptions) => {
156
+ calls.push(['write', path]);
157
+ const owner = modes.get(parent(path))?.uid ?? (implied(parent(path)) ? 0 : undefined);
158
+ if (owner === undefined) throw errno('ENOENT', parent(path));
159
+ if (owner !== euid && !root) throw errno('EACCES', path);
160
+ if (write.uid !== undefined && write.uid !== euid && !root) throw errno('EPERM', path);
161
+ files.set(path, {
162
+ bytes,
163
+ gid: write.gid ?? 0,
164
+ kind: 'file',
165
+ mode: write.mode,
166
+ uid: write.uid ?? euid,
167
+ });
168
+ },
169
+ };
170
+
171
+ /** Put a unit on the host the way a package or a person would, outside any declaration. */
172
+ const placeUnit = (path: string, text: string, state: Partial<UnitState> = {}) => {
173
+ const name = path.slice(path.lastIndexOf('/') + 1);
174
+ files.set(path, {
175
+ bytes: new TextEncoder().encode(text),
176
+ gid: 0,
177
+ kind: 'file',
178
+ mode: 0o644,
179
+ uid: 0,
180
+ });
181
+ unitPaths.set(name, path);
182
+ modes.set(path.slice(0, path.lastIndexOf('/')) || '/', { gid: 0, mode: 0o755, uid: 0 });
183
+ units.set(name, {
184
+ active: state.active ?? false,
185
+ enabled: state.enabled ?? false,
186
+ loadedSha: state.loadedSha === undefined ? digest(text) : state.loadedSha,
187
+ ...(state.masked === undefined ? {} : { masked: state.masked }),
188
+ });
189
+ };
190
+
191
+ /** Tell the fake systemd which file a unit name belongs to, before that file exists. */
192
+ const expectUnit = (path: string) => {
193
+ unitPaths.set(path.slice(path.lastIndexOf('/') + 1), path);
194
+ };
195
+
196
+ return { calls, dirs, expectUnit, files, modes, placeUnit, runner, units };
197
+ };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The systemd half of the fake Linux host: a `systemctl` that behaves the way systemctl.ts measured
3
+ * the real one behaving. ⛔ TEST-ONLY, and not on the barrel.
4
+ *
5
+ * ★ THE ONE BEHAVIOUR EVERYTHING ELSE RESTS ON: `show` of a unit that does not exist EXITS 0 and
6
+ * answers `LoadState=not-found`. A fake that returned an error there would let a provider bug
7
+ * through that reads the exit code instead of the load state — which is the bug the real host's
8
+ * measurement exists to prevent.
9
+ */
10
+ import type { ExecResult } from '../launchd/runner.ts';
11
+
12
+ export type UnitState = {
13
+ /** What systemd has loaded; `undefined` once it has been unloaded. */
14
+ loadedSha: string | undefined;
15
+ active: boolean;
16
+ enabled: boolean;
17
+ masked?: boolean;
18
+ };
19
+
20
+ export type FakeSystemdDeps = {
21
+ readonly units: Map<string, UnitState>;
22
+ /** Every unit-file path the host holds, so a reload can notice a new one. */
23
+ readonly files: { keys: () => Iterable<string> };
24
+ /** The text of a unit's file, or `undefined` when it has none. */
25
+ readonly fileFor: (name: string) => string | undefined;
26
+ /** The text at one path, for the reload sweep. */
27
+ readonly fileAt: (path: string) => string | undefined;
28
+ readonly pathOf: (name: string) => string;
29
+ readonly digest: (text: string) => string;
30
+ /** ⛔ Make every `show` fail, to prove a failed READ refuses instead of writing. */
31
+ readonly failShow: boolean;
32
+ };
33
+
34
+ const ok = (stdout = ''): ExecResult => ({ exitCode: 0, stderr: '', stdout });
35
+ const fail = (exitCode: number, stderr: string): ExecResult => ({ exitCode, stderr, stdout: '' });
36
+
37
+ export const fakeSystemd = (deps: FakeSystemdDeps) => {
38
+ return (args: readonly string[]): ExecResult => {
39
+ const verb = args[0];
40
+ const name = args.at(-1) ?? '';
41
+ const state = deps.units.get(name);
42
+ const onDisk = deps.fileFor(name);
43
+ if (verb === 'show') {
44
+ // ⛔ MEASURED: exit 0 even for a unit that does not exist.
45
+ if (deps.failShow) return fail(1, 'Failed to connect to bus: No such file');
46
+ const known = state !== undefined || onDisk !== undefined;
47
+ const loaded = state?.loadedSha !== undefined;
48
+ return ok(
49
+ [
50
+ `Id=${name}`,
51
+ `LoadState=${state?.masked === true ? 'masked' : known && loaded ? 'loaded' : 'not-found'}`,
52
+ `ActiveState=${state?.active === true ? 'active' : 'inactive'}`,
53
+ `SubState=${state?.active === true ? 'running' : 'dead'}`,
54
+ `UnitFileState=${state?.masked === true ? 'masked' : onDisk === undefined ? '' : state?.enabled === true ? 'enabled' : 'disabled'}`,
55
+ `FragmentPath=${onDisk === undefined ? '' : deps.pathOf(name)}`,
56
+ `NeedDaemonReload=${onDisk !== undefined && state?.loadedSha !== deps.digest(onDisk) && loaded ? 'yes' : 'no'}`,
57
+ ].join('\n'),
58
+ );
59
+ }
60
+ if (verb === 'daemon-reload') {
61
+ for (const [unit, current] of deps.units) {
62
+ const text = deps.fileFor(unit);
63
+ if (text !== undefined) current.loadedSha = deps.digest(text);
64
+ // ★ A unit whose file is gone and that is not running stops being a unit systemd knows.
65
+ else if (!current.active) deps.units.delete(unit);
66
+ }
67
+ // ★ A unit file that appeared since the last reload becomes a unit systemd knows about.
68
+ for (const path of deps.files.keys()) {
69
+ const unit = path.slice(path.lastIndexOf('/') + 1);
70
+ const text = deps.fileAt(path);
71
+ if (text !== undefined && deps.pathOf(unit) === path && !deps.units.has(unit)) {
72
+ deps.units.set(unit, { active: false, enabled: false, loadedSha: deps.digest(text) });
73
+ }
74
+ }
75
+ return ok();
76
+ }
77
+ const current = deps.units.get(name);
78
+ if (current === undefined || onDisk === undefined) {
79
+ return fail(5, `Failed to ${String(verb)} ${name}: Unit ${name} not found.`);
80
+ }
81
+ if (current.masked === true) return fail(1, `Unit ${name} is masked.`);
82
+ if (verb === 'enable') {
83
+ if (!onDisk.includes('[Install]')) {
84
+ return fail(1, `The unit files have no installation config (WantedBy=, ...).`);
85
+ }
86
+ current.enabled = true;
87
+ return ok();
88
+ }
89
+ if (verb === 'disable') {
90
+ current.enabled = false;
91
+ return ok();
92
+ }
93
+ if (verb === 'start' || verb === 'restart') {
94
+ current.active = true;
95
+ return ok();
96
+ }
97
+ if (verb === 'stop') {
98
+ current.active = false;
99
+ return ok();
100
+ }
101
+ return fail(64, `fake systemctl: unsupported ${args.join(' ')}`);
102
+ };
103
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Linux host providers for Alchemy — a host's directories, its files (whole, or one managed block
3
+ * inside somebody else's file) and its systemd units, over the SAME HostRunner seam the launchd
4
+ * subpath drives a Mac through.
5
+ *
6
+ * ⛔ THIS BARREL IS THE PUBLIC API, AND IT IS DELIBERATELY SMALLER THAN THE DIRECTORY. The lifecycle
7
+ * files, the systemctl parsers and the fake host are internals; an `export *` would publish them
8
+ * as API and make the next refactor a breaking change.
9
+ * ⛔ A DEPLOY NEVER MASS-RESTARTS: a unit is restarted only when its own file changed, or a digest
10
+ * the declaration listed changed. See docs/linux-host.md.
11
+ * ★ THE SEAM IS SHARED, NOT COPIED. `HostRunner` is re-exported here so a Linux stack never has to
12
+ * import from the launchd subpath to type its own runner.
13
+ */
14
+ export type { HostDirectoryAttributes, HostDirectoryProps } from './directory-lifecycle.ts';
15
+ export { HostDirectory, HostDirectoryProvider } from './directory.ts';
16
+ export { linuxProviders } from './providers.ts';
17
+ export type { RegionSpec } from './region.ts';
18
+ export type { RemoteFileAttributes, RemoteFileProps } from './remote-file-form.ts';
19
+ export { RemoteFile, RemoteFileProvider } from './remote-file.ts';
20
+ export type { SshRunnerOptions } from './ssh-runner.ts';
21
+ export { sshRunner } from './ssh-runner.ts';
22
+ export type {
23
+ ExecResult,
24
+ FileStat,
25
+ HostRunner,
26
+ HostUser,
27
+ WriteOptions,
28
+ } from '../launchd/runner.ts';
29
+ export { HostRunnerService, canActAsRoot, hostRunnerLayer } from '../launchd/runner.ts';
30
+ export type { SystemdUnitAttributes, SystemdUnitProps, UnitSection } from './unit-form.ts';
31
+ export { DEFAULT_UNIT_DIRECTORY, renderUnit } from './unit-form.ts';
32
+ export { SystemdTimer, SystemdTimerProvider, SystemdUnit, SystemdUnitProvider } from './unit.ts';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Every provider in this subpath with ONE HostRunner, as a single layer for a host stack.
3
+ *
4
+ * const runner = await sshRunner({ host: 'the-host' });
5
+ * Layer.mergeAll(linuxProviders(runner), …the stack's other providers)
6
+ *
7
+ * ★ ONE RUNNER FOR ALL OF THEM, so a directory, the config file inside it and the unit that reads
8
+ * that file are written by the same identity on the same host — a stack cannot create the
9
+ * directory on one host and start the unit on another by accident.
10
+ * ⛔ THERE IS NO DEFAULT. launchdProviders() defaults to localRunner() because a Mac stack runs on
11
+ * the Mac it declares; a Linux host is somewhere else, and a silent default to "this machine"
12
+ * would write the estate's `/etc/systemd/system` onto the laptop running the deploy.
13
+ */
14
+ import * as Layer from 'effect/Layer';
15
+ import { type HostRunner, hostRunnerLayer } from '../launchd/runner.ts';
16
+ import { HostDirectoryProvider } from './directory.ts';
17
+ import { RemoteFileProvider } from './remote-file.ts';
18
+ import { SystemdTimerProvider, SystemdUnitProvider } from './unit.ts';
19
+
20
+ export const linuxProviders = (runner: HostRunner) =>
21
+ Layer.mergeAll(
22
+ HostDirectoryProvider(),
23
+ RemoteFileProvider(),
24
+ SystemdTimerProvider(),
25
+ SystemdUnitProvider(),
26
+ ).pipe(Layer.provide(hostRunnerLayer(runner)));
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The managed region, byte for byte. ⛔ The load-bearing assertion in this file is that everything
3
+ * outside the markers is IDENTICAL — not similar, not re-terminated. Every consumer of this mode
4
+ * owns four lines inside a file that belongs to a vendor, a package or a person.
5
+ */
6
+ import { describe, expect, test } from 'bun:test';
7
+ import { readRegion, regionProblems, removeRegion, spliceRegion } from './region.ts';
8
+
9
+ const spec = { name: 'homeflare-example' };
10
+ const HOST_FILE = [
11
+ '# Loopback, from the OS vendor',
12
+ '127.0.0.1\tlocalhost',
13
+ '::1\t\tlocalhost ip6-localhost',
14
+ '',
15
+ '10.0.0.1\tsomething.invalid',
16
+ '',
17
+ ].join('\n');
18
+
19
+ describe('splice', () => {
20
+ test('appends the block and leaves every other byte identical', () => {
21
+ const next = spliceRegion(HOST_FILE, spec, '10.0.0.2\tdeclared.invalid\n');
22
+ expect(next.startsWith(HOST_FILE)).toBe(true);
23
+ expect(next.slice(HOST_FILE.length)).toBe(
24
+ '# BEGIN homeflare-example\n10.0.0.2\tdeclared.invalid\n# END homeflare-example\n',
25
+ );
26
+ });
27
+
28
+ test('replaces the block in place, and the surroundings survive a rewrite', () => {
29
+ const once = spliceRegion(HOST_FILE, spec, 'one\n');
30
+ const twice = spliceRegion(once, spec, 'two\nthree\n');
31
+ expect(readRegion(twice, spec)).toBe('two\nthree\n');
32
+ expect(removeRegion(twice, spec)).toBe(HOST_FILE);
33
+ // ★ The block stayed where it was: nothing after it moved above it.
34
+ expect(twice.indexOf('# BEGIN')).toBe(once.indexOf('# BEGIN'));
35
+ });
36
+
37
+ test('a file with no final newline gets exactly one, and nothing else', () => {
38
+ const next = spliceRegion('alpha', spec, 'body\n');
39
+ expect(next).toBe('alpha\n# BEGIN homeflare-example\nbody\n# END homeflare-example\n');
40
+ });
41
+
42
+ test('an empty body is still a block, so the markers hold the place', () => {
43
+ const next = spliceRegion('', spec, '');
44
+ expect(next).toBe('# BEGIN homeflare-example\n# END homeflare-example\n');
45
+ expect(readRegion(next, spec)).toBe('');
46
+ });
47
+
48
+ test('the comment token is the host file’s own', () => {
49
+ const next = spliceRegion('// vendor\n', { comment: '//', name: 'block' }, 'x\n');
50
+ expect(next).toBe('// vendor\n// BEGIN block\nx\n// END block\n');
51
+ });
52
+
53
+ test('two blocks in one file do not see each other', () => {
54
+ const first = spliceRegion(HOST_FILE, { name: 'a' }, 'alpha\n');
55
+ const both = spliceRegion(first, { name: 'b' }, 'beta\n');
56
+ expect(readRegion(both, { name: 'a' })).toBe('alpha\n');
57
+ expect(readRegion(both, { name: 'b' })).toBe('beta\n');
58
+ expect(removeRegion(removeRegion(both, { name: 'b' }), { name: 'a' })).toBe(HOST_FILE);
59
+ });
60
+ });
61
+
62
+ describe('refusals', () => {
63
+ const half = '# BEGIN homeflare-example\nbody\n';
64
+ test('a BEGIN with no END is a refusal, not a guess', () => {
65
+ expect(() => readRegion(half, spec)).toThrow(/BEGIN marker and no match/);
66
+ });
67
+
68
+ test('an END before its BEGIN is a refusal', () => {
69
+ const broken = '# END homeflare-example\nbody\n# BEGIN homeflare-example\n';
70
+ expect(() => readRegion(broken, spec)).toThrow(/END marker before its BEGIN/);
71
+ });
72
+
73
+ test('two BEGIN markers name the lines rather than picking one', () => {
74
+ const doubled = `${half}# END homeflare-example\n${half}# END homeflare-example\n`;
75
+ expect(() => spliceRegion(doubled, spec, 'x\n')).toThrow(/two BEGIN markers .*lines 1, 4/);
76
+ });
77
+
78
+ test('a name or comment that could not be found verbatim is refused up front', () => {
79
+ expect(regionProblems({ name: 'has\nnewline' })).toHaveLength(1);
80
+ expect(regionProblems({ comment: '# ', name: 'ok' })).toHaveLength(1);
81
+ expect(regionProblems({ comment: ';', name: 'ok-1' })).toEqual([]);
82
+ });
83
+ });
84
+
85
+ describe('remove', () => {
86
+ test('is a no-op when the block is not there', () => {
87
+ expect(removeRegion(HOST_FILE, spec)).toBe(HOST_FILE);
88
+ });
89
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * A MANAGED REGION: a block a resource owns inside a file it does not own.
3
+ *
4
+ * ★ WHY THIS EXISTS. Four files on this estate are owned by something else and still need one
5
+ * declared block: a packet-filter anchor line inside the OS vendor's own ruleset, a host table,
6
+ * a generated Nix module, and an operator's ssh config. Declaring the WHOLE file would mean
7
+ * taking it from its owner — which puts it back, or refuses its next activation. So the resource
8
+ * owns the lines between two markers and promises that every other byte is untouched.
9
+ * ⛔ THE REST OF THE FILE IS BYTE-IDENTICAL. Not "semantically the same", not re-indented, not
10
+ * re-terminated: the bytes before the BEGIN line and after the END line are copied through. That
11
+ * is the property region.test.ts asserts, because it is the whole reason the other owner tolerates
12
+ * this.
13
+ * ⛔ AMBIGUITY IS A REFUSAL, NEVER A GUESS. Two BEGIN markers with the same name, an END before its
14
+ * BEGIN, or a BEGIN with no END all mean someone edited inside the block. Picking one would
15
+ * silently delete their edit, so each is an Error naming the line.
16
+ * ★ THE MARKER IS A COMMENT IN THE HOST FILE'S OWN LANGUAGE, which is why the comment token is a
17
+ * prop: `#` covers the four consumers above, and a file whose comments are `//` or `;` says so.
18
+ */
19
+
20
+ export type RegionSpec = {
21
+ /** Distinguishes this block from any other managed block in the same file. */
22
+ readonly name: string;
23
+ /** The file's comment token. @default '#' */
24
+ readonly comment?: string;
25
+ };
26
+
27
+ export const DEFAULT_COMMENT = '#';
28
+
29
+ const tokens = (spec: RegionSpec) => {
30
+ const comment = spec.comment ?? DEFAULT_COMMENT;
31
+ return {
32
+ begin: `${comment} BEGIN ${spec.name}`,
33
+ end: `${comment} END ${spec.name}`,
34
+ };
35
+ };
36
+
37
+ export const regionProblems = (spec: RegionSpec): string[] => {
38
+ const found: string[] = [];
39
+ const comment = spec.comment ?? DEFAULT_COMMENT;
40
+ if (!/^\S{1,4}$/.test(comment) || comment.includes('\u0000')) {
41
+ found.push('region comment must be 1–4 non-space characters');
42
+ }
43
+ // ⛔ A marker must be one line and must be findable verbatim; a name with a newline would split
44
+ // it, and leading or trailing space would not match what we later search for.
45
+ if (!/^[A-Za-z0-9][A-Za-z0-9 ._:@/-]{0,79}$/.test(spec.name)) {
46
+ found.push('region name must be 1–80 printable characters and start alphanumeric');
47
+ }
48
+ return found;
49
+ };
50
+
51
+ const refuse = (message: string): Error => new Error(`managed region: ${message}`);
52
+
53
+ const duplicate = (what: string, at: readonly number[], line: string): Error =>
54
+ refuse(
55
+ `${what} for ${JSON.stringify(line)} (lines ${at.map((index) => index + 1).join(', ')}). ` +
56
+ 'Remove the duplicate; this resource will not choose between them.',
57
+ );
58
+
59
+ /** Where the block sits: the line indices of its markers, or `undefined` when it is not there. */
60
+ const locate = (lines: readonly string[], spec: RegionSpec) => {
61
+ const { begin, end } = tokens(spec);
62
+ const begins = lines.flatMap((line, index) => (line === begin ? [index] : []));
63
+ const ends = lines.flatMap((line, index) => (line === end ? [index] : []));
64
+ if (begins.length > 1) throw duplicate('two BEGIN markers', begins, begin);
65
+ if (ends.length > 1) throw duplicate('two END markers', ends, end);
66
+ const [from] = begins;
67
+ const [to] = ends;
68
+ if (from === undefined && to === undefined) return undefined;
69
+ if (from === undefined || to === undefined) {
70
+ throw refuse(
71
+ `${JSON.stringify(spec.name)} has a ${from === undefined ? 'END' : 'BEGIN'} marker and no ` +
72
+ 'match. Repair the file by hand — a half-marked block means an edit inside it.',
73
+ );
74
+ }
75
+ if (to < from) throw refuse(`${JSON.stringify(spec.name)} has its END marker before its BEGIN`);
76
+ return { from, to };
77
+ };
78
+
79
+ /**
80
+ * The file's bytes with the block set to `body`, appended at the end when it is not there yet.
81
+ * ⚠️ A file that does not end in a newline gets one before the block is appended — the ONLY byte
82
+ * this function adds outside its own block, and without it the last existing line and the BEGIN
83
+ * marker would become one line.
84
+ */
85
+ export const spliceRegion = (file: string, spec: RegionSpec, body: string): string => {
86
+ const { begin, end } = tokens(spec);
87
+ // ★ A body is stored as whole lines: exactly one newline after the last one, none doubled.
88
+ const block = [begin, ...(body === '' ? [] : body.replace(/\n$/, '').split('\n')), end];
89
+ const lines = file.split('\n');
90
+ const found = locate(lines, spec);
91
+ if (found !== undefined) {
92
+ return [...lines.slice(0, found.from), ...block, ...lines.slice(found.to + 1)].join('\n');
93
+ }
94
+ const prefix = file === '' || file.endsWith('\n') ? file : `${file}\n`;
95
+ return `${prefix}${block.join('\n')}\n`;
96
+ };
97
+
98
+ /** The block's body, or `undefined` when the file has no such block. */
99
+ export const readRegion = (file: string, spec: RegionSpec): string | undefined => {
100
+ const lines = file.split('\n');
101
+ const found = locate(lines, spec);
102
+ if (found === undefined) return undefined;
103
+ const body = lines.slice(found.from + 1, found.to);
104
+ return body.length === 0 ? '' : `${body.join('\n')}\n`;
105
+ };
106
+
107
+ /**
108
+ * The file with the block and both markers removed — what a delete leaves behind.
109
+ * ★ Byte-for-byte the file as it was before the block was appended, with one exception already
110
+ * named on `spliceRegion`: a file that had no final newline when the block was added keeps the
111
+ * newline that adding it required. Nothing else is added or dropped.
112
+ */
113
+ export const removeRegion = (file: string, spec: RegionSpec): string => {
114
+ const lines = file.split('\n');
115
+ const found = locate(lines, spec);
116
+ if (found === undefined) return file;
117
+ return [...lines.slice(0, found.from), ...lines.slice(found.to + 1)].join('\n');
118
+ };
@@ -0,0 +1,96 @@
1
+ /**
2
+ * RemoteFile's props, attributes and validation — the pure half of the provider.
3
+ *
4
+ * ⛔ `content` IS NEVER A SECRET, for the same reason it is not on HostFile: Alchemy stores props
5
+ * unencrypted, so every byte here sits in the state store and in every plan diff. A secret file
6
+ * stays rendered at run time by a secret renderer; a stack declares at most the PATH it writes to.
7
+ * secret-tripwire.ts refuses the obvious case and catches nothing subtle.
8
+ * ★ TWO MODES, ONE RESOURCE. Without `region` this owns the whole file, exactly as HostFile does on
9
+ * a Mac. With `region` it owns only the lines between two markers and promises every other byte
10
+ * is untouched (region.ts) — which is the only way to declare a block inside a file another tool,
11
+ * a vendor package or a person owns.
12
+ */
13
+ import { pathProblems } from '../launchd/host-file-form.ts';
14
+ import { fileSecretProblems } from '../launchd/secret-tripwire.ts';
15
+ import { type RegionSpec, regionProblems } from './region.ts';
16
+
17
+ export interface RemoteFileProps {
18
+ /** Absolute path on the host. ⚠️ The parent directory must exist — declare a HostDirectory. */
19
+ path: string;
20
+ /** UTF-8 text: the whole file, or the region's body. ⛔ Never a secret — see the file header. */
21
+ content: string;
22
+ /**
23
+ * Own a block inside a file this resource does not own, instead of the whole file.
24
+ * ⛔ In region mode `mode`, `owner` and `group` apply ONLY to a file this resource creates: an
25
+ * existing file keeps the mode and ownership its owner gave it, because changing them would be
26
+ * a second, undeclared claim on a file we only borrowed four lines of.
27
+ */
28
+ region?: RegionSpec;
29
+ /**
30
+ * Create the file when it is absent. Region mode only; whole-file mode always creates.
31
+ * ⛔ Default `false`: a region declared against a path that does not exist is far more often a
32
+ * typo (a wrong `/etc` path) than an intention, and creating it would hide the typo.
33
+ */
34
+ create?: boolean;
35
+ /** Permission bits. @default 0o644 */
36
+ mode?: number;
37
+ /** User name or numeric uid. Omitted: whoever the runner writes as. */
38
+ owner?: string | number;
39
+ /** Group name or numeric gid. Omitted: the directory's default group. */
40
+ group?: string | number;
41
+ }
42
+
43
+ export interface RemoteFileAttributes {
44
+ path: string;
45
+ /** SHA-256 of the WHOLE file on disk, in both modes. */
46
+ sha256: string;
47
+ /** SHA-256 of the part this resource owns — in region mode, of the region's body alone. */
48
+ contentSha256: string;
49
+ /**
50
+ * The block this resource owns, when it owns only a block.
51
+ * ⛔ IT IS IN THE ATTRIBUTES ON PURPOSE. Alchemy's `delete` handler is given the attributes and
52
+ * nothing else, and without the marker name a delete could not find the block — it could only
53
+ * delete the whole file, which belongs to someone else.
54
+ */
55
+ region?: RegionSpec;
56
+ mode: number;
57
+ uid: number;
58
+ gid: number;
59
+ size: number;
60
+ }
61
+
62
+ export const DEFAULT_MODE = 0o644;
63
+
64
+ const identityProblems = (name: string, value: string | number | undefined): string[] => {
65
+ if (value === undefined) return [];
66
+ if (typeof value === 'number') {
67
+ return Number.isSafeInteger(value) && value >= 0
68
+ ? []
69
+ : [`${name} must be a non-negative integer id`];
70
+ }
71
+ return /^[A-Za-z_][A-Za-z0-9_.-]*$/.test(value) || /^\d+$/.test(value)
72
+ ? []
73
+ : [`${name} ${JSON.stringify(value)} is not a valid user or group name`];
74
+ };
75
+
76
+ export const fileProblems = (props: RemoteFileProps): string[] => {
77
+ const found = pathProblems(props.path);
78
+ const mode = props.mode ?? DEFAULT_MODE;
79
+ if (!Number.isSafeInteger(mode) || mode < 0 || mode > 0o7777) found.push('mode must be 0–0o7777');
80
+ found.push(...identityProblems('owner', props.owner));
81
+ found.push(...identityProblems('group', props.group));
82
+ found.push(...fileSecretProblems(props.content));
83
+ if (props.region !== undefined) {
84
+ found.push(...regionProblems(props.region));
85
+ // ⛔ Say it at plan time rather than let a deploy silently ignore the prop: in region mode
86
+ // these describe a file we do not own, and they are honoured only for a `create`.
87
+ if (props.create !== true && (props.mode !== undefined || props.owner !== undefined)) {
88
+ found.push(
89
+ 'mode and owner apply only to a file this resource creates; drop them, or set create: true',
90
+ );
91
+ }
92
+ } else if (props.create === true) {
93
+ found.push('create is for region mode only; a whole-file declaration always creates the file');
94
+ }
95
+ return found;
96
+ };