openship 0.6.1 → 0.6.5

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 (59) hide show
  1. package/README.md +7 -2
  2. package/dist/{bare-IWS2QHJ3.js → bare-XQIPDO4R.js} +10 -9
  3. package/dist/{chunk-RRJJGVKE.js → chunk-3ZAGC5NT.js} +64 -17
  4. package/dist/{chunk-6ODUBKUB.js → chunk-5OUWIPOB.js} +69 -7
  5. package/dist/chunk-6HOA5JT7.js +124 -0
  6. package/dist/{chunk-TWJCLD4W.js → chunk-6JELW75A.js} +4 -4
  7. package/dist/{chunk-IOT5ZX23.js → chunk-7334NSJL.js} +399 -369
  8. package/dist/chunk-7S4USQYF.js +1753 -0
  9. package/dist/{chunk-3LDWWY7U.js → chunk-A7EWM2KA.js} +41 -7
  10. package/dist/chunk-ELGRMZOW.js +25 -0
  11. package/dist/chunk-EZPPTWJQ.js +292 -0
  12. package/dist/{chunk-IXM5245F.js → chunk-GRTXMUHA.js} +553 -538
  13. package/dist/chunk-HPXP6LXZ.js +428 -0
  14. package/dist/{chunk-OTLSJYAC.js → chunk-LEYSL46B.js} +2135 -85
  15. package/dist/{chunk-EMXI4UCU.js → chunk-LFCSC4IU.js} +29 -10
  16. package/dist/{chunk-JIMUYEKN.js → chunk-MSZUCNPV.js} +1 -1
  17. package/dist/{chunk-YIPMDGG6.js → chunk-PZRRL5PC.js} +68 -23
  18. package/dist/{chunk-2NZSUXOB.js → chunk-QLX7NBEA.js} +1208 -599
  19. package/dist/chunk-RFWJE7ZN.js +68 -0
  20. package/dist/{chunk-VDDMADOF.js → chunk-TM2IQPYU.js} +16 -2
  21. package/dist/{chunk-3FYIXVX5.js → chunk-VDESJSG6.js} +451 -106
  22. package/dist/{chunk-UC7YIEHT.js → chunk-VHMOQL5Y.js} +254 -45
  23. package/dist/{chunk-EW6KB2J7.js → chunk-VLQ3UIVO.js} +393 -53
  24. package/dist/{chunk-VZ2ZHPS2.js → chunk-WE7BXW6L.js} +3 -9
  25. package/dist/chunk-WFRXYWK2.js +444 -0
  26. package/dist/chunk-XRNRT37W.js +316 -0
  27. package/dist/{chunk-3NZICQHF.js → chunk-XVOBL7BC.js} +31 -8
  28. package/dist/cloud-3DODOWTR.js +36 -0
  29. package/dist/{detect-ALF4GXRX.js → detect-P4CIQBRH.js} +3 -3
  30. package/dist/{docker-QECQJ5P4.js → docker-3K2NEBD6.js} +15 -8
  31. package/dist/{docker-edge-executor-62N64MGS.js → docker-edge-executor-YZZ6P7IC.js} +5 -5
  32. package/dist/{edge-import-SAYTRJLO.js → edge-import-MCJNVTZ2.js} +26 -23
  33. package/dist/edge-preflight-ES6FH4JK.js +42 -0
  34. package/dist/{ensure-container-edge-CFSCPGZK.js → ensure-container-edge-YPITNH45.js} +9 -8
  35. package/dist/{executor-PT6UEJ5M.js → executor-DZGX55K2.js} +12 -5
  36. package/dist/host-channel-preflight-MUXE3KZU.js +49 -0
  37. package/dist/index.js +4089 -4538
  38. package/dist/{nginx-YOEEFBTE.js → nginx-NSAHWGJV.js} +11 -5
  39. package/dist/node-bootstrap.js +241 -0
  40. package/dist/node-entry.js +52 -1
  41. package/dist/{openresty-lua-WGBO2M3Z.js → openresty-lua-DR4D3EZ6.js} +6 -2
  42. package/dist/privilege-YTVI4SKF.js +19 -0
  43. package/dist/server/engine/samples/fail2ban/jail.d/sshd.local +4 -1
  44. package/dist/server/index.js +21391 -13110
  45. package/dist/server/migrations/0098_instance_product_mode.sql +12 -0
  46. package/dist/server/migrations/0099_mail_webmail_project.sql +24 -0
  47. package/dist/server/migrations/0100_server_ssh_private_key.sql +11 -0
  48. package/dist/server/migrations/0101_mail_resume_step.sql +12 -0
  49. package/dist/server/migrations/0102_project_workload_axes.sql +19 -0
  50. package/dist/server/migrations/0103_dns_credential.sql +41 -0
  51. package/dist/server/migrations/0104_instance_host_control.sql +15 -0
  52. package/dist/server/migrations/meta/_journal.json +49 -0
  53. package/dist/{setup-M2ZDGH3O.js → setup-WUUZURHS.js} +10 -9
  54. package/dist/up-XEO3SR6Z.js +58 -0
  55. package/package.json +4 -2
  56. package/dist/chunk-IW2UKBVF.js +0 -144
  57. package/dist/chunk-ZCFVHA25.js +0 -89
  58. package/dist/cloud-M2YLWEXO.js +0 -31
  59. /package/dist/{dist-es-JGBPABJI.js → dist-es-ZP5QDSJ3.js} +0 -0
@@ -2,6 +2,44 @@
2
2
  ":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
3
3
  import { createRequire as __ospCreateRequire } from "node:module";
4
4
  const require = __ospCreateRequire(import.meta.url);
5
+ import {
6
+ formatDuration,
7
+ isRemoteConnectionError,
8
+ safeErrorMessage,
9
+ systemDebug
10
+ } from "./chunk-LEYSL46B.js";
11
+
12
+ // ../../packages/adapters/src/system/probe-exec.ts
13
+ async function tryExec(executor, command, opts) {
14
+ try {
15
+ return opts ? await executor.exec(command, opts) : await executor.exec(command);
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+ async function probeExec(executor, command, scope) {
21
+ const startedAt = Date.now();
22
+ systemDebug(scope, `exec:start ${command}`);
23
+ try {
24
+ const result = await executor.exec(command, { timeout: 1e4 });
25
+ systemDebug(scope, `exec:ok ${command} (${formatDuration(startedAt)})`);
26
+ return { output: result.trim(), error: null };
27
+ } catch (err) {
28
+ const msg = safeErrorMessage(err);
29
+ if (isRemoteConnectionError(err)) {
30
+ systemDebug(scope, `exec:abort ${command} (${formatDuration(startedAt)}) ${msg}`);
31
+ throw err;
32
+ }
33
+ systemDebug(scope, `exec:fail ${command} (${formatDuration(startedAt)}) ${msg}`);
34
+ return { output: "", error: msg || `\`${command}\` failed` };
35
+ }
36
+ }
37
+ function withReason(message, error) {
38
+ if (!error) return message;
39
+ const first = error.split("\n").map((line) => line.trim()).find(Boolean);
40
+ if (!first) return message;
41
+ return `${message} \u2014 ${first.length > 200 ? `${first.slice(0, 199)}\u2026` : first}`;
42
+ }
5
43
 
6
44
  // ../../packages/adapters/src/system/port-listen.ts
7
45
  var PROC_NET_TCP_CMD = "cat /proc/net/tcp 2>/dev/null; cat /proc/net/tcp6 2>/dev/null; true";
@@ -75,13 +113,6 @@ async function waitForPortFree(executor, port, opts = {}) {
75
113
  }
76
114
 
77
115
  // ../../packages/adapters/src/runtime/port-conflict.ts
78
- async function tryExec(executor, command) {
79
- try {
80
- return await executor.exec(command);
81
- } catch {
82
- return null;
83
- }
84
- }
85
116
  async function resolveSystemdUnit(executor, pid) {
86
117
  const cgroup = await tryExec(executor, `cat /proc/${pid}/cgroup 2>/dev/null || true`);
87
118
  const unitMatch = cgroup?.match(/(?:^|\/)([^/\n]+\.service)(?:$|\n|\/)/m) ?? cgroup?.match(/(?:^|\/)([^/\n]+\.service)(?:$|\n|\/)/m);
@@ -144,6 +175,9 @@ async function describeProcess(executor, pid) {
144
175
  }
145
176
 
146
177
  export {
178
+ tryExec,
179
+ probeExec,
180
+ withReason,
147
181
  waitForPortListening,
148
182
  waitForPortFree,
149
183
  probeListeningPort,
@@ -0,0 +1,25 @@
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
+
6
+ // src/lib/paths.ts
7
+ import { homedir } from "os";
8
+ import { join, resolve } from "path";
9
+ var DEFAULT_HOME = join(homedir(), ".openship");
10
+ var override = process.env.OPENSHIP_HOME?.trim();
11
+ var OS_DIR = override ? resolve(override) : DEFAULT_HOME;
12
+ var IS_ALT_HOME = OS_DIR !== DEFAULT_HOME;
13
+ var DATA_DIR = join(OS_DIR, "data");
14
+ var LOG_DIR = join(OS_DIR, "logs");
15
+ var AUTH_SECRET_FILE = join(OS_DIR, "auth-secret");
16
+ var INTERNAL_TOKEN_FILE = join(OS_DIR, "internal-token");
17
+
18
+ export {
19
+ OS_DIR,
20
+ IS_ALT_HOME,
21
+ DATA_DIR,
22
+ LOG_DIR,
23
+ AUTH_SECRET_FILE,
24
+ INTERNAL_TOKEN_FILE
25
+ };
@@ -0,0 +1,292 @@
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
+ answered,
7
+ firewallAllowSteps,
8
+ firewallManager,
9
+ firewallPersists,
10
+ refused,
11
+ shellQuote
12
+ } from "./chunk-LEYSL46B.js";
13
+
14
+ // ../../packages/adapters/src/system/environment-ops.ts
15
+ function opScript(steps) {
16
+ return steps.join(" && ");
17
+ }
18
+ function releaseArchOf(profile) {
19
+ return profile.arch === "unknown" ? refused(
20
+ "This host's CPU architecture could not be determined, and Openship will not default to amd64 \u2014 a release artifact for the wrong architecture installs cleanly and then fails to execute."
21
+ ) : answered(profile.arch);
22
+ }
23
+ var HOST_STATE_DIR = "/root/.openship";
24
+ var PACKAGE_NAME = /^[A-Za-z0-9][A-Za-z0-9._+@-]*$/;
25
+ function joinPackages(packages) {
26
+ if (packages.length === 0) {
27
+ return refused("No packages were named, so there is nothing to run.");
28
+ }
29
+ const bad = packages.find((name) => !PACKAGE_NAME.test(name));
30
+ if (bad !== void 0) {
31
+ return refused(`${JSON.stringify(bad)} is not a package name.`);
32
+ }
33
+ return answered(packages.join(" "));
34
+ }
35
+ var INSTALL = {
36
+ // `update` first because an install against a stale index fails on a
37
+ // just-provisioned box; `-qq` because this output streams into the UI.
38
+ apt: (names) => answered(["apt-get update -qq", `apt-get install -y -qq ${names}`]),
39
+ dnf: (names) => answered([`dnf install -y ${names}`]),
40
+ yum: (names) => answered([`yum install -y ${names}`]),
41
+ apk: (names) => answered([`apk add --no-cache ${names}`]),
42
+ brew: (names) => answered([`brew install ${names}`]),
43
+ none: (names) => refused(
44
+ `No package manager is available to install ${names} \u2014 Openship looked for apt-get, dnf, yum, apk and brew and found none.`
45
+ )
46
+ };
47
+ var REMOVE = {
48
+ // `purge` + `autoremove`, matching what the uninstallers did before this table: a
49
+ // component Openship installed should leave no config behind when removed.
50
+ apt: (names) => answered([`apt-get purge -y -qq ${names}`, "apt-get autoremove -y -qq"]),
51
+ dnf: (names) => answered([`dnf remove -y ${names}`]),
52
+ yum: (names) => answered([`yum remove -y ${names}`]),
53
+ apk: (names) => answered([`apk del ${names}`]),
54
+ brew: (names) => answered([`brew uninstall --force ${names}`]),
55
+ none: (names) => refused(`No package manager is available to remove ${names}.`)
56
+ };
57
+ var AVAILABLE_VERSION = {
58
+ apt: (names) => answered([`apt-cache policy ${names} 2>/dev/null`]),
59
+ dnf: (names) => answered([`dnf -q --cacheonly list available ${names} 2>/dev/null | tail -n1`]),
60
+ yum: (names) => answered([`yum -q --cacheonly list available ${names} 2>/dev/null | tail -n1`]),
61
+ apk: (names) => answered([`apk policy ${names} 2>/dev/null`]),
62
+ brew: (names) => refused(
63
+ `Openship does not read Homebrew's available version for ${names} \u2014 it reports the installed version only on macOS.`
64
+ ),
65
+ none: (names) => refused(`No package manager is available to look up an installable version of ${names}.`)
66
+ };
67
+ function noServiceManager(reason) {
68
+ const refuse = () => refused(reason);
69
+ return { enableStart: refuse, isActive: refuse };
70
+ }
71
+ var SERVICE = {
72
+ systemd: {
73
+ enableStart: (unit) => answered([`systemctl enable --now ${unit}`]),
74
+ // `--quiet` because the caller wants the exit code, and a unit name echoed onto
75
+ // stdout is the kind of thing a `!output` check reads as "running".
76
+ isActive: (unit) => answered([`systemctl is-active --quiet ${unit}`])
77
+ },
78
+ openrc: {
79
+ // Two steps, not one: `rc-update add` is persistence and `rc-service start` is now.
80
+ // OpenRC has no `--now`, which is why Alpine used to get a start command of
81
+ // `undefined` from a table that assumed systemd.
82
+ enableStart: (unit) => answered([`rc-update add ${unit} default`, `rc-service ${unit} start`]),
83
+ // Exits 0 started / 3 stopped, and prints to stdout — silenced so the contract
84
+ // matches systemd's `is-active --quiet`.
85
+ isActive: (unit) => answered([`rc-service ${unit} status >/dev/null 2>&1`])
86
+ },
87
+ launchd: noServiceManager(
88
+ "This host runs launchd, and Openship does not manage macOS system services \u2014 launchd jobs are addressed by label rather than by unit name, and starting or stopping one is the operator's call."
89
+ ),
90
+ none: noServiceManager(
91
+ "This host has no service manager Openship can drive (no running systemd, no OpenRC, no launchd), so services cannot be started, stopped or inspected. A container started without an init system is the usual cause."
92
+ )
93
+ };
94
+ var GET_DOCKER_IDS = /* @__PURE__ */ new Set([
95
+ "ubuntu",
96
+ "debian",
97
+ "raspbian",
98
+ "centos",
99
+ "fedora",
100
+ "rhel",
101
+ "rocky"
102
+ ]);
103
+ var DOCKER_CE_REPO_FILE = "/etc/yum.repos.d/docker-ce.repo";
104
+ function dockerCeRepoSteps(profile) {
105
+ const write = `curl -fsSL https://download.docker.com/linux/centos/docker-ce.repo -o ${DOCKER_CE_REPO_FILE}`;
106
+ const major = /^(\d+)/.exec(profile.versionId ?? "")?.[1];
107
+ if (!major) return [write];
108
+ return [write, `sed -i 's|\\$releasever|${major}|g' ${DOCKER_CE_REPO_FILE}`];
109
+ }
110
+ var DOCKER_CE_PACKAGES = "docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin";
111
+ var COMPOSE_PLUGIN_VERSION = "v5.4.0";
112
+ var COMPOSE_PLUGIN_DIR = "/usr/local/lib/docker/cli-plugins";
113
+ var COMPOSE_PLUGIN_PATH = `${COMPOSE_PLUGIN_DIR}/docker-compose`;
114
+ var COMPOSE_PLUGIN_ASSET = {
115
+ amd64: "x86_64",
116
+ arm64: "aarch64"
117
+ };
118
+ function withComposePlugin(profile, engine) {
119
+ const arch = releaseArchOf(profile);
120
+ if (!arch.supported) return arch;
121
+ const asset = `docker-compose-linux-${COMPOSE_PLUGIN_ASSET[arch.value]}`;
122
+ return answered([
123
+ ...engine,
124
+ `mkdir -p ${COMPOSE_PLUGIN_DIR}`,
125
+ `curl -fsSL https://github.com/docker/compose/releases/download/${COMPOSE_PLUGIN_VERSION}/${asset} -o ${COMPOSE_PLUGIN_PATH}`,
126
+ `chmod 0755 ${COMPOSE_PLUGIN_PATH}`
127
+ ]);
128
+ }
129
+ function dockerInstallSteps(profile) {
130
+ if (profile.os === "darwin") {
131
+ return refused(
132
+ "Openship does not install Docker on macOS \u2014 Docker Desktop (or OrbStack/Colima) is a GUI application an operator installs and licenses themselves."
133
+ );
134
+ }
135
+ if (profile.os !== "linux") {
136
+ return refused("Docker can only be installed on a Linux or macOS host.");
137
+ }
138
+ const id = profile.distroId?.trim().toLowerCase() ?? "";
139
+ if (GET_DOCKER_IDS.has(id)) {
140
+ return answered(["curl -fsSL https://get.docker.com | sh"]);
141
+ }
142
+ switch (profile.distroFamily) {
143
+ case "rhel":
144
+ if (id === "amzn") {
145
+ return withComposePlugin(
146
+ profile,
147
+ profile.packageManager === "yum" ? (
148
+ // `amazon-linux-extras` exits 13 with "Installation failed" AFTER yum has
149
+ // printed "Complete!" and left dockerd on disk — seen once in four runs
150
+ // against a real amazonlinux:2. `opScript` joins with `&&`, so that exit
151
+ // aborted the plugin steps below and produced the engine-without-`docker
152
+ // compose` host {@link withComposePlugin} exists to prevent. Tolerate a
153
+ // non-zero exit only when the binary is actually there; a real failure has
154
+ // nothing to find and still aborts. Braced so it stays one step if a step
155
+ // is ever prepended — `&&` and `||` bind equally and left-associatively.
156
+ ["{ amazon-linux-extras install -y docker || command -v docker >/dev/null 2>&1; }"]
157
+ ) : ["dnf install -y docker"]
158
+ );
159
+ }
160
+ return answered([
161
+ ...dockerCeRepoSteps(profile),
162
+ `${profile.packageManager} install -y ${DOCKER_CE_PACKAGES}`
163
+ ]);
164
+ case "debian":
165
+ return withComposePlugin(profile, [
166
+ "apt-get update -qq",
167
+ "apt-get install -y -qq docker.io"
168
+ ]);
169
+ case "alpine":
170
+ return answered(["apk add --no-cache docker docker-cli docker-cli-compose"]);
171
+ case "suse":
172
+ case "arch":
173
+ case "unknown":
174
+ return refused(
175
+ `Openship has no Docker install path for this host (os-release ID=${JSON.stringify(profile.distroId ?? "")}, family ${profile.distroFamily}).`
176
+ );
177
+ }
178
+ }
179
+ function homeOf(profile) {
180
+ const home = profile.home.trim();
181
+ if (home) return home;
182
+ if (profile.isRoot) return "/root";
183
+ const user = profile.loginUser.trim();
184
+ return user ? `/home/${user}` : "/root";
185
+ }
186
+ function describeProfile(profile) {
187
+ const name = profile.prettyName?.trim() || profile.distroId?.trim() || profile.os;
188
+ const parts = [
189
+ name,
190
+ `${profile.os}/${profile.arch}`,
191
+ `pm=${profile.packageManager}`,
192
+ `init=${profile.serviceManager}`,
193
+ `firewall=${profile.firewall}`,
194
+ profile.isRoot ? "root" : profile.canSudo ? `${profile.loginUser}+sudo` : profile.loginUser
195
+ ];
196
+ if (profile.isContainer) parts.push("container");
197
+ return parts.filter(Boolean).join(" \xB7 ");
198
+ }
199
+ function sshdEnableHintFor(profile) {
200
+ switch (profile.serviceManager) {
201
+ case "systemd":
202
+ return profile.distroFamily === "debian" ? "sudo systemctl enable --now ssh" : "sudo systemctl enable --now sshd";
203
+ case "openrc":
204
+ return "sudo rc-update add sshd default && sudo rc-service sshd start";
205
+ case "launchd":
206
+ return "sudo systemsetup -setremotelogin on";
207
+ case "none":
208
+ return "start this host's SSH server (no service manager was detected, so start it however this host does)";
209
+ }
210
+ }
211
+ function hostFacts(profile) {
212
+ const home = homeOf(profile);
213
+ return {
214
+ profile,
215
+ describe: () => describeProfile(profile),
216
+ sshdEnableHint: () => sshdEnableHintFor(profile),
217
+ // Both delegate: these are facts about a `SystemFirewall`, not about a profile, and
218
+ // the CLI reads them off a locally-measured firewall with no profile in hand.
219
+ firewallManager: () => firewallManager(profile.firewall),
220
+ firewallPersists: () => firewallPersists(profile.firewall),
221
+ home: () => home,
222
+ // macOS has no /root to own anything, and there it is a local target rather than
223
+ // a provisioned server, so the invoking user's home is the only sensible anchor.
224
+ stateDir: () => profile.os === "darwin" ? `${home}/.openship` : HOST_STATE_DIR,
225
+ scratchDir: () => `${home}/.openship`,
226
+ installsNeedRoot: () => profile.os !== "darwin"
227
+ };
228
+ }
229
+ function withPackages(packages, rule) {
230
+ const names = joinPackages(packages);
231
+ return names.supported ? rule(names.value) : names;
232
+ }
233
+ function hostCommands(profile) {
234
+ const pm = profile.packageManager;
235
+ const service = SERVICE[profile.serviceManager];
236
+ const unit = (name) => shellQuote(name);
237
+ const firewallScoped = (scope, steps) => {
238
+ const manager = firewallManager(profile.firewall);
239
+ return manager.supported ? steps(manager.value, scope) : manager;
240
+ };
241
+ return {
242
+ pkgInstall: (packages) => withPackages(packages, INSTALL[pm]),
243
+ pkgInstallVariants: (variants) => {
244
+ if (pm === "none") return INSTALL.none("this package");
245
+ const chosen = variants[pm];
246
+ return chosen.supported ? withPackages(chosen.value, INSTALL[pm]) : chosen;
247
+ },
248
+ pkgRemove: (packages) => withPackages(packages, REMOVE[pm]),
249
+ pkgAvailableVersion: (pkg) => withPackages([pkg], AVAILABLE_VERSION[pm]),
250
+ // Unit names are quoted rather than validated: unlike package names they are
251
+ // data-derived (a project slug, a unit read off a `systemctl list-units` scan), so
252
+ // the guarantee has to hold for a string nobody vetted.
253
+ serviceEnableStart: (name) => service.enableStart(unit(name)),
254
+ serviceIsActive: (name) => service.isActive(unit(name)),
255
+ firewallAllow: (scope) => firewallScoped(scope, firewallAllowSteps),
256
+ dockerInstall: () => dockerInstallSteps(profile),
257
+ // Through `unit()` like every other unit name, so there is one path to a service verb
258
+ // rather than one path plus an exception that happens to be safe today.
259
+ dockerStart: () => service.enableStart(unit("docker")),
260
+ releaseArch: () => releaseArchOf(profile)
261
+ };
262
+ }
263
+ function mapRefusals(commands, onRefusal, gate) {
264
+ const out = {};
265
+ for (const name of Object.keys(commands)) {
266
+ const fn = commands[name];
267
+ out[name] = ((...args) => {
268
+ if (gate !== null) return refused(gate);
269
+ const answer = fn(...args);
270
+ return answer.supported ? answer : refused(onRefusal(answer.reason));
271
+ });
272
+ }
273
+ return out;
274
+ }
275
+ function hostRefusal(profile) {
276
+ if (profile.supported) return null;
277
+ const reason = profile.unsupportedReason ?? "Openship cannot drive this host, and the detector recorded no reason.";
278
+ return `${reason} (host: ${describeProfile(profile)})`;
279
+ }
280
+ function envOps(profile) {
281
+ const facts = hostFacts(profile);
282
+ const label = describeProfile(profile);
283
+ const withHost = (reason) => `${reason} (host: ${label})`;
284
+ return { ...facts, ...mapRefusals(hostCommands(profile), withHost, hostRefusal(profile)) };
285
+ }
286
+
287
+ export {
288
+ opScript,
289
+ HOST_STATE_DIR,
290
+ hostRefusal,
291
+ envOps
292
+ };