@homeflare/alchemy 0.5.0 → 0.6.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 (180) hide show
  1. package/README.md +17 -48
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/launchd/fake-runner.d.ts +36 -0
  5. package/dist/launchd/fake-runner.d.ts.map +1 -0
  6. package/dist/launchd/host-effect.d.ts +19 -0
  7. package/dist/launchd/host-effect.d.ts.map +1 -0
  8. package/dist/launchd/host-file-form.d.ts +24 -0
  9. package/dist/launchd/host-file-form.d.ts.map +1 -0
  10. package/dist/launchd/host-file-lifecycle.d.ts +29 -0
  11. package/dist/launchd/host-file-lifecycle.d.ts.map +1 -0
  12. package/dist/launchd/host-file.d.ts +25 -0
  13. package/dist/launchd/host-file.d.ts.map +1 -0
  14. package/dist/launchd/host-lookup.d.ts +27 -0
  15. package/dist/launchd/host-lookup.d.ts.map +1 -0
  16. package/dist/launchd/index.d.ts +21 -0
  17. package/dist/launchd/index.d.ts.map +1 -0
  18. package/dist/launchd/index.js +969 -0
  19. package/dist/launchd/index.js.map +25 -0
  20. package/dist/launchd/job-form.d.ts +90 -0
  21. package/dist/launchd/job-form.d.ts.map +1 -0
  22. package/dist/launchd/job-lifecycle.d.ts +34 -0
  23. package/dist/launchd/job-lifecycle.d.ts.map +1 -0
  24. package/dist/launchd/job-preflight.d.ts +50 -0
  25. package/dist/launchd/job-preflight.d.ts.map +1 -0
  26. package/dist/launchd/job-validate.d.ts +25 -0
  27. package/dist/launchd/job-validate.d.ts.map +1 -0
  28. package/dist/launchd/job.d.ts +34 -0
  29. package/dist/launchd/job.d.ts.map +1 -0
  30. package/dist/launchd/launchctl.d.ts +62 -0
  31. package/dist/launchd/launchctl.d.ts.map +1 -0
  32. package/dist/launchd/local-runner.d.ts +7 -0
  33. package/dist/launchd/local-runner.d.ts.map +1 -0
  34. package/dist/launchd/plist.d.ts +41 -0
  35. package/dist/launchd/plist.d.ts.map +1 -0
  36. package/dist/launchd/providers.d.ts +15 -0
  37. package/dist/launchd/providers.d.ts.map +1 -0
  38. package/dist/launchd/runner.d.ts +91 -0
  39. package/dist/launchd/runner.d.ts.map +1 -0
  40. package/dist/launchd/secret-tripwire.d.ts +23 -0
  41. package/dist/launchd/secret-tripwire.d.ts.map +1 -0
  42. package/dist/openbao/auth-method-form.d.ts +7 -0
  43. package/dist/openbao/auth-method-form.d.ts.map +1 -1
  44. package/dist/openbao/auth-method-reconcile.d.ts +15 -0
  45. package/dist/openbao/auth-method-reconcile.d.ts.map +1 -0
  46. package/dist/openbao/auth-method.d.ts +6 -0
  47. package/dist/openbao/auth-method.d.ts.map +1 -1
  48. package/dist/openbao/auth-role-form.d.ts +9 -0
  49. package/dist/openbao/auth-role-form.d.ts.map +1 -1
  50. package/dist/openbao/auth-role.d.ts.map +1 -1
  51. package/dist/openbao/auth-token-form.d.ts +48 -0
  52. package/dist/openbao/auth-token-form.d.ts.map +1 -0
  53. package/dist/openbao/bao-identity.d.ts +81 -0
  54. package/dist/openbao/bao-identity.d.ts.map +1 -0
  55. package/dist/openbao/bao-namespace.d.ts +20 -0
  56. package/dist/openbao/bao-namespace.d.ts.map +1 -0
  57. package/dist/openbao/cloudflare-roles-expand.d.ts.map +1 -1
  58. package/dist/openbao/host-approles.d.ts +62 -0
  59. package/dist/openbao/host-approles.d.ts.map +1 -0
  60. package/dist/openbao/index.d.ts +19 -0
  61. package/dist/openbao/index.d.ts.map +1 -1
  62. package/dist/openbao/index.js +1433 -305
  63. package/dist/openbao/index.js.map +34 -13
  64. package/dist/openbao/jwt-config-form.d.ts +51 -0
  65. package/dist/openbao/jwt-config-form.d.ts.map +1 -0
  66. package/dist/openbao/jwt-config.d.ts +25 -0
  67. package/dist/openbao/jwt-config.d.ts.map +1 -0
  68. package/dist/openbao/jwt-role-form.d.ts +91 -0
  69. package/dist/openbao/jwt-role-form.d.ts.map +1 -0
  70. package/dist/openbao/jwt-role.d.ts +31 -0
  71. package/dist/openbao/jwt-role.d.ts.map +1 -0
  72. package/dist/openbao/kubernetes-role-form.d.ts +68 -0
  73. package/dist/openbao/kubernetes-role-form.d.ts.map +1 -0
  74. package/dist/openbao/kubernetes-role.d.ts +25 -0
  75. package/dist/openbao/kubernetes-role.d.ts.map +1 -0
  76. package/dist/openbao/mfa-enforcement-form.d.ts +46 -0
  77. package/dist/openbao/mfa-enforcement-form.d.ts.map +1 -0
  78. package/dist/openbao/mfa-enforcement.d.ts +41 -0
  79. package/dist/openbao/mfa-enforcement.d.ts.map +1 -0
  80. package/dist/openbao/mfa-totp-form.d.ts +52 -0
  81. package/dist/openbao/mfa-totp-form.d.ts.map +1 -0
  82. package/dist/openbao/mfa-totp.d.ts +33 -0
  83. package/dist/openbao/mfa-totp.d.ts.map +1 -0
  84. package/dist/openbao/mfa-wire.d.ts +63 -0
  85. package/dist/openbao/mfa-wire.d.ts.map +1 -0
  86. package/dist/openbao/mount-form.d.ts +7 -0
  87. package/dist/openbao/mount-form.d.ts.map +1 -1
  88. package/dist/openbao/mount-move.d.ts +49 -0
  89. package/dist/openbao/mount-move.d.ts.map +1 -0
  90. package/dist/openbao/mount-path.d.ts +8 -4
  91. package/dist/openbao/mount-path.d.ts.map +1 -1
  92. package/dist/openbao/mount-reconcile.d.ts +16 -0
  93. package/dist/openbao/mount-reconcile.d.ts.map +1 -0
  94. package/dist/openbao/mount.d.ts +9 -0
  95. package/dist/openbao/mount.d.ts.map +1 -1
  96. package/dist/openbao/pki-role-canonical.d.ts +46 -0
  97. package/dist/openbao/pki-role-canonical.d.ts.map +1 -0
  98. package/dist/openbao/pki-role-form.d.ts +38 -30
  99. package/dist/openbao/pki-role-form.d.ts.map +1 -1
  100. package/dist/openbao/pki-role.d.ts.map +1 -1
  101. package/dist/openbao/remount-wire.d.ts +44 -0
  102. package/dist/openbao/remount-wire.d.ts.map +1 -0
  103. package/dist/openbao/role-reconcile.d.ts +40 -0
  104. package/dist/openbao/role-reconcile.d.ts.map +1 -0
  105. package/docs/launchd.md +167 -0
  106. package/package.json +7 -2
  107. package/src/index.ts +1 -0
  108. package/src/launchd/fake-runner.ts +176 -0
  109. package/src/launchd/host-effect.ts +28 -0
  110. package/src/launchd/host-file-form.ts +71 -0
  111. package/src/launchd/host-file-lifecycle.test.ts +198 -0
  112. package/src/launchd/host-file-lifecycle.ts +164 -0
  113. package/src/launchd/host-file.ts +64 -0
  114. package/src/launchd/host-lookup.test.ts +92 -0
  115. package/src/launchd/host-lookup.ts +94 -0
  116. package/src/launchd/index.ts +26 -0
  117. package/src/launchd/job-form.test.ts +171 -0
  118. package/src/launchd/job-form.ts +180 -0
  119. package/src/launchd/job-identity.test.ts +147 -0
  120. package/src/launchd/job-lifecycle.test.ts +220 -0
  121. package/src/launchd/job-lifecycle.ts +194 -0
  122. package/src/launchd/job-preflight.ts +152 -0
  123. package/src/launchd/job-validate.ts +134 -0
  124. package/src/launchd/job.ts +96 -0
  125. package/src/launchd/launchctl.test.ts +181 -0
  126. package/src/launchd/launchctl.ts +188 -0
  127. package/src/launchd/local-runner.test.ts +125 -0
  128. package/src/launchd/local-runner.ts +143 -0
  129. package/src/launchd/plist.test.ts +186 -0
  130. package/src/launchd/plist.ts +156 -0
  131. package/src/launchd/providers.test.ts +222 -0
  132. package/src/launchd/providers.ts +21 -0
  133. package/src/launchd/runner.ts +95 -0
  134. package/src/launchd/secret-tripwire.test.ts +110 -0
  135. package/src/launchd/secret-tripwire.ts +81 -0
  136. package/src/openbao/README.md +181 -0
  137. package/src/openbao/REPLACE.md +87 -0
  138. package/src/openbao/auth-method-form.ts +7 -0
  139. package/src/openbao/auth-method-reconcile.ts +74 -0
  140. package/src/openbao/auth-method.ts +20 -41
  141. package/src/openbao/auth-role-form.test.ts +16 -1
  142. package/src/openbao/auth-role-form.ts +11 -0
  143. package/src/openbao/auth-role.ts +12 -0
  144. package/src/openbao/auth-token-form.ts +161 -0
  145. package/src/openbao/bao-identity.test.ts +131 -0
  146. package/src/openbao/bao-identity.ts +188 -0
  147. package/src/openbao/bao-namespace.ts +32 -0
  148. package/src/openbao/cloudflare-roles-expand.ts +2 -5
  149. package/src/openbao/host-approles.test.ts +121 -0
  150. package/src/openbao/host-approles.ts +126 -0
  151. package/src/openbao/index.ts +19 -0
  152. package/src/openbao/jwt-config-form.ts +161 -0
  153. package/src/openbao/jwt-config.ts +85 -0
  154. package/src/openbao/jwt-role-form.test.ts +130 -0
  155. package/src/openbao/jwt-role-form.ts +231 -0
  156. package/src/openbao/jwt-role.ts +102 -0
  157. package/src/openbao/kubernetes-role-form.test.ts +82 -0
  158. package/src/openbao/kubernetes-role-form.ts +149 -0
  159. package/src/openbao/kubernetes-role.ts +94 -0
  160. package/src/openbao/mfa-enforcement-form.ts +147 -0
  161. package/src/openbao/mfa-enforcement.ts +143 -0
  162. package/src/openbao/mfa-totp-form.ts +155 -0
  163. package/src/openbao/mfa-totp.ts +118 -0
  164. package/src/openbao/mfa-wire.ts +117 -0
  165. package/src/openbao/mfa.test.ts +200 -0
  166. package/src/openbao/mount-form.ts +9 -1
  167. package/src/openbao/mount-move.test.ts +164 -0
  168. package/src/openbao/mount-move.ts +113 -0
  169. package/src/openbao/mount-path.test.ts +11 -1
  170. package/src/openbao/mount-path.ts +17 -11
  171. package/src/openbao/mount-reconcile.ts +75 -0
  172. package/src/openbao/mount.ts +26 -43
  173. package/src/openbao/pki-role-canonical.ts +163 -0
  174. package/src/openbao/pki-role-form.test.ts +112 -0
  175. package/src/openbao/pki-role-form.ts +76 -110
  176. package/src/openbao/pki-role.ts +12 -27
  177. package/src/openbao/remount-wire.test.ts +87 -0
  178. package/src/openbao/remount-wire.ts +112 -0
  179. package/src/openbao/role-reconcile.test.ts +104 -0
  180. package/src/openbao/role-reconcile.ts +84 -0
@@ -0,0 +1,969 @@
1
+ // src/launchd/host-file.ts
2
+ import { Resource } from "alchemy";
3
+ import { Unowned } from "alchemy/AdoptPolicy";
4
+ import { isResolved } from "alchemy/Diff";
5
+ import * as Provider from "alchemy/Provider";
6
+ import * as Effect2 from "effect/Effect";
7
+
8
+ // src/launchd/host-effect.ts
9
+ import * as Effect from "effect/Effect";
10
+ var lift = (step) => Effect.tryPromise({
11
+ catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
12
+ try: step
13
+ });
14
+ var resolvedString = (news, key) => {
15
+ if (typeof news !== "object" || news === null)
16
+ return;
17
+ const value = news[key];
18
+ return typeof value === "string" ? value : undefined;
19
+ };
20
+
21
+ // src/launchd/secret-tripwire.ts
22
+ var ALTERNATIVE = "props are stored unencrypted in Alchemy state. Have a secret renderer (e.g. openbao-agent) " + "write it to a root-owned file and declare the file path instead (FOO_FILE=/path, --foo-file /path).";
23
+ var SECRET_NAME = /(?:^|_)(?:TOKEN|SECRET|PASSWORD|PASSWD|PASSPHRASE|API_?KEY|ACCESS_KEY|PRIVATE_KEY|SECRET_KEY|CREDENTIALS?)$/i;
24
+ var INLINE_FLAG = /^--?[a-z0-9-]*(?:token|password|passwd|passphrase|secret|api-?key)=./i;
25
+ var BARE_FLAG = /^--?(?:[a-z0-9]+-)*(?:token|password|passwd|passphrase|secret|api-?key)$/i;
26
+ var PRIVATE_KEY = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/;
27
+ var keyLeaves = (value, path) => {
28
+ if (typeof value === "string")
29
+ return PRIVATE_KEY.test(value) ? [path] : [];
30
+ if (Array.isArray(value)) {
31
+ return value.flatMap((item, index) => keyLeaves(item, `${path}[${String(index)}]`));
32
+ }
33
+ if (typeof value === "object" && value !== null) {
34
+ return Object.entries(value).flatMap(([key, item]) => keyLeaves(item, `${path}.${key}`));
35
+ }
36
+ return [];
37
+ };
38
+ var jobSecretProblems = (environment, argv, extraKeys) => {
39
+ const found = [];
40
+ for (const [name, value] of Object.entries(environment ?? {})) {
41
+ if (SECRET_NAME.test(name))
42
+ found.push(`environment ${name} looks like a secret: ${ALTERNATIVE}`);
43
+ else if (PRIVATE_KEY.test(value))
44
+ found.push(`environment ${name} holds a private key: ${ALTERNATIVE}`);
45
+ }
46
+ for (const [index, arg] of argv.entries()) {
47
+ if (INLINE_FLAG.test(arg)) {
48
+ found.push(`programArguments[${String(index)}] passes a secret inline: ${ALTERNATIVE}`);
49
+ } else if (BARE_FLAG.test(arg) && index + 1 < argv.length) {
50
+ found.push(`programArguments[${String(index + 1)}] follows a secret flag: ${ALTERNATIVE}`);
51
+ } else if (PRIVATE_KEY.test(arg)) {
52
+ found.push(`programArguments[${String(index)}] holds a private key: ${ALTERNATIVE}`);
53
+ }
54
+ }
55
+ for (const path of keyLeaves(extraKeys ?? {}, "extraKeys")) {
56
+ found.push(`${path} holds a private key: ${ALTERNATIVE}`);
57
+ }
58
+ return found;
59
+ };
60
+ var fileSecretProblems = (content) => PRIVATE_KEY.test(content) ? [`content holds a private key: ${ALTERNATIVE}`] : [];
61
+
62
+ // src/launchd/host-file-form.ts
63
+ var DEFAULT_MODE = 420;
64
+ var identityProblems = (name, value) => {
65
+ if (value === undefined)
66
+ return [];
67
+ if (typeof value === "number") {
68
+ return Number.isSafeInteger(value) && value >= 0 ? [] : [`${name} must be a non-negative integer id`];
69
+ }
70
+ return /^[A-Za-z_][A-Za-z0-9_.-]*$/.test(value) || /^\d+$/.test(value) ? [] : [`${name} ${JSON.stringify(value)} is not a valid user or group name`];
71
+ };
72
+ var fileProblems = (props) => {
73
+ const found = [];
74
+ const { path } = props;
75
+ if (!path.startsWith("/"))
76
+ found.push("path must be absolute");
77
+ if (path.endsWith("/") || path.split("/").slice(1).some((part) => part === "" || part === "." || part === "..")) {
78
+ found.push('path must be normalised (no trailing slash, no empty, "." or ".." segments)');
79
+ }
80
+ if (path.includes("\x00"))
81
+ found.push("path contains NUL");
82
+ const mode = props.mode ?? DEFAULT_MODE;
83
+ if (!Number.isSafeInteger(mode) || mode < 0 || mode > 4095)
84
+ found.push("mode must be 0–0o7777");
85
+ found.push(...identityProblems("owner", props.owner));
86
+ found.push(...identityProblems("group", props.group));
87
+ found.push(...fileSecretProblems(props.content));
88
+ return found;
89
+ };
90
+
91
+ // src/launchd/job-form.ts
92
+ import { createHash } from "node:crypto";
93
+
94
+ // src/launchd/plist.ts
95
+ class PlistError extends Error {
96
+ path;
97
+ constructor(path, detail) {
98
+ super(`plist ${path === "" ? "(root)" : path}: ${detail}`);
99
+ this.path = path;
100
+ this.name = "PlistError";
101
+ }
102
+ }
103
+ var HEADER = `<?xml version="1.0" encoding="UTF-8"?>
104
+ ` + '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" ' + `"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
105
+ ` + `<plist version="1.0">
106
+ `;
107
+ var MAX_DEPTH = 32;
108
+ var FORBIDDEN = /[\x00-\x08\x0B\x0C\x0E-\x1F\uD800-\uDFFF\uFFFE\uFFFF]/u;
109
+ var escapeText = (text, path) => {
110
+ const bad = FORBIDDEN.exec(text);
111
+ if (bad !== null) {
112
+ const code = bad[0].codePointAt(0) ?? 0;
113
+ throw new PlistError(path, `U+${code.toString(16).toUpperCase().padStart(4, "0")} cannot appear in XML 1.0`);
114
+ }
115
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\r", "&#13;");
116
+ };
117
+ var isPlainObject = (value) => {
118
+ const proto = Object.getPrototypeOf(value);
119
+ return proto === Object.prototype || proto === null;
120
+ };
121
+ var renderValue = (value, path, depth, out) => {
122
+ const pad = "\t".repeat(depth);
123
+ if (depth > MAX_DEPTH)
124
+ throw new PlistError(path, `nested deeper than ${String(MAX_DEPTH)}`);
125
+ if (typeof value === "string") {
126
+ out.push(`${pad}<string>${escapeText(value, path)}</string>`);
127
+ return;
128
+ }
129
+ if (typeof value === "boolean") {
130
+ out.push(`${pad}<${value ? "true" : "false"}/>`);
131
+ return;
132
+ }
133
+ if (typeof value === "number") {
134
+ if (!Number.isSafeInteger(value)) {
135
+ throw new PlistError(path, `${String(value)} is not a safe integer (no <real> support)`);
136
+ }
137
+ out.push(`${pad}<integer>${String(value === 0 ? 0 : value)}</integer>`);
138
+ return;
139
+ }
140
+ if (Array.isArray(value)) {
141
+ if (value.length === 0) {
142
+ out.push(`${pad}<array/>`);
143
+ return;
144
+ }
145
+ out.push(`${pad}<array>`);
146
+ for (const [index, item] of value.entries()) {
147
+ renderValue(item, `${path}[${String(index)}]`, depth + 1, out);
148
+ }
149
+ out.push(`${pad}</array>`);
150
+ return;
151
+ }
152
+ if (typeof value === "object" && value !== null && isPlainObject(value)) {
153
+ renderDict(value, path, depth, out);
154
+ return;
155
+ }
156
+ const kind = value === null ? "null" : typeof value;
157
+ throw new PlistError(path, `${kind} has no plist form — omit the key instead`);
158
+ };
159
+ var renderDict = (dict, path, depth, out) => {
160
+ const pad = "\t".repeat(depth);
161
+ const keys = Object.keys(dict).sort();
162
+ if (keys.length === 0) {
163
+ out.push(`${pad}<dict/>`);
164
+ return;
165
+ }
166
+ out.push(`${pad}<dict>`);
167
+ for (const key of keys) {
168
+ const child = path === "" ? key : `${path}.${key}`;
169
+ out.push(`${pad} <key>${escapeText(key, child)}</key>`);
170
+ renderValue(dict[key], child, depth + 1, out);
171
+ }
172
+ out.push(`${pad}</dict>`);
173
+ };
174
+ var renderPlist = (dict) => {
175
+ if (typeof dict !== "object" || dict === null || Array.isArray(dict) || !isPlainObject(dict)) {
176
+ throw new PlistError("", "the top level of a launchd plist must be a dict");
177
+ }
178
+ const out = [];
179
+ renderDict(dict, "", 0, out);
180
+ return `${HEADER}${out.join(`
181
+ `)}
182
+ </plist>
183
+ `;
184
+ };
185
+
186
+ // src/launchd/job-form.ts
187
+ var parseDomain = (domain) => {
188
+ if (domain === "system")
189
+ return { kind: "system" };
190
+ const match = /^gui\/(\d+)$/.exec(domain);
191
+ return match?.[1] === undefined ? undefined : { kind: "gui", uid: Number(match[1]) };
192
+ };
193
+ var serviceTarget = (label, domain) => `${domain}/${label}`;
194
+ var plistPathFor = (label, domain, home) => {
195
+ if (domain.kind === "system")
196
+ return `/Library/LaunchDaemons/${label}.plist`;
197
+ if (home === undefined || !home.startsWith("/")) {
198
+ throw new Error(`gui/${String(domain.uid)}: no home directory for that uid on this host`);
199
+ }
200
+ return `${home}/Library/LaunchAgents/${label}.plist`;
201
+ };
202
+ var OWNED_KEYS = [
203
+ "EnvironmentVariables",
204
+ "GroupName",
205
+ "KeepAlive",
206
+ "Label",
207
+ "Program",
208
+ "ProgramArguments",
209
+ "RunAtLoad",
210
+ "StandardErrorPath",
211
+ "StandardOutPath",
212
+ "StartCalendarInterval",
213
+ "StartInterval",
214
+ "ThrottleInterval",
215
+ "UserName",
216
+ "WorkingDirectory"
217
+ ];
218
+ var calendarDict = (entry) => {
219
+ const dict = {};
220
+ if (entry.minute !== undefined)
221
+ dict["Minute"] = entry.minute;
222
+ if (entry.hour !== undefined)
223
+ dict["Hour"] = entry.hour;
224
+ if (entry.day !== undefined)
225
+ dict["Day"] = entry.day;
226
+ if (entry.weekday !== undefined)
227
+ dict["Weekday"] = entry.weekday;
228
+ if (entry.month !== undefined)
229
+ dict["Month"] = entry.month;
230
+ return dict;
231
+ };
232
+ var keepAliveValue = (keepAlive) => {
233
+ if (typeof keepAlive === "boolean")
234
+ return keepAlive;
235
+ const dict = {};
236
+ if (keepAlive.successfulExit !== undefined)
237
+ dict["SuccessfulExit"] = keepAlive.successfulExit;
238
+ if (keepAlive.crashed !== undefined)
239
+ dict["Crashed"] = keepAlive.crashed;
240
+ return dict;
241
+ };
242
+ var jobPlistDict = (props) => {
243
+ const dict = { ...props.extraKeys };
244
+ dict["Label"] = props.label;
245
+ dict["ProgramArguments"] = [...props.programArguments];
246
+ const env = props.environment ?? {};
247
+ if (Object.keys(env).length > 0)
248
+ dict["EnvironmentVariables"] = { ...env };
249
+ if (props.runAtLoad !== undefined)
250
+ dict["RunAtLoad"] = props.runAtLoad;
251
+ if (props.keepAlive !== undefined)
252
+ dict["KeepAlive"] = keepAliveValue(props.keepAlive);
253
+ if (props.startInterval !== undefined)
254
+ dict["StartInterval"] = props.startInterval;
255
+ const calendar = props.startCalendarInterval;
256
+ if (calendar !== undefined) {
257
+ dict["StartCalendarInterval"] = Array.isArray(calendar) ? calendar.map(calendarDict) : calendarDict(calendar);
258
+ }
259
+ if (props.throttleInterval !== undefined)
260
+ dict["ThrottleInterval"] = props.throttleInterval;
261
+ if (props.standardOutPath !== undefined)
262
+ dict["StandardOutPath"] = props.standardOutPath;
263
+ if (props.standardErrorPath !== undefined)
264
+ dict["StandardErrorPath"] = props.standardErrorPath;
265
+ if (props.workingDirectory !== undefined)
266
+ dict["WorkingDirectory"] = props.workingDirectory;
267
+ if (props.userName !== undefined)
268
+ dict["UserName"] = props.userName;
269
+ if (props.groupName !== undefined)
270
+ dict["GroupName"] = props.groupName;
271
+ return dict;
272
+ };
273
+ var sha256Hex = (bytes) => createHash("sha256").update(bytes).digest("hex");
274
+ var renderJob = (props) => {
275
+ const text = renderPlist(jobPlistDict(props));
276
+ const bytes = new TextEncoder().encode(text);
277
+ return { bytes, sha256: sha256Hex(bytes), text };
278
+ };
279
+
280
+ // src/launchd/runner.ts
281
+ import * as Context from "effect/Context";
282
+ import * as Layer from "effect/Layer";
283
+
284
+ class HostRunnerService extends Context.Service()("homeflare/launchd/HostRunner") {
285
+ }
286
+ var hostRunnerLayer = (runner) => Layer.succeed(HostRunnerService, runner);
287
+ var canActAsRoot = (runner) => runner.privileged || runner.effectiveUid() === 0;
288
+
289
+ // src/launchd/host-file-lifecycle.ts
290
+ var refuse = (path, message) => new Error(`Host.File ${path}: ${message}`);
291
+ var numericId = (value) => typeof value === "number" ? value : /^\d+$/.test(value) ? Number(value) : undefined;
292
+ var desiredFile = async (runner, props) => {
293
+ const found = fileProblems(props);
294
+ if (found.length > 0)
295
+ throw refuse(props.path, found.join("; "));
296
+ const bytes = new TextEncoder().encode(props.content);
297
+ let uid;
298
+ let gid;
299
+ if (props.owner !== undefined) {
300
+ uid = numericId(props.owner) ?? (await runner.lookupUser(String(props.owner)))?.uid;
301
+ if (uid === undefined)
302
+ throw refuse(props.path, `no user ${String(props.owner)} on this host`);
303
+ }
304
+ if (props.group !== undefined) {
305
+ gid = numericId(props.group) ?? await runner.lookupGroup(String(props.group));
306
+ if (gid === undefined)
307
+ throw refuse(props.path, `no group ${String(props.group)} on this host`);
308
+ }
309
+ return {
310
+ bytes,
311
+ mode: props.mode ?? DEFAULT_MODE,
312
+ sha256: sha256Hex(bytes),
313
+ ...uid === undefined ? {} : { uid },
314
+ ...gid === undefined ? {} : { gid }
315
+ };
316
+ };
317
+ var readFileAttributes = async (runner, path) => {
318
+ const stat = await runner.stat(path);
319
+ if (stat === undefined)
320
+ return;
321
+ const bytes = stat.kind === "file" ? await runner.readFile(path) : undefined;
322
+ return {
323
+ gid: stat.gid,
324
+ mode: stat.mode,
325
+ path,
326
+ sha256: bytes === undefined ? "" : sha256Hex(bytes),
327
+ size: stat.size,
328
+ uid: stat.uid
329
+ };
330
+ };
331
+ var matches = (live, want) => live.sha256 === want.sha256 && live.mode === want.mode && (want.uid === undefined || live.uid === want.uid) && (want.gid === undefined || live.gid === want.gid);
332
+ var diffFile = async (runner, news, output) => {
333
+ if (news.path !== output.path)
334
+ return { action: "replace" };
335
+ const want = await desiredFile(runner, news);
336
+ if (want.sha256 !== output.sha256)
337
+ return { action: "update" };
338
+ const live = await readFileAttributes(runner, news.path);
339
+ return live !== undefined && matches(live, want) ? { action: "noop" } : { action: "update" };
340
+ };
341
+ var reconcileFile = async (runner, props, output) => {
342
+ const want = await desiredFile(runner, props);
343
+ if (want.uid !== undefined && want.uid !== runner.effectiveUid() && !canActAsRoot(runner)) {
344
+ throw refuse(props.path, `owner ${String(props.owner)} is not the deploying user; only root may chown. Deploy as root ` + "or through a privileged HostRunner (privileged: true). This provider never calls sudo.");
345
+ }
346
+ const stat = await runner.stat(props.path);
347
+ if (stat !== undefined && stat.kind !== "file") {
348
+ throw refuse(props.path, `is a ${stat.kind}; remove it from whatever owns it before declaring it here`);
349
+ }
350
+ const before = stat === undefined ? undefined : await readFileAttributes(runner, props.path);
351
+ const moved = output !== undefined && output.path !== props.path;
352
+ const prior = moved ? undefined : output;
353
+ if (prior === undefined && before !== undefined && !matches(before, want)) {
354
+ throw refuse(props.path, "already exists and is not this resource. Remove it, or declare it as a new resource and " + "deploy with --adopt.");
355
+ }
356
+ if (before === undefined || !matches(before, want)) {
357
+ await runner.writeFileAtomic(props.path, want.bytes, {
358
+ mode: want.mode,
359
+ ...want.uid === undefined ? {} : { uid: want.uid },
360
+ ...want.gid === undefined ? {} : { gid: want.gid }
361
+ });
362
+ }
363
+ const after = await readFileAttributes(runner, props.path);
364
+ if (after === undefined || !matches(after, want)) {
365
+ if (before === undefined)
366
+ await runner.removeFile(props.path).catch(() => {
367
+ return;
368
+ });
369
+ throw refuse(props.path, "the write returned but the file on disk does not match the declaration");
370
+ }
371
+ if (moved)
372
+ await deleteFile(runner, output);
373
+ return after;
374
+ };
375
+ var deleteFile = async (runner, output) => {
376
+ const stat = await runner.stat(output.path);
377
+ if (stat === undefined)
378
+ return;
379
+ if (stat.kind !== "file")
380
+ throw refuse(output.path, `is now a ${stat.kind}; not removing it`);
381
+ await runner.removeFile(output.path);
382
+ };
383
+
384
+ // src/launchd/host-file.ts
385
+ var HostFile = Resource("Host.File");
386
+ var HostFileProvider = () => Provider.effect(HostFile, Effect2.gen(function* () {
387
+ const runner = yield* HostRunnerService;
388
+ return HostFile.Provider.of({
389
+ list: () => Effect2.succeed([]),
390
+ read: ({ olds, output }) => lift(async () => {
391
+ const found = await readFileAttributes(runner, olds.path);
392
+ if (found === undefined)
393
+ return;
394
+ return output === undefined ? Unowned(found) : found;
395
+ }),
396
+ diff: ({ news, output }) => {
397
+ if (output === undefined)
398
+ return Effect2.succeed(undefined);
399
+ if (isResolved(news))
400
+ return lift(() => diffFile(runner, news, output));
401
+ const path = resolvedString(news, "path");
402
+ return Effect2.succeed(path !== undefined && path !== output.path ? { action: "replace" } : undefined);
403
+ },
404
+ reconcile: ({ news, output }) => lift(() => reconcileFile(runner, news, output)),
405
+ delete: ({ output }) => lift(() => deleteFile(runner, output))
406
+ });
407
+ }));
408
+ // src/launchd/job.ts
409
+ import { Resource as Resource2 } from "alchemy";
410
+ import { Unowned as Unowned2 } from "alchemy/AdoptPolicy";
411
+ import { isResolved as isResolved2 } from "alchemy/Diff";
412
+ import * as Provider2 from "alchemy/Provider";
413
+ import * as Effect3 from "effect/Effect";
414
+
415
+ // src/launchd/job-validate.ts
416
+ var LABEL = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
417
+ var RESERVED_LABEL_PREFIXES = [
418
+ "org.nixos.",
419
+ "com.apple.",
420
+ "homebrew.mxcl."
421
+ ];
422
+ var ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
423
+ var RANGES = [
424
+ ["minute", 0, 59],
425
+ ["hour", 0, 23],
426
+ ["day", 1, 31],
427
+ ["weekday", 0, 7],
428
+ ["month", 1, 12]
429
+ ];
430
+ var isInt = (value, min, max = Number.MAX_SAFE_INTEGER) => Number.isSafeInteger(value) && value >= min && value <= max;
431
+ var calendarProblems = (entries) => {
432
+ if (entries.length === 0)
433
+ return ["startCalendarInterval is an empty list; omit it instead"];
434
+ const found = [];
435
+ for (const [index, entry] of entries.entries()) {
436
+ for (const [field, min, max] of RANGES) {
437
+ const value = entry[field];
438
+ if (value !== undefined && !isInt(value, min, max)) {
439
+ found.push(`startCalendarInterval[${String(index)}].${field} must be ${String(min)}–${String(max)}`);
440
+ }
441
+ }
442
+ }
443
+ return found;
444
+ };
445
+ var absolute = (name, value) => value === undefined || value.startsWith("/") ? [] : [`${name} must be an absolute path`];
446
+ var identityProblems2 = (label, domainText) => {
447
+ const found = [];
448
+ if (!LABEL.test(label)) {
449
+ found.push("label must be 1–200 of [A-Za-z0-9._-], starting with a letter or digit");
450
+ }
451
+ const reserved = RESERVED_LABEL_PREFIXES.find((prefix) => label.startsWith(prefix));
452
+ if (reserved !== undefined) {
453
+ found.push(`label ${label} is under ${reserved}, which another tool owns; declare a new label`);
454
+ }
455
+ if (parseDomain(domainText) === undefined)
456
+ found.push(`domain must be 'system' or 'gui/<uid>', not ${domainText}`);
457
+ return found;
458
+ };
459
+ var jobProblems = (props) => {
460
+ const found = identityProblems2(props.label, props.domain);
461
+ const domain = parseDomain(props.domain);
462
+ const [program] = props.programArguments;
463
+ if (program === undefined)
464
+ found.push("programArguments must name a program");
465
+ else if (!program.startsWith("/"))
466
+ found.push("programArguments[0] must be an absolute path");
467
+ for (const name of Object.keys(props.environment ?? {})) {
468
+ if (!ENV_NAME.test(name))
469
+ found.push(`environment name ${JSON.stringify(name)} is not a valid variable name`);
470
+ }
471
+ found.push(...jobSecretProblems(props.environment, props.programArguments, props.extraKeys));
472
+ if (props.startInterval !== undefined && !isInt(props.startInterval, 1)) {
473
+ found.push("startInterval must be a positive whole number of seconds");
474
+ }
475
+ if (props.throttleInterval !== undefined && !isInt(props.throttleInterval, 0)) {
476
+ found.push("throttleInterval must be a non-negative whole number of seconds");
477
+ }
478
+ const calendar = props.startCalendarInterval;
479
+ if (calendar !== undefined) {
480
+ found.push(...calendarProblems(Array.isArray(calendar) ? calendar : [calendar]));
481
+ }
482
+ const keepAlive = props.keepAlive;
483
+ if (typeof keepAlive === "object" && keepAlive.successfulExit === undefined && keepAlive.crashed === undefined) {
484
+ found.push("keepAlive {} names no condition; use true, false, or a condition");
485
+ }
486
+ found.push(...absolute("standardOutPath", props.standardOutPath));
487
+ found.push(...absolute("standardErrorPath", props.standardErrorPath));
488
+ found.push(...absolute("workingDirectory", props.workingDirectory));
489
+ if (domain?.kind === "gui" && (props.userName !== undefined || props.groupName !== undefined)) {
490
+ found.push("userName/groupName apply only to the system domain; launchd ignores them for agents");
491
+ }
492
+ for (const key of Object.keys(props.extraKeys ?? {})) {
493
+ if (OWNED_KEYS.includes(key))
494
+ found.push(`extraKeys.${key} is owned by a typed prop; use that instead`);
495
+ }
496
+ return found;
497
+ };
498
+
499
+ // src/launchd/launchctl.ts
500
+ var LAUNCHCTL = "/bin/launchctl";
501
+ var NOT_FOUND = 113;
502
+ var NO_DOMAIN = 112;
503
+ var NO_DOMAIN_TEXT = "Could not find domain";
504
+
505
+ class LaunchctlError extends Error {
506
+ constructor(command, exitCode, stderr) {
507
+ super(`launchctl ${command} -> ${String(exitCode)}: ${stderr.trim().slice(0, 300)}`);
508
+ this.name = "LaunchctlError";
509
+ }
510
+ }
511
+ var printFields = (stdout) => {
512
+ const fields = new Map;
513
+ const closers = [];
514
+ for (const line of stdout.split(`
515
+ `)) {
516
+ if (closers.length === 0) {
517
+ if (fields.size === 0 && line.endsWith(" = {"))
518
+ closers.push("}");
519
+ continue;
520
+ }
521
+ if (line === closers[closers.length - 1]) {
522
+ closers.pop();
523
+ continue;
524
+ }
525
+ const opened = /^(\t+)\S.* = \{$/.exec(line);
526
+ if (opened?.[1] !== undefined && opened[1].length === closers.length) {
527
+ closers.push(`${opened[1]}}`);
528
+ continue;
529
+ }
530
+ const field = closers.length === 1 ? /^\t([a-z][a-z ]*[a-z]) = (.*)$/.exec(line) : null;
531
+ if (field?.[1] !== undefined && field[2] !== undefined && !fields.has(field[1])) {
532
+ fields.set(field[1], field[2]);
533
+ }
534
+ }
535
+ return fields;
536
+ };
537
+ var leadingInt = (text) => {
538
+ const match = text === undefined ? null : /^(-?\d+)/.exec(text);
539
+ return match?.[1] === undefined ? undefined : Number(match[1]);
540
+ };
541
+ var parsePrint = (stdout) => {
542
+ const fields = printFields(stdout);
543
+ const state = fields.get("state");
544
+ const pid = leadingInt(fields.get("pid"));
545
+ const lastExitCode = leadingInt(fields.get("last exit code"));
546
+ return {
547
+ loaded: true,
548
+ ...state === undefined ? {} : { state },
549
+ ...pid === undefined ? {} : { pid },
550
+ ...lastExitCode === undefined ? {} : { lastExitCode }
551
+ };
552
+ };
553
+ var parseDisabled = (stdout) => {
554
+ const disabled = new Set;
555
+ for (const match of stdout.matchAll(/^\s*"([^"]+)" => (\w+)\s*$/gm)) {
556
+ if (match[1] !== undefined && (match[2] === "disabled" || match[2] === "true")) {
557
+ disabled.add(match[1]);
558
+ }
559
+ }
560
+ return disabled;
561
+ };
562
+ var printService = async (runner, target) => {
563
+ const result = await runner.exec([LAUNCHCTL, "print", target]);
564
+ if (result.exitCode === NOT_FOUND)
565
+ return { loaded: false };
566
+ if (result.exitCode === NO_DOMAIN && result.stderr.includes(NO_DOMAIN_TEXT)) {
567
+ return { domainMissing: true, loaded: false };
568
+ }
569
+ if (result.exitCode !== 0)
570
+ throw new LaunchctlError(`print ${target}`, result.exitCode, result.stderr);
571
+ return parsePrint(result.stdout);
572
+ };
573
+ var isDisabled = async (runner, domain, label) => {
574
+ const result = await runner.exec([LAUNCHCTL, "print-disabled", domain]);
575
+ if (result.exitCode !== 0) {
576
+ throw new LaunchctlError(`print-disabled ${domain}`, result.exitCode, result.stderr);
577
+ }
578
+ return parseDisabled(result.stdout).has(label);
579
+ };
580
+ var bootstrap = async (runner, domain, plistPath) => {
581
+ const result = await runner.exec([LAUNCHCTL, "bootstrap", domain, plistPath]);
582
+ if (result.exitCode !== 0) {
583
+ throw new LaunchctlError(`bootstrap ${domain} ${plistPath}`, result.exitCode, result.stderr);
584
+ }
585
+ };
586
+ var waitUnloaded = async (runner, target, attempts = 120, intervalMs = 250) => {
587
+ for (let attempt = 0;attempt < attempts; attempt += 1) {
588
+ if (!(await printService(runner, target)).loaded)
589
+ return;
590
+ await runner.sleep(intervalMs);
591
+ }
592
+ throw new Error(`${target} is still loaded ${String(attempts * intervalMs / 1000)}s after bootout`);
593
+ };
594
+ var bootoutIfLoaded = async (runner, target) => {
595
+ if (!(await printService(runner, target)).loaded)
596
+ return;
597
+ const result = await runner.exec([LAUNCHCTL, "bootout", target]);
598
+ try {
599
+ await waitUnloaded(runner, target);
600
+ } catch (cause) {
601
+ if (result.exitCode === 0)
602
+ throw cause;
603
+ throw new LaunchctlError(`bootout ${target}`, result.exitCode, `${result.stderr.trim()} (and still loaded after waiting)`);
604
+ }
605
+ };
606
+
607
+ // src/launchd/job-preflight.ts
608
+ var refuse2 = (label, message) => new Error(`Launchd.Job ${label}: ${message}`);
609
+ var assertValid = (props) => {
610
+ const found = jobProblems(props);
611
+ if (found.length > 0)
612
+ throw refuse2(props.label, found.join("; "));
613
+ };
614
+ var locate = async (runner, props) => {
615
+ const domain = parseDomain(props.domain);
616
+ if (domain === undefined)
617
+ throw refuse2(props.label, `unknown domain ${props.domain}`);
618
+ const home = domain.kind === "gui" ? (await runner.lookupUser(String(domain.uid)))?.home : undefined;
619
+ return {
620
+ domain,
621
+ plistPath: plistPathFor(props.label, domain, home),
622
+ target: serviceTarget(props.label, props.domain)
623
+ };
624
+ };
625
+ var assertMayWrite = (runner, label, domain) => {
626
+ if (canActAsRoot(runner))
627
+ return;
628
+ if (domain.kind === "system") {
629
+ throw refuse2(label, "the system domain needs root. Run the deploy as root, or provide a HostRunner that is " + "deliberately privileged (privileged: true). This provider never calls sudo.");
630
+ }
631
+ if (domain.uid !== runner.effectiveUid()) {
632
+ throw refuse2(label, `gui/${String(domain.uid)} belongs to another user; only root may write it. ` + "Deploy as that user, as root, or through a privileged HostRunner.");
633
+ }
634
+ };
635
+ var preflight = async (runner, props) => {
636
+ const location = await locate(runner, props);
637
+ assertMayWrite(runner, props.label, location.domain);
638
+ const status = await printService(runner, location.target);
639
+ if (status.domainMissing === true) {
640
+ throw refuse2(props.label, `${props.domain} does not exist: that user has no login session, and launchd creates a gui ` + "domain only at login. Log the user in (or declare a system-domain job with userName), " + "then redeploy. Nothing was written.");
641
+ }
642
+ if (await isDisabled(runner, props.domain, props.label)) {
643
+ throw refuse2(props.label, `${location.target} is disabled (launchctl print-disabled ${props.domain}). ` + `If that is stale, run \`launchctl enable ${location.target}\` deliberately, then redeploy.`);
644
+ }
645
+ return { location, status };
646
+ };
647
+ var assertUnclaimed = async (runner, label, location, status, renderedSha256) => {
648
+ const bytes = await runner.readFile(location.plistPath);
649
+ const foreignFile = bytes !== undefined && sha256Hex(bytes) !== renderedSha256;
650
+ if (foreignFile || bytes === undefined && status.loaded) {
651
+ throw refuse2(label, `${location.target} is already on this host (${foreignFile ? location.plistPath : "loaded"})` + " and is not this resource. Remove it, or declare it as a new resource and deploy with " + "--adopt.");
652
+ }
653
+ };
654
+ var assertReplaceable = async (runner, next, full, old, renderedSha256) => {
655
+ const found = full === undefined ? identityProblems2(next.label, next.domain) : jobProblems(full);
656
+ if (found.length > 0)
657
+ throw refuse2(next.label, found.join("; "));
658
+ const oldDomain = parseDomain(old.domain);
659
+ if (oldDomain === undefined)
660
+ throw refuse2(old.label, `unknown domain ${old.domain}`);
661
+ assertMayWrite(runner, old.label, oldDomain);
662
+ const { location, status } = await preflight(runner, next);
663
+ await assertUnclaimed(runner, next.label, location, status, renderedSha256);
664
+ };
665
+
666
+ // src/launchd/job-lifecycle.ts
667
+ var attributesOf = (props, location, plistSha256, status) => ({
668
+ domain: props.domain,
669
+ label: props.label,
670
+ loaded: status.loaded,
671
+ plistPath: location.plistPath,
672
+ plistSha256,
673
+ serviceTarget: location.target,
674
+ ...status.state === undefined ? {} : { state: status.state },
675
+ ...status.pid === undefined ? {} : { pid: status.pid },
676
+ ...status.lastExitCode === undefined ? {} : { lastExitCode: status.lastExitCode }
677
+ });
678
+ var readJob = async (runner, props) => {
679
+ const location = await locate(runner, props);
680
+ const bytes = await runner.readFile(location.plistPath);
681
+ const status = await printService(runner, location.target);
682
+ if (bytes === undefined && !status.loaded)
683
+ return;
684
+ return attributesOf(props, location, bytes === undefined ? "" : sha256Hex(bytes), status);
685
+ };
686
+ var replaceDiff = async (runner, next, full, output) => {
687
+ if (full !== undefined)
688
+ assertValid(full);
689
+ const rendered = full === undefined ? undefined : renderJob(full).sha256;
690
+ await assertReplaceable(runner, next, full, output, rendered);
691
+ return { action: "replace", deleteFirst: true };
692
+ };
693
+ var diffJob = async (runner, news, output) => {
694
+ if (news.label !== output.label || news.domain !== output.domain) {
695
+ return replaceDiff(runner, news, news, output);
696
+ }
697
+ assertValid(news);
698
+ const desired = renderJob(news).sha256;
699
+ if (desired !== output.plistSha256)
700
+ return { action: "update" };
701
+ const live = await readJob(runner, news);
702
+ if (live === undefined || !live.loaded || live.plistSha256 !== desired)
703
+ return { action: "update" };
704
+ return { action: "noop" };
705
+ };
706
+ var reconcileJob = async (runner, props, output) => {
707
+ assertValid(props);
708
+ const { location, status } = await preflight(runner, props);
709
+ const rendered = renderJob(props);
710
+ const renamed = output !== undefined && (output.label !== props.label || output.domain !== props.domain);
711
+ const prior = renamed ? undefined : output;
712
+ if (prior === undefined) {
713
+ await assertUnclaimed(runner, props.label, location, status, rendered.sha256);
714
+ }
715
+ if (renamed)
716
+ await deleteJob(runner, output);
717
+ const before = await runner.readFile(location.plistPath);
718
+ if (status.loaded && before !== undefined && sha256Hex(before) === rendered.sha256 && prior?.plistSha256 === rendered.sha256) {
719
+ return attributesOf(props, location, rendered.sha256, status);
720
+ }
721
+ await runner.writeFileAtomic(location.plistPath, rendered.bytes, location.domain.kind === "system" ? { gid: 0, mode: 420, uid: 0 } : { mode: 420, uid: location.domain.uid });
722
+ await bootoutIfLoaded(runner, location.target);
723
+ const failed = async (detail) => {
724
+ const removed = before === undefined && await runner.removeFile(location.plistPath).then(() => true, () => false);
725
+ throw refuse2(props.label, removed ? `bootstrap failed, so the job is NOT RUNNING; the plist this deploy wrote was removed ` + `again, so the next deploy starts clean. ${detail}` : `bootstrap failed, so the job is NOT RUNNING; the new plist is at ${location.plistPath} ` + `and the next deploy retries. ${detail}`);
726
+ };
727
+ try {
728
+ await bootstrap(runner, props.domain, location.plistPath);
729
+ } catch (cause) {
730
+ return failed(cause instanceof Error ? cause.message : String(cause));
731
+ }
732
+ const after = await printService(runner, location.target);
733
+ if (!after.loaded)
734
+ return failed("bootstrap exited 0 but launchctl print cannot find the job");
735
+ return attributesOf(props, location, rendered.sha256, after);
736
+ };
737
+ var deleteJob = async (runner, output) => {
738
+ const domain = parseDomain(output.domain);
739
+ if (domain === undefined)
740
+ throw refuse2(output.label, `unknown domain ${output.domain}`);
741
+ assertMayWrite(runner, output.label, domain);
742
+ await bootoutIfLoaded(runner, output.serviceTarget);
743
+ await runner.removeFile(output.plistPath);
744
+ };
745
+
746
+ // src/launchd/job.ts
747
+ var LaunchdJob = Resource2("Launchd.Job");
748
+ var LaunchdJobProvider = () => Provider2.effect(LaunchdJob, Effect3.gen(function* () {
749
+ const runner = yield* HostRunnerService;
750
+ return LaunchdJob.Provider.of({
751
+ list: () => Effect3.succeed([]),
752
+ read: ({ olds, output }) => lift(async () => {
753
+ const found = await readJob(runner, olds);
754
+ if (found === undefined)
755
+ return;
756
+ return output === undefined ? Unowned2(found) : found;
757
+ }),
758
+ diff: ({ news, output }) => {
759
+ if (output === undefined)
760
+ return Effect3.succeed(undefined);
761
+ if (isResolved2(news))
762
+ return lift(() => diffJob(runner, news, output));
763
+ const label = resolvedString(news, "label");
764
+ const domain = resolvedString(news, "domain");
765
+ return label !== undefined && domain !== undefined && (label !== output.label || domain !== output.domain) ? lift(() => replaceDiff(runner, { domain, label }, undefined, output)) : Effect3.succeed(undefined);
766
+ },
767
+ reconcile: ({ news, output }) => lift(() => reconcileJob(runner, news, output)),
768
+ delete: ({ output }) => lift(() => deleteJob(runner, output))
769
+ });
770
+ }));
771
+ // src/launchd/local-runner.ts
772
+ import { spawn } from "node:child_process";
773
+ import { randomBytes } from "node:crypto";
774
+ import { lstat, open, readFile, rename, unlink } from "node:fs/promises";
775
+ import { basename, dirname, join } from "node:path";
776
+
777
+ // src/launchd/host-lookup.ts
778
+ var NUMERIC = /^\d+$/;
779
+ var userQuery = (platform, nameOrId) => platform === "darwin" ? [
780
+ "/usr/bin/dscacheutil",
781
+ "-q",
782
+ "user",
783
+ "-a",
784
+ NUMERIC.test(nameOrId) ? "uid" : "name",
785
+ nameOrId
786
+ ] : ["getent", "passwd", nameOrId];
787
+ var groupQuery = (platform, nameOrId) => platform === "darwin" ? [
788
+ "/usr/bin/dscacheutil",
789
+ "-q",
790
+ "group",
791
+ "-a",
792
+ NUMERIC.test(nameOrId) ? "gid" : "name",
793
+ nameOrId
794
+ ] : ["getent", "group", nameOrId];
795
+ var firstRecord = (stdout) => {
796
+ const fields = new Map;
797
+ for (const line of stdout.split(`
798
+ `)) {
799
+ if (line.trim() === "") {
800
+ if (fields.size > 0)
801
+ break;
802
+ continue;
803
+ }
804
+ const at = line.indexOf(": ");
805
+ if (at <= 0)
806
+ continue;
807
+ const key = line.slice(0, at);
808
+ if (key === "password")
809
+ continue;
810
+ fields.set(key, line.slice(at + 2).trim());
811
+ }
812
+ return fields;
813
+ };
814
+ var toId = (text) => text !== undefined && NUMERIC.test(text) ? Number(text) : undefined;
815
+ var parseUser = (platform, stdout) => {
816
+ if (platform === "darwin") {
817
+ const record = firstRecord(stdout);
818
+ const uid2 = toId(record.get("uid"));
819
+ const gid2 = toId(record.get("gid"));
820
+ const home2 = record.get("dir");
821
+ return uid2 === undefined || gid2 === undefined || home2 === undefined ? undefined : { gid: gid2, home: home2, uid: uid2 };
822
+ }
823
+ const fields = (stdout.split(`
824
+ `)[0] ?? "").split(":");
825
+ const uid = toId(fields[2]);
826
+ const gid = toId(fields[3]);
827
+ const home = fields[5];
828
+ return uid === undefined || gid === undefined || home === undefined || home === "" ? undefined : { gid, home, uid };
829
+ };
830
+ var parseGroup = (platform, stdout) => {
831
+ if (platform === "darwin")
832
+ return toId(firstRecord(stdout).get("gid"));
833
+ return toId((stdout.split(`
834
+ `)[0] ?? "").split(":")[2]);
835
+ };
836
+
837
+ // src/launchd/local-runner.ts
838
+ var code = (cause) => typeof cause === "object" && cause !== null && ("code" in cause) ? String(cause.code) : undefined;
839
+ var absent = (cause) => code(cause) === "ENOENT" || code(cause) === "ENOTDIR";
840
+ var run = (argv, timeoutMs) => new Promise((resolve, reject) => {
841
+ const [program, ...args] = argv;
842
+ if (program === undefined) {
843
+ reject(new Error("exec: empty argv"));
844
+ return;
845
+ }
846
+ const child = spawn(program, args, {
847
+ shell: false,
848
+ stdio: ["ignore", "pipe", "pipe"],
849
+ timeout: timeoutMs
850
+ });
851
+ const out = [];
852
+ const err = [];
853
+ child.stdout.on("data", (chunk) => out.push(chunk));
854
+ child.stderr.on("data", (chunk) => err.push(chunk));
855
+ child.on("error", (cause) => {
856
+ reject(new Error(`exec ${program}: ${cause.message}`));
857
+ });
858
+ child.on("close", (exitCode, signal) => {
859
+ const stderr = Buffer.concat(err).toString("utf8");
860
+ resolve({
861
+ exitCode: exitCode ?? -1,
862
+ stderr: signal === null ? stderr : `${stderr}
863
+ (killed by ${signal})`.trim(),
864
+ stdout: Buffer.concat(out).toString("utf8")
865
+ });
866
+ });
867
+ });
868
+ var kindOf = (stats) => {
869
+ if (stats.isSymbolicLink())
870
+ return "symlink";
871
+ if (stats.isFile())
872
+ return "file";
873
+ if (stats.isDirectory())
874
+ return "directory";
875
+ return "other";
876
+ };
877
+ var writeAtomic = async (path, bytes, options) => {
878
+ const temp = join(dirname(path), `.${basename(path)}.${randomBytes(6).toString("hex")}.tmp`);
879
+ const handle = await open(temp, "wx", 384);
880
+ try {
881
+ try {
882
+ await handle.writeFile(bytes);
883
+ if (options.uid !== undefined || options.gid !== undefined) {
884
+ await handle.chown(options.uid ?? -1, options.gid ?? -1);
885
+ }
886
+ await handle.chmod(options.mode);
887
+ await handle.sync();
888
+ } finally {
889
+ await handle.close();
890
+ }
891
+ await rename(temp, path);
892
+ } catch (cause) {
893
+ await unlink(temp).catch(() => {
894
+ return;
895
+ });
896
+ throw cause;
897
+ }
898
+ };
899
+ var localRunner = (options = {}) => {
900
+ const timeoutMs = options.execTimeoutMs ?? 120000;
901
+ const platform = process.platform;
902
+ return {
903
+ effectiveUid: () => process.geteuid?.() ?? -1,
904
+ exec: (argv) => run(argv, timeoutMs),
905
+ lookupGroup: async (nameOrId) => {
906
+ const result = await run(groupQuery(platform, nameOrId), timeoutMs);
907
+ return result.exitCode === 0 ? parseGroup(platform, result.stdout) : undefined;
908
+ },
909
+ lookupUser: async (nameOrId) => {
910
+ const result = await run(userQuery(platform, nameOrId), timeoutMs);
911
+ return result.exitCode === 0 ? parseUser(platform, result.stdout) : undefined;
912
+ },
913
+ privileged: false,
914
+ readFile: async (path) => {
915
+ try {
916
+ return new Uint8Array(await readFile(path));
917
+ } catch (cause) {
918
+ if (absent(cause))
919
+ return;
920
+ throw cause;
921
+ }
922
+ },
923
+ removeFile: async (path) => {
924
+ try {
925
+ await unlink(path);
926
+ } catch (cause) {
927
+ if (!absent(cause))
928
+ throw cause;
929
+ }
930
+ },
931
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
932
+ stat: async (path) => {
933
+ try {
934
+ const stats = await lstat(path);
935
+ return {
936
+ gid: stats.gid,
937
+ kind: kindOf(stats),
938
+ mode: stats.mode & 4095,
939
+ size: stats.size,
940
+ uid: stats.uid
941
+ };
942
+ } catch (cause) {
943
+ if (absent(cause))
944
+ return;
945
+ throw cause;
946
+ }
947
+ },
948
+ writeFileAtomic: writeAtomic
949
+ };
950
+ };
951
+ // src/launchd/providers.ts
952
+ import * as Layer2 from "effect/Layer";
953
+ var launchdProviders = (runner = localRunner()) => Layer2.mergeAll(LaunchdJobProvider(), HostFileProvider()).pipe(Layer2.provide(hostRunnerLayer(runner)));
954
+ export {
955
+ HostFile,
956
+ HostFileProvider,
957
+ HostRunnerService,
958
+ LaunchdJob,
959
+ LaunchdJobProvider,
960
+ PlistError,
961
+ canActAsRoot,
962
+ hostRunnerLayer,
963
+ launchdProviders,
964
+ localRunner,
965
+ renderPlist
966
+ };
967
+
968
+ //# debugId=86750D103F13F7F764756E2164756E21
969
+ //# sourceMappingURL=index.js.map