@homeflare/alchemy 0.5.0 → 0.7.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 (288) hide show
  1. package/README.md +68 -55
  2. package/dist/caddy/admin-calls.d.ts +48 -0
  3. package/dist/caddy/admin-calls.d.ts.map +1 -0
  4. package/dist/caddy/admin-guard.d.ts +40 -0
  5. package/dist/caddy/admin-guard.d.ts.map +1 -0
  6. package/dist/caddy/admin.d.ts +73 -0
  7. package/dist/caddy/admin.d.ts.map +1 -0
  8. package/dist/caddy/caddyfile-tripwire.d.ts +5 -0
  9. package/dist/caddy/caddyfile-tripwire.d.ts.map +1 -0
  10. package/dist/caddy/config-form.d.ts +27 -0
  11. package/dist/caddy/config-form.d.ts.map +1 -0
  12. package/dist/caddy/config-lifecycle.d.ts +45 -0
  13. package/dist/caddy/config-lifecycle.d.ts.map +1 -0
  14. package/dist/caddy/config.d.ts +30 -0
  15. package/dist/caddy/config.d.ts.map +1 -0
  16. package/dist/caddy/digest.d.ts +7 -0
  17. package/dist/caddy/digest.d.ts.map +1 -0
  18. package/dist/caddy/fake-caddy.d.ts +43 -0
  19. package/dist/caddy/fake-caddy.d.ts.map +1 -0
  20. package/dist/caddy/index.d.ts +21 -0
  21. package/dist/caddy/index.d.ts.map +1 -0
  22. package/dist/caddy/index.js +912 -0
  23. package/dist/caddy/index.js.map +28 -0
  24. package/dist/caddy/local-admin.d.ts +45 -0
  25. package/dist/caddy/local-admin.d.ts.map +1 -0
  26. package/dist/caddy/providers.d.ts +14 -0
  27. package/dist/caddy/providers.d.ts.map +1 -0
  28. package/dist/caddy/with-file.d.ts +47 -0
  29. package/dist/caddy/with-file.d.ts.map +1 -0
  30. package/dist/cloudflare/fake-mesh.d.ts +58 -0
  31. package/dist/cloudflare/fake-mesh.d.ts.map +1 -0
  32. package/dist/cloudflare/index.d.ts +9 -3
  33. package/dist/cloudflare/index.d.ts.map +1 -1
  34. package/dist/cloudflare/index.js +218 -7
  35. package/dist/cloudflare/index.js.map +9 -4
  36. package/dist/cloudflare/mesh-node-api.d.ts +75 -0
  37. package/dist/cloudflare/mesh-node-api.d.ts.map +1 -0
  38. package/dist/cloudflare/mesh-node-form.d.ts +82 -0
  39. package/dist/cloudflare/mesh-node-form.d.ts.map +1 -0
  40. package/dist/cloudflare/mesh-node-harness.d.ts +46 -0
  41. package/dist/cloudflare/mesh-node-harness.d.ts.map +1 -0
  42. package/dist/cloudflare/mesh-node-lifecycle.d.ts +22 -0
  43. package/dist/cloudflare/mesh-node-lifecycle.d.ts.map +1 -0
  44. package/dist/cloudflare/mesh-node-token.d.ts +23 -0
  45. package/dist/cloudflare/mesh-node-token.d.ts.map +1 -0
  46. package/dist/cloudflare/mesh-node.d.ts +48 -0
  47. package/dist/cloudflare/mesh-node.d.ts.map +1 -0
  48. package/dist/cloudflare/providers.d.ts +1 -12
  49. package/dist/cloudflare/providers.d.ts.map +1 -1
  50. package/dist/index.d.ts +2 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/launchd/fake-runner.d.ts +36 -0
  53. package/dist/launchd/fake-runner.d.ts.map +1 -0
  54. package/dist/launchd/host-effect.d.ts +19 -0
  55. package/dist/launchd/host-effect.d.ts.map +1 -0
  56. package/dist/launchd/host-file-form.d.ts +24 -0
  57. package/dist/launchd/host-file-form.d.ts.map +1 -0
  58. package/dist/launchd/host-file-lifecycle.d.ts +29 -0
  59. package/dist/launchd/host-file-lifecycle.d.ts.map +1 -0
  60. package/dist/launchd/host-file.d.ts +25 -0
  61. package/dist/launchd/host-file.d.ts.map +1 -0
  62. package/dist/launchd/host-lookup.d.ts +27 -0
  63. package/dist/launchd/host-lookup.d.ts.map +1 -0
  64. package/dist/launchd/index.d.ts +21 -0
  65. package/dist/launchd/index.d.ts.map +1 -0
  66. package/dist/launchd/index.js +969 -0
  67. package/dist/launchd/index.js.map +25 -0
  68. package/dist/launchd/job-form.d.ts +90 -0
  69. package/dist/launchd/job-form.d.ts.map +1 -0
  70. package/dist/launchd/job-lifecycle.d.ts +34 -0
  71. package/dist/launchd/job-lifecycle.d.ts.map +1 -0
  72. package/dist/launchd/job-preflight.d.ts +50 -0
  73. package/dist/launchd/job-preflight.d.ts.map +1 -0
  74. package/dist/launchd/job-validate.d.ts +25 -0
  75. package/dist/launchd/job-validate.d.ts.map +1 -0
  76. package/dist/launchd/job.d.ts +34 -0
  77. package/dist/launchd/job.d.ts.map +1 -0
  78. package/dist/launchd/launchctl.d.ts +62 -0
  79. package/dist/launchd/launchctl.d.ts.map +1 -0
  80. package/dist/launchd/local-runner.d.ts +7 -0
  81. package/dist/launchd/local-runner.d.ts.map +1 -0
  82. package/dist/launchd/plist.d.ts +41 -0
  83. package/dist/launchd/plist.d.ts.map +1 -0
  84. package/dist/launchd/providers.d.ts +15 -0
  85. package/dist/launchd/providers.d.ts.map +1 -0
  86. package/dist/launchd/runner.d.ts +91 -0
  87. package/dist/launchd/runner.d.ts.map +1 -0
  88. package/dist/launchd/secret-tripwire.d.ts +24 -0
  89. package/dist/launchd/secret-tripwire.d.ts.map +1 -0
  90. package/dist/openbao/auth-method-form.d.ts +7 -0
  91. package/dist/openbao/auth-method-form.d.ts.map +1 -1
  92. package/dist/openbao/auth-method-reconcile.d.ts +15 -0
  93. package/dist/openbao/auth-method-reconcile.d.ts.map +1 -0
  94. package/dist/openbao/auth-method.d.ts +6 -0
  95. package/dist/openbao/auth-method.d.ts.map +1 -1
  96. package/dist/openbao/auth-role-form.d.ts +9 -0
  97. package/dist/openbao/auth-role-form.d.ts.map +1 -1
  98. package/dist/openbao/auth-role.d.ts.map +1 -1
  99. package/dist/openbao/auth-token-form.d.ts +48 -0
  100. package/dist/openbao/auth-token-form.d.ts.map +1 -0
  101. package/dist/openbao/bao-identity.d.ts +81 -0
  102. package/dist/openbao/bao-identity.d.ts.map +1 -0
  103. package/dist/openbao/bao-namespace.d.ts +20 -0
  104. package/dist/openbao/bao-namespace.d.ts.map +1 -0
  105. package/dist/openbao/cloudflare-role.d.ts.map +1 -1
  106. package/dist/openbao/cloudflare-roles-expand.d.ts.map +1 -1
  107. package/dist/openbao/fake-engines.d.ts +30 -0
  108. package/dist/openbao/fake-engines.d.ts.map +1 -0
  109. package/dist/openbao/fake-stack.d.ts +29 -0
  110. package/dist/openbao/fake-stack.d.ts.map +1 -0
  111. package/dist/openbao/host-approles.d.ts +62 -0
  112. package/dist/openbao/host-approles.d.ts.map +1 -0
  113. package/dist/openbao/index.d.ts +19 -0
  114. package/dist/openbao/index.d.ts.map +1 -1
  115. package/dist/openbao/index.js +1518 -310
  116. package/dist/openbao/index.js.map +40 -17
  117. package/dist/openbao/jwt-config-form.d.ts +51 -0
  118. package/dist/openbao/jwt-config-form.d.ts.map +1 -0
  119. package/dist/openbao/jwt-config.d.ts +25 -0
  120. package/dist/openbao/jwt-config.d.ts.map +1 -0
  121. package/dist/openbao/jwt-role-form.d.ts +91 -0
  122. package/dist/openbao/jwt-role-form.d.ts.map +1 -0
  123. package/dist/openbao/jwt-role.d.ts +31 -0
  124. package/dist/openbao/jwt-role.d.ts.map +1 -0
  125. package/dist/openbao/kubernetes-role-form.d.ts +68 -0
  126. package/dist/openbao/kubernetes-role-form.d.ts.map +1 -0
  127. package/dist/openbao/kubernetes-role.d.ts +25 -0
  128. package/dist/openbao/kubernetes-role.d.ts.map +1 -0
  129. package/dist/openbao/mfa-enforcement-form.d.ts +46 -0
  130. package/dist/openbao/mfa-enforcement-form.d.ts.map +1 -0
  131. package/dist/openbao/mfa-enforcement.d.ts +41 -0
  132. package/dist/openbao/mfa-enforcement.d.ts.map +1 -0
  133. package/dist/openbao/mfa-totp-form.d.ts +52 -0
  134. package/dist/openbao/mfa-totp-form.d.ts.map +1 -0
  135. package/dist/openbao/mfa-totp.d.ts +33 -0
  136. package/dist/openbao/mfa-totp.d.ts.map +1 -0
  137. package/dist/openbao/mfa-wire.d.ts +63 -0
  138. package/dist/openbao/mfa-wire.d.ts.map +1 -0
  139. package/dist/openbao/mount-form.d.ts +7 -0
  140. package/dist/openbao/mount-form.d.ts.map +1 -1
  141. package/dist/openbao/mount-move.d.ts +49 -0
  142. package/dist/openbao/mount-move.d.ts.map +1 -0
  143. package/dist/openbao/mount-path.d.ts +8 -4
  144. package/dist/openbao/mount-path.d.ts.map +1 -1
  145. package/dist/openbao/mount-reconcile.d.ts +16 -0
  146. package/dist/openbao/mount-reconcile.d.ts.map +1 -0
  147. package/dist/openbao/mount.d.ts +9 -0
  148. package/dist/openbao/mount.d.ts.map +1 -1
  149. package/dist/openbao/pki-role-canonical.d.ts +46 -0
  150. package/dist/openbao/pki-role-canonical.d.ts.map +1 -0
  151. package/dist/openbao/pki-role-form.d.ts +38 -30
  152. package/dist/openbao/pki-role-form.d.ts.map +1 -1
  153. package/dist/openbao/pki-role.d.ts.map +1 -1
  154. package/dist/openbao/policy.d.ts +2 -0
  155. package/dist/openbao/policy.d.ts.map +1 -1
  156. package/dist/openbao/proxmox-role-form.d.ts +18 -8
  157. package/dist/openbao/proxmox-role-form.d.ts.map +1 -1
  158. package/dist/openbao/proxmox-role-wire.d.ts +18 -0
  159. package/dist/openbao/proxmox-role-wire.d.ts.map +1 -0
  160. package/dist/openbao/proxmox-role.d.ts.map +1 -1
  161. package/dist/openbao/remount-wire.d.ts +44 -0
  162. package/dist/openbao/remount-wire.d.ts.map +1 -0
  163. package/dist/openbao/rename.d.ts +106 -0
  164. package/dist/openbao/rename.d.ts.map +1 -0
  165. package/dist/openbao/role-reconcile.d.ts +40 -0
  166. package/dist/openbao/role-reconcile.d.ts.map +1 -0
  167. package/docs/caddy.md +180 -0
  168. package/docs/launchd.md +167 -0
  169. package/docs/mesh-node.md +188 -0
  170. package/package.json +12 -2
  171. package/src/caddy/admin-calls.ts +128 -0
  172. package/src/caddy/admin-guard.ts +170 -0
  173. package/src/caddy/admin.ts +74 -0
  174. package/src/caddy/caddyfile-tripwire.ts +133 -0
  175. package/src/caddy/config-form.ts +64 -0
  176. package/src/caddy/config-lifecycle.test.ts +218 -0
  177. package/src/caddy/config-lifecycle.ts +148 -0
  178. package/src/caddy/config.ts +116 -0
  179. package/src/caddy/digest.ts +44 -0
  180. package/src/caddy/fake-caddy.ts +160 -0
  181. package/src/caddy/guards.test.ts +156 -0
  182. package/src/caddy/index.ts +25 -0
  183. package/src/caddy/local-admin.test.ts +160 -0
  184. package/src/caddy/local-admin.ts +189 -0
  185. package/src/caddy/providers.test.ts +244 -0
  186. package/src/caddy/providers.ts +17 -0
  187. package/src/caddy/with-file.ts +62 -0
  188. package/src/cloudflare/fake-mesh.ts +181 -0
  189. package/src/cloudflare/index.ts +9 -3
  190. package/src/cloudflare/mesh-node-api.ts +127 -0
  191. package/src/cloudflare/mesh-node-edges.test.ts +103 -0
  192. package/src/cloudflare/mesh-node-form.test.ts +80 -0
  193. package/src/cloudflare/mesh-node-form.ts +115 -0
  194. package/src/cloudflare/mesh-node-harness.ts +124 -0
  195. package/src/cloudflare/mesh-node-lifecycle.ts +187 -0
  196. package/src/cloudflare/mesh-node-policy.test.ts +151 -0
  197. package/src/cloudflare/mesh-node-token.test.ts +117 -0
  198. package/src/cloudflare/mesh-node-token.ts +125 -0
  199. package/src/cloudflare/mesh-node.test.ts +172 -0
  200. package/src/cloudflare/mesh-node.ts +120 -0
  201. package/src/cloudflare/providers.ts +20 -2
  202. package/src/index.ts +2 -0
  203. package/src/launchd/fake-runner.ts +176 -0
  204. package/src/launchd/host-effect.ts +28 -0
  205. package/src/launchd/host-file-form.ts +71 -0
  206. package/src/launchd/host-file-lifecycle.test.ts +198 -0
  207. package/src/launchd/host-file-lifecycle.ts +164 -0
  208. package/src/launchd/host-file.ts +64 -0
  209. package/src/launchd/host-lookup.test.ts +92 -0
  210. package/src/launchd/host-lookup.ts +94 -0
  211. package/src/launchd/index.ts +26 -0
  212. package/src/launchd/job-form.test.ts +171 -0
  213. package/src/launchd/job-form.ts +180 -0
  214. package/src/launchd/job-identity.test.ts +147 -0
  215. package/src/launchd/job-lifecycle.test.ts +220 -0
  216. package/src/launchd/job-lifecycle.ts +194 -0
  217. package/src/launchd/job-preflight.ts +152 -0
  218. package/src/launchd/job-validate.ts +134 -0
  219. package/src/launchd/job.ts +96 -0
  220. package/src/launchd/launchctl.test.ts +181 -0
  221. package/src/launchd/launchctl.ts +188 -0
  222. package/src/launchd/local-runner.test.ts +125 -0
  223. package/src/launchd/local-runner.ts +143 -0
  224. package/src/launchd/plist.test.ts +186 -0
  225. package/src/launchd/plist.ts +156 -0
  226. package/src/launchd/providers.test.ts +222 -0
  227. package/src/launchd/providers.ts +21 -0
  228. package/src/launchd/runner.ts +95 -0
  229. package/src/launchd/secret-tripwire.test.ts +110 -0
  230. package/src/launchd/secret-tripwire.ts +81 -0
  231. package/src/openbao/README.md +181 -0
  232. package/src/openbao/REPLACE.md +163 -0
  233. package/src/openbao/auth-method-form.ts +7 -0
  234. package/src/openbao/auth-method-reconcile.ts +74 -0
  235. package/src/openbao/auth-method.ts +20 -41
  236. package/src/openbao/auth-role-form.test.ts +16 -1
  237. package/src/openbao/auth-role-form.ts +11 -0
  238. package/src/openbao/auth-role.ts +12 -0
  239. package/src/openbao/auth-token-form.ts +161 -0
  240. package/src/openbao/bao-identity.test.ts +131 -0
  241. package/src/openbao/bao-identity.ts +188 -0
  242. package/src/openbao/bao-namespace.ts +32 -0
  243. package/src/openbao/cloudflare-role-wire.test.ts +8 -34
  244. package/src/openbao/cloudflare-role.ts +30 -8
  245. package/src/openbao/cloudflare-roles-expand.ts +2 -5
  246. package/src/openbao/fake-engines.ts +119 -0
  247. package/src/openbao/fake-stack.ts +112 -0
  248. package/src/openbao/host-approles.test.ts +121 -0
  249. package/src/openbao/host-approles.ts +126 -0
  250. package/src/openbao/index.ts +19 -0
  251. package/src/openbao/jwt-config-form.ts +161 -0
  252. package/src/openbao/jwt-config.ts +85 -0
  253. package/src/openbao/jwt-role-form.test.ts +130 -0
  254. package/src/openbao/jwt-role-form.ts +231 -0
  255. package/src/openbao/jwt-role.ts +102 -0
  256. package/src/openbao/kubernetes-role-form.test.ts +82 -0
  257. package/src/openbao/kubernetes-role-form.ts +149 -0
  258. package/src/openbao/kubernetes-role.ts +94 -0
  259. package/src/openbao/mfa-enforcement-form.ts +147 -0
  260. package/src/openbao/mfa-enforcement.ts +143 -0
  261. package/src/openbao/mfa-totp-form.ts +155 -0
  262. package/src/openbao/mfa-totp.ts +118 -0
  263. package/src/openbao/mfa-wire.ts +117 -0
  264. package/src/openbao/mfa.test.ts +200 -0
  265. package/src/openbao/mount-form.ts +9 -1
  266. package/src/openbao/mount-move.test.ts +164 -0
  267. package/src/openbao/mount-move.ts +113 -0
  268. package/src/openbao/mount-path.test.ts +11 -1
  269. package/src/openbao/mount-path.ts +17 -11
  270. package/src/openbao/mount-reconcile.ts +75 -0
  271. package/src/openbao/mount.ts +26 -43
  272. package/src/openbao/pki-role-canonical.ts +163 -0
  273. package/src/openbao/pki-role-form.test.ts +112 -0
  274. package/src/openbao/pki-role-form.ts +76 -110
  275. package/src/openbao/pki-role.ts +12 -27
  276. package/src/openbao/policy-rename.test.ts +112 -0
  277. package/src/openbao/policy-wire.test.ts +7 -26
  278. package/src/openbao/policy.ts +27 -4
  279. package/src/openbao/proxmox-role-form.ts +27 -8
  280. package/src/openbao/proxmox-role-wire.ts +24 -0
  281. package/src/openbao/proxmox-role.ts +62 -37
  282. package/src/openbao/remount-wire.test.ts +87 -0
  283. package/src/openbao/remount-wire.ts +112 -0
  284. package/src/openbao/rename-occupied.test.ts +166 -0
  285. package/src/openbao/rename.ts +162 -0
  286. package/src/openbao/role-reconcile.test.ts +104 -0
  287. package/src/openbao/role-reconcile.ts +84 -0
  288. package/src/openbao/role-rename.test.ts +205 -0
@@ -0,0 +1,25 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/launchd/host-file.ts", "../src/launchd/host-effect.ts", "../src/launchd/secret-tripwire.ts", "../src/launchd/host-file-form.ts", "../src/launchd/job-form.ts", "../src/launchd/plist.ts", "../src/launchd/runner.ts", "../src/launchd/host-file-lifecycle.ts", "../src/launchd/job.ts", "../src/launchd/job-validate.ts", "../src/launchd/launchctl.ts", "../src/launchd/job-preflight.ts", "../src/launchd/job-lifecycle.ts", "../src/launchd/local-runner.ts", "../src/launchd/host-lookup.ts", "../src/launchd/providers.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * `Host.File` — one text file on the host, declared: content, mode, owner, group.\n *\n * - create / update — atomic write (temp file in the same directory, mode and owner set, rename),\n * skipped when the file already matches; then read back and compared.\n * - read — lstat plus the SHA-256 of the bytes.\n * - diff — declared SHA-256 against the stored and on-disk digest, plus mode and owner.\n * - replace — only when `path` changes; create-before-delete.\n * - delete — removes the file.\n *\n * ⛔ NEVER A SECRET — see host-file-form.ts. Secret files stay rendered by openbao-agent.\n * ⚠️ NOTHING IS ADOPTED WITHOUT `--adopt`: a file already at the path reads as `Unowned`.\n * ★ IT LIVES IN THE launchd SUBPATH because the two travel together: a Mac host stack declares a\n * daemon's config file and the daemon that reads it, through the same HostRunner.\n */\nimport { Resource } from 'alchemy';\nimport { Unowned } from 'alchemy/AdoptPolicy';\nimport { isResolved } from 'alchemy/Diff';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport { lift, resolvedString } from './host-effect.ts';\nimport type { HostFileAttributes, HostFileProps } from './host-file-form.ts';\nimport { deleteFile, diffFile, readFileAttributes, reconcileFile } from './host-file-lifecycle.ts';\nimport { HostRunnerService } from './runner.ts';\n\nexport type { HostFileAttributes, HostFileProps } from './host-file-form.ts';\n\nexport interface HostFile extends Resource<'Host.File', HostFileProps, HostFileAttributes> {}\n\nexport const HostFile = Resource<HostFile>('Host.File');\n\nexport const HostFileProvider = () =>\n Provider.effect(\n HostFile,\n Effect.gen(function* () {\n const runner = yield* HostRunnerService;\n return HostFile.Provider.of({\n list: () => Effect.succeed([]),\n\n read: ({ olds, output }) =>\n lift(async () => {\n const found = await readFileAttributes(runner, olds.path);\n if (found === undefined) return undefined;\n return output === undefined ? Unowned(found) : found;\n }),\n\n diff: ({ news, output }) => {\n if (output === undefined) return Effect.succeed(undefined);\n if (isResolved(news)) return lift(() => diffFile(runner, news, output));\n // ⛔ A new path is a replace even while `content` is unresolved (an Output templated in):\n // the engine's default would be an update, which writes the new path and never\n // removes the old one. reconcileFile also defends, for a path that is an Output.\n const path = resolvedString(news, 'path');\n return Effect.succeed(\n path !== undefined && path !== output.path ? { action: 'replace' as const } : undefined,\n );\n },\n\n reconcile: ({ news, output }) => lift(() => reconcileFile(runner, news, output)),\n\n delete: ({ output }) => lift(() => deleteFile(runner, output)),\n });\n }),\n );\n",
6
+ "/**\n * The one place a HostRunner's promises become Effects — plus the one probe of an unresolved\n * `news` both providers' diffs share.\n *\n * ★ The lifecycle files are plain async functions over a HostRunner (see runner.ts for why the\n * runner is promise-based), so each provider handler is a single `lift(() => step(runner, …))`.\n * ⚠️ A thrown Error stays that Error — its message is the refusal a deploy prints. Anything else\n * thrown is wrapped, so the failure channel is always an Error with a readable message.\n */\nimport * as Effect from 'effect/Effect';\n\nexport const lift = <A>(step: () => Promise<A>): Effect.Effect<A, Error> =>\n Effect.tryPromise({\n catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),\n try: step,\n });\n\n/**\n * A string prop of a diff's `news`, when that one prop is already resolved; `undefined` while it\n * is still an Output (a function-typed proxy), an Effect or a Config.\n * ★ For the identity props a diff must see even when the rest of `news` is unresolved — a plain\n * string is resolved by definition, so `typeof` is the whole test.\n */\nexport const resolvedString = (news: unknown, key: string): string | undefined => {\n if (typeof news !== 'object' || news === null) return undefined;\n const value = (news as Record<string, unknown>)[key];\n return typeof value === 'string' ? value : undefined;\n};\n",
7
+ "/**\n * A tripwire for the obvious ways a secret ends up in Alchemy state through these providers.\n *\n * ⛔ WHY IT EXISTS. Alchemy persists every prop UNENCRYPTED in its state store — the same reason\n * the OpenBao families are metadata-only (openbao/policy.ts). A LaunchdJob's `environment` and\n * `programArguments`, and a HostFile's `content`, are props. A token declared there is a token\n * copied into the state store, into every plan diff, and into the plist or file on disk.\n * ★ THE ALTERNATIVE, NAMED IN EVERY REFUSAL: let a secret renderer (openbao-agent, or any other)\n * write the secret to a root-owned file, and declare only its PATH — `FOO_TOKEN_FILE=/path`,\n * `--token-file /path`. The job reads the file at start; the state holds a path.\n *\n * ⚠️ A TRIPWIRE, NOT A SCANNER. It catches the names and shapes people actually type. A token in\n * an innocently named variable passes; nothing here can make a secret safe to declare.\n */\nimport type { PlistDict, PlistValue } from './plist.ts';\n\nconst ALTERNATIVE =\n 'props are stored unencrypted in Alchemy state. Have a secret renderer (e.g. openbao-agent) ' +\n 'write it to a root-owned file and declare the file path instead (FOO_FILE=/path, --foo-file /path).';\n\n/** Names that end in a word meaning \"this is the secret itself\". `*_FILE`, `*_PATH` never match. */\nconst SECRET_NAME =\n /(?:^|_)(?:TOKEN|SECRET|PASSWORD|PASSWD|PASSPHRASE|API_?KEY|ACCESS_KEY|PRIVATE_KEY|SECRET_KEY|CREDENTIALS?)$/i;\n\n/** `--token=…` / `-password=…` style flags carrying their value inline. */\nconst INLINE_FLAG = /^--?[a-z0-9-]*(?:token|password|passwd|passphrase|secret|api-?key)=./i;\n\n/** A flag whose NEXT argv element is the secret: `--token abc`. */\nconst BARE_FLAG = /^--?(?:[a-z0-9]+-)*(?:token|password|passwd|passphrase|secret|api-?key)$/i;\n\nexport const PRIVATE_KEY = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/;\n\n/** Paths of every string leaf in a plist value holding a private key. */\nconst keyLeaves = (value: PlistValue, path: string): string[] => {\n if (typeof value === 'string') return PRIVATE_KEY.test(value) ? [path] : [];\n if (Array.isArray(value)) {\n return (value as readonly PlistValue[]).flatMap((item, index) =>\n keyLeaves(item, `${path}[${String(index)}]`),\n );\n }\n if (typeof value === 'object' && value !== null) {\n return Object.entries(value).flatMap(([key, item]) => keyLeaves(item, `${path}.${key}`));\n }\n return [];\n};\n\n/**\n * Refusals for a job's environment, argv and extraKeys. Empty means nothing tripped.\n * ★ extraKeys too: it is a prop like the others, so it lands in state just the same.\n */\nexport const jobSecretProblems = (\n environment: Readonly<Record<string, string>> | undefined,\n argv: readonly string[],\n extraKeys?: PlistDict,\n): string[] => {\n const found: string[] = [];\n for (const [name, value] of Object.entries(environment ?? {})) {\n if (SECRET_NAME.test(name))\n found.push(`environment ${name} looks like a secret: ${ALTERNATIVE}`);\n else if (PRIVATE_KEY.test(value))\n found.push(`environment ${name} holds a private key: ${ALTERNATIVE}`);\n }\n for (const [index, arg] of argv.entries()) {\n // ⛔ Never echo the argument itself: if it IS a secret, the refusal would print it.\n if (INLINE_FLAG.test(arg)) {\n found.push(`programArguments[${String(index)}] passes a secret inline: ${ALTERNATIVE}`);\n } else if (BARE_FLAG.test(arg) && index + 1 < argv.length) {\n found.push(`programArguments[${String(index + 1)}] follows a secret flag: ${ALTERNATIVE}`);\n } else if (PRIVATE_KEY.test(arg)) {\n found.push(`programArguments[${String(index)}] holds a private key: ${ALTERNATIVE}`);\n }\n }\n for (const path of keyLeaves(extraKeys ?? {}, 'extraKeys')) {\n found.push(`${path} holds a private key: ${ALTERNATIVE}`);\n }\n return found;\n};\n\n/** Refusals for a HostFile's content. */\nexport const fileSecretProblems = (content: string): string[] =>\n PRIVATE_KEY.test(content) ? [`content holds a private key: ${ALTERNATIVE}`] : [];\n",
8
+ "/**\n * HostFile's props, attributes and validation — the pure half of the provider.\n *\n * ⛔ `content` IS NEVER A SECRET. Alchemy stores props unencrypted, so the content of every HostFile\n * sits in the state store and in every plan diff. A secret file stays RENDERED BY A SECRET\n * RENDERER (openbao-agent templates, on this estate) that fetches it at run time; the stack\n * declares only non-secret config, and at most the PATH the renderer writes to.\n * secret-tripwire.ts refuses the obvious case (a PEM private key); nothing can catch all of them.\n */\nimport { fileSecretProblems } from './secret-tripwire.ts';\n\nexport interface HostFileProps {\n /** Absolute path. The parent directory must already exist. */\n path: string;\n /** UTF-8 text. ⛔ Never a secret — see the file header. */\n content: string;\n /** Permission bits. @default 0o644 */\n mode?: number;\n /** User name or numeric uid. Omitted: whoever the runner writes as (a rewrite keeps no owner). */\n owner?: string | number;\n /** Group name or numeric gid. Omitted: the directory's default group. */\n group?: string | number;\n}\n\nexport interface HostFileAttributes {\n path: string;\n /** SHA-256 of the bytes on disk. */\n sha256: string;\n mode: number;\n uid: number;\n gid: number;\n size: number;\n}\n\nexport const DEFAULT_MODE = 0o644;\n\nconst identityProblems = (name: string, value: string | number | undefined): string[] => {\n if (value === undefined) return [];\n if (typeof value === 'number') {\n return Number.isSafeInteger(value) && value >= 0\n ? []\n : [`${name} must be a non-negative integer id`];\n }\n return /^[A-Za-z_][A-Za-z0-9_.-]*$/.test(value) || /^\\d+$/.test(value)\n ? []\n : [`${name} ${JSON.stringify(value)} is not a valid user or group name`];\n};\n\nexport const fileProblems = (props: HostFileProps): string[] => {\n const found: string[] = [];\n const { path } = props;\n if (!path.startsWith('/')) found.push('path must be absolute');\n // ★ One spelling per file: `a/./b`, `a//b` and `a/../b` would each be a second resource for the\n // same inode, and `..` is how a derived path walks out of the directory it was meant for.\n if (\n path.endsWith('/') ||\n path\n .split('/')\n .slice(1)\n .some((part) => part === '' || part === '.' || part === '..')\n ) {\n found.push('path must be normalised (no trailing slash, no empty, \".\" or \"..\" segments)');\n }\n if (path.includes('\\x00')) found.push('path contains NUL');\n const mode = props.mode ?? DEFAULT_MODE;\n if (!Number.isSafeInteger(mode) || mode < 0 || mode > 0o7777) found.push('mode must be 0–0o7777');\n found.push(...identityProblems('owner', props.owner));\n found.push(...identityProblems('group', props.group));\n found.push(...fileSecretProblems(props.content));\n return found;\n};\n",
9
+ "/**\n * LaunchdJob's props, attributes and the pure half of the provider: domain parsing, the plist path,\n * and the props → plist mapping whose SHA-256 is the whole diff.\n *\n * ★ KEY NAMES AND MEANINGS FROM launchd.plist(5), checked against the 148 nix-darwin daemons on a\n * macOS 27.2 host on 2026-09-21 (read-only, from /nix/store): Label, ProgramArguments, RunAtLoad,\n * KeepAlive (boolean or {SuccessfulExit}), ThrottleInterval, StandardOutPath/StandardErrorPath,\n * EnvironmentVariables, UserName, StartInterval and StartCalendarInterval are the keys in real\n * use. Everything rarer goes through `extraKeys`.\n */\nimport { createHash } from 'node:crypto';\nimport { type PlistDict, type PlistValue, renderPlist } from './plist.ts';\n\n/** `system` (LaunchDaemons, root) or one user's login session, `gui/<uid>` (LaunchAgents). */\nexport type LaunchdDomain = 'system' | `gui/${number}`;\n\n/** One StartCalendarInterval entry. An omitted field is a wildcard, as in cron. */\nexport type CalendarInterval = {\n readonly minute?: number;\n readonly hour?: number;\n readonly day?: number;\n /** 0 and 7 are both Sunday. */\n readonly weekday?: number;\n readonly month?: number;\n};\n\n/** `true` = always restart; the object form restarts only on the named conditions. */\nexport type KeepAlive = boolean | { readonly successfulExit?: boolean; readonly crashed?: boolean };\n\nexport interface LaunchdJobProps {\n /** Reverse-DNS label. Unique per domain; also the plist's file name. */\n label: string;\n domain: LaunchdDomain;\n /** argv; the first element must be an absolute path. */\n programArguments: readonly string[];\n /**\n * ⛔ NON-SECRET VALUES ONLY. Props are stored unencrypted in Alchemy state, and these also land in\n * the plist on disk and in `launchctl print`. Pass a secret as a FILE PATH that a secret renderer\n * (openbao-agent) maintains — secret-tripwire.ts refuses the obvious mistakes.\n */\n environment?: Readonly<Record<string, string>>;\n runAtLoad?: boolean;\n keepAlive?: KeepAlive;\n /** Seconds between starts. */\n startInterval?: number;\n startCalendarInterval?: CalendarInterval | readonly CalendarInterval[];\n /** Minimum seconds between (re)starts; launchd's default is 10. */\n throttleInterval?: number;\n standardOutPath?: string;\n standardErrorPath?: string;\n workingDirectory?: string;\n /** Run as this user. ⚠️ System domain only — launchd ignores it for agents, so it is refused. */\n userName?: string;\n /** Run as this group. ⚠️ System domain only, like `userName`. */\n groupName?: string;\n /**\n * Any other launchd.plist(5) key, verbatim (`ProcessType`, `SoftResourceLimits`, …).\n * ⛔ A key the typed props own is refused, so one job can never say two things.\n */\n extraKeys?: PlistDict;\n}\n\nexport interface LaunchdJobAttributes {\n label: string;\n domain: LaunchdDomain;\n /** `<domain>/<label>` — what `launchctl print` / `bootout` take. */\n serviceTarget: string;\n plistPath: string;\n /** SHA-256 of the plist bytes last bootstrapped (reconcile) or found on disk (read). */\n plistSha256: string;\n /** Bootstrapped into the domain — not necessarily running. */\n loaded: boolean;\n /** launchd's own word: `running`, `not running`, … */\n state?: string;\n pid?: number;\n lastExitCode?: number;\n}\n\nexport type ParsedDomain =\n | { readonly kind: 'system' }\n | { readonly kind: 'gui'; readonly uid: number };\n\nexport const parseDomain = (domain: string): ParsedDomain | undefined => {\n if (domain === 'system') return { kind: 'system' };\n const match = /^gui\\/(\\d+)$/.exec(domain);\n return match?.[1] === undefined ? undefined : { kind: 'gui', uid: Number(match[1]) };\n};\n\nexport const serviceTarget = (label: string, domain: LaunchdDomain): string => `${domain}/${label}`;\n\n/**\n * Where launchd expects the job's plist. ★ Derived, never declared: a daemon's plist outside\n * /Library/LaunchDaemons is not bootstrapped at boot, so a free-form path would be a job that\n * silently stops existing after the next restart.\n */\nexport const plistPathFor = (\n label: string,\n domain: ParsedDomain,\n home: string | undefined,\n): string => {\n if (domain.kind === 'system') return `/Library/LaunchDaemons/${label}.plist`;\n // ⛔ No `~` fallback: the provider may run as root, whose `~` is not the agent owner's home.\n if (home === undefined || !home.startsWith('/')) {\n throw new Error(`gui/${String(domain.uid)}: no home directory for that uid on this host`);\n }\n return `${home}/Library/LaunchAgents/${label}.plist`;\n};\n\n/** Plist keys the typed props own — `extraKeys` may not name them. `Program` is refused too. */\nexport const OWNED_KEYS: readonly string[] = [\n 'EnvironmentVariables',\n 'GroupName',\n 'KeepAlive',\n 'Label',\n 'Program',\n 'ProgramArguments',\n 'RunAtLoad',\n 'StandardErrorPath',\n 'StandardOutPath',\n 'StartCalendarInterval',\n 'StartInterval',\n 'ThrottleInterval',\n 'UserName',\n 'WorkingDirectory',\n];\n\nconst calendarDict = (entry: CalendarInterval): PlistDict => {\n const dict: Record<string, PlistValue> = {};\n if (entry.minute !== undefined) dict['Minute'] = entry.minute;\n if (entry.hour !== undefined) dict['Hour'] = entry.hour;\n if (entry.day !== undefined) dict['Day'] = entry.day;\n if (entry.weekday !== undefined) dict['Weekday'] = entry.weekday;\n if (entry.month !== undefined) dict['Month'] = entry.month;\n return dict;\n};\n\nconst keepAliveValue = (keepAlive: KeepAlive): PlistValue => {\n if (typeof keepAlive === 'boolean') return keepAlive;\n const dict: Record<string, PlistValue> = {};\n if (keepAlive.successfulExit !== undefined) dict['SuccessfulExit'] = keepAlive.successfulExit;\n if (keepAlive.crashed !== undefined) dict['Crashed'] = keepAlive.crashed;\n return dict;\n};\n\n/** The plist dict for a job. ⚠️ Assumes jobProblems() came back empty. */\nexport const jobPlistDict = (props: LaunchdJobProps): PlistDict => {\n const dict: Record<string, PlistValue> = { ...props.extraKeys };\n dict['Label'] = props.label;\n dict['ProgramArguments'] = [...props.programArguments];\n const env = props.environment ?? {};\n if (Object.keys(env).length > 0) dict['EnvironmentVariables'] = { ...env };\n if (props.runAtLoad !== undefined) dict['RunAtLoad'] = props.runAtLoad;\n if (props.keepAlive !== undefined) dict['KeepAlive'] = keepAliveValue(props.keepAlive);\n if (props.startInterval !== undefined) dict['StartInterval'] = props.startInterval;\n const calendar = props.startCalendarInterval;\n if (calendar !== undefined) {\n dict['StartCalendarInterval'] = Array.isArray(calendar)\n ? (calendar as readonly CalendarInterval[]).map(calendarDict)\n : calendarDict(calendar as CalendarInterval);\n }\n if (props.throttleInterval !== undefined) dict['ThrottleInterval'] = props.throttleInterval;\n if (props.standardOutPath !== undefined) dict['StandardOutPath'] = props.standardOutPath;\n if (props.standardErrorPath !== undefined) dict['StandardErrorPath'] = props.standardErrorPath;\n if (props.workingDirectory !== undefined) dict['WorkingDirectory'] = props.workingDirectory;\n if (props.userName !== undefined) dict['UserName'] = props.userName;\n if (props.groupName !== undefined) dict['GroupName'] = props.groupName;\n return dict;\n};\n\nexport const sha256Hex = (bytes: Uint8Array | string): string =>\n createHash('sha256').update(bytes).digest('hex');\n\n/** The rendered plist and its digest — the only thing LaunchdJob compares. */\nexport const renderJob = (\n props: LaunchdJobProps,\n): { text: string; bytes: Uint8Array; sha256: string } => {\n const text = renderPlist(jobPlistDict(props));\n const bytes = new TextEncoder().encode(text);\n return { bytes, sha256: sha256Hex(bytes), text };\n};\n",
10
+ "/**\n * An XML property list serializer — the subset launchd.plist(5) reads, and nothing else.\n *\n * ★ WHY NOT AN npm LIBRARY. Nothing in this workspace depends on one (checked 2026-09-21: no\n * `plist`, `xmlbuilder` or `@xmldom/xmldom` anywhere under node_modules), and the common one pulls\n * an XML DOM in for PARSING, which a provider that only RENDERS never does. The subset a launchd\n * job needs is five node types, so this is ~100 lines plus exhaustive tests (plist.test.ts), which\n * also round-trip every case through Apple's own `plutil` when the tests run on macOS.\n *\n * ⛔ THE OUTPUT MUST BE DETERMINISTIC. LaunchdJob diffs by the SHA-256 of these bytes, so two\n * renders of one declaration must be byte-identical: dict keys are sorted, indentation is fixed,\n * and nothing depends on insertion order. A non-deterministic render is a forever-`update`.\n * ⚠️ AND A CHANGE TO THIS FILE'S OUTPUT RESTARTS EVERY JOB. A new header, indent or escape changes\n * every digest, so the next deploy after the kit upgrade updates — restarts — every declared\n * job at once. Treat any byte change here as a breaking change and say so in its changeset.\n *\n * ⛔ REFUSE, NEVER COERCE. `undefined`, `null`, a fraction, NaN, a class instance or a character XML\n * 1.0 cannot carry is a thrown PlistError naming the path, not a silently dropped key. A dropped\n * `KeepAlive` is a daemon that never restarts, and nothing downstream would say why.\n */\n\n/** Every value launchd reads. ⚠️ No `<real>`, `<date>` or `<data>`: no launchd key needs one. */\nexport type PlistValue =\n | string\n | number\n | boolean\n | readonly PlistValue[]\n | { readonly [key: string]: PlistValue };\n\nexport type PlistDict = { readonly [key: string]: PlistValue };\n\nexport class PlistError extends Error {\n constructor(\n readonly path: string,\n detail: string,\n ) {\n super(`plist ${path === '' ? '(root)' : path}: ${detail}`);\n this.name = 'PlistError';\n }\n}\n\nconst HEADER =\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n' +\n '<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" ' +\n '\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\\n' +\n '<plist version=\"1.0\">\\n';\n\n/** ⚠️ A cycle, or a runaway generator, would otherwise recurse until the stack dies. */\nconst MAX_DEPTH = 32;\n\n/**\n * ⛔ CHARACTERS XML 1.0 CANNOT CARRY AT ALL — not even escaped (XML 1.0 §2.2 `Char`). C0 controls\n * other than tab, LF and CR; U+FFFE / U+FFFF; and an unpaired surrogate, which has no UTF-8 form.\n */\n// ★ The `u` flag makes a VALID surrogate pair one code point above U+FFFF, outside this class, so\n// only an unpaired half matches the \\uD800-\\uDFFF range.\n// oxlint-disable-next-line no-control-regex -- matching control characters is the point\nconst FORBIDDEN = /[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F\\uD800-\\uDFFF\\uFFFE\\uFFFF]/u;\n\n/**\n * Escape text content.\n * ⚠️ CR IS A CHARACTER REFERENCE, NOT A LITERAL. An XML parser normalises a literal CR (and CRLF)\n * to LF before the application sees it (XML 1.0 §2.11), so `a\\r\\nb` would come back as `a\\nb` and\n * the argument launchd passes would differ from the one declared. `&#13;` survives normalisation.\n */\nexport const escapeText = (text: string, path: string): string => {\n const bad = FORBIDDEN.exec(text);\n if (bad !== null) {\n const code = bad[0].codePointAt(0) ?? 0;\n throw new PlistError(\n path,\n `U+${code.toString(16).toUpperCase().padStart(4, '0')} cannot appear in XML 1.0`,\n );\n }\n return text\n .replaceAll('&', '&amp;')\n .replaceAll('<', '&lt;')\n .replaceAll('>', '&gt;')\n .replaceAll('\\r', '&#13;');\n};\n\nconst isPlainObject = (value: object): boolean => {\n const proto = Object.getPrototypeOf(value) as unknown;\n return proto === Object.prototype || proto === null;\n};\n\nconst renderValue = (value: unknown, path: string, depth: number, out: string[]): void => {\n const pad = '\\t'.repeat(depth);\n if (depth > MAX_DEPTH) throw new PlistError(path, `nested deeper than ${String(MAX_DEPTH)}`);\n if (typeof value === 'string') {\n out.push(`${pad}<string>${escapeText(value, path)}</string>`);\n return;\n }\n if (typeof value === 'boolean') {\n out.push(`${pad}<${value ? 'true' : 'false'}/>`);\n return;\n }\n if (typeof value === 'number') {\n // ⛔ launchd reads every numeric key as an integer; 1.5 would be a silently different job.\n if (!Number.isSafeInteger(value)) {\n throw new PlistError(path, `${String(value)} is not a safe integer (no <real> support)`);\n }\n out.push(`${pad}<integer>${String(value === 0 ? 0 : value)}</integer>`);\n return;\n }\n if (Array.isArray(value)) {\n if (value.length === 0) {\n out.push(`${pad}<array/>`);\n return;\n }\n out.push(`${pad}<array>`);\n for (const [index, item] of (value as readonly unknown[]).entries()) {\n renderValue(item, `${path}[${String(index)}]`, depth + 1, out);\n }\n out.push(`${pad}</array>`);\n return;\n }\n if (typeof value === 'object' && value !== null && isPlainObject(value)) {\n renderDict(value as Record<string, unknown>, path, depth, out);\n return;\n }\n const kind = value === null ? 'null' : typeof value;\n throw new PlistError(path, `${kind} has no plist form — omit the key instead`);\n};\n\nconst renderDict = (\n dict: Record<string, unknown>,\n path: string,\n depth: number,\n out: string[],\n): void => {\n const pad = '\\t'.repeat(depth);\n // ★ Code-unit order, not localeCompare: the same bytes on every machine and every locale.\n const keys = Object.keys(dict).sort();\n if (keys.length === 0) {\n out.push(`${pad}<dict/>`);\n return;\n }\n out.push(`${pad}<dict>`);\n for (const key of keys) {\n const child = path === '' ? key : `${path}.${key}`;\n out.push(`${pad}\\t<key>${escapeText(key, child)}</key>`);\n renderValue(dict[key], child, depth + 1, out);\n }\n out.push(`${pad}</dict>`);\n};\n\n/** Render a top-level dict as a complete XML plist document, ending in a newline. */\nexport const renderPlist = (dict: PlistDict): string => {\n if (typeof dict !== 'object' || dict === null || Array.isArray(dict) || !isPlainObject(dict)) {\n throw new PlistError('', 'the top level of a launchd plist must be a dict');\n }\n const out: string[] = [];\n renderDict(dict as Record<string, unknown>, '', 0, out);\n return `${HEADER}${out.join('\\n')}\\n</plist>\\n`;\n};\n",
11
+ "/**\n * The one seam between the launchd providers and the host: every file read, file write and\n * `launchctl` call goes through a HostRunner, and nothing in this directory touches the host any\n * other way.\n *\n * ★ WHY A SEAM AND NOT DIRECT node:fs CALLS. launchd has no SDK — the provider renders a plist and\n * drives `launchctl` — so the only way to test the lifecycle without a real launchd (and without\n * writing /Library) is to swap the host out. fake-runner.ts is an in-memory host with a scripted\n * launchd; local-runner.ts is the real one. The same seam is where a consumer plugs in a runner\n * that reaches a host some other way.\n *\n * ★ PROMISES, NOT EFFECTS, ON PURPOSE. A consumer writing their own runner implements plain async\n * functions; the providers lift them into Effect at the call site (host-effect.ts).\n */\nimport * as Context from 'effect/Context';\nimport * as Layer from 'effect/Layer';\n\n/** One finished process. ⛔ `stdout` may carry anything the job's environment holds — never log it. */\nexport type ExecResult = {\n readonly exitCode: number;\n readonly stdout: string;\n readonly stderr: string;\n};\n\n/**\n * What `lstat` says about a path. ⚠️ lstat, not stat: a symlink is reported as a symlink, because\n * writing through one would change a file this resource never named.\n */\nexport type FileStat = {\n readonly kind: 'file' | 'directory' | 'symlink' | 'other';\n /** Permission bits only (`& 0o7777`), never the file-type bits. */\n readonly mode: number;\n readonly uid: number;\n readonly gid: number;\n readonly size: number;\n};\n\nexport type WriteOptions = {\n /** Permission bits for the final file, applied BEFORE it becomes visible at `path`. */\n readonly mode: number;\n /** Numeric owner; omitted means whoever the runner writes as. */\n readonly uid?: number;\n /** Numeric group; omitted means the directory's default. */\n readonly gid?: number;\n};\n\nexport type HostUser = { readonly uid: number; readonly gid: number; readonly home: string };\n\nexport interface HostRunner {\n /**\n * ⛔ DECLARED, NEVER INFERRED. `true` only when every call this runner makes already runs with\n * root's authority — a runner that talks to a root helper, say. The providers refuse\n * system-domain writes unless `effectiveUid()` is 0 or this is `true`, so claiming it falsely\n * turns that refusal into an EACCES halfway through a deploy.\n * ⛔ THE KIT NEVER ELEVATES. No runner here calls `sudo`: a deploy that prompts for a password\n * mid-plan, or silently uses a cached sudo ticket, is exactly the \"no silent sudo\" failure. A\n * consumer who wants elevation writes that runner deliberately and sets this flag.\n */\n readonly privileged: boolean;\n /** The uid file writes and launchctl calls run as. */\n effectiveUid(): number;\n /** Run a program with an argv — ⛔ never through a shell. */\n exec(argv: readonly string[]): Promise<ExecResult>;\n /** File bytes, or `undefined` when nothing is at `path`. */\n readFile(path: string): Promise<Uint8Array | undefined>;\n /** lstat, or `undefined` when nothing is at `path`. */\n stat(path: string): Promise<FileStat | undefined>;\n /**\n * Write `bytes` so that `path` holds either the old file or the new one, never a torn mix:\n * a temporary file in the same directory, mode and owner set on it, then rename(2) over `path`.\n * ⚠️ The parent directory must exist; creating it would invent an owner and mode nobody declared.\n */\n writeFileAtomic(path: string, bytes: Uint8Array, options: WriteOptions): Promise<void>;\n /** Remove a file. Idempotent: nothing at `path` is success. */\n removeFile(path: string): Promise<void>;\n /** A user by name or numeric id, or `undefined` when the host has none. */\n lookupUser(nameOrId: string): Promise<HostUser | undefined>;\n /** A group's gid by name or numeric id, or `undefined` when the host has none. */\n lookupGroup(nameOrId: string): Promise<number | undefined>;\n /** Wait between launchctl polls. ★ Injected so a test never sleeps for real. */\n sleep(ms: number): Promise<void>;\n}\n\n/** The Effect service every launchd provider reads its runner from. */\nexport class HostRunnerService extends Context.Service<HostRunnerService, HostRunner>()(\n 'homeflare/launchd/HostRunner',\n) {}\n\n/** Provide a runner to the providers: `Layer.provide(hostRunnerLayer(localRunner()))`. */\nexport const hostRunnerLayer = (runner: HostRunner): Layer.Layer<HostRunnerService> =>\n Layer.succeed(HostRunnerService, runner);\n\n/** True when this runner may write root-owned paths and drive the system domain. */\nexport const canActAsRoot = (runner: HostRunner): boolean =>\n runner.privileged || runner.effectiveUid() === 0;\n",
12
+ "/**\n * HostFile's read / diff / reconcile / delete as plain async functions over a HostRunner, so the\n * lifecycle runs against fake-runner.ts in tests.\n *\n * ⛔ SYMLINKS AND DIRECTORIES ARE REFUSED, NOT REPLACED. rename(2) over a symlink replaces the LINK,\n * so a path that is a symlink (nix-darwin's /etc entries point into /nix/store) would be quietly\n * taken from the tool that owns it — which puts it back, or refuses its next activation. Remove\n * the path from its owner first, then declare it here.\n */\nimport type { Diff } from 'alchemy/Diff';\nimport {\n DEFAULT_MODE,\n type HostFileAttributes,\n type HostFileProps,\n fileProblems,\n} from './host-file-form.ts';\nimport { sha256Hex } from './job-form.ts';\nimport { type HostRunner, canActAsRoot } from './runner.ts';\n\ntype Desired = {\n readonly bytes: Uint8Array;\n readonly sha256: string;\n readonly mode: number;\n readonly uid?: number;\n readonly gid?: number;\n};\n\nconst refuse = (path: string, message: string): Error => new Error(`Host.File ${path}: ${message}`);\n\nconst numericId = (value: string | number): number | undefined =>\n typeof value === 'number' ? value : /^\\d+$/.test(value) ? Number(value) : undefined;\n\n/** Validate, then resolve owner/group names to ids through the runner. */\nexport const desiredFile = async (runner: HostRunner, props: HostFileProps): Promise<Desired> => {\n const found = fileProblems(props);\n if (found.length > 0) throw refuse(props.path, found.join('; '));\n const bytes = new TextEncoder().encode(props.content);\n let uid: number | undefined;\n let gid: number | undefined;\n if (props.owner !== undefined) {\n uid = numericId(props.owner) ?? (await runner.lookupUser(String(props.owner)))?.uid;\n if (uid === undefined) throw refuse(props.path, `no user ${String(props.owner)} on this host`);\n }\n if (props.group !== undefined) {\n gid = numericId(props.group) ?? (await runner.lookupGroup(String(props.group)));\n if (gid === undefined) throw refuse(props.path, `no group ${String(props.group)} on this host`);\n }\n return {\n bytes,\n mode: props.mode ?? DEFAULT_MODE,\n sha256: sha256Hex(bytes),\n ...(uid === undefined ? {} : { uid }),\n ...(gid === undefined ? {} : { gid }),\n };\n};\n\n/** The file as it is now, or `undefined` when nothing is at the path. Non-files read with no digest. */\nexport const readFileAttributes = async (\n runner: HostRunner,\n path: string,\n): Promise<HostFileAttributes | undefined> => {\n const stat = await runner.stat(path);\n if (stat === undefined) return undefined;\n const bytes = stat.kind === 'file' ? await runner.readFile(path) : undefined;\n return {\n gid: stat.gid,\n mode: stat.mode,\n path,\n sha256: bytes === undefined ? '' : sha256Hex(bytes),\n size: stat.size,\n uid: stat.uid,\n };\n};\n\nconst matches = (live: HostFileAttributes, want: Desired): boolean =>\n live.sha256 === want.sha256 &&\n live.mode === want.mode &&\n (want.uid === undefined || live.uid === want.uid) &&\n (want.gid === undefined || live.gid === want.gid);\n\nexport const diffFile = async (\n runner: HostRunner,\n news: HostFileProps,\n output: HostFileAttributes,\n): Promise<Diff> => {\n // ★ Create-before-delete: two paths can hold two files at once, so nothing forces deleteFirst.\n if (news.path !== output.path) return { action: 'replace' };\n const want = await desiredFile(runner, news);\n if (want.sha256 !== output.sha256) return { action: 'update' };\n const live = await readFileAttributes(runner, news.path);\n return live !== undefined && matches(live, want) ? { action: 'noop' } : { action: 'update' };\n};\n\nexport const reconcileFile = async (\n runner: HostRunner,\n props: HostFileProps,\n output?: HostFileAttributes,\n): Promise<HostFileAttributes> => {\n const want = await desiredFile(runner, props);\n // ⛔ NO SILENT SUDO: handing a file to another user is root's call (chown(2)), so say so up front.\n if (want.uid !== undefined && want.uid !== runner.effectiveUid() && !canActAsRoot(runner)) {\n throw refuse(\n props.path,\n `owner ${String(props.owner)} is not the deploying user; only root may chown. Deploy as root ` +\n 'or through a privileged HostRunner (privileged: true). This provider never calls sudo.',\n );\n }\n const stat = await runner.stat(props.path);\n if (stat !== undefined && stat.kind !== 'file') {\n throw refuse(\n props.path,\n `is a ${stat.kind}; remove it from whatever owns it before declaring it here`,\n );\n }\n const before = stat === undefined ? undefined : await readFileAttributes(runner, props.path);\n // ⚠️ An `output` at another path: the engine planned an UPDATE across a move, which it does\n // when diff could not see the new path (host-file.ts). Finish it the way a replace would.\n const moved = output !== undefined && output.path !== props.path;\n const prior = moved ? undefined : output;\n /**\n * ⛔ A FILE THIS RESOURCE DOES NOT OWN IS NEVER OVERWRITTEN. With no prior state for this path,\n * the engine's adoption probe has already refused anything it found — except at the new path of\n * a replace, where it reads nothing. A path typo there would otherwise overwrite, say, a system\n * file that a fresh declaration of the same path would have been refused as `Unowned`.\n */\n if (prior === undefined && before !== undefined && !matches(before, want)) {\n throw refuse(\n props.path,\n 'already exists and is not this resource. Remove it, or declare it as a new resource and ' +\n 'deploy with --adopt.',\n );\n }\n if (before === undefined || !matches(before, want)) {\n await runner.writeFileAtomic(props.path, want.bytes, {\n mode: want.mode,\n ...(want.uid === undefined ? {} : { uid: want.uid }),\n ...(want.gid === undefined ? {} : { gid: want.gid }),\n });\n }\n // ⚠️ READ BACK, never echo the declaration: a umask, an ACL or a runner that ignored `uid` shows\n // up here as a refusal instead of as a forever-`update`.\n const after = await readFileAttributes(runner, props.path);\n if (after === undefined || !matches(after, want)) {\n // ⚠️ Roll back a CREATE: left behind, the next plan's recovery `read` finds a file with no\n // state, reports it `Unowned`, and every later deploy demands --adopt for our own file.\n if (before === undefined) await runner.removeFile(props.path).catch(() => undefined);\n throw refuse(\n props.path,\n 'the write returned but the file on disk does not match the declaration',\n );\n }\n // ★ Create-before-delete, as the replace would have been: the old path goes only once the new\n // one is written and verified.\n if (moved) await deleteFile(runner, output);\n return after;\n};\n\n/** Remove the file. Idempotent. ⛔ Refuses a path that has since become a symlink or directory. */\nexport const deleteFile = async (runner: HostRunner, output: HostFileAttributes): Promise<void> => {\n const stat = await runner.stat(output.path);\n if (stat === undefined) return;\n if (stat.kind !== 'file') throw refuse(output.path, `is now a ${stat.kind}; not removing it`);\n await runner.removeFile(output.path);\n};\n",
13
+ "/**\n * `Launchd.Job` — one launchd job (a LaunchDaemon in `system`, a LaunchAgent in `gui/<uid>`),\n * declared rather than hand-loaded.\n *\n * ★ launchd HAS NO SDK. The provider renders the plist (plist.ts), writes it where launchd looks\n * (job-form.ts plistPathFor), and drives `launchctl` (launchctl.ts) — all through the HostRunner\n * the stack provides (runner.ts), so none of it is reachable except through that seam.\n *\n * Lifecycle (job-lifecycle.ts has the reasoning for each step):\n * - create / update — write the plist atomically, boot out the old job if loaded, bootstrap.\n * An update is therefore a RESTART; a converged job is left alone.\n * - read — `launchctl print` (loaded? pid? last exit?) plus the plist's SHA-256.\n * - diff — the rendered plist's SHA-256 against the stored AND the on-disk digest.\n * - replace — only when `label` or `domain` changes, and DELETE FIRST (labels are unique); so\n * everything the new job would be refused for is refused at plan time (job-preflight.ts).\n * - delete — boot out, remove the plist.\n *\n * ⛔ ENVIRONMENT IS NON-SECRET ONLY — see the ⛔ on LaunchdJobProps.environment.\n * ⛔ NO SILENT SUDO — see assertMayWrite in job-preflight.ts.\n * ⚠️ NOTHING IS ADOPTED WITHOUT `--adopt`. A label already loaded, or a plist already on disk, reads\n * as `Unowned`, so Alchemy refuses to take it over unless asked. Labels under `org.nixos.`,\n * `com.apple.` and `homebrew.mxcl.` are refused outright (job-validate.ts) — declare a new label\n * and cut over instead (docs/launchd.md).\n */\nimport { Resource } from 'alchemy';\nimport { Unowned } from 'alchemy/AdoptPolicy';\nimport { isResolved } from 'alchemy/Diff';\nimport * as Provider from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport { lift, resolvedString } from './host-effect.ts';\nimport type { LaunchdJobAttributes, LaunchdJobProps } from './job-form.ts';\nimport { deleteJob, diffJob, readJob, reconcileJob, replaceDiff } from './job-lifecycle.ts';\nimport { HostRunnerService } from './runner.ts';\n\nexport type {\n CalendarInterval,\n KeepAlive,\n LaunchdDomain,\n LaunchdJobAttributes,\n LaunchdJobProps,\n} from './job-form.ts';\n\nexport interface LaunchdJob extends Resource<\n 'Launchd.Job',\n LaunchdJobProps,\n LaunchdJobAttributes\n> {}\n\nexport const LaunchdJob = Resource<LaunchdJob>('Launchd.Job');\n\nexport const LaunchdJobProvider = () =>\n Provider.effect(\n LaunchdJob,\n Effect.gen(function* () {\n const runner = yield* HostRunnerService;\n return LaunchdJob.Provider.of({\n /**\n * ⛔ launchd's job list is not a list of things this stack owns — it is every Apple, Nix\n * and hand-loaded job on the host. Returning them would invite a nuke to boot them out.\n */\n list: () => Effect.succeed([]),\n\n read: ({ olds, output }) =>\n lift(async () => {\n const found = await readJob(runner, olds);\n if (found === undefined) return undefined;\n return output === undefined ? Unowned(found) : found;\n }),\n\n diff: ({ news, output }) => {\n if (output === undefined) return Effect.succeed(undefined);\n if (isResolved(news)) return lift(() => diffJob(runner, news, output));\n /**\n * ⛔ A RENAME MUST BE SEEN EVEN WHILE OTHER PROPS ARE UNRESOLVED. Returning undefined\n * hands the call to the engine's default — `update` for any prop change (Plan.ts,\n * havePropsChanged) — and an update under a new label bootstraps the new job and\n * orphans the old one, still running. `config.path` in programArguments (the pattern\n * docs/launchd.md recommends) is unresolved exactly when its HostFile changes in the\n * same deploy. reconcileJob also defends against this, for a label that is itself an\n * Output; seeing it here is what gets it planned as the replace it is.\n */\n const label = resolvedString(news, 'label');\n const domain = resolvedString(news, 'domain') as LaunchdJobProps['domain'] | undefined;\n return label !== undefined &&\n domain !== undefined &&\n (label !== output.label || domain !== output.domain)\n ? lift(() => replaceDiff(runner, { domain, label }, undefined, output))\n : Effect.succeed(undefined);\n },\n\n reconcile: ({ news, output }) => lift(() => reconcileJob(runner, news, output)),\n\n delete: ({ output }) => lift(() => deleteJob(runner, output)),\n });\n }),\n );\n",
14
+ "/**\n * Everything LaunchdJob refuses before it touches the host. Run at plan time (diff) and again at\n * reconcile, so a bad declaration fails the plan instead of half-applying.\n *\n * ★ REFUSE WHAT launchd WOULD SILENTLY IGNORE OR MISREAD. launchd loads a plist with an out-of-range\n * calendar field or a UserName on an agent without complaint and then simply never does what the\n * declaration says; a plan that fails is cheaper than a job that quietly does nothing.\n */\nimport {\n type CalendarInterval,\n type LaunchdJobProps,\n OWNED_KEYS,\n parseDomain,\n} from './job-form.ts';\nimport { jobSecretProblems } from './secret-tripwire.ts';\n\n/** ★ Labels become file names: no `/`, no leading dot, nothing a path could re-interpret. */\nconst LABEL = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;\n\n/**\n * ⛔ LABELS OTHER TOOLS OWN ARE NEVER DECLARED HERE. nix-darwin (`org.nixos.*`) rewrites its plists\n * on every activation and unloads any it no longer lists; macOS owns `com.apple.*`; `brew\n * services` owns `homebrew.mxcl.*`. Declaring one of those makes two tools fight over one file.\n * To move a job off such a tool, declare a NEW label and cut over (docs/launchd.md).\n */\nexport const RESERVED_LABEL_PREFIXES: readonly string[] = [\n 'org.nixos.',\n 'com.apple.',\n 'homebrew.mxcl.',\n];\n\nconst ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\nconst RANGES: ReadonlyArray<readonly [keyof CalendarInterval, number, number]> = [\n ['minute', 0, 59],\n ['hour', 0, 23],\n ['day', 1, 31],\n ['weekday', 0, 7],\n ['month', 1, 12],\n];\n\nconst isInt = (value: number, min: number, max = Number.MAX_SAFE_INTEGER) =>\n Number.isSafeInteger(value) && value >= min && value <= max;\n\nconst calendarProblems = (entries: readonly CalendarInterval[]): string[] => {\n if (entries.length === 0) return ['startCalendarInterval is an empty list; omit it instead'];\n const found: string[] = [];\n for (const [index, entry] of entries.entries()) {\n for (const [field, min, max] of RANGES) {\n const value = entry[field];\n if (value !== undefined && !isInt(value, min, max)) {\n found.push(\n `startCalendarInterval[${String(index)}].${field} must be ${String(min)}–${String(max)}`,\n );\n }\n }\n }\n return found;\n};\n\nconst absolute = (name: string, value: string | undefined): string[] =>\n value === undefined || value.startsWith('/') ? [] : [`${name} must be an absolute path`];\n\n/**\n * The label and domain alone — the job's identity. ★ Split out because a rename is a delete-first\n * replace, and it must be refusable at plan time even while other props are still unresolved\n * Outputs (see job.ts diff): otherwise the old job is deleted and only then is the new label\n * found to be invalid.\n */\nexport const identityProblems = (label: string, domainText: string): string[] => {\n const found: string[] = [];\n if (!LABEL.test(label)) {\n found.push('label must be 1–200 of [A-Za-z0-9._-], starting with a letter or digit');\n }\n const reserved = RESERVED_LABEL_PREFIXES.find((prefix) => label.startsWith(prefix));\n if (reserved !== undefined) {\n found.push(`label ${label} is under ${reserved}, which another tool owns; declare a new label`);\n }\n if (parseDomain(domainText) === undefined)\n found.push(`domain must be 'system' or 'gui/<uid>', not ${domainText}`);\n return found;\n};\n\nexport const jobProblems = (props: LaunchdJobProps): string[] => {\n const found = identityProblems(props.label, props.domain);\n const domain = parseDomain(props.domain);\n\n const [program] = props.programArguments;\n // ★ Absolute, because launchd's PATH for a job is not the deploying shell's PATH.\n if (program === undefined) found.push('programArguments must name a program');\n else if (!program.startsWith('/')) found.push('programArguments[0] must be an absolute path');\n\n for (const name of Object.keys(props.environment ?? {})) {\n if (!ENV_NAME.test(name))\n found.push(`environment name ${JSON.stringify(name)} is not a valid variable name`);\n }\n found.push(...jobSecretProblems(props.environment, props.programArguments, props.extraKeys));\n\n if (props.startInterval !== undefined && !isInt(props.startInterval, 1)) {\n found.push('startInterval must be a positive whole number of seconds');\n }\n if (props.throttleInterval !== undefined && !isInt(props.throttleInterval, 0)) {\n found.push('throttleInterval must be a non-negative whole number of seconds');\n }\n const calendar = props.startCalendarInterval;\n if (calendar !== undefined) {\n found.push(\n ...calendarProblems(Array.isArray(calendar) ? calendar : [calendar as CalendarInterval]),\n );\n }\n const keepAlive = props.keepAlive;\n if (\n typeof keepAlive === 'object' &&\n keepAlive.successfulExit === undefined &&\n keepAlive.crashed === undefined\n ) {\n found.push('keepAlive {} names no condition; use true, false, or a condition');\n }\n\n found.push(...absolute('standardOutPath', props.standardOutPath));\n found.push(...absolute('standardErrorPath', props.standardErrorPath));\n found.push(...absolute('workingDirectory', props.workingDirectory));\n\n if (domain?.kind === 'gui' && (props.userName !== undefined || props.groupName !== undefined)) {\n found.push(\n 'userName/groupName apply only to the system domain; launchd ignores them for agents',\n );\n }\n for (const key of Object.keys(props.extraKeys ?? {})) {\n if (OWNED_KEYS.includes(key))\n found.push(`extraKeys.${key} is owned by a typed prop; use that instead`);\n }\n return found;\n};\n",
15
+ "/**\n * The four `launchctl` subcommands LaunchdJob uses — print, print-disabled, bootstrap, bootout —\n * over a HostRunner, plus the parsers for the two read-only ones.\n *\n * ★ MEASURED 2026-09-21 ON macOS 27.2 (read-only `print` / `print-disabled` only; nothing was\n * bootstrapped or booted out to write this):\n * - `print system/<unknown>` exits **113** with \"Could not find service … in domain for system\"\n * on stderr. 113 is therefore \"not loaded\", not an error.\n * - `print` of a loaded job is a `<target> = {` block. Its own fields sit one tab deep\n * (`\\tstate = running`, `\\tpid = 1219`, `\\tlast exit code = 0` or `(never exited)`); nested\n * blocks (`resource coalition`, `semaphores`) repeat names like `state` two tabs deep.\n * - `print-disabled <domain>` lists `\"label\" => enabled|disabled`.\n * - reading needs no root: a uid-501 shell printed system-domain jobs.\n * - `print gui/<uid>/<label>` for a uid with NO LOGIN SESSION exits **112** with \"Could not find\n * domain for user gui: <uid>\" (and so does `print-disabled gui/<uid>`); a malformed target is\n * 64, a missing service in a live gui domain is 113. So 112 + that text is \"the domain does\n * not exist\", which means the job cannot be loaded — not an error for a read or a delete.\n * ⚠️ REASONED, NOT MEASURED: bootstrap/bootout exit codes. nix-darwin's own activation script on the\n * same host notes \"`bootout` on an already-absent job is a no-op that returns non-zero\", which is\n * why bootout here is always preceded by a print and followed by a poll, never trusted alone.\n * ⛔ NEVER PUT `print` STDOUT IN AN ERROR. It contains the job's whole environment.\n */\nimport type { HostRunner } from './runner.ts';\n\nexport const LAUNCHCTL = '/bin/launchctl';\n/** `launchctl print` of an unknown service — measured, see above. */\nexport const NOT_FOUND = 113;\n/** `launchctl print` into a gui domain with no login session — measured, see above. */\nexport const NO_DOMAIN = 112;\nconst NO_DOMAIN_TEXT = 'Could not find domain';\n\nexport class LaunchctlError extends Error {\n constructor(command: string, exitCode: number, stderr: string) {\n super(`launchctl ${command} -> ${String(exitCode)}: ${stderr.trim().slice(0, 300)}`);\n this.name = 'LaunchctlError';\n }\n}\n\nexport type ServiceStatus = {\n readonly loaded: boolean;\n /** The domain itself is absent (a gui/<uid> with no login session). Implies `loaded: false`. */\n readonly domainMissing?: true;\n readonly state?: string;\n readonly pid?: number;\n readonly lastExitCode?: number;\n};\n\n/**\n * The job block's own `key = value` lines, ignoring nested blocks.\n * ⚠️ A multi-line ProgramArguments entry is printed raw inside `arguments = {`, so a close is only\n * accepted when its indentation matches the open. An argument that itself contains a line of\n * exactly one tab and `}` would still desynchronise this; no real job has one.\n */\nexport const printFields = (stdout: string): Map<string, string> => {\n const fields = new Map<string, string>();\n const closers: string[] = [];\n for (const line of stdout.split('\\n')) {\n if (closers.length === 0) {\n if (fields.size === 0 && line.endsWith(' = {')) closers.push('}');\n continue;\n }\n if (line === closers[closers.length - 1]) {\n closers.pop();\n continue;\n }\n const opened = /^(\\t+)\\S.* = \\{$/.exec(line);\n if (opened?.[1] !== undefined && opened[1].length === closers.length) {\n closers.push(`${opened[1]}}`);\n continue;\n }\n const field = closers.length === 1 ? /^\\t([a-z][a-z ]*[a-z]) = (.*)$/.exec(line) : null;\n if (field?.[1] !== undefined && field[2] !== undefined && !fields.has(field[1])) {\n fields.set(field[1], field[2]);\n }\n }\n return fields;\n};\n\nconst leadingInt = (text: string | undefined): number | undefined => {\n const match = text === undefined ? null : /^(-?\\d+)/.exec(text);\n return match?.[1] === undefined ? undefined : Number(match[1]);\n};\n\nexport const parsePrint = (stdout: string): ServiceStatus => {\n const fields = printFields(stdout);\n const state = fields.get('state');\n const pid = leadingInt(fields.get('pid'));\n // \"(never exited)\" parses to undefined; \"78: EX_CONFIG\" to 78.\n const lastExitCode = leadingInt(fields.get('last exit code'));\n return {\n loaded: true,\n ...(state === undefined ? {} : { state }),\n ...(pid === undefined ? {} : { pid }),\n ...(lastExitCode === undefined ? {} : { lastExitCode }),\n };\n};\n\n/** Labels `print-disabled` reports as disabled. Older releases printed `=> true` for disabled. */\nexport const parseDisabled = (stdout: string): Set<string> => {\n const disabled = new Set<string>();\n for (const match of stdout.matchAll(/^\\s*\"([^\"]+)\" => (\\w+)\\s*$/gm)) {\n if (match[1] !== undefined && (match[2] === 'disabled' || match[2] === 'true')) {\n disabled.add(match[1]);\n }\n }\n return disabled;\n};\n\nexport const printService = async (runner: HostRunner, target: string): Promise<ServiceStatus> => {\n const result = await runner.exec([LAUNCHCTL, 'print', target]);\n if (result.exitCode === NOT_FOUND) return { loaded: false };\n // ⚠️ Without this, deleting an agent while its user is logged out fails on the read, and the\n // plist stays in LaunchAgents — where launchd loads it again at that user's next login.\n if (result.exitCode === NO_DOMAIN && result.stderr.includes(NO_DOMAIN_TEXT)) {\n return { domainMissing: true, loaded: false };\n }\n if (result.exitCode !== 0)\n throw new LaunchctlError(`print ${target}`, result.exitCode, result.stderr);\n return parsePrint(result.stdout);\n};\n\nexport const isDisabled = async (\n runner: HostRunner,\n domain: string,\n label: string,\n): Promise<boolean> => {\n const result = await runner.exec([LAUNCHCTL, 'print-disabled', domain]);\n if (result.exitCode !== 0) {\n throw new LaunchctlError(`print-disabled ${domain}`, result.exitCode, result.stderr);\n }\n return parseDisabled(result.stdout).has(label);\n};\n\nexport const bootstrap = async (\n runner: HostRunner,\n domain: string,\n plistPath: string,\n): Promise<void> => {\n const result = await runner.exec([LAUNCHCTL, 'bootstrap', domain, plistPath]);\n if (result.exitCode !== 0) {\n throw new LaunchctlError(`bootstrap ${domain} ${plistPath}`, result.exitCode, result.stderr);\n }\n};\n\n/**\n * Poll `print` until the service is gone. ⚠️ bootout can return before launchd lets go of it.\n * ★ 30 s, because a job gets SIGTERM and then its ExitTimeOut before SIGKILL: launchd.plist(5)\n * documents a 20 s default (a macOS 27.2 `print` showed `exit timeout = 5` for a job that sets\n * none). Giving up sooner would fail a deploy over a job that was, correctly, still exiting.\n */\nexport const waitUnloaded = async (\n runner: HostRunner,\n target: string,\n attempts = 120,\n intervalMs = 250,\n): Promise<void> => {\n for (let attempt = 0; attempt < attempts; attempt += 1) {\n if (!(await printService(runner, target)).loaded) return;\n await runner.sleep(intervalMs);\n }\n throw new Error(\n `${target} is still loaded ${String((attempts * intervalMs) / 1000)}s after bootout`,\n );\n};\n\n/** Boot a service out if it is loaded, and wait until launchd has let go. Idempotent. */\nexport const bootoutIfLoaded = async (runner: HostRunner, target: string): Promise<void> => {\n if (!(await printService(runner, target)).loaded) return;\n const result = await runner.exec([LAUNCHCTL, 'bootout', target]);\n /**\n * ⚠️ A non-zero bootout is only an error if the service is STILL there after the wait (see\n * header) — so wait FIRST, then judge. A job slower to exit than launchctl's own wait is widely\n * reported to fail bootout with `36: Operation now in progress` (EINPROGRESS) while it is\n * still going away; REASONED from those reports, not measured here. Judging on an immediate\n * print would fail the deploy with the new plist written and the old job already exiting —\n * the job ends up down either way, and the error would blame the wrong step.\n */\n try {\n await waitUnloaded(runner, target);\n } catch (cause) {\n if (result.exitCode === 0) throw cause;\n throw new LaunchctlError(\n `bootout ${target}`,\n result.exitCode,\n `${result.stderr.trim()} (and still loaded after waiting)`,\n );\n }\n};\n",
16
+ "/**\n * Everything LaunchdJob checks before it touches the host — split from job-lifecycle.ts so that a\n * reconcile and a plan-time REPLACE run the very same checks.\n *\n * ★ WHY A REPLACE IS CHECKED AT PLAN TIME. A label or domain change is a delete-first replace, and\n * Alchemy runs `delete` on the old job BEFORE `reconcile` on the new one (Apply.ts,\n * deleteOldGenerations under `node.deleteFirst`; read in alchemy 2.0.0-beta.79 on 2026-09-21).\n * A refusal that fired only in reconcile would therefore arrive after the old job was already\n * booted out: nothing running, and a plan that had promised a clean swap.\n * ★ EVERY CHECK HERE IS READ-ONLY: validation, a user lookup, `launchctl print` and\n * `print-disabled` — none of which needs root (measured, see launchctl.ts).\n */\nimport {\n type LaunchdJobAttributes,\n type LaunchdJobProps,\n type ParsedDomain,\n parseDomain,\n plistPathFor,\n serviceTarget,\n sha256Hex,\n} from './job-form.ts';\nimport { identityProblems, jobProblems } from './job-validate.ts';\nimport { type ServiceStatus, isDisabled, printService } from './launchctl.ts';\nimport { type HostRunner, canActAsRoot } from './runner.ts';\n\nexport type Location = {\n readonly domain: ParsedDomain;\n readonly plistPath: string;\n readonly target: string;\n};\n\nexport type Identity = Pick<LaunchdJobProps, 'label' | 'domain'>;\n\nexport const refuse = (label: string, message: string): Error =>\n new Error(`Launchd.Job ${label}: ${message}`);\n\n/** Throw every refusal at once, so one plan shows the whole list. */\nexport const assertValid = (props: LaunchdJobProps): void => {\n const found = jobProblems(props);\n if (found.length > 0) throw refuse(props.label, found.join('; '));\n};\n\nexport const locate = async (runner: HostRunner, props: Identity): Promise<Location> => {\n const domain = parseDomain(props.domain);\n if (domain === undefined) throw refuse(props.label, `unknown domain ${props.domain}`);\n const home =\n domain.kind === 'gui' ? (await runner.lookupUser(String(domain.uid)))?.home : undefined;\n return {\n domain,\n plistPath: plistPathFor(props.label, domain, home),\n target: serviceTarget(props.label, props.domain),\n };\n};\n\n/**\n * ⛔ NO SILENT SUDO. The system domain, and another user's gui domain, need root. Refuse up front\n * with the two ways out, rather than let launchctl or rename(2) fail halfway through.\n */\nexport const assertMayWrite = (runner: HostRunner, label: string, domain: ParsedDomain): void => {\n if (canActAsRoot(runner)) return;\n if (domain.kind === 'system') {\n throw refuse(\n label,\n 'the system domain needs root. Run the deploy as root, or provide a HostRunner that is ' +\n 'deliberately privileged (privileged: true). This provider never calls sudo.',\n );\n }\n if (domain.uid !== runner.effectiveUid()) {\n throw refuse(\n label,\n `gui/${String(domain.uid)} belongs to another user; only root may write it. ` +\n 'Deploy as that user, as root, or through a privileged HostRunner.',\n );\n }\n};\n\n/** Where the job goes, whether we may write it there, and launchd's current view of it. */\nexport const preflight = async (\n runner: HostRunner,\n props: Identity,\n): Promise<{ location: Location; status: ServiceStatus }> => {\n const location = await locate(runner, props);\n assertMayWrite(runner, props.label, location.domain);\n const status = await printService(runner, location.target);\n if (status.domainMissing === true) {\n throw refuse(\n props.label,\n `${props.domain} does not exist: that user has no login session, and launchd creates a gui ` +\n 'domain only at login. Log the user in (or declare a system-domain job with userName), ' +\n 'then redeploy. Nothing was written.',\n );\n }\n /**\n * ⛔ A DISABLED LABEL IS SOMEONE'S DECISION, NOT DRIFT. `launchctl disable` persists across boots\n * and makes bootstrap fail; enabling it here would silently overrule whoever disabled it.\n */\n if (await isDisabled(runner, props.domain, props.label)) {\n throw refuse(\n props.label,\n `${location.target} is disabled (launchctl print-disabled ${props.domain}). ` +\n `If that is stale, run \\`launchctl enable ${location.target}\\` deliberately, then redeploy.`,\n );\n }\n return { location, status };\n};\n\n/**\n * ⛔ A JOB THIS RESOURCE DOES NOT OWN IS NEVER OVERWRITTEN. Alchemy's adoption probe guards a\n * create, but NOT the new label of a replace — the engine reads nothing there — so a rename onto\n * a label someone already loaded would boot their job out and write ours over its plist.\n * ★ A plist byte-identical to our render is exempt: it is this declaration's own leftover (a\n * deploy that died between write and bootstrap), and rewriting it changes nothing.\n */\nexport const assertUnclaimed = async (\n runner: HostRunner,\n label: string,\n location: Location,\n status: ServiceStatus,\n renderedSha256: string | undefined,\n): Promise<void> => {\n const bytes = await runner.readFile(location.plistPath);\n const foreignFile = bytes !== undefined && sha256Hex(bytes) !== renderedSha256;\n if (foreignFile || (bytes === undefined && status.loaded)) {\n throw refuse(\n label,\n `${location.target} is already on this host (${foreignFile ? location.plistPath : 'loaded'})` +\n ' and is not this resource. Remove it, or declare it as a new resource and deploy with ' +\n '--adopt.',\n );\n }\n};\n\n/**\n * A rename, checked at plan time: the new identity must be valid, writable, bootstrappable and\n * free, and the old one deletable — before the plan may promise a delete-first replace.\n * `full` is the whole declaration when it is already resolved; otherwise only the identity is.\n */\nexport const assertReplaceable = async (\n runner: HostRunner,\n next: Identity,\n full: LaunchdJobProps | undefined,\n old: LaunchdJobAttributes,\n renderedSha256: string | undefined,\n): Promise<void> => {\n const found = full === undefined ? identityProblems(next.label, next.domain) : jobProblems(full);\n if (found.length > 0) throw refuse(next.label, found.join('; '));\n const oldDomain = parseDomain(old.domain);\n if (oldDomain === undefined) throw refuse(old.label, `unknown domain ${old.domain}`);\n assertMayWrite(runner, old.label, oldDomain);\n const { location, status } = await preflight(runner, next);\n await assertUnclaimed(runner, next.label, location, status, renderedSha256);\n};\n",
17
+ "/**\n * LaunchdJob's read / diff / reconcile / delete as plain async functions over a HostRunner — split\n * from job.ts so the whole lifecycle runs against fake-runner.ts in tests, with no real launchd.\n * The checks made before any write live in job-preflight.ts.\n *\n * ★ THE ORDER IS WRITE, BOOTOUT, BOOTSTRAP — not bootout first. launchd reads the plist only at\n * bootstrap, so writing the new file while the old job runs changes nothing yet, and a write that\n * fails (disk full, EACCES) leaves the old job running instead of leaving nothing running.\n * ⚠️ A FAILED BOOTSTRAP LEAVES THE JOB DOWN. On an UPDATE the new plist stays on disk, the state\n * keeps the previous digest, and the next deploy retries. On a CREATE the plist this deploy wrote\n * is removed again: left behind, the next plan's recovery `read` would find a plist with no\n * state and report it `Unowned`, and every later deploy would demand `--adopt` for our own file.\n * There is no rollback to an older plist: one that launchd refuses is a declaration to fix, and\n * re-bootstrapping the old one could fail for the same host-side reason.\n */\nimport type { Diff } from 'alchemy/Diff';\nimport {\n type LaunchdJobAttributes,\n type LaunchdJobProps,\n parseDomain,\n renderJob,\n sha256Hex,\n} from './job-form.ts';\nimport {\n type Identity,\n type Location,\n assertMayWrite,\n assertReplaceable,\n assertUnclaimed,\n assertValid,\n locate,\n preflight,\n refuse,\n} from './job-preflight.ts';\nimport { type ServiceStatus, bootoutIfLoaded, bootstrap, printService } from './launchctl.ts';\nimport type { HostRunner } from './runner.ts';\n\nconst attributesOf = (\n props: Identity,\n location: Location,\n plistSha256: string,\n status: ServiceStatus,\n): LaunchdJobAttributes => ({\n domain: props.domain,\n label: props.label,\n loaded: status.loaded,\n plistPath: location.plistPath,\n plistSha256,\n serviceTarget: location.target,\n ...(status.state === undefined ? {} : { state: status.state }),\n ...(status.pid === undefined ? {} : { pid: status.pid }),\n ...(status.lastExitCode === undefined ? {} : { lastExitCode: status.lastExitCode }),\n});\n\n/** What is on the host now: the plist's digest and launchd's view. `undefined` when neither exists. */\nexport const readJob = async (\n runner: HostRunner,\n props: Identity,\n): Promise<LaunchdJobAttributes | undefined> => {\n const location = await locate(runner, props);\n const bytes = await runner.readFile(location.plistPath);\n const status = await printService(runner, location.target);\n if (bytes === undefined && !status.loaded) return undefined;\n return attributesOf(props, location, bytes === undefined ? '' : sha256Hex(bytes), status);\n};\n\n/**\n * ★ `deleteFirst`: labels are unique per domain, and the old and new job would otherwise run the\n * same program side by side — same port, same files — until the old one is deleted. A brief\n * outage beats two copies. ⛔ Which is why everything the new job's reconcile would refuse is\n * refused HERE, at plan time (job-preflight.ts): after the delete it would be too late.\n * `full` is the whole declaration when resolved; job.ts passes only the identity otherwise.\n */\nexport const replaceDiff = async (\n runner: HostRunner,\n next: Identity,\n full: LaunchdJobProps | undefined,\n output: LaunchdJobAttributes,\n): Promise<Diff> => {\n // ★ Validate before rendering, so a bad declaration shows its whole list, not a render error.\n if (full !== undefined) assertValid(full);\n const rendered = full === undefined ? undefined : renderJob(full).sha256;\n await assertReplaceable(runner, next, full, output, rendered);\n return { action: 'replace', deleteFirst: true };\n};\n\nexport const diffJob = async (\n runner: HostRunner,\n news: LaunchdJobProps,\n output: LaunchdJobAttributes,\n): Promise<Diff> => {\n if (news.label !== output.label || news.domain !== output.domain) {\n return replaceDiff(runner, news, news, output);\n }\n assertValid(news);\n const desired = renderJob(news).sha256;\n // ⛔ BOTH digests: the stored one catches a bootstrap that failed after the write landed, the live\n // one catches a hand edit. Either alone would report `noop` over a job that is not what we say.\n if (desired !== output.plistSha256) return { action: 'update' };\n const live = await readJob(runner, news);\n if (live === undefined || !live.loaded || live.plistSha256 !== desired)\n return { action: 'update' };\n return { action: 'noop' };\n};\n\nexport const reconcileJob = async (\n runner: HostRunner,\n props: LaunchdJobProps,\n output: LaunchdJobAttributes | undefined,\n): Promise<LaunchdJobAttributes> => {\n assertValid(props);\n const { location, status } = await preflight(runner, props);\n const rendered = renderJob(props);\n /**\n * ⚠️ AN `output` UNDER ANOTHER LABEL OR DOMAIN means the engine planned an UPDATE across a rename —\n * which it does whenever diff could not see the rename (a label that was itself an unresolved\n * Output at plan time; job.ts). Bootstrapping the new label alone would orphan the old job,\n * still running, with nothing left in state to ever remove it. So do what the replace would\n * have done: every check above first, then delete the old job, then create the new one.\n */\n const renamed =\n output !== undefined && (output.label !== props.label || output.domain !== props.domain);\n const prior = renamed ? undefined : output;\n // ⛔ No prior state for THIS identity: whatever is there already belongs to someone else.\n if (prior === undefined) {\n await assertUnclaimed(runner, props.label, location, status, rendered.sha256);\n }\n if (renamed) await deleteJob(runner, output);\n const before = await runner.readFile(location.plistPath);\n // ★ Already converged (a retried deploy, or an adopted job that matches): do not restart it.\n // ⛔ The STORED digest is part of the test: a write that landed before a failed bootout leaves\n // the new plist on disk and the OLD job loaded — both look right, and only state knows better.\n if (\n status.loaded &&\n before !== undefined &&\n sha256Hex(before) === rendered.sha256 &&\n prior?.plistSha256 === rendered.sha256\n ) {\n return attributesOf(props, location, rendered.sha256, status);\n }\n /**\n * ⚠️ launchd refuses a daemon plist that is not root:wheel or is group/world-writable (\"bad\n * ownership/permissions\"), so the system domain always writes 0644 root:wheel. An agent's plist\n * belongs to its user. REASONED from launchd's documented behaviour, not measured here (no\n * bootstrap was run to write this); the nix-darwin daemons on the reference host are 0644 root.\n */\n await runner.writeFileAtomic(\n location.plistPath,\n rendered.bytes,\n location.domain.kind === 'system'\n ? { gid: 0, mode: 0o644, uid: 0 }\n : { mode: 0o644, uid: location.domain.uid },\n );\n await bootoutIfLoaded(runner, location.target);\n const failed = async (detail: string): Promise<never> => {\n // ⚠️ A failed removal must not replace the bootstrap error, nor let the message claim it worked.\n const removed =\n before === undefined &&\n (await runner.removeFile(location.plistPath).then(\n () => true,\n () => false,\n ));\n throw refuse(\n props.label,\n removed\n ? `bootstrap failed, so the job is NOT RUNNING; the plist this deploy wrote was removed ` +\n `again, so the next deploy starts clean. ${detail}`\n : `bootstrap failed, so the job is NOT RUNNING; the new plist is at ${location.plistPath} ` +\n `and the next deploy retries. ${detail}`,\n );\n };\n try {\n await bootstrap(runner, props.domain, location.plistPath);\n } catch (cause) {\n return failed(cause instanceof Error ? cause.message : String(cause));\n }\n const after = await printService(runner, location.target);\n if (!after.loaded) return failed('bootstrap exited 0 but launchctl print cannot find the job');\n return attributesOf(props, location, rendered.sha256, after);\n};\n\n/** Boot out, then remove the plist. Idempotent: an absent job and an absent file are success. */\nexport const deleteJob = async (\n runner: HostRunner,\n output: LaunchdJobAttributes,\n): Promise<void> => {\n const domain = parseDomain(output.domain);\n if (domain === undefined) throw refuse(output.label, `unknown domain ${output.domain}`);\n assertMayWrite(runner, output.label, domain);\n // ★ A gui domain with no login session reads as \"not loaded\" (launchctl.ts), so the plist is\n // still removed — which is what stops launchd loading it again at that user's next login.\n await bootoutIfLoaded(runner, output.serviceTarget);\n await runner.removeFile(output.plistPath);\n};\n",
18
+ "/**\n * The real HostRunner: this process, this machine, this uid.\n *\n * ★ node:fs AND node:child_process, NOT Bun APIs. An Alchemy stack usually runs under bun, but\n * the published dist also loads under node (the package's smoke test and the kit's AGENTS.md:\n * consumers run dist/, not src/), and both runtimes implement these modules.\n * ⛔ NO ELEVATION, EVER. `privileged` is false; a system-domain write succeeds only when the whole\n * deploy was started as root. See the ⛔ on HostRunner.privileged for why nothing here calls sudo.\n * ⛔ NO SHELL. `exec` spawns argv directly, so a label, path or argument can never be re-parsed\n * as shell syntax.\n */\nimport { spawn } from 'node:child_process';\nimport { randomBytes } from 'node:crypto';\nimport { lstat, open, readFile, rename, unlink } from 'node:fs/promises';\nimport { basename, dirname, join } from 'node:path';\nimport { groupQuery, parseGroup, parseUser, userQuery } from './host-lookup.ts';\nimport type { ExecResult, FileStat, HostRunner, WriteOptions } from './runner.ts';\n\nexport type LocalRunnerOptions = {\n /** Kill a spawned program after this long. @default 120_000 */\n readonly execTimeoutMs?: number;\n};\n\nconst code = (cause: unknown): string | undefined =>\n typeof cause === 'object' && cause !== null && 'code' in cause\n ? String((cause as { code: unknown }).code)\n : undefined;\n\n/** ENOENT and ENOTDIR both mean \"nothing is at this path\". */\nconst absent = (cause: unknown): boolean => code(cause) === 'ENOENT' || code(cause) === 'ENOTDIR';\n\nconst run = (argv: readonly string[], timeoutMs: number): Promise<ExecResult> =>\n new Promise((resolve, reject) => {\n const [program, ...args] = argv;\n if (program === undefined) {\n reject(new Error('exec: empty argv'));\n return;\n }\n const child = spawn(program, args, {\n shell: false,\n stdio: ['ignore', 'pipe', 'pipe'],\n timeout: timeoutMs,\n });\n const out: Buffer[] = [];\n const err: Buffer[] = [];\n child.stdout.on('data', (chunk: Buffer) => out.push(chunk));\n child.stderr.on('data', (chunk: Buffer) => err.push(chunk));\n child.on('error', (cause) => {\n reject(new Error(`exec ${program}: ${cause.message}`));\n });\n child.on('close', (exitCode, signal) => {\n const stderr = Buffer.concat(err).toString('utf8');\n resolve({\n // ⚠️ A signal (including our own timeout) has no exit code; -1 keeps it a failure.\n exitCode: exitCode ?? -1,\n stderr: signal === null ? stderr : `${stderr}\\n(killed by ${signal})`.trim(),\n stdout: Buffer.concat(out).toString('utf8'),\n });\n });\n });\n\nconst kindOf = (stats: Awaited<ReturnType<typeof lstat>>): FileStat['kind'] => {\n if (stats.isSymbolicLink()) return 'symlink';\n if (stats.isFile()) return 'file';\n if (stats.isDirectory()) return 'directory';\n return 'other';\n};\n\nconst writeAtomic = async (path: string, bytes: Uint8Array, options: WriteOptions) => {\n // ★ Same directory as the target, so rename(2) never crosses a filesystem (which would EXDEV).\n const temp = join(dirname(path), `.${basename(path)}.${randomBytes(6).toString('hex')}.tmp`);\n // ⛔ 'wx' = O_CREAT|O_EXCL, and 0600 until the final mode: the temp file is never readable by\n // anyone else in the moment before its mode is set, and never follows a planted symlink.\n const handle = await open(temp, 'wx', 0o600);\n try {\n try {\n await handle.writeFile(bytes);\n // ⚠️ chown BEFORE chmod: a chown by root clears setuid/setgid bits, so the other order\n // would silently drop a declared 4755.\n if (options.uid !== undefined || options.gid !== undefined) {\n await handle.chown(options.uid ?? -1, options.gid ?? -1);\n }\n await handle.chmod(options.mode);\n await handle.sync();\n } finally {\n await handle.close();\n }\n await rename(temp, path);\n } catch (cause) {\n await unlink(temp).catch(() => undefined);\n throw cause;\n }\n};\n\nexport const localRunner = (options: LocalRunnerOptions = {}): HostRunner => {\n const timeoutMs = options.execTimeoutMs ?? 120_000;\n const platform = process.platform;\n return {\n effectiveUid: () => process.geteuid?.() ?? -1,\n exec: (argv) => run(argv, timeoutMs),\n lookupGroup: async (nameOrId) => {\n const result = await run(groupQuery(platform, nameOrId), timeoutMs);\n return result.exitCode === 0 ? parseGroup(platform, result.stdout) : undefined;\n },\n lookupUser: async (nameOrId) => {\n const result = await run(userQuery(platform, nameOrId), timeoutMs);\n return result.exitCode === 0 ? parseUser(platform, result.stdout) : undefined;\n },\n privileged: false,\n readFile: async (path) => {\n try {\n return new Uint8Array(await readFile(path));\n } catch (cause) {\n if (absent(cause)) return undefined;\n throw cause;\n }\n },\n removeFile: async (path) => {\n try {\n await unlink(path);\n } catch (cause) {\n if (!absent(cause)) throw cause;\n }\n },\n sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\n stat: async (path) => {\n try {\n const stats = await lstat(path);\n return {\n gid: stats.gid,\n kind: kindOf(stats),\n mode: stats.mode & 0o7777,\n size: stats.size,\n uid: stats.uid,\n };\n } catch (cause) {\n if (absent(cause)) return undefined;\n throw cause;\n }\n },\n writeFileAtomic: writeAtomic,\n };\n};\n",
19
+ "/**\n * User and group lookups, as argv plus a parser — split out so the parsing is tested against the\n * real output shapes without running anything.\n *\n * ★ macOS FIRST, getent ELSEWHERE. macOS has no `getent`; its directory service answers through\n * `dscacheutil`. Shape MEASURED 2026-09-21 on macOS 27.2 (read-only query):\n *\n * name: someone\n * password: ********\n * uid: 501\n * gid: 20\n * dir: /Users/someone\n * shell: /bin/zsh\n *\n * An unknown uid printed NOTHING and exited 0 — so \"no record\" is an empty parse, never an\n * exit code. `getent` exits 2 for an unknown key; that also parses as no record.\n * ⛔ NEVER PARSE OR KEEP THE `password:` LINE. It is a placeholder on macOS, but a parser that\n * copies every field would carry a hash from any host where it is not.\n */\nimport type { HostUser } from './runner.ts';\n\nconst NUMERIC = /^\\d+$/;\n\n/** argv for looking up a user, by name or numeric id. */\nexport const userQuery = (platform: string, nameOrId: string): readonly string[] =>\n platform === 'darwin'\n ? [\n '/usr/bin/dscacheutil',\n '-q',\n 'user',\n '-a',\n NUMERIC.test(nameOrId) ? 'uid' : 'name',\n nameOrId,\n ]\n : ['getent', 'passwd', nameOrId];\n\n/** argv for looking up a group, by name or numeric id. */\nexport const groupQuery = (platform: string, nameOrId: string): readonly string[] =>\n platform === 'darwin'\n ? [\n '/usr/bin/dscacheutil',\n '-q',\n 'group',\n '-a',\n NUMERIC.test(nameOrId) ? 'gid' : 'name',\n nameOrId,\n ]\n : ['getent', 'group', nameOrId];\n\n/** The first `key: value` record in dscacheutil output, minus any password field. */\nconst firstRecord = (stdout: string): Map<string, string> => {\n const fields = new Map<string, string>();\n for (const line of stdout.split('\\n')) {\n if (line.trim() === '') {\n if (fields.size > 0) break; // ⚠️ records are blank-line separated; keep the first only\n continue;\n }\n const at = line.indexOf(': ');\n if (at <= 0) continue;\n const key = line.slice(0, at);\n if (key === 'password') continue;\n fields.set(key, line.slice(at + 2).trim());\n }\n return fields;\n};\n\nconst toId = (text: string | undefined): number | undefined =>\n text !== undefined && NUMERIC.test(text) ? Number(text) : undefined;\n\nexport const parseUser = (platform: string, stdout: string): HostUser | undefined => {\n if (platform === 'darwin') {\n const record = firstRecord(stdout);\n const uid = toId(record.get('uid'));\n const gid = toId(record.get('gid'));\n const home = record.get('dir');\n return uid === undefined || gid === undefined || home === undefined\n ? undefined\n : { gid, home, uid };\n }\n // getent passwd: name:x:uid:gid:gecos:home:shell\n const fields = (stdout.split('\\n')[0] ?? '').split(':');\n const uid = toId(fields[2]);\n const gid = toId(fields[3]);\n const home = fields[5];\n return uid === undefined || gid === undefined || home === undefined || home === ''\n ? undefined\n : { gid, home, uid };\n};\n\nexport const parseGroup = (platform: string, stdout: string): number | undefined => {\n if (platform === 'darwin') return toId(firstRecord(stdout).get('gid'));\n // getent group: name:x:gid:members\n return toId((stdout.split('\\n')[0] ?? '').split(':')[2]);\n};\n",
20
+ "/**\n * Both launchd-subpath providers with one HostRunner, as a single layer for a host stack.\n *\n * Layer.mergeAll(launchdProviders(), …the stack's other providers)\n *\n * ★ ONE RUNNER FOR BOTH, so a daemon's config file (HostFile) and the daemon (LaunchdJob) are\n * written by the same identity on the same host — a stack cannot write the file as one user and\n * bootstrap the job as another by accident.\n * ⚠️ The default is localRunner(): this process, never elevated. A system-domain job therefore\n * needs the deploy itself started as root, or a runner passed here with `privileged: true`.\n */\nimport * as Layer from 'effect/Layer';\nimport { HostFileProvider } from './host-file.ts';\nimport { LaunchdJobProvider } from './job.ts';\nimport { localRunner } from './local-runner.ts';\nimport { type HostRunner, hostRunnerLayer } from './runner.ts';\n\nexport const launchdProviders = (runner: HostRunner = localRunner()) =>\n Layer.mergeAll(LaunchdJobProvider(), HostFileProvider()).pipe(\n Layer.provide(hostRunnerLayer(runner)),\n );\n"
21
+ ],
22
+ "mappings": "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",
23
+ "debugId": "86750D103F13F7F764756E2164756E21",
24
+ "names": []
25
+ }
@@ -0,0 +1,90 @@
1
+ import { type PlistDict } from './plist.ts';
2
+ /** `system` (LaunchDaemons, root) or one user's login session, `gui/<uid>` (LaunchAgents). */
3
+ export type LaunchdDomain = 'system' | `gui/${number}`;
4
+ /** One StartCalendarInterval entry. An omitted field is a wildcard, as in cron. */
5
+ export type CalendarInterval = {
6
+ readonly minute?: number;
7
+ readonly hour?: number;
8
+ readonly day?: number;
9
+ /** 0 and 7 are both Sunday. */
10
+ readonly weekday?: number;
11
+ readonly month?: number;
12
+ };
13
+ /** `true` = always restart; the object form restarts only on the named conditions. */
14
+ export type KeepAlive = boolean | {
15
+ readonly successfulExit?: boolean;
16
+ readonly crashed?: boolean;
17
+ };
18
+ export interface LaunchdJobProps {
19
+ /** Reverse-DNS label. Unique per domain; also the plist's file name. */
20
+ label: string;
21
+ domain: LaunchdDomain;
22
+ /** argv; the first element must be an absolute path. */
23
+ programArguments: readonly string[];
24
+ /**
25
+ * ⛔ NON-SECRET VALUES ONLY. Props are stored unencrypted in Alchemy state, and these also land in
26
+ * the plist on disk and in `launchctl print`. Pass a secret as a FILE PATH that a secret renderer
27
+ * (openbao-agent) maintains — secret-tripwire.ts refuses the obvious mistakes.
28
+ */
29
+ environment?: Readonly<Record<string, string>>;
30
+ runAtLoad?: boolean;
31
+ keepAlive?: KeepAlive;
32
+ /** Seconds between starts. */
33
+ startInterval?: number;
34
+ startCalendarInterval?: CalendarInterval | readonly CalendarInterval[];
35
+ /** Minimum seconds between (re)starts; launchd's default is 10. */
36
+ throttleInterval?: number;
37
+ standardOutPath?: string;
38
+ standardErrorPath?: string;
39
+ workingDirectory?: string;
40
+ /** Run as this user. ⚠️ System domain only — launchd ignores it for agents, so it is refused. */
41
+ userName?: string;
42
+ /** Run as this group. ⚠️ System domain only, like `userName`. */
43
+ groupName?: string;
44
+ /**
45
+ * Any other launchd.plist(5) key, verbatim (`ProcessType`, `SoftResourceLimits`, …).
46
+ * ⛔ A key the typed props own is refused, so one job can never say two things.
47
+ */
48
+ extraKeys?: PlistDict;
49
+ }
50
+ export interface LaunchdJobAttributes {
51
+ label: string;
52
+ domain: LaunchdDomain;
53
+ /** `<domain>/<label>` — what `launchctl print` / `bootout` take. */
54
+ serviceTarget: string;
55
+ plistPath: string;
56
+ /** SHA-256 of the plist bytes last bootstrapped (reconcile) or found on disk (read). */
57
+ plistSha256: string;
58
+ /** Bootstrapped into the domain — not necessarily running. */
59
+ loaded: boolean;
60
+ /** launchd's own word: `running`, `not running`, … */
61
+ state?: string;
62
+ pid?: number;
63
+ lastExitCode?: number;
64
+ }
65
+ export type ParsedDomain = {
66
+ readonly kind: 'system';
67
+ } | {
68
+ readonly kind: 'gui';
69
+ readonly uid: number;
70
+ };
71
+ export declare const parseDomain: (domain: string) => ParsedDomain | undefined;
72
+ export declare const serviceTarget: (label: string, domain: LaunchdDomain) => string;
73
+ /**
74
+ * Where launchd expects the job's plist. ★ Derived, never declared: a daemon's plist outside
75
+ * /Library/LaunchDaemons is not bootstrapped at boot, so a free-form path would be a job that
76
+ * silently stops existing after the next restart.
77
+ */
78
+ export declare const plistPathFor: (label: string, domain: ParsedDomain, home: string | undefined) => string;
79
+ /** Plist keys the typed props own — `extraKeys` may not name them. `Program` is refused too. */
80
+ export declare const OWNED_KEYS: readonly string[];
81
+ /** The plist dict for a job. ⚠️ Assumes jobProblems() came back empty. */
82
+ export declare const jobPlistDict: (props: LaunchdJobProps) => PlistDict;
83
+ export declare const sha256Hex: (bytes: Uint8Array | string) => string;
84
+ /** The rendered plist and its digest — the only thing LaunchdJob compares. */
85
+ export declare const renderJob: (props: LaunchdJobProps) => {
86
+ text: string;
87
+ bytes: Uint8Array;
88
+ sha256: string;
89
+ };
90
+ //# sourceMappingURL=job-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-form.d.ts","sourceRoot":"","sources":["../../src/launchd/job-form.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * LaunchdJob's read / diff / reconcile / delete as plain async functions over a HostRunner — split
3
+ * from job.ts so the whole lifecycle runs against fake-runner.ts in tests, with no real launchd.
4
+ * The checks made before any write live in job-preflight.ts.
5
+ *
6
+ * ★ THE ORDER IS WRITE, BOOTOUT, BOOTSTRAP — not bootout first. launchd reads the plist only at
7
+ * bootstrap, so writing the new file while the old job runs changes nothing yet, and a write that
8
+ * fails (disk full, EACCES) leaves the old job running instead of leaving nothing running.
9
+ * ⚠️ A FAILED BOOTSTRAP LEAVES THE JOB DOWN. On an UPDATE the new plist stays on disk, the state
10
+ * keeps the previous digest, and the next deploy retries. On a CREATE the plist this deploy wrote
11
+ * is removed again: left behind, the next plan's recovery `read` would find a plist with no
12
+ * state and report it `Unowned`, and every later deploy would demand `--adopt` for our own file.
13
+ * There is no rollback to an older plist: one that launchd refuses is a declaration to fix, and
14
+ * re-bootstrapping the old one could fail for the same host-side reason.
15
+ */
16
+ import type { Diff } from 'alchemy/Diff';
17
+ import { type LaunchdJobAttributes, type LaunchdJobProps } from './job-form.ts';
18
+ import { type Identity } from './job-preflight.ts';
19
+ import type { HostRunner } from './runner.ts';
20
+ /** What is on the host now: the plist's digest and launchd's view. `undefined` when neither exists. */
21
+ export declare const readJob: (runner: HostRunner, props: Identity) => Promise<LaunchdJobAttributes | undefined>;
22
+ /**
23
+ * ★ `deleteFirst`: labels are unique per domain, and the old and new job would otherwise run the
24
+ * same program side by side — same port, same files — until the old one is deleted. A brief
25
+ * outage beats two copies. ⛔ Which is why everything the new job's reconcile would refuse is
26
+ * refused HERE, at plan time (job-preflight.ts): after the delete it would be too late.
27
+ * `full` is the whole declaration when resolved; job.ts passes only the identity otherwise.
28
+ */
29
+ export declare const replaceDiff: (runner: HostRunner, next: Identity, full: LaunchdJobProps | undefined, output: LaunchdJobAttributes) => Promise<Diff>;
30
+ export declare const diffJob: (runner: HostRunner, news: LaunchdJobProps, output: LaunchdJobAttributes) => Promise<Diff>;
31
+ export declare const reconcileJob: (runner: HostRunner, props: LaunchdJobProps, output: LaunchdJobAttributes | undefined) => Promise<LaunchdJobAttributes>;
32
+ /** Boot out, then remove the plist. Idempotent: an absent job and an absent file are success. */
33
+ export declare const deleteJob: (runner: HostRunner, output: LaunchdJobAttributes) => Promise<void>;
34
+ //# sourceMappingURL=job-lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-lifecycle.d.ts","sourceRoot":"","sources":["../../src/launchd/job-lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,eAAe,EAIrB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,KAAK,QAAQ,EASd,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAmB9C,uGAAuG;AACvG,eAAO,MAAM,OAAO,WACV,UAAU,SACX,QAAQ,KACd,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAM1C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,WACd,UAAU,QACZ,QAAQ,QACR,eAAe,GAAG,SAAS,UACzB,oBAAoB,KAC3B,OAAO,CAAC,IAAI,CAMd,CAAC;AAEF,eAAO,MAAM,OAAO,WACV,UAAU,QACZ,eAAe,UACb,oBAAoB,KAC3B,OAAO,CAAC,IAAI,CAad,CAAC;AAEF,eAAO,MAAM,YAAY,WACf,UAAU,SACX,eAAe,UACd,oBAAoB,GAAG,SAAS,KACvC,OAAO,CAAC,oBAAoB,CAsE9B,CAAC;AAEF,iGAAiG;AACjG,eAAO,MAAM,SAAS,WACZ,UAAU,UACV,oBAAoB,KAC3B,OAAO,CAAC,IAAI,CAQd,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Everything LaunchdJob checks before it touches the host — split from job-lifecycle.ts so that a
3
+ * reconcile and a plan-time REPLACE run the very same checks.
4
+ *
5
+ * ★ WHY A REPLACE IS CHECKED AT PLAN TIME. A label or domain change is a delete-first replace, and
6
+ * Alchemy runs `delete` on the old job BEFORE `reconcile` on the new one (Apply.ts,
7
+ * deleteOldGenerations under `node.deleteFirst`; read in alchemy 2.0.0-beta.79 on 2026-09-21).
8
+ * A refusal that fired only in reconcile would therefore arrive after the old job was already
9
+ * booted out: nothing running, and a plan that had promised a clean swap.
10
+ * ★ EVERY CHECK HERE IS READ-ONLY: validation, a user lookup, `launchctl print` and
11
+ * `print-disabled` — none of which needs root (measured, see launchctl.ts).
12
+ */
13
+ import { type LaunchdJobAttributes, type LaunchdJobProps, type ParsedDomain } from './job-form.ts';
14
+ import { type ServiceStatus } from './launchctl.ts';
15
+ import { type HostRunner } from './runner.ts';
16
+ export type Location = {
17
+ readonly domain: ParsedDomain;
18
+ readonly plistPath: string;
19
+ readonly target: string;
20
+ };
21
+ export type Identity = Pick<LaunchdJobProps, 'label' | 'domain'>;
22
+ export declare const refuse: (label: string, message: string) => Error;
23
+ /** Throw every refusal at once, so one plan shows the whole list. */
24
+ export declare const assertValid: (props: LaunchdJobProps) => void;
25
+ export declare const locate: (runner: HostRunner, props: Identity) => Promise<Location>;
26
+ /**
27
+ * ⛔ NO SILENT SUDO. The system domain, and another user's gui domain, need root. Refuse up front
28
+ * with the two ways out, rather than let launchctl or rename(2) fail halfway through.
29
+ */
30
+ export declare const assertMayWrite: (runner: HostRunner, label: string, domain: ParsedDomain) => void;
31
+ /** Where the job goes, whether we may write it there, and launchd's current view of it. */
32
+ export declare const preflight: (runner: HostRunner, props: Identity) => Promise<{
33
+ location: Location;
34
+ status: ServiceStatus;
35
+ }>;
36
+ /**
37
+ * ⛔ A JOB THIS RESOURCE DOES NOT OWN IS NEVER OVERWRITTEN. Alchemy's adoption probe guards a
38
+ * create, but NOT the new label of a replace — the engine reads nothing there — so a rename onto
39
+ * a label someone already loaded would boot their job out and write ours over its plist.
40
+ * ★ A plist byte-identical to our render is exempt: it is this declaration's own leftover (a
41
+ * deploy that died between write and bootstrap), and rewriting it changes nothing.
42
+ */
43
+ export declare const assertUnclaimed: (runner: HostRunner, label: string, location: Location, status: ServiceStatus, renderedSha256: string | undefined) => Promise<void>;
44
+ /**
45
+ * A rename, checked at plan time: the new identity must be valid, writable, bootstrappable and
46
+ * free, and the old one deletable — before the plan may promise a delete-first replace.
47
+ * `full` is the whole declaration when it is already resolved; otherwise only the identity is.
48
+ */
49
+ export declare const assertReplaceable: (runner: HostRunner, next: Identity, full: LaunchdJobProps | undefined, old: LaunchdJobAttributes, renderedSha256: string | undefined) => Promise<void>;
50
+ //# sourceMappingURL=job-preflight.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-preflight.d.ts","sourceRoot":"","sources":["../../src/launchd/job-preflight.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,YAAY,EAKlB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,KAAK,aAAa,EAA4B,MAAM,gBAAgB,CAAC;AAC9E,OAAO,EAAE,KAAK,UAAU,EAAgB,MAAM,aAAa,CAAC;AAE5D,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,EAAE,OAAO,GAAG,QAAQ,CAAC,CAAC;AAEjE,eAAO,MAAM,MAAM,UAAW,MAAM,WAAW,MAAM,KAAG,KACT,CAAC;AAEhD,qEAAqE;AACrE,eAAO,MAAM,WAAW,UAAW,eAAe,KAAG,IAGpD,CAAC;AAEF,eAAO,MAAM,MAAM,WAAkB,UAAU,SAAS,QAAQ,KAAG,OAAO,CAAC,QAAQ,CAUlF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,WAAY,UAAU,SAAS,MAAM,UAAU,YAAY,KAAG,IAgBxF,CAAC;AAEF,2FAA2F;AAC3F,eAAO,MAAM,SAAS,WACZ,UAAU,SACX,QAAQ,KACd,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,aAAa,CAAA;CAAE,CAwBvD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,WAClB,UAAU,SACX,MAAM,YACH,QAAQ,UACV,aAAa,kBACL,MAAM,GAAG,SAAS,KACjC,OAAO,CAAC,IAAI,CAWd,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,WACpB,UAAU,QACZ,QAAQ,QACR,eAAe,GAAG,SAAS,OAC5B,oBAAoB,kBACT,MAAM,GAAG,SAAS,KACjC,OAAO,CAAC,IAAI,CAQd,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Everything LaunchdJob refuses before it touches the host. Run at plan time (diff) and again at
3
+ * reconcile, so a bad declaration fails the plan instead of half-applying.
4
+ *
5
+ * ★ REFUSE WHAT launchd WOULD SILENTLY IGNORE OR MISREAD. launchd loads a plist with an out-of-range
6
+ * calendar field or a UserName on an agent without complaint and then simply never does what the
7
+ * declaration says; a plan that fails is cheaper than a job that quietly does nothing.
8
+ */
9
+ import { type LaunchdJobProps } from './job-form.ts';
10
+ /**
11
+ * ⛔ LABELS OTHER TOOLS OWN ARE NEVER DECLARED HERE. nix-darwin (`org.nixos.*`) rewrites its plists
12
+ * on every activation and unloads any it no longer lists; macOS owns `com.apple.*`; `brew
13
+ * services` owns `homebrew.mxcl.*`. Declaring one of those makes two tools fight over one file.
14
+ * To move a job off such a tool, declare a NEW label and cut over (docs/launchd.md).
15
+ */
16
+ export declare const RESERVED_LABEL_PREFIXES: readonly string[];
17
+ /**
18
+ * The label and domain alone — the job's identity. ★ Split out because a rename is a delete-first
19
+ * replace, and it must be refusable at plan time even while other props are still unresolved
20
+ * Outputs (see job.ts diff): otherwise the old job is deleted and only then is the new label
21
+ * found to be invalid.
22
+ */
23
+ export declare const identityProblems: (label: string, domainText: string) => string[];
24
+ export declare const jobProblems: (props: LaunchdJobProps) => string[];
25
+ //# sourceMappingURL=job-validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-validate.d.ts","sourceRoot":"","sources":["../../src/launchd/job-validate.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAEL,KAAK,eAAe,EAGrB,MAAM,eAAe,CAAC;AAMvB;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,SAAS,MAAM,EAIpD,CAAC;AAkCF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,UAAW,MAAM,cAAc,MAAM,KAAG,MAAM,EAY1E,CAAC;AAEF,eAAO,MAAM,WAAW,UAAW,eAAe,KAAG,MAAM,EAkD1D,CAAC"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `Launchd.Job` — one launchd job (a LaunchDaemon in `system`, a LaunchAgent in `gui/<uid>`),
3
+ * declared rather than hand-loaded.
4
+ *
5
+ * ★ launchd HAS NO SDK. The provider renders the plist (plist.ts), writes it where launchd looks
6
+ * (job-form.ts plistPathFor), and drives `launchctl` (launchctl.ts) — all through the HostRunner
7
+ * the stack provides (runner.ts), so none of it is reachable except through that seam.
8
+ *
9
+ * Lifecycle (job-lifecycle.ts has the reasoning for each step):
10
+ * - create / update — write the plist atomically, boot out the old job if loaded, bootstrap.
11
+ * An update is therefore a RESTART; a converged job is left alone.
12
+ * - read — `launchctl print` (loaded? pid? last exit?) plus the plist's SHA-256.
13
+ * - diff — the rendered plist's SHA-256 against the stored AND the on-disk digest.
14
+ * - replace — only when `label` or `domain` changes, and DELETE FIRST (labels are unique); so
15
+ * everything the new job would be refused for is refused at plan time (job-preflight.ts).
16
+ * - delete — boot out, remove the plist.
17
+ *
18
+ * ⛔ ENVIRONMENT IS NON-SECRET ONLY — see the ⛔ on LaunchdJobProps.environment.
19
+ * ⛔ NO SILENT SUDO — see assertMayWrite in job-preflight.ts.
20
+ * ⚠️ NOTHING IS ADOPTED WITHOUT `--adopt`. A label already loaded, or a plist already on disk, reads
21
+ * as `Unowned`, so Alchemy refuses to take it over unless asked. Labels under `org.nixos.`,
22
+ * `com.apple.` and `homebrew.mxcl.` are refused outright (job-validate.ts) — declare a new label
23
+ * and cut over instead (docs/launchd.md).
24
+ */
25
+ import { Resource } from 'alchemy';
26
+ import * as Provider from 'alchemy/Provider';
27
+ import type { LaunchdJobAttributes, LaunchdJobProps } from './job-form.ts';
28
+ import { HostRunnerService } from './runner.ts';
29
+ export type { CalendarInterval, KeepAlive, LaunchdDomain, LaunchdJobAttributes, LaunchdJobProps, } from './job-form.ts';
30
+ export interface LaunchdJob extends Resource<'Launchd.Job', LaunchdJobProps, LaunchdJobAttributes> {
31
+ }
32
+ export declare const LaunchdJob: import("alchemy").ResourceClass<LaunchdJob>;
33
+ export declare const LaunchdJobProvider: () => import("effect/Layer").Layer<Provider.Provider<LaunchdJob>, never, HostRunnerService>;
34
+ //# sourceMappingURL=job.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job.d.ts","sourceRoot":"","sources":["../../src/launchd/job.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAG7C,OAAO,KAAK,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAE3E,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD,YAAY,EACV,gBAAgB,EAChB,SAAS,EACT,aAAa,EACb,oBAAoB,EACpB,eAAe,GAChB,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,UAAW,SAAQ,QAAQ,CAC1C,aAAa,EACb,eAAe,EACf,oBAAoB,CACrB;CAAG;AAEJ,eAAO,MAAM,UAAU,6CAAsC,CAAC;AAE9D,eAAO,MAAM,kBAAkB,6FA6C5B,CAAC"}