openship 0.6.1 → 0.6.6

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 (67) hide show
  1. package/README.md +7 -2
  2. package/dist/{bare-IWS2QHJ3.js → bare-OXJVW3AK.js} +11 -9
  3. package/dist/{chunk-VZ2ZHPS2.js → chunk-2YM3NI2E.js} +3 -9
  4. package/dist/chunk-4I2BM2DT.js +68 -0
  5. package/dist/{chunk-2NZSUXOB.js → chunk-5DMWSTOM.js} +310 -1459
  6. package/dist/{chunk-3FYIXVX5.js → chunk-77QDF6UD.js} +581 -122
  7. package/dist/{chunk-IOT5ZX23.js → chunk-A4GXYE4L.js} +449 -383
  8. package/dist/chunk-CBLD2HS5.js +428 -0
  9. package/dist/chunk-ELGRMZOW.js +25 -0
  10. package/dist/{chunk-TWJCLD4W.js → chunk-FE7ZZQSE.js} +5 -5
  11. package/dist/{chunk-3NZICQHF.js → chunk-HKRQXPTI.js} +31 -8
  12. package/dist/{chunk-IXM5245F.js → chunk-LFE5KQ7Y.js} +553 -538
  13. package/dist/{chunk-VDDMADOF.js → chunk-LH2PLRQO.js} +16 -2
  14. package/dist/{chunk-EMXI4UCU.js → chunk-LUKYPVJ4.js} +29 -10
  15. package/dist/{chunk-UC7YIEHT.js → chunk-MLYSEZQB.js} +352 -47
  16. package/dist/chunk-NK5N5LXN.js +1681 -0
  17. package/dist/{chunk-RRJJGVKE.js → chunk-OBPKYXQL.js} +75 -17
  18. package/dist/{chunk-6ODUBKUB.js → chunk-PCPVTH6M.js} +78 -88
  19. package/dist/chunk-Q7WGTQQ5.js +88 -0
  20. package/dist/{chunk-EW6KB2J7.js → chunk-QVW76WDL.js} +440 -74
  21. package/dist/{chunk-YIPMDGG6.js → chunk-RSR7PFIQ.js} +79 -23
  22. package/dist/{chunk-3LDWWY7U.js → chunk-SM26HDS2.js} +41 -7
  23. package/dist/{chunk-OTLSJYAC.js → chunk-UJ6JZM65.js} +5185 -341
  24. package/dist/chunk-UVNMQM7W.js +444 -0
  25. package/dist/chunk-VTGPEC6B.js +308 -0
  26. package/dist/chunk-WVAML4H6.js +2145 -0
  27. package/dist/{chunk-JIMUYEKN.js → chunk-YIMOCZBZ.js} +1 -1
  28. package/dist/chunk-YMMQRE5J.js +292 -0
  29. package/dist/cloud-75VIT4KV.js +37 -0
  30. package/dist/{detect-ALF4GXRX.js → detect-OIE5V3VZ.js} +3 -3
  31. package/dist/{docker-QECQJ5P4.js → docker-VTR2GK5Y.js} +16 -8
  32. package/dist/{docker-edge-executor-62N64MGS.js → docker-edge-executor-22IBXNYJ.js} +5 -5
  33. package/dist/{edge-import-SAYTRJLO.js → edge-import-WJUB4M7F.js} +27 -23
  34. package/dist/edge-preflight-GBNCPNVU.js +42 -0
  35. package/dist/{ensure-container-edge-CFSCPGZK.js → ensure-container-edge-KPI2CVXJ.js} +9 -8
  36. package/dist/{executor-PT6UEJ5M.js → executor-7LXXZPBR.js} +15 -5
  37. package/dist/host-channel-preflight-KE6OONYK.js +50 -0
  38. package/dist/index.js +3783 -4221
  39. package/dist/{nginx-YOEEFBTE.js → nginx-KUFMLMAC.js} +13 -5
  40. package/dist/node-bootstrap.js +241 -0
  41. package/dist/node-entry.js +52 -1
  42. package/dist/{openresty-lua-WGBO2M3Z.js → openresty-lua-ATTH35BE.js} +8 -2
  43. package/dist/privilege-I4W3ZNZK.js +19 -0
  44. package/dist/server/engine/samples/amavisd/amavisd.conf +25 -0
  45. package/dist/server/engine/samples/fail2ban/jail.d/sshd.local +4 -1
  46. package/dist/server/engine/samples/postfix/master.cf +14 -0
  47. package/dist/server/index.js +77659 -61909
  48. package/dist/server/migrations/0098_instance_product_mode.sql +12 -0
  49. package/dist/server/migrations/0099_mail_webmail_project.sql +24 -0
  50. package/dist/server/migrations/0100_server_ssh_private_key.sql +11 -0
  51. package/dist/server/migrations/0101_mail_resume_step.sql +12 -0
  52. package/dist/server/migrations/0102_project_workload_axes.sql +19 -0
  53. package/dist/server/migrations/0103_dns_credential.sql +41 -0
  54. package/dist/server/migrations/0104_instance_host_control.sql +15 -0
  55. package/dist/server/migrations/0105_mail_inbound_rules.sql +80 -0
  56. package/dist/server/migrations/0106_mcp_call_tracking.sql +23 -0
  57. package/dist/server/migrations/0107_build_minute_metering_indexes.sql +20 -0
  58. package/dist/server/migrations/0108_credential.sql +68 -0
  59. package/dist/server/migrations/meta/_journal.json +77 -0
  60. package/dist/{setup-M2ZDGH3O.js → setup-XGLGUUFW.js} +10 -9
  61. package/dist/ssh-client-JBMA7SN4.js +28 -0
  62. package/dist/up-K4BV7466.js +57 -0
  63. package/package.json +4 -2
  64. package/dist/chunk-IW2UKBVF.js +0 -144
  65. package/dist/chunk-ZCFVHA25.js +0 -89
  66. package/dist/cloud-M2YLWEXO.js +0 -31
  67. /package/dist/{dist-es-JGBPABJI.js → dist-es-AXFRS7PS.js} +0 -0
@@ -0,0 +1,444 @@
1
+ #!/usr/bin/env sh
2
+ ":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
3
+ import { createRequire as __ospCreateRequire } from "node:module";
4
+ const require = __ospCreateRequire(import.meta.url);
5
+ import {
6
+ sq
7
+ } from "./chunk-ZFJXPCQP.js";
8
+ import {
9
+ assessHostSupport,
10
+ classifyDistro,
11
+ formatDuration,
12
+ isRemoteConnectionError,
13
+ parseOsRelease,
14
+ parseSystemArch,
15
+ parseSystemOs,
16
+ safeErrorMessage,
17
+ systemDebug
18
+ } from "./chunk-UJ6JZM65.js";
19
+
20
+ // ../../packages/adapters/src/system/elevated-executor.ts
21
+ import { randomBytes } from "crypto";
22
+ var INSTALL_ENV = "DEBIAN_FRONTEND=noninteractive DPKG_FORCE=confnew";
23
+ function elevateCommand(command) {
24
+ return `sudo -n sh -c ${sq(`export ${INSTALL_ENV}; ${command}`)}`;
25
+ }
26
+ var EXECUTOR_DELEGATE = /* @__PURE__ */ Symbol.for("openship.executor.delegate");
27
+ function dirOf(path) {
28
+ const idx = path.lastIndexOf("/");
29
+ if (idx < 0) return ".";
30
+ return idx === 0 ? "/" : path.slice(0, idx);
31
+ }
32
+ function elevatedExecutor(inner) {
33
+ const readFileElevated = async (path) => {
34
+ try {
35
+ return await inner.readFile(path);
36
+ } catch (err) {
37
+ try {
38
+ return await inner.exec(elevateCommand(`cat ${sq(path)}`));
39
+ } catch {
40
+ throw err;
41
+ }
42
+ }
43
+ };
44
+ const existsElevated = async (path) => {
45
+ if (await inner.exists(path).catch(() => false)) return true;
46
+ return await inner.exec(elevateCommand(`test -e ${sq(path)}`)).then(() => true).catch(() => false);
47
+ };
48
+ const writeFileElevated = async (path, content) => {
49
+ const stage = `/tmp/.openship-elev-${randomBytes(12).toString("hex")}`;
50
+ await inner.exec(`mkdir -m 700 ${sq(stage)}`);
51
+ try {
52
+ const staged = `${stage}/payload`;
53
+ await inner.writeFile(staged, content);
54
+ await inner.exec(
55
+ elevateCommand(
56
+ `mkdir -p ${sq(dirOf(path))} && chown 0:0 ${sq(staged)} && mv -f ${sq(staged)} ${sq(path)}`
57
+ )
58
+ );
59
+ } finally {
60
+ await inner.exec(`rm -rf ${sq(stage)}`).catch(() => void 0);
61
+ }
62
+ };
63
+ const overrides = {
64
+ exec: (command, opts) => inner.exec(elevateCommand(command), opts),
65
+ streamExec: (command, onLog, opts) => inner.streamExec(elevateCommand(command), onLog, opts),
66
+ writeFile: writeFileElevated,
67
+ readFile: readFileElevated,
68
+ exists: existsElevated,
69
+ // Elevated for the same reason as writeFile: the vhost/conf dirs are root-owned,
70
+ // and the Proxy would otherwise forward this to the inner executor unelevated —
71
+ // EACCES immediately after an elevated write succeeded.
72
+ rename: async (from, to) => {
73
+ await inner.exec(elevateCommand(`mv -f ${sq(from)} ${sq(to)}`));
74
+ },
75
+ mkdir: async (path) => {
76
+ await inner.exec(elevateCommand(`mkdir -p ${sq(path)}`));
77
+ },
78
+ rm: async (path) => {
79
+ await inner.exec(elevateCommand(`rm -rf ${sq(path)}`));
80
+ }
81
+ };
82
+ return new Proxy(inner, {
83
+ get(target, prop) {
84
+ if (prop === EXECUTOR_DELEGATE) return target;
85
+ if (Object.prototype.hasOwnProperty.call(overrides, prop)) {
86
+ return overrides[prop];
87
+ }
88
+ const value = Reflect.get(target, prop, target);
89
+ return typeof value === "function" ? value.bind(target) : value;
90
+ }
91
+ });
92
+ }
93
+ async function ensureOwnedDir(executor, path) {
94
+ const quoted = sq(path);
95
+ try {
96
+ await executor.exec(`mkdir -p ${quoted} && [ -w ${quoted} ]`);
97
+ return;
98
+ } catch {
99
+ }
100
+ const { isRoot, canSudo } = await resolveEnvironment(executor);
101
+ if (isRoot) {
102
+ await executor.exec(`mkdir -p ${quoted}`);
103
+ return;
104
+ }
105
+ if (!canSudo) {
106
+ throw new Error(
107
+ `Cannot create ${path} on the target server: the deploy user cannot write there and has no passwordless sudo. Run these once on the server, then redeploy:
108
+ sudo mkdir -p ${path}
109
+ sudo chown -R "$(id -un)" ${path}`
110
+ );
111
+ }
112
+ const loginUser = await executor.exec("id -un").then((out) => out.trim()).catch(() => "");
113
+ const owner = loginUser ? sq(loginUser) : '"$SUDO_USER"';
114
+ await executor.exec(
115
+ elevateCommand(
116
+ [
117
+ // Without it the `;`-joined steps fall through a failed mkdir, and the
118
+ // chown's exit status — not the mkdir's — is what the caller sees. That
119
+ // reported a permission failure as success.
120
+ "set -e",
121
+ `p=${quoted}`,
122
+ "d=$p",
123
+ "top=",
124
+ 'while [ ! -d "$d" ]; do top=$d; d=$(dirname "$d"); done',
125
+ 'mkdir -p "$p"',
126
+ // Expansion, not `[ -z "$top" ] && top=…`: that AND-list returns 1 whenever
127
+ // $top is already set, which under `set -e` would exit before the chown.
128
+ `chown -R ${owner} "\${top:-$p}"`
129
+ ].join("; ")
130
+ )
131
+ );
132
+ }
133
+
134
+ // ../../packages/adapters/src/system/environment.ts
135
+ var ENVIRONMENT_PROBE_SCRIPT = `echo "opsh_begin=1"
136
+ # Appended, never replaced: \`ufw\`, \`nft\` and \`iptables\` live in /usr/sbin, which is off
137
+ # a non-root user's PATH on Debian \u2014 so "no firewall installed" was sometimes just "not on
138
+ # this user's PATH", and the box came back \`unknown\`.
139
+ PATH="$PATH:/usr/sbin:/sbin"
140
+ echo "opsh_os=$(uname -s 2>/dev/null || true)"
141
+ echo "opsh_arch=$(uname -m 2>/dev/null || true)"
142
+ echo "opsh_uid=$(id -u 2>/dev/null || true)"
143
+ echo "opsh_user=$(id -un 2>/dev/null || true)"
144
+ echo "opsh_home=$HOME"
145
+
146
+ OSR=/etc/os-release
147
+ [ -r "$OSR" ] || OSR=/usr/lib/os-release
148
+ # awk and not sed: \`print\` always terminates its record, while sed reproduces the input
149
+ # byte for byte \u2014 and an os-release with no trailing newline glued the next \`echo\` onto
150
+ # the last forwarded line, so \`opsh_pm\` vanished and a working apt host was refused for
151
+ # having no package manager, brackets intact and nothing to flag it.
152
+ [ -r "$OSR" ] && awk '{ print "opsh_osr:" $0 }' "$OSR" 2>/dev/null || true
153
+
154
+ PM=none
155
+ if command -v apt-get >/dev/null 2>&1; then PM=apt
156
+ elif command -v dnf >/dev/null 2>&1; then PM=dnf
157
+ elif command -v yum >/dev/null 2>&1; then PM=yum
158
+ elif command -v apk >/dev/null 2>&1; then PM=apk
159
+ elif command -v brew >/dev/null 2>&1; then PM=brew
160
+ fi
161
+ echo "opsh_pm=$PM"
162
+
163
+ SM=none
164
+ if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then SM=systemd
165
+ elif command -v rc-service >/dev/null 2>&1; then SM=openrc
166
+ elif command -v launchctl >/dev/null 2>&1; then SM=launchd
167
+ fi
168
+ echo "opsh_sm=$SM"
169
+
170
+ SUDO=n
171
+ if [ "$(id -u 2>/dev/null)" != 0 ]; then
172
+ sudo -n true >/dev/null 2>&1 && SUDO=y
173
+ fi
174
+ echo "opsh_sudo=$SUDO"
175
+
176
+ # Established BEFORE the firewall probes, because reading a ruleset is privileged:
177
+ # \`iptables -S\` and \`firewall-cmd --state\` refuse outright for an unprivileged user, and a
178
+ # refused read used to come back as "no firewall" \u2014 the one answer that makes us skip
179
+ # opening a port. \`-n\` so a box without passwordless sudo fails instead of waiting for a
180
+ # password on a channel with no TTY, and every elevated command below is a READ.
181
+ PRIV=
182
+ [ "$SUDO" = y ] && PRIV="sudo -n"
183
+
184
+ HAS_UFW=n; HAS_FWD=n; HAS_NFT=n; HAS_IPT=n
185
+ command -v ufw >/dev/null 2>&1 && HAS_UFW=y
186
+ command -v firewall-cmd >/dev/null 2>&1 && HAS_FWD=y
187
+ command -v nft >/dev/null 2>&1 && HAS_NFT=y
188
+ command -v iptables >/dev/null 2>&1 && HAS_IPT=y
189
+ FW=unknown
190
+ if [ "$HAS_UFW" = y ]; then
191
+ case "$($PRIV ufw status 2>/dev/null || true)" in *"Status: active"*) FW=ufw ;; esac
192
+ if [ "$FW" = unknown ] && grep -qiE "^[[:space:]]*ENABLED[[:space:]]*=[[:space:]]*yes" /etc/ufw/ufw.conf 2>/dev/null; then FW=ufw; fi
193
+ fi
194
+ if [ "$FW" = unknown ] && [ "$HAS_FWD" = y ]; then
195
+ case "$($PRIV firewall-cmd --state 2>/dev/null || true)" in *running*) FW=firewalld ;; esac
196
+ fi
197
+ if [ "$FW" = unknown ] && [ "$HAS_NFT" = y ]; then
198
+ case "$($PRIV nft list tables 2>/dev/null || true)" in *"table inet filter"*) FW=nftables ;; esac
199
+ fi
200
+ if [ "$FW" = unknown ] && [ "$HAS_IPT" = y ]; then
201
+ IPT="$($PRIV iptables -S INPUT 2>/dev/null || true)"
202
+ case "$IPT" in
203
+ *"-P INPUT DROP"*) FW=iptables ;;
204
+ # A permissive POLICY does not mean a permissive CHAIN. RHEL's stock ruleset and every
205
+ # hand-written iptables-persistent file end INPUT in a catch-all reject with the policy
206
+ # left at ACCEPT, and reading the policy alone reported those hosts as unfirewalled \u2014
207
+ # so nothing opened a port and the service came up unreachable. Any terminal drop counts:
208
+ # a single banned IP over-reports slightly, and over-reporting only costs a warning
209
+ # while under-reporting costs the port. (There is deliberately no \`-P INPUT REJECT\`
210
+ # arm: netfilter has no such built-in policy, so the old one was unreachable.)
211
+ *"-P INPUT "*)
212
+ if printf '%s\\n' "$IPT" | grep -qE '^-A INPUT .*-j (DROP|REJECT)'; then FW=iptables; else FW=none; fi
213
+ ;;
214
+ esac
215
+ fi
216
+ if [ "$FW" = unknown ] && [ "$HAS_UFW$HAS_FWD$HAS_NFT$HAS_IPT" = nnnn ]; then FW=none; fi
217
+ echo "opsh_fw=$FW"
218
+
219
+ LIBC=unknown
220
+ if command -v ldd >/dev/null 2>&1; then
221
+ case "$(ldd --version 2>&1 || true)" in
222
+ *musl*) LIBC=musl ;;
223
+ *GNU*|*GLIBC*|*glibc*) LIBC=glibc ;;
224
+ esac
225
+ fi
226
+ echo "opsh_libc=$LIBC"
227
+
228
+ SEL=absent
229
+ if command -v getenforce >/dev/null 2>&1; then
230
+ SEL=disabled
231
+ case "$(getenforce 2>/dev/null || true)" in
232
+ Enforcing*) SEL=enforcing ;;
233
+ Permissive*) SEL=permissive ;;
234
+ esac
235
+ fi
236
+ echo "opsh_selinux=$SEL"
237
+
238
+ CT=n
239
+ [ -f /.dockerenv ] && CT=y
240
+ [ -f /run/.containerenv ] && CT=y
241
+ if [ "$CT" = n ] && grep -qaE "(docker|lxc|containerd|kubepods|libpod)" /proc/1/cgroup 2>/dev/null; then CT=y; fi
242
+ if [ "$CT" = n ] && command -v systemd-detect-virt >/dev/null 2>&1; then
243
+ systemd-detect-virt --container --quiet >/dev/null 2>&1 && CT=y
244
+ fi
245
+ echo "opsh_container=$CT"
246
+
247
+ echo "opsh_end=1"
248
+ exit 0`;
249
+ var ENVIRONMENT_PROBE_TIMEOUT_MS = 2e4;
250
+ var MAX_INTERCEPT_CHARS = 400;
251
+ var ENVIRONMENT_PROFILE_TTL_MS = 5 * 6e4;
252
+ function profileIsFresh(measuredAt) {
253
+ return Date.now() - measuredAt < ENVIRONMENT_PROFILE_TTL_MS;
254
+ }
255
+ var profileCache = /* @__PURE__ */ new WeakMap();
256
+ function isUnansweredProbe(message) {
257
+ const text = message.trim().toLowerCase();
258
+ if (text.includes("timed out") || text.includes("without an exit status")) return true;
259
+ return EXIT_STATUS_ONLY.test(text);
260
+ }
261
+ var EXIT_STATUS_ONLY = /^exit code (?:-?\d+|null|undefined)$/;
262
+ var SILENT_PROBE = "the probe produced no output at all";
263
+ async function runProbe(executor) {
264
+ const startedAt = Date.now();
265
+ systemDebug("environment", "probe:start");
266
+ try {
267
+ const text = await executor.exec(ENVIRONMENT_PROBE_SCRIPT, {
268
+ timeout: ENVIRONMENT_PROBE_TIMEOUT_MS
269
+ });
270
+ systemDebug("environment", `probe:ok (${formatDuration(startedAt)}) ${text.length}b`);
271
+ return { text, failed: false, unanswered: false };
272
+ } catch (err) {
273
+ if (isRemoteConnectionError(err)) {
274
+ systemDebug(
275
+ "environment",
276
+ `probe:abort (${formatDuration(startedAt)}) ${safeErrorMessage(err)}`
277
+ );
278
+ throw err;
279
+ }
280
+ const message = safeErrorMessage(err);
281
+ systemDebug("environment", `probe:fail (${formatDuration(startedAt)}) ${message}`);
282
+ return { text: message, failed: true, unanswered: isUnansweredProbe(message) };
283
+ }
284
+ }
285
+ function members(all) {
286
+ return Object.keys(all);
287
+ }
288
+ var PACKAGE_MANAGERS = members({
289
+ apt: true,
290
+ dnf: true,
291
+ yum: true,
292
+ apk: true,
293
+ brew: true,
294
+ none: true
295
+ });
296
+ var SERVICE_MANAGERS = members({
297
+ systemd: true,
298
+ openrc: true,
299
+ launchd: true,
300
+ none: true
301
+ });
302
+ var FIREWALLS = members({
303
+ ufw: true,
304
+ firewalld: true,
305
+ nftables: true,
306
+ iptables: true,
307
+ none: true,
308
+ unknown: true
309
+ });
310
+ var LIBCS = members({ glibc: true, musl: true, unknown: true });
311
+ var SELINUX_STATES = members({
312
+ enforcing: true,
313
+ permissive: true,
314
+ disabled: true,
315
+ absent: true
316
+ });
317
+ function oneOf(value, allowed, fallback) {
318
+ const found = allowed.find((member) => member === value?.trim());
319
+ return found ?? fallback;
320
+ }
321
+ function splitProbeOutput(text) {
322
+ const keys = {};
323
+ const osReleaseLines = [];
324
+ for (const line of text.split("\n")) {
325
+ if (line.startsWith("opsh_osr:")) {
326
+ osReleaseLines.push(line.slice("opsh_osr:".length));
327
+ continue;
328
+ }
329
+ if (!line.startsWith("opsh_")) continue;
330
+ const eq = line.indexOf("=");
331
+ if (eq <= 0) continue;
332
+ keys[line.slice(0, eq)] = line.slice(eq + 1).trimEnd();
333
+ }
334
+ return { keys, osRelease: osReleaseLines.join("\n") };
335
+ }
336
+ function summarizeInterception(text) {
337
+ const collapsed = text.split("\n").map((line) => line.trim()).filter(Boolean).join(" ");
338
+ if (!collapsed) return null;
339
+ return collapsed.length > MAX_INTERCEPT_CHARS ? `${collapsed.slice(0, MAX_INTERCEPT_CHARS)}\u2026` : collapsed;
340
+ }
341
+ function parseEnvironmentProbe(probe) {
342
+ const { keys, osRelease } = splitProbeOutput(probe.text);
343
+ const complete = keys.opsh_begin === "1" && keys.opsh_end === "1";
344
+ const said = complete ? null : summarizeInterception(probe.text);
345
+ const unanswered = !complete && (probe.unanswered || said === null);
346
+ const forcedCommand = unanswered ? null : said;
347
+ const probeError = unanswered ? said ?? SILENT_PROBE : null;
348
+ const os = complete ? parseSystemOs(keys.opsh_os) : "unknown";
349
+ const osRaw = complete ? keys.opsh_os?.trim() || null : null;
350
+ const archRaw = complete ? keys.opsh_arch?.trim() || null : null;
351
+ const arch = complete ? parseSystemArch(keys.opsh_arch) : "unknown";
352
+ const osReleaseFields = parseOsRelease(osRelease);
353
+ const distroId = osReleaseFields.ID ?? null;
354
+ const { distro, family } = classifyDistro(distroId, osReleaseFields.ID_LIKE);
355
+ const isRoot = keys.opsh_uid?.trim() === "0";
356
+ const loginUser = keys.opsh_user?.trim() || (isRoot ? "root" : "");
357
+ const home = keys.opsh_home?.trim() || (isRoot ? "/root" : loginUser ? `/home/${loginUser}` : "");
358
+ const packageManager = oneOf(keys.opsh_pm, PACKAGE_MANAGERS, "none");
359
+ const profile = {
360
+ os,
361
+ osRaw,
362
+ arch,
363
+ archRaw,
364
+ // `distro` stays null off Linux, where the concept doesn't apply — the existing
365
+ // contract every call site was written against.
366
+ distro: os === "linux" ? distro : null,
367
+ distroFamily: os === "linux" ? family : "unknown",
368
+ distroId,
369
+ versionId: osReleaseFields.VERSION_ID ?? null,
370
+ prettyName: osReleaseFields.PRETTY_NAME ?? null,
371
+ packageManager,
372
+ serviceManager: oneOf(keys.opsh_sm, SERVICE_MANAGERS, "none"),
373
+ firewall: oneOf(keys.opsh_fw, FIREWALLS, "unknown"),
374
+ libc: oneOf(keys.opsh_libc, LIBCS, "unknown"),
375
+ selinux: oneOf(keys.opsh_selinux, SELINUX_STATES, "absent"),
376
+ isContainer: keys.opsh_container?.trim() === "y",
377
+ isRoot,
378
+ // Meaningless when root, and the script never probes sudo in that case — the
379
+ // deliberate `false` the previous detector also returned.
380
+ canSudo: !isRoot && keys.opsh_sudo?.trim() === "y",
381
+ loginUser,
382
+ home,
383
+ forcedCommand,
384
+ probeError,
385
+ supported: false,
386
+ unsupportedReason: null
387
+ };
388
+ return { ...profile, ...assessHostSupport(profile) };
389
+ }
390
+ function cacheKeyFor(executor) {
391
+ let root = executor;
392
+ for (let hops = 0; hops < 8; hops += 1) {
393
+ const delegate = root[EXECUTOR_DELEGATE];
394
+ if (!delegate || delegate === root) break;
395
+ root = delegate;
396
+ }
397
+ return root;
398
+ }
399
+ async function resolveEnvironment(executor) {
400
+ const key = cacheKeyFor(executor);
401
+ const cached = profileCache.get(key);
402
+ if (cached && profileIsFresh(cached.measuredAt)) {
403
+ systemDebug("environment", "cache:hit");
404
+ return cached.profile;
405
+ }
406
+ systemDebug("environment", cached ? "cache:stale" : "cache:miss");
407
+ const promise = (async () => {
408
+ const profile = parseEnvironmentProbe(await runProbe(key));
409
+ if (profile.probeError && cached) {
410
+ systemDebug("environment", `detect:keep-previous ${profile.probeError}`);
411
+ return cached.profile;
412
+ }
413
+ systemDebug("environment", `detect:done ${JSON.stringify(profile)}`);
414
+ return profile;
415
+ })();
416
+ const entry = { measuredAt: Date.now(), profile: promise };
417
+ const forget = () => {
418
+ if (profileCache.get(key) === entry) profileCache.delete(key);
419
+ };
420
+ profileCache.set(key, entry);
421
+ promise.then((profile) => {
422
+ if (profile.probeError) forget();
423
+ }, forget);
424
+ return promise;
425
+ }
426
+ function cacheEnvironment(executor, profile) {
427
+ profileCache.set(cacheKeyFor(executor), {
428
+ measuredAt: Date.now(),
429
+ profile: Promise.resolve(profile)
430
+ });
431
+ }
432
+
433
+ export {
434
+ ENVIRONMENT_PROBE_SCRIPT,
435
+ ENVIRONMENT_PROBE_TIMEOUT_MS,
436
+ profileIsFresh,
437
+ parseEnvironmentProbe,
438
+ resolveEnvironment,
439
+ cacheEnvironment,
440
+ EXECUTOR_DELEGATE,
441
+ dirOf,
442
+ elevatedExecutor,
443
+ ensureOwnedDir
444
+ };