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
@@ -2,14 +2,6 @@
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
- resolveEnvironment
7
- } from "./chunk-IW2UKBVF.js";
8
- import {
9
- formatDuration,
10
- isRemoteConnectionError,
11
- systemDebug
12
- } from "./chunk-ZCFVHA25.js";
13
5
  import {
14
6
  containerCommand,
15
7
  edgeContainerExecutor,
@@ -17,7 +9,28 @@ import {
17
9
  readDeclaredPair,
18
10
  readMaybeInContainer,
19
11
  validateCertFor
20
- } from "./chunk-EW6KB2J7.js";
12
+ } from "./chunk-QVW76WDL.js";
13
+ import {
14
+ EDGE_CHALLENGE_HOST_DIR,
15
+ EDGE_CONTAINER_MOUNTS,
16
+ EDGE_HOST_PATHS,
17
+ OPENRESTY_DEFAULT_PATHS
18
+ } from "./chunk-RSR7PFIQ.js";
19
+ import {
20
+ privilegedExecutor,
21
+ rootChecked,
22
+ rootOrDegrade
23
+ } from "./chunk-4I2BM2DT.js";
24
+ import {
25
+ envOps,
26
+ opScript
27
+ } from "./chunk-YMMQRE5J.js";
28
+ import {
29
+ resolveEnvironment
30
+ } from "./chunk-UVNMQM7W.js";
31
+ import {
32
+ sq
33
+ } from "./chunk-ZFJXPCQP.js";
21
34
  import {
22
35
  EDGE_CONTAINER_NAME,
23
36
  EdgeConflictError,
@@ -32,28 +45,25 @@ import {
32
45
  sanitizeEdgeVhosts,
33
46
  sq as sq2,
34
47
  stopTargetsForStatus
35
- } from "./chunk-VZ2ZHPS2.js";
48
+ } from "./chunk-2YM3NI2E.js";
36
49
  import {
37
- waitForPortListening
38
- } from "./chunk-3LDWWY7U.js";
39
- import {
40
- EDGE_CHALLENGE_HOST_DIR,
41
- EDGE_CONTAINER_MOUNTS,
42
- EDGE_HOST_PATHS,
43
- OPENRESTY_DEFAULT_PATHS
44
- } from "./chunk-YIPMDGG6.js";
45
- import {
46
- sq
47
- } from "./chunk-ZFJXPCQP.js";
50
+ probeExec,
51
+ tryExec,
52
+ waitForPortListening,
53
+ withReason
54
+ } from "./chunk-SM26HDS2.js";
48
55
  import {
49
56
  AppError,
50
57
  PROXY_DIRECTIVES,
58
+ answered,
51
59
  buildEdgeImageRef,
52
60
  compareSemver,
61
+ formatDuration,
53
62
  parseProxyValue,
54
63
  safeErrorMessage,
55
- sanitizeProxySettings
56
- } from "./chunk-OTLSJYAC.js";
64
+ sanitizeProxySettings,
65
+ systemDebug
66
+ } from "./chunk-UJ6JZM65.js";
57
67
 
58
68
  // ../../packages/adapters/src/system/managed-image.ts
59
69
  import { posix } from "path";
@@ -120,14 +130,7 @@ var JOURNAL_PATH = `${JOURNAL_DIR}/edge-takeover.json`;
120
130
  function log2(message, level = "info") {
121
131
  return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
122
132
  }
123
- async function tryExec(executor, cmd) {
124
- try {
125
- return await executor.exec(cmd);
126
- } catch {
127
- return null;
128
- }
129
- }
130
- async function buildJournal(executor, status) {
133
+ async function buildJournal(executor, status, onLog) {
131
134
  const units = /* @__PURE__ */ new Map();
132
135
  const containers = /* @__PURE__ */ new Map();
133
136
  const processes = /* @__PURE__ */ new Map();
@@ -137,7 +140,17 @@ async function buildJournal(executor, status) {
137
140
  executor,
138
141
  `docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' ${sq2(o.containerName)} 2>/dev/null`
139
142
  );
140
- containers.set(o.containerName, { name: o.containerName, restart: r?.trim() || "no" });
143
+ if (r === null) {
144
+ onLog?.(
145
+ log2(
146
+ `Could not read ${o.containerName}'s restart policy. If this takeover is rolled back it will be started again, but its policy left untouched \u2014 check it with \`docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' ${o.containerName}\`.`,
147
+ "warn"
148
+ )
149
+ );
150
+ containers.set(o.containerName, { name: o.containerName });
151
+ } else {
152
+ containers.set(o.containerName, { name: o.containerName, restart: r.trim() || "no" });
153
+ }
141
154
  } else if (o.systemdUnit && !units.has(o.systemdUnit)) {
142
155
  const en = await tryExec(executor, `systemctl is-enabled ${sq2(o.systemdUnit)} 2>/dev/null`);
143
156
  units.set(o.systemdUnit, { unit: o.systemdUnit, wasEnabled: en?.trim() === "enabled" });
@@ -152,19 +165,44 @@ async function buildJournal(executor, status) {
152
165
  processes: [...processes.values()]
153
166
  };
154
167
  }
168
+ function journalFileExecutor(executor) {
169
+ return rootOrDegrade(executor, {
170
+ purpose: "Writing the edge-takeover rollback journal",
171
+ consequence: "An interrupted takeover won't be repaired automatically on the next boot.",
172
+ report: () => {
173
+ }
174
+ });
175
+ }
155
176
  async function writeJournal(executor, journal) {
156
177
  try {
157
- await executor.mkdir(JOURNAL_DIR);
158
- await executor.writeFile(JOURNAL_PATH, JSON.stringify(journal, null, 2));
178
+ const privileged = await journalFileExecutor(executor);
179
+ await privileged.mkdir(JOURNAL_DIR);
180
+ await privileged.writeFile(JOURNAL_PATH, JSON.stringify(journal, null, 2));
159
181
  } catch {
160
182
  }
161
183
  }
162
184
  async function clearJournal(executor) {
163
- await tryExec(executor, `rm -f ${JOURNAL_PATH}`);
164
- }
165
- async function readJournal(executor) {
166
- const raw = await tryExec(executor, `cat ${JOURNAL_PATH} 2>/dev/null`);
167
- if (!raw?.trim()) return null;
185
+ await tryExec(await journalFileExecutor(executor), `rm -f ${JOURNAL_PATH}`);
186
+ }
187
+ async function readJournal(executor, onLog) {
188
+ const privileged = await journalFileExecutor(executor);
189
+ const raw = await tryExec(privileged, `cat ${JOURNAL_PATH} 2>/dev/null`);
190
+ if (raw === null) {
191
+ const listed = await tryExec(privileged, `ls ${JOURNAL_PATH} 2>/dev/null`);
192
+ if (listed?.trim()) {
193
+ onLog?.(
194
+ log2(
195
+ `An unfinished edge takeover is recorded at ${JOURNAL_PATH}, but Openship could not read it as this user \u2014 the previous proxy will NOT be restored automatically. Re-run as root, or restore it by hand.`,
196
+ "error"
197
+ )
198
+ );
199
+ }
200
+ return null;
201
+ }
202
+ if (!raw.trim()) {
203
+ await clearJournal(executor);
204
+ return null;
205
+ }
168
206
  try {
169
207
  return JSON.parse(raw);
170
208
  } catch {
@@ -174,26 +212,43 @@ async function readJournal(executor) {
174
212
  }
175
213
  async function rollback(executor, journal, onLog) {
176
214
  onLog(log2("Rolling back \u2014 restoring the previous proxy...", "warn"));
177
- await tryExec(executor, `docker update --restart=no ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
178
- await tryExec(executor, `docker stop ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
215
+ const restore = await rootOrDegrade(executor, {
216
+ purpose: "Restarting the proxy that was holding ports 80/443",
217
+ consequence: "Restarting and re-enabling it will likely be refused \u2014 so if :80 is served after this, it is because nothing was ever stopped, not because Openship put it back.",
218
+ report: (message) => onLog(log2(message, "warn"))
219
+ });
220
+ await tryExec(restore, `docker update --restart=no ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
221
+ await tryExec(restore, `docker stop ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
179
222
  await tryExec(
180
- executor,
223
+ restore,
181
224
  "systemctl disable --now openresty 2>/dev/null || systemctl stop openresty 2>/dev/null || true; systemctl reset-failed openresty 2>/dev/null || true"
182
225
  );
183
226
  for (const u of journal.units) {
184
227
  await tryExec(
185
- executor,
228
+ restore,
186
229
  u.wasEnabled ? `systemctl enable --now ${sq2(u.unit)} 2>/dev/null || true` : `systemctl start ${sq2(u.unit)} 2>/dev/null || true`
187
230
  );
188
231
  }
189
232
  for (const c of journal.containers) {
190
- await tryExec(executor, `docker update --restart=${sq2(c.restart)} ${sq2(c.name)} 2>/dev/null || true`);
191
- await tryExec(executor, `docker start ${sq2(c.name)} 2>/dev/null || true`);
233
+ if (c.restart) {
234
+ await tryExec(
235
+ restore,
236
+ `docker update --restart=${sq2(c.restart)} ${sq2(c.name)} 2>/dev/null || true`
237
+ );
238
+ } else {
239
+ onLog(
240
+ log2(
241
+ `Starting ${c.name} without setting a restart policy \u2014 Openship never managed to read the one it had. Confirm it still comes back on reboot.`,
242
+ "warn"
243
+ )
244
+ );
245
+ }
246
+ await tryExec(restore, `docker start ${sq2(c.name)} 2>/dev/null || true`);
192
247
  }
193
248
  for (const p of journal.processes ?? []) {
194
249
  if (p.command) {
195
250
  await tryExec(
196
- executor,
251
+ restore,
197
252
  `setsid -f sh -c ${sq2(p.command)} 2>/dev/null || (nohup sh -c ${sq2(p.command)} >/dev/null 2>&1 &) || true`
198
253
  );
199
254
  } else {
@@ -219,16 +274,29 @@ async function portIsServed(executor, port) {
219
274
  );
220
275
  return (out ?? "").includes("yes");
221
276
  }
277
+ var EDGE_STOP_ELEVATION = {
278
+ purpose: "Stopping the proxy that holds ports 80/443",
279
+ consequence: "Stopping and disabling it will likely be refused, leaving the ports bound."
280
+ };
222
281
  async function beginEdgeTakeover(executor, status, onLog, targets) {
223
- await writeJournal(executor, await buildJournal(executor, status));
224
- return freeEdgeTargets(
225
- executor,
282
+ await writeJournal(executor, await buildJournal(executor, status, onLog));
283
+ let privilegeDegraded = false;
284
+ const stopExecutor = await rootOrDegrade(executor, {
285
+ ...EDGE_STOP_ELEVATION,
286
+ report: (message) => {
287
+ privilegeDegraded = true;
288
+ onLog(log2(message, "warn"));
289
+ }
290
+ });
291
+ const freed = await freeEdgeTargets(
292
+ stopExecutor,
226
293
  targets?.length ? targets : stopTargetsForStatus(status),
227
294
  (m, l) => onLog(log2(m, l))
228
295
  );
296
+ return { ...freed, privilegeDegraded };
229
297
  }
230
298
  async function rollbackEdgeTakeover(executor, onLog) {
231
- const journal = await readJournal(executor);
299
+ const journal = await readJournal(executor, onLog);
232
300
  if (!journal || journal.completed) return false;
233
301
  const restored = await rollback(executor, journal, onLog);
234
302
  await clearJournal(executor);
@@ -243,7 +311,7 @@ async function completeEdgeTakeover(executor) {
243
311
  await clearJournal(executor);
244
312
  }
245
313
  async function recoverInterruptedTakeover(executor, onLog, isEdgeHealthy) {
246
- const journal = await readJournal(executor);
314
+ const journal = await readJournal(executor, onLog);
247
315
  if (!journal) return;
248
316
  if (journal.completed) {
249
317
  await clearJournal(executor);
@@ -260,13 +328,6 @@ async function recoverInterruptedTakeover(executor, onLog, isEdgeHealthy) {
260
328
  }
261
329
 
262
330
  // ../../packages/adapters/src/system/proxy/import/parse-utils.ts
263
- async function tryExec2(executor, command) {
264
- try {
265
- return await executor.exec(command);
266
- } catch {
267
- return null;
268
- }
269
- }
270
331
  function stripComments(text) {
271
332
  return text.split("\n").map((line) => {
272
333
  let quote;
@@ -330,15 +391,23 @@ function sq3(value) {
330
391
  // ../../packages/adapters/src/system/proxy/import/nginx.ts
331
392
  async function dumpResolvedConfig(executor, bins) {
332
393
  for (const bin of bins) {
333
- const dumped = await tryExec2(executor, `${bin} -T 2>/dev/null`);
394
+ const dumped = await tryExec(executor, `${bin} -T 2>/dev/null`);
334
395
  if (dumped && /server\s*\{/.test(dumped)) return dumped;
335
396
  }
336
397
  return null;
337
398
  }
399
+ async function nginxPrefix(executor, bins) {
400
+ for (const bin of bins) {
401
+ const out = await tryExec(executor, `${bin} -V 2>&1`);
402
+ const prefix = out?.match(/--prefix=(\S+)/)?.[1];
403
+ if (prefix) return prefix;
404
+ }
405
+ return null;
406
+ }
338
407
  async function loadNginxConfig(executor) {
339
408
  const dumped = await dumpResolvedConfig(executor, ["nginx", "openresty"]);
340
409
  if (dumped) return dumped;
341
- const cat = await tryExec2(
410
+ const cat = await tryExec(
342
411
  executor,
343
412
  "cat /etc/nginx/nginx.conf /etc/nginx/sites-enabled/* /etc/nginx/conf.d/*.conf 2>/dev/null"
344
413
  );
@@ -450,9 +519,24 @@ function parseProxyDirectives(body) {
450
519
  ...Object.keys(raw).length > 0 ? { raw } : {}
451
520
  };
452
521
  }
453
- function parseServer(body, source, upstreams) {
522
+ var LOOPBACK_SERVER_NAMES = /* @__PURE__ */ new Set([
523
+ "localhost",
524
+ "localhost.localdomain",
525
+ "ip6-localhost",
526
+ "ip6-loopback"
527
+ ]);
528
+ function resolveStaticRoot(root, prefix) {
529
+ if (root.startsWith("/")) return { root };
530
+ if (!prefix) {
531
+ return {
532
+ reason: `static root "${root}" is relative to nginx's compiled prefix, which this host didn't report`
533
+ };
534
+ }
535
+ return { root: `${prefix.replace(/\/+$/, "")}/${root}` };
536
+ }
537
+ function parseServer(body, source, upstreams, prefix) {
454
538
  const warnings = [];
455
- const names = firstDirective(body, "server_name")?.split(/\s+/).filter((n) => n && n !== "_" && !n.startsWith("~")) ?? [];
539
+ const names = firstDirective(body, "server_name")?.split(/\s+/).filter((n) => n && n !== "_" && !n.startsWith("~") && !LOOPBACK_SERVER_NAMES.has(n.toLowerCase())) ?? [];
456
540
  const listens = [...body.matchAll(/(?:^|[;{\s])listen\s+([^;]+);/g)].map((m) => m[1]);
457
541
  const ssl = listens.some((l) => /\bssl\b/.test(l) || /\b443\b/.test(l));
458
542
  const root = firstDirective(body, "root");
@@ -479,7 +563,12 @@ function parseServer(body, source, upstreams) {
479
563
  } else if (isHttpsUpgradeForSelf(body, names)) {
480
564
  return { warnings: [] };
481
565
  } else if (root && !isAcmeWebrootOnly(body)) {
482
- target = { kind: "static", root: root.replace(/;$/, "") };
566
+ const resolved2 = resolveStaticRoot(root.replace(/;$/, ""), prefix);
567
+ if ("reason" in resolved2) {
568
+ warnings.push(`nginx: ${names[0]} \u2014 ${resolved2.reason} (skipped)`);
569
+ return { warnings };
570
+ }
571
+ target = { kind: "static", root: resolved2.root };
483
572
  } else if (root) {
484
573
  return { warnings: [] };
485
574
  } else {
@@ -494,7 +583,7 @@ function parseServer(body, source, upstreams) {
494
583
  if (tunables.raw) site.proxyRaw = tunables.raw;
495
584
  return { site, warnings };
496
585
  }
497
- function parseNginxConfig(raw) {
586
+ function parseNginxConfig(raw, prefix) {
498
587
  const warnings = [];
499
588
  const sites = [];
500
589
  if (!raw.trim()) {
@@ -507,7 +596,7 @@ function parseNginxConfig(raw) {
507
596
  warnings.push("nginx: no server blocks found");
508
597
  }
509
598
  for (const body of blocks) {
510
- const { site, warnings: blockWarnings } = parseServer(body, "nginx", upstreams);
599
+ const { site, warnings: blockWarnings } = parseServer(body, "nginx", upstreams, prefix);
511
600
  warnings.push(...blockWarnings);
512
601
  if (site) sites.push(site);
513
602
  }
@@ -519,7 +608,10 @@ function parseNginxConfig(raw) {
519
608
  return { proxy: "nginx", sites: kept, warnings };
520
609
  }
521
610
  async function scanNginx(executor) {
522
- return parseNginxConfig(await loadNginxConfig(executor));
611
+ const raw = await loadNginxConfig(executor);
612
+ const relativeRoot = /(?:^|[;{\s])root\s+[^/;\s]/.test(raw);
613
+ const prefix = relativeRoot ? await nginxPrefix(executor, ["nginx", "openresty"]) : null;
614
+ return parseNginxConfig(raw, prefix ?? void 0);
523
615
  }
524
616
  var OUR_EDGE_SITE_GLOBS = [
525
617
  `${EDGE_HOST_PATHS.sitesDir}/*.conf`,
@@ -533,11 +625,11 @@ async function scanForeignOpenResty(executor) {
533
625
  }
534
626
  async function scanOpenshipEdge(executor) {
535
627
  const cat = `cat ${OUR_EDGE_SITE_GLOBS.join(" ")} 2>/dev/null`;
536
- const raw = await tryExec2(executor, cat);
628
+ const raw = await tryExec(executor, cat);
537
629
  if (raw?.trim()) return parseNginxConfig(raw);
538
630
  const container = await resolveOurEdgeContainer(executor);
539
631
  if (!container) return parseNginxConfig("");
540
- const fromContainer = await tryExec2(executor, containerCommand(container, cat));
632
+ const fromContainer = await tryExec(executor, containerCommand(container, cat));
541
633
  return parseNginxConfig(fromContainer ?? "");
542
634
  }
543
635
 
@@ -545,12 +637,12 @@ async function scanOpenshipEdge(executor) {
545
637
  var CADDYFILE_PATHS = ["/etc/caddy/Caddyfile", "/etc/Caddyfile"];
546
638
  async function loadCaddyAdminConfig(executor) {
547
639
  const endpoint = "http://localhost:2019/config/";
548
- const out = await tryExec2(executor, `curl -fsS ${endpoint} 2>/dev/null`) || await tryExec2(executor, `wget -qO- ${endpoint} 2>/dev/null`);
640
+ const out = await tryExec(executor, `curl -fsS ${endpoint} 2>/dev/null`) || await tryExec(executor, `wget -qO- ${endpoint} 2>/dev/null`);
549
641
  return out && out.trim().startsWith("{") ? out : null;
550
642
  }
551
643
  async function loadCaddyJson(executor) {
552
644
  for (const p of CADDYFILE_PATHS) {
553
- const out = await tryExec2(executor, `caddy adapt --adapter caddyfile --config ${p} 2>/dev/null`);
645
+ const out = await tryExec(executor, `caddy adapt --adapter caddyfile --config ${p} 2>/dev/null`);
554
646
  if (out && out.trim().startsWith("{")) return out;
555
647
  }
556
648
  return null;
@@ -665,7 +757,7 @@ function parseCaddyJson(raw) {
665
757
  }
666
758
  async function loadCaddyfile(executor) {
667
759
  for (const p of CADDYFILE_PATHS) {
668
- const out = await tryExec2(executor, `cat ${p} 2>/dev/null`);
760
+ const out = await tryExec(executor, `cat ${p} 2>/dev/null`);
669
761
  if (out && out.trim()) return out;
670
762
  }
671
763
  return "";
@@ -800,7 +892,7 @@ async function scanCaddyText(executor) {
800
892
 
801
893
  // ../../packages/adapters/src/system/proxy/import/apache.ts
802
894
  async function vhostFilesFromDump(executor) {
803
- const dump = await tryExec2(
895
+ const dump = await tryExec(
804
896
  executor,
805
897
  "apachectl -S 2>/dev/null || apache2ctl -S 2>/dev/null || httpd -S 2>/dev/null"
806
898
  );
@@ -815,10 +907,10 @@ async function vhostFilesFromDump(executor) {
815
907
  async function loadApacheConfig(executor) {
816
908
  const files = await vhostFilesFromDump(executor);
817
909
  if (files.length > 0) {
818
- const cat2 = await tryExec2(executor, `cat ${files.map(sq3).join(" ")} 2>/dev/null`);
910
+ const cat2 = await tryExec(executor, `cat ${files.map(sq3).join(" ")} 2>/dev/null`);
819
911
  if (cat2 && cat2.trim()) return cat2;
820
912
  }
821
- const cat = await tryExec2(
913
+ const cat = await tryExec(
822
914
  executor,
823
915
  "cat /etc/apache2/sites-enabled/*.conf /etc/apache2/apache2.conf /etc/httpd/conf.d/*.conf /etc/httpd/conf/httpd.conf 2>/dev/null"
824
916
  );
@@ -1145,7 +1237,7 @@ function parseTraefikLabels(containers) {
1145
1237
  };
1146
1238
  }
1147
1239
  async function scanTraefik(executor) {
1148
- const out = await tryExec2(
1240
+ const out = await tryExec(
1149
1241
  executor,
1150
1242
  `docker ps -q 2>/dev/null | xargs -r docker inspect --format '{{.Name}} {{json .Config.Labels}} {{range $n, $v := .NetworkSettings.Networks}}{{$n}}={{$v.IPAddress}} {{end}} {{range $p, $v := .Config.ExposedPorts}}{{$p}} {{end}} {{if .Config.Cmd}}{{join .Config.Cmd " "}}{{end}}' 2>/dev/null`
1151
1243
  );
@@ -1277,10 +1369,10 @@ function candidateNames(host) {
1277
1369
  async function findPairIn(exec, root, host, container) {
1278
1370
  for (const name of candidateNames(host)) {
1279
1371
  const glob = `${root}/certificates/*/${name}/${name}.crt`;
1280
- let certPath = firstCrt(await tryExec2(exec, `ls -1 ${glob} 2>/dev/null`));
1372
+ let certPath = firstCrt(await tryExec(exec, `ls -1 ${glob} 2>/dev/null`));
1281
1373
  if (!certPath && container) {
1282
1374
  certPath = firstCrt(
1283
- await tryExec2(exec, containerCommand(container, `ls -1 ${glob} 2>/dev/null`))
1375
+ await tryExec(exec, containerCommand(container, `ls -1 ${glob} 2>/dev/null`))
1284
1376
  );
1285
1377
  }
1286
1378
  if (certPath) return { certPath, keyPath: certPath.replace(/\.crt$/, ".key") };
@@ -1326,7 +1418,7 @@ async function candidateStoragePaths(exec, container) {
1326
1418
  };
1327
1419
  if (container) {
1328
1420
  collect(
1329
- await tryExec2(
1421
+ await tryExec(
1330
1422
  exec,
1331
1423
  `docker inspect ${sq2(container)} --format '{{json .Config.Cmd}} {{json .Args}} {{json .Config.Env}}' 2>/dev/null`
1332
1424
  )
@@ -1558,61 +1650,21 @@ function makeApi(exec, kind, ours, container) {
1558
1650
  }
1559
1651
 
1560
1652
  // ../../packages/adapters/src/system/catalog.ts
1653
+ var MIN_DOCKER_VERSION = "20.10";
1561
1654
  function dockerInstallPlan(profile) {
1562
- if (profile.os !== "linux") {
1563
- return {
1564
- supported: false,
1565
- unsupportedReason: "Docker installation is only supported on Linux servers"
1566
- };
1567
- }
1568
- return {
1569
- supported: true,
1570
- installCommand: "curl -fsSL https://get.docker.com | sh",
1571
- startCommand: profile.serviceManager === "systemd" ? "systemctl enable docker && systemctl start docker" : void 0,
1655
+ const ops = envOps(profile);
1656
+ const install = ops.dockerInstall();
1657
+ if (!install.supported) return install;
1658
+ const start = ops.dockerStart();
1659
+ return answered({
1660
+ installCommand: opScript(install.value),
1661
+ service: start.supported ? answered(opScript(start.value)) : start,
1572
1662
  verifyCommand: "docker --version"
1573
- };
1574
- }
1575
- function gitInstallPlan(profile) {
1576
- const commands = {
1577
- apt: "apt-get update -qq && apt-get install -y -qq git",
1578
- dnf: "dnf install -y git",
1579
- yum: "yum install -y git",
1580
- apk: "apk add --no-cache git",
1581
- brew: "brew install git"
1582
- };
1583
- const installCommand = commands[profile.packageManager];
1584
- if (!installCommand) {
1585
- return {
1586
- supported: false,
1587
- unsupportedReason: "No supported package manager found for Git installation"
1588
- };
1589
- }
1590
- return {
1591
- supported: true,
1592
- installCommand,
1593
- verifyCommand: "git --version"
1594
- };
1663
+ });
1595
1664
  }
1596
- function rsyncInstallPlan(profile) {
1597
- const commands = {
1598
- apt: "apt-get update -qq && apt-get install -y -qq rsync",
1599
- dnf: "dnf install -y rsync",
1600
- yum: "yum install -y rsync",
1601
- apk: "apk add --no-cache rsync",
1602
- brew: "brew install rsync"
1603
- };
1604
- const installCommand = commands[profile.packageManager];
1605
- if (!installCommand) {
1606
- return {
1607
- supported: false,
1608
- unsupportedReason: "No supported package manager found for rsync installation"
1609
- };
1610
- }
1611
- return {
1612
- supported: true,
1613
- installCommand,
1614
- verifyCommand: "rsync --version | head -n 1"
1615
- };
1665
+ function packagePlan(profile, pkg, verifyCommand) {
1666
+ const install = envOps(profile).pkgInstall([pkg]);
1667
+ return install.supported ? answered({ installCommand: opScript(install.value), verifyCommand }) : install;
1616
1668
  }
1617
1669
  var systemCatalog = {
1618
1670
  checks: {
@@ -1626,7 +1678,11 @@ var systemCatalog = {
1626
1678
  daemonCommand: "docker info --format '{{.ServerVersion}}'",
1627
1679
  parseVersion: (output) => output.match(/Docker version ([^\s,]+)/)?.[1] ?? output,
1628
1680
  missingMessage: "Docker is not installed",
1629
- notRunningMessage: "Docker is installed but the daemon is not running"
1681
+ notRunningMessage: "Docker is installed but the daemon is not running",
1682
+ // Deliberately does NOT claim the daemon is up or down: the socket's permission
1683
+ // is checked before the daemon is asked, so this arm proves only that we may not
1684
+ // ask. What it can state is the fix, which is the part "not running" got wrong.
1685
+ deniedMessage: "Docker is installed but this connection is not allowed to use its socket \u2014 add the login user to the `docker` group, or connect as root"
1630
1686
  },
1631
1687
  openresty: {
1632
1688
  versionCommand: "openresty -v 2>&1 || /usr/local/openresty/bin/openresty -v 2>&1",
@@ -1660,185 +1716,9 @@ var systemCatalog = {
1660
1716
  // CONTAINER. See installer.ts `installContainerEdge`.
1661
1717
  installs: {
1662
1718
  docker: dockerInstallPlan,
1663
- git: gitInstallPlan,
1664
- rsync: rsyncInstallPlan
1665
- }
1666
- };
1667
-
1668
- // ../../packages/adapters/src/system/elevated-executor.ts
1669
- var INSTALL_ENV = "DEBIAN_FRONTEND=noninteractive DPKG_FORCE=confnew";
1670
- function elevateCommand(command) {
1671
- return `sudo -n sh -c ${sq(`export ${INSTALL_ENV}; ${command}`)}`;
1672
- }
1673
- function dirOf(path) {
1674
- const idx = path.lastIndexOf("/");
1675
- if (idx < 0) return ".";
1676
- return idx === 0 ? "/" : path.slice(0, idx);
1677
- }
1678
- function elevatedExecutor(inner) {
1679
- const writeFileElevated = async (path, content) => {
1680
- const tmp = `/tmp/.openship-elev-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
1681
- await inner.writeFile(tmp, content);
1682
- await inner.exec(
1683
- elevateCommand(`mkdir -p ${sq(dirOf(path))} && mv -f ${sq(tmp)} ${sq(path)}`)
1684
- );
1685
- };
1686
- const overrides = {
1687
- exec: (command, opts) => inner.exec(elevateCommand(command), opts),
1688
- streamExec: (command, onLog, opts) => inner.streamExec(elevateCommand(command), onLog, opts),
1689
- writeFile: writeFileElevated,
1690
- // Elevated for the same reason as writeFile: the vhost/conf dirs are root-owned,
1691
- // and the Proxy would otherwise forward this to the inner executor unelevated —
1692
- // EACCES immediately after an elevated write succeeded.
1693
- rename: async (from, to) => {
1694
- await inner.exec(elevateCommand(`mv -f ${sq(from)} ${sq(to)}`));
1695
- },
1696
- mkdir: async (path) => {
1697
- await inner.exec(elevateCommand(`mkdir -p ${sq(path)}`));
1698
- },
1699
- rm: async (path) => {
1700
- await inner.exec(elevateCommand(`rm -rf ${sq(path)}`));
1701
- }
1702
- };
1703
- return new Proxy(inner, {
1704
- get(target, prop2) {
1705
- if (Object.prototype.hasOwnProperty.call(overrides, prop2)) {
1706
- return overrides[prop2];
1707
- }
1708
- const value = Reflect.get(target, prop2, target);
1709
- return typeof value === "function" ? value.bind(target) : value;
1710
- }
1711
- });
1712
- }
1713
-
1714
- // ../../packages/adapters/src/system/installer.ts
1715
- function log3(message, level = "info") {
1716
- return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
1717
- }
1718
- async function prepareExecutor(executor, component) {
1719
- const profile = await resolveEnvironment(executor);
1720
- if (profile.isRoot) return { ok: true, executor, profile };
1721
- if (profile.canSudo) return { ok: true, executor: elevatedExecutor(executor), profile };
1722
- return {
1723
- ok: false,
1724
- result: {
1725
- component,
1726
- success: false,
1727
- error: `Installing ${component} needs root. Connect this server as root, or as a user with passwordless sudo.`
1728
- }
1729
- };
1730
- }
1731
- async function installDocker(executor, onLog) {
1732
- const prep = await prepareExecutor(executor, "docker");
1733
- if (!prep.ok) return prep.result;
1734
- executor = prep.executor;
1735
- const profile = prep.profile;
1736
- const plan = systemCatalog.installs.docker(profile);
1737
- if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
1738
- return { component: "docker", success: false, error: plan.unsupportedReason ?? "Docker install not supported" };
1739
- }
1740
- onLog(log3("Installing Docker Engine..."));
1741
- try {
1742
- const { code } = await executor.streamExec(plan.installCommand, onLog);
1743
- if (code !== 0) return { component: "docker", success: false, error: "Docker install failed" };
1744
- if (plan.startCommand) {
1745
- onLog(log3("Starting Docker service..."));
1746
- await executor.streamExec(plan.startCommand, onLog);
1747
- }
1748
- const version = await executor.exec(plan.verifyCommand);
1749
- const parsed = systemCatalog.checks.docker.parseVersion(version);
1750
- onLog(log3(`Docker ${parsed} installed`));
1751
- return { component: "docker", success: true, version: parsed };
1752
- } catch (err) {
1753
- const msg = safeErrorMessage(err);
1754
- onLog(log3(`Docker installation failed: ${msg}`, "error"));
1755
- return { component: "docker", success: false, error: msg };
1756
- }
1757
- }
1758
- async function installGit(executor, onLog, opts) {
1759
- const prep = await prepareExecutor(executor, "git");
1760
- if (!prep.ok) return prep.result;
1761
- executor = prep.executor;
1762
- const profile = prep.profile;
1763
- const plan = systemCatalog.installs.git(profile);
1764
- if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
1765
- return { component: "git", success: false, error: plan.unsupportedReason ?? "Git install not supported" };
1766
- }
1767
- onLog(log3(opts?.label ? `Installing Git in ${opts.label}...` : "Installing Git..."));
1768
- try {
1769
- const { code } = await executor.streamExec(plan.installCommand, onLog);
1770
- if (code !== 0) return { component: "git", success: false, error: "Git installation failed" };
1771
- const version = await executor.exec(plan.verifyCommand);
1772
- const parsed = systemCatalog.checks.git.parseVersion(version);
1773
- onLog(log3(opts?.label ? `Git ${parsed} installed in ${opts.label}` : `Git ${parsed} installed`));
1774
- return { component: "git", success: true, version: parsed };
1775
- } catch (err) {
1776
- const msg = safeErrorMessage(err);
1777
- onLog(log3(`Git installation failed: ${msg}`, "error"));
1778
- return { component: "git", success: false, error: msg };
1779
- }
1780
- }
1781
- async function installRsync(executor, onLog) {
1782
- const prep = await prepareExecutor(executor, "rsync");
1783
- if (!prep.ok) return prep.result;
1784
- executor = prep.executor;
1785
- const profile = prep.profile;
1786
- const plan = systemCatalog.installs.rsync(profile);
1787
- if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
1788
- return { component: "rsync", success: false, error: plan.unsupportedReason ?? "rsync install not supported" };
1789
- }
1790
- onLog(log3("Installing rsync..."));
1791
- try {
1792
- const { code } = await executor.streamExec(plan.installCommand, onLog);
1793
- if (code !== 0) return { component: "rsync", success: false, error: "rsync installation failed" };
1794
- const version = await executor.exec(plan.verifyCommand);
1795
- const parsed = systemCatalog.checks.rsync.parseVersion(version);
1796
- onLog(log3(`rsync ${parsed} installed`));
1797
- return { component: "rsync", success: true, version: parsed };
1798
- } catch (err) {
1799
- const msg = safeErrorMessage(err);
1800
- onLog(log3(`rsync installation failed: ${msg}`, "error"));
1801
- return { component: "rsync", success: false, error: msg };
1802
- }
1803
- }
1804
- async function installContainerEdge(executor, onLog, config) {
1805
- const prep = await prepareExecutor(executor, "edge");
1806
- if (!prep.ok) return prep.result;
1807
- executor = prep.executor;
1808
- if (!await dockerAvailable(executor)) {
1809
- const error = "The edge is a container image and needs Docker on this server. Install the Docker component first, then install the edge.";
1810
- onLog(log3(error, "error"));
1811
- return { component: "edge", success: false, error };
1719
+ git: (profile) => packagePlan(profile, "git", "git --version"),
1720
+ rsync: (profile) => packagePlan(profile, "rsync", "rsync --version | head -n 1")
1812
1721
  }
1813
- try {
1814
- const result = await ensureContainerEdge(executor, {
1815
- onLog,
1816
- config,
1817
- image: config?.edgeImage
1818
- });
1819
- if (result.converted) {
1820
- onLog(log3("Edge moved from host OpenResty to the container edge", "warn"));
1821
- }
1822
- const version = await executor.exec(containerCommand(result.container, "openresty -v 2>&1")).catch(() => "");
1823
- return {
1824
- component: "edge",
1825
- success: true,
1826
- version: version ? systemCatalog.checks.openresty.parseVersion(version) : void 0
1827
- };
1828
- } catch (err) {
1829
- if (err instanceof EdgeMigrateRequested) throw err;
1830
- const msg = safeErrorMessage(err);
1831
- onLog(log3(`Edge setup failed: ${msg}`, "error"));
1832
- return { component: "edge", success: false, error: msg };
1833
- }
1834
- }
1835
- var COMPONENT_INSTALLERS = {
1836
- docker: (exec, log6) => installDocker(exec, log6),
1837
- // The edge is ONE component and it is a container image — there is no
1838
- // host-OpenResty or host-certbot installer to reach for any more.
1839
- edge: (exec, log6, config) => installContainerEdge(exec, log6, config),
1840
- git: (exec, log6) => installGit(exec, log6),
1841
- rsync: (exec, log6) => installRsync(exec, log6)
1842
1722
  };
1843
1723
 
1844
1724
  // ../../packages/adapters/src/system/available-version.ts
@@ -1858,32 +1738,30 @@ function normalizePkgVersion(raw) {
1858
1738
  if (dash !== -1) v = v.slice(0, dash);
1859
1739
  return v.trim();
1860
1740
  }
1861
- async function tryExec3(executor, cmd) {
1862
- try {
1863
- return await executor.exec(cmd);
1864
- } catch {
1865
- return null;
1866
- }
1867
- }
1868
- async function probeCandidate(executor, pm, pkg) {
1869
- const q = `'${pkg.replace(/'/g, "'\\''")}'`;
1870
- if (pm === "apt") {
1871
- const out = await tryExec3(executor, `apt-cache policy ${q} 2>/dev/null`);
1872
- return out?.match(/Candidate:\s*(\S+)/)?.[1]?.trim() ?? null;
1873
- }
1874
- if (pm === "dnf" || pm === "yum") {
1875
- const out = await tryExec3(
1876
- executor,
1877
- `${pm} -q --cacheonly list available ${q} 2>/dev/null | tail -n1`
1878
- );
1879
- return out?.trim().split(/\s+/)[1]?.trim() ?? null;
1880
- }
1881
- if (pm === "apk") {
1882
- const out = await tryExec3(executor, `apk policy ${q} 2>/dev/null`);
1883
- const versions = [...out?.matchAll(/^\s+([\w.+~-]+):\s*$/gm) ?? []].map((m) => m[1]);
1741
+ var READ_CANDIDATE = {
1742
+ // `apt-cache policy` prints " Candidate: <version>".
1743
+ apt: (out) => out.match(/Candidate:\s*(\S+)/)?.[1]?.trim() ?? null,
1744
+ // Cached list; the last "pkg.arch version repo" row carries the newest avail.
1745
+ dnf: (out) => out.trim().split(/\s+/)[1]?.trim() ?? null,
1746
+ yum: (out) => out.trim().split(/\s+/)[1]?.trim() ?? null,
1747
+ // `apk policy` prints one ` <version>:` line per available version (colon at end
1748
+ // of line), each followed by indented source lines (installed marker, repo URLs);
1749
+ // the last version line is newest.
1750
+ apk: (out) => {
1751
+ const versions = [...out.matchAll(/^\s+([\w.+~-]+):\s*$/gm)].map((m) => m[1]);
1884
1752
  return versions.length ? versions[versions.length - 1] : null;
1885
- }
1886
- return null;
1753
+ },
1754
+ // Both refuse the probe itself, so there is no output to read. Stated rather than
1755
+ // omitted — `null` here is paired with a refusal, not with a missing branch.
1756
+ brew: null,
1757
+ none: null
1758
+ };
1759
+ async function probeCandidate(executor, profile, pkg) {
1760
+ const read = READ_CANDIDATE[profile.packageManager];
1761
+ const probe = envOps(profile).pkgAvailableVersion(pkg);
1762
+ if (!read || !probe.supported) return null;
1763
+ const out = await tryExec(executor, opScript(probe.value));
1764
+ return out ? read(out) : null;
1887
1765
  }
1888
1766
  async function enrichAvailableVersions(executor, profile, statuses) {
1889
1767
  const targets = statuses.filter(
@@ -1893,7 +1771,7 @@ async function enrichAvailableVersions(executor, profile, statuses) {
1893
1771
  targets.map(async (s) => {
1894
1772
  try {
1895
1773
  const pkg = PACKAGE_NAMES[s.name];
1896
- const candidate = await probeCandidate(executor, profile.packageManager, pkg);
1774
+ const candidate = await probeCandidate(executor, profile, pkg);
1897
1775
  if (!candidate) return;
1898
1776
  const avail = normalizePkgVersion(candidate);
1899
1777
  const installed = normalizePkgVersion(s.version);
@@ -1957,37 +1835,8 @@ function getSystemComponentDefinition(name) {
1957
1835
  }
1958
1836
 
1959
1837
  // ../../packages/adapters/src/system/checks.ts
1960
- async function tryExec4(executor, command) {
1961
- const startedAt = Date.now();
1962
- systemDebug("checks", `exec:start ${command}`);
1963
- try {
1964
- const result = await executor.exec(command, { timeout: 1e4 });
1965
- systemDebug(
1966
- "checks",
1967
- `exec:ok ${command} (${formatDuration(startedAt)})`
1968
- );
1969
- return { output: result.trim(), error: null };
1970
- } catch (err) {
1971
- if (isRemoteConnectionError(err)) {
1972
- systemDebug(
1973
- "checks",
1974
- `exec:abort ${command} (${formatDuration(startedAt)}) ${safeErrorMessage(err)}`
1975
- );
1976
- throw err;
1977
- }
1978
- const msg = safeErrorMessage(err);
1979
- systemDebug(
1980
- "checks",
1981
- `exec:fail ${command} (${formatDuration(startedAt)}) ${msg}`
1982
- );
1983
- return { output: "", error: msg || `\`${command}\` failed` };
1984
- }
1985
- }
1986
- function withReason(message, error) {
1987
- if (!error) return message;
1988
- const first = error.split("\n").map((line) => line.trim()).find(Boolean);
1989
- if (!first) return message;
1990
- return `${message} \u2014 ${first.length > 200 ? `${first.slice(0, 199)}\u2026` : first}`;
1838
+ function isSocketDenied(error) {
1839
+ return error != null && /permission denied|eacces/i.test(error);
1991
1840
  }
1992
1841
  function healthy(name, version, running) {
1993
1842
  const component = getSystemComponentDefinition(name);
@@ -2020,22 +1869,31 @@ function unhealthy(name, message, opts) {
2020
1869
  async function checkDocker(executor) {
2021
1870
  const startedAt = Date.now();
2022
1871
  const recipe = systemCatalog.checks.docker;
2023
- const version = await tryExec4(executor, recipe.versionCommand);
1872
+ const version = await probeExec(executor, recipe.versionCommand, "checks");
2024
1873
  if (!version.output) {
2025
1874
  systemDebug("checks", `docker:missing (${formatDuration(startedAt)})`);
2026
1875
  return unhealthy("docker", withReason(recipe.missingMessage, version.error));
2027
1876
  }
2028
1877
  const parsed = recipe.parseVersion(version.output);
2029
- const info = await tryExec4(executor, recipe.daemonCommand);
1878
+ const info = await probeExec(executor, recipe.daemonCommand, "checks");
2030
1879
  if (!info.output) {
1880
+ const denied = isSocketDenied(info.error);
2031
1881
  systemDebug(
2032
1882
  "checks",
2033
- `docker:not-running (${formatDuration(startedAt)}) ${info.error ?? "probe exited 0 with no server version"}`
1883
+ `docker:${denied ? "denied" : "not-running"} (${formatDuration(startedAt)}) ${info.error ?? "probe exited 0 with no server version"}`
1884
+ );
1885
+ return unhealthy(
1886
+ "docker",
1887
+ withReason(denied && recipe.deniedMessage || recipe.notRunningMessage, info.error),
1888
+ {
1889
+ version: parsed,
1890
+ // Left UNSET when denied: `running: false` would be a fact we don't have. The
1891
+ // permission check happens before the daemon is consulted, so a refusal is
1892
+ // equally consistent with a healthy daemon, and asserting it is stopped is the
1893
+ // same class of guess the message above stopped making.
1894
+ ...denied ? {} : { running: false }
1895
+ }
2034
1896
  );
2035
- return unhealthy("docker", withReason(recipe.notRunningMessage, info.error), {
2036
- version: parsed,
2037
- running: false
2038
- });
2039
1897
  }
2040
1898
  systemDebug("checks", `docker:healthy (${formatDuration(startedAt)})`);
2041
1899
  return healthy("docker", parsed, true);
@@ -2043,7 +1901,7 @@ async function checkDocker(executor) {
2043
1901
  async function checkGit(executor) {
2044
1902
  const startedAt = Date.now();
2045
1903
  const recipe = systemCatalog.checks.git;
2046
- const version = await tryExec4(executor, recipe.versionCommand);
1904
+ const version = await probeExec(executor, recipe.versionCommand, "checks");
2047
1905
  if (!version.output) {
2048
1906
  systemDebug("checks", `git:missing (${formatDuration(startedAt)})`);
2049
1907
  return unhealthy("git", withReason(recipe.missingMessage, version.error));
@@ -2055,7 +1913,7 @@ async function checkGit(executor) {
2055
1913
  async function checkRsync(executor) {
2056
1914
  const startedAt = Date.now();
2057
1915
  const recipe = systemCatalog.checks.rsync;
2058
- const version = await tryExec4(executor, recipe.versionCommand);
1916
+ const version = await probeExec(executor, recipe.versionCommand, "checks");
2059
1917
  if (!version.output) {
2060
1918
  systemDebug("checks", `rsync:missing (${formatDuration(startedAt)})`);
2061
1919
  return unhealthy("rsync", withReason(recipe.missingMessage, version.error));
@@ -2083,7 +1941,7 @@ async function checkEdge(executor) {
2083
1941
  { running: false }
2084
1942
  );
2085
1943
  }
2086
- const version = await tryExec4(executor, containerCommand(container, "openresty -v 2>&1"));
1944
+ const version = await probeExec(executor, containerCommand(container, "openresty -v 2>&1"), "checks");
2087
1945
  if (!version.output) {
2088
1946
  systemDebug("checks", `edge:container-unresponsive (${formatDuration(startedAt)})`);
2089
1947
  return unhealthy(
@@ -2159,6 +2017,182 @@ async function checkComponents(executor, names) {
2159
2017
  return results;
2160
2018
  }
2161
2019
 
2020
+ // ../../packages/adapters/src/system/installer.ts
2021
+ function log3(message, level = "info") {
2022
+ return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
2023
+ }
2024
+ async function prepareExecutor(executor, component) {
2025
+ const grant = await privilegedExecutor(executor, `Installing ${component}`);
2026
+ if (!grant.supported) {
2027
+ return { ok: false, result: { component, success: false, error: grant.reason } };
2028
+ }
2029
+ return { ok: true, executor: grant.value.executor, profile: grant.value.profile };
2030
+ }
2031
+ function describeInstallFailure(label, code, output) {
2032
+ const lines = output.split("\n").map((line) => line.replace(/\r/g, "").trim()).filter(Boolean);
2033
+ const flagged = lines.filter((line) => /^(e:|err:|error:|dpkg:|fatal:)/i.test(line));
2034
+ const picked = (flagged.length ? flagged : lines).slice(-3).join(" \xB7 ");
2035
+ const detail = picked.length > 400 ? `${picked.slice(0, 399)}\u2026` : picked;
2036
+ const held = /held packages?|--allow-change-held-packages/i.test(output) ? " Packages are held on this host (`apt-mark hold`) and Openship does not override a hold \u2014 unhold them first if you do want this install." : "";
2037
+ return `${label} install failed (exit ${code})${detail ? `: ${detail}` : ""}.${held}`;
2038
+ }
2039
+ async function probeDocker(executor) {
2040
+ const status = await checkDocker(executor);
2041
+ const version = status.version;
2042
+ if (!status.installed) return { verdict: "missing", message: status.message };
2043
+ if (!status.healthy) return { verdict: "stopped", version, message: status.message };
2044
+ if (version && compareSemver(version, MIN_DOCKER_VERSION) < 0) {
2045
+ return { verdict: "outdated", version, message: status.message };
2046
+ }
2047
+ return { verdict: "ok", version, message: status.message };
2048
+ }
2049
+ function dockerAlreadyThere(present, onLog) {
2050
+ const named = present.version ? `Docker ${present.version}` : "Docker";
2051
+ onLog(log3(`${named} is installed and its daemon is answering \u2014 nothing to install`));
2052
+ return { component: "docker", success: true, version: present.version };
2053
+ }
2054
+ async function installDocker(executor, onLog, config) {
2055
+ const forced = config?.reinstall === true;
2056
+ let present = forced ? null : await probeDocker(executor);
2057
+ if (present?.verdict === "ok") return dockerAlreadyThere(present, onLog);
2058
+ const prep = await prepareExecutor(executor, "docker");
2059
+ if (!prep.ok) return prep.result;
2060
+ executor = prep.executor;
2061
+ const profile = prep.profile;
2062
+ if (present && present.verdict !== "missing" && !profile.isRoot) {
2063
+ present = await probeDocker(executor);
2064
+ if (present.verdict === "ok") return dockerAlreadyThere(present, onLog);
2065
+ }
2066
+ const plan = systemCatalog.installs.docker(profile);
2067
+ const service = plan.supported ? plan.value.service : plan;
2068
+ if (present?.verdict === "stopped" && service?.supported) {
2069
+ onLog(log3(`${present.message} \u2014 starting the Docker service`, "warn"));
2070
+ await executor.streamExec(service.value, onLog).catch(() => void 0);
2071
+ present = await probeDocker(executor);
2072
+ if (present.verdict === "ok") {
2073
+ onLog(log3(`Docker ${present.version} is running`));
2074
+ return { component: "docker", success: true, version: present.version };
2075
+ }
2076
+ }
2077
+ if (present && present.verdict !== "missing") {
2078
+ const cannotStart = present.verdict === "stopped" && service && !service.supported ? ` Openship could not start it for you: ${service.reason}` : "";
2079
+ const error = (present.verdict === "outdated" ? `Docker ${present.version} is installed but Openship needs ${MIN_DOCKER_VERSION} or newer. Upgrading Docker restarts the daemon and every container on this server, so it is never done as part of another step \u2014 upgrade it on the host, or use Reinstall on the Docker component to run the official installer.` : `${present.message}. Start Docker on this server and retry. Openship will not reinstall Docker over a daemon that is merely unreachable: the installer would also upgrade the engine and restart every container on this host. Use Reinstall on the Docker component if that is what you want.`) + cannotStart;
2080
+ onLog(log3(error, "error"));
2081
+ return { component: "docker", success: false, version: present.version, error };
2082
+ }
2083
+ if (!plan.supported) {
2084
+ onLog(log3(plan.reason, "error"));
2085
+ return { component: "docker", success: false, error: plan.reason };
2086
+ }
2087
+ onLog(
2088
+ forced ? log3(
2089
+ "Running the Docker installer \u2014 it may upgrade the engine, which restarts the daemon and every container on this host...",
2090
+ "warn"
2091
+ ) : log3("Installing Docker Engine...")
2092
+ );
2093
+ try {
2094
+ const { code, output } = await executor.streamExec(
2095
+ plan.value.installCommand,
2096
+ onLog
2097
+ );
2098
+ if (code !== 0) {
2099
+ const error = describeInstallFailure("Docker", code, output);
2100
+ onLog(log3(error, "error"));
2101
+ return { component: "docker", success: false, error };
2102
+ }
2103
+ if (service?.supported) {
2104
+ onLog(log3("Starting Docker service..."));
2105
+ await executor.streamExec(service.value, onLog);
2106
+ } else if (service) {
2107
+ onLog(log3(`Docker is installed but Openship cannot start it here: ${service.reason}`, "warn"));
2108
+ }
2109
+ const version = await executor.exec(plan.value.verifyCommand);
2110
+ const parsed = systemCatalog.checks.docker.parseVersion(version);
2111
+ onLog(log3(`Docker ${parsed} installed`));
2112
+ return { component: "docker", success: true, version: parsed };
2113
+ } catch (err) {
2114
+ const msg = safeErrorMessage(err);
2115
+ onLog(log3(`Docker installation failed: ${msg}`, "error"));
2116
+ return { component: "docker", success: false, error: msg };
2117
+ }
2118
+ }
2119
+ async function installPackaged(executor, onLog, component, label, where) {
2120
+ const prep = await prepareExecutor(executor, component);
2121
+ if (!prep.ok) return prep.result;
2122
+ executor = prep.executor;
2123
+ const plan = systemCatalog.installs[component](prep.profile);
2124
+ if (!plan.supported) {
2125
+ onLog(log3(plan.reason, "error"));
2126
+ return { component, success: false, error: plan.reason };
2127
+ }
2128
+ const inside = where ? ` in ${where}` : "";
2129
+ onLog(log3(`Installing ${label}${inside}...`));
2130
+ try {
2131
+ const { code, output } = await executor.streamExec(
2132
+ plan.value.installCommand,
2133
+ onLog
2134
+ );
2135
+ if (code !== 0) {
2136
+ const error = describeInstallFailure(label, code, output);
2137
+ onLog(log3(error, "error"));
2138
+ return { component, success: false, error };
2139
+ }
2140
+ const version = await executor.exec(plan.value.verifyCommand);
2141
+ const parsed = systemCatalog.checks[component].parseVersion(version);
2142
+ onLog(log3(`${label} ${parsed} installed${inside}`));
2143
+ return { component, success: true, version: parsed };
2144
+ } catch (err) {
2145
+ const msg = safeErrorMessage(err);
2146
+ onLog(log3(`${label} installation failed: ${msg}`, "error"));
2147
+ return { component, success: false, error: msg };
2148
+ }
2149
+ }
2150
+ function installGit(executor, onLog, opts) {
2151
+ return installPackaged(executor, onLog, "git", "Git", opts?.label);
2152
+ }
2153
+ function installRsync(executor, onLog) {
2154
+ return installPackaged(executor, onLog, "rsync", "rsync");
2155
+ }
2156
+ async function installContainerEdge(executor, onLog, config) {
2157
+ const prep = await prepareExecutor(executor, "edge");
2158
+ if (!prep.ok) return prep.result;
2159
+ executor = prep.executor;
2160
+ if (!await dockerAvailable(executor)) {
2161
+ const error = "The edge is a container image and needs Docker on this server. Install the Docker component first, then install the edge.";
2162
+ onLog(log3(error, "error"));
2163
+ return { component: "edge", success: false, error };
2164
+ }
2165
+ try {
2166
+ const result = await ensureContainerEdge(executor, {
2167
+ onLog,
2168
+ config,
2169
+ image: config?.edgeImage
2170
+ });
2171
+ if (result.converted) {
2172
+ onLog(log3("Edge moved from host OpenResty to the container edge", "warn"));
2173
+ }
2174
+ const version = await executor.exec(containerCommand(result.container, "openresty -v 2>&1")).catch(() => "");
2175
+ return {
2176
+ component: "edge",
2177
+ success: true,
2178
+ version: version ? systemCatalog.checks.openresty.parseVersion(version) : void 0
2179
+ };
2180
+ } catch (err) {
2181
+ if (err instanceof EdgeMigrateRequested) throw err;
2182
+ const msg = safeErrorMessage(err);
2183
+ onLog(log3(`Edge setup failed: ${msg}`, "error"));
2184
+ return { component: "edge", success: false, error: msg };
2185
+ }
2186
+ }
2187
+ var COMPONENT_INSTALLERS = {
2188
+ docker: (exec, log6, config) => installDocker(exec, log6, config),
2189
+ // The edge is ONE component and it is a container image — there is no
2190
+ // host-OpenResty or host-certbot installer to reach for any more.
2191
+ edge: (exec, log6, config) => installContainerEdge(exec, log6, config),
2192
+ git: (exec, log6) => installGit(exec, log6),
2193
+ rsync: (exec, log6) => installRsync(exec, log6)
2194
+ };
2195
+
2162
2196
  // ../../packages/adapters/src/system/proxy/index.ts
2163
2197
  async function importSites(executor, status) {
2164
2198
  const proxy = status.occupants.find((o) => o.proxy)?.proxy;
@@ -2206,8 +2240,12 @@ async function ensureEdgeClear(executor, config, onLog) {
2206
2240
  if (!freed.freed) {
2207
2241
  await rollbackEdgeTakeover(executor, onLog);
2208
2242
  const plural = freed.stillBound.length > 1;
2243
+ const ports = `port${plural ? "s" : ""} ${freed.stillBound.join(" and ")} ${plural ? "are" : "is"} still in use, so the edge can't bind. Nothing was installed`;
2209
2244
  throw new AppError(
2210
- `Stopped the existing proxy, but port${plural ? "s" : ""} ${freed.stillBound.join(" and ")} ${plural ? "are" : "is"} still in use, so the edge can't bind. Nothing was installed and the previous proxy has been restored \u2014 find what else is holding the port and retry.`,
2245
+ // The advice has to match the actual cause. Told to hunt for another holder, an
2246
+ // operator retries the same unprivileged takeover forever — and "stopped the
2247
+ // existing proxy" is untrue on this arm, since the stop is what was refused.
2248
+ freed.privilegeDegraded ? `The ${ports} \u2014 and Openship could not run the stop as root on this host (see the privilege warning above), so stopping the existing proxy was almost certainly refused. Retrying as this user will fail the same way: reconnect as root, or as a user with passwordless sudo.` : `Stopped the existing proxy, but ${ports} and the previous proxy has been restored \u2014 find what else is holding the port and retry.`,
2211
2249
  409,
2212
2250
  "EDGE_PORTS_STILL_BOUND"
2213
2251
  );
@@ -2262,15 +2300,22 @@ function setDefaultEdgeImage(image) {
2262
2300
  function resolveEdgeImage(explicit) {
2263
2301
  return buildEdgeImageRef({ explicit, injectedDefault: injectedDefaultImage });
2264
2302
  }
2303
+ function edgeHostExecutor(executor) {
2304
+ return rootOrDegrade(executor, {
2305
+ purpose: "Configuring routing and TLS",
2306
+ consequence: "Vhosts and certificates may not be writable, and routing will be incomplete \u2014 the deploy continues and the app still runs on its port.",
2307
+ report: (message) => console.error(`[edge] ${message}`)
2308
+ });
2309
+ }
2265
2310
  async function containerEdgeProvider(executor, container, opts) {
2266
- const { NginxProvider: Provider } = await import("./nginx-YOEEFBTE.js");
2311
+ const { NginxProvider: Provider } = await import("./nginx-KUFMLMAC.js");
2267
2312
  return new Provider({
2268
2313
  ...opts,
2269
2314
  // Vhosts go to the BIND-MOUNTED HOST dir (so every host-side reader — the
2270
2315
  // migrate proxy scan, cert reuse, the mail cert symlinks — keeps working),
2271
2316
  // while reload/certbot run inside the container.
2272
2317
  paths: EDGE_HOST_PATHS,
2273
- executor: edgeContainerExecutor(executor, container),
2318
+ executor: edgeContainerExecutor(await edgeHostExecutor(executor), container),
2274
2319
  // Challenge tokens go to the HOST side of the ACME mount, for the same reason
2275
2320
  // vhosts do. This is the ONE mount whose two sides differ
2276
2321
  // (/var/lib/openship/edge/acme → /var/www/acme), so the write path and the
@@ -2287,12 +2332,21 @@ async function containerEdgeProvider(executor, container, opts) {
2287
2332
  });
2288
2333
  }
2289
2334
  async function localContainerEdgeProvider(container, opts) {
2290
- const { DockerEdgeExecutor } = await import("./docker-edge-executor-62N64MGS.js");
2291
- const { NginxProvider: Provider } = await import("./nginx-YOEEFBTE.js");
2335
+ const { DockerEdgeExecutor } = await import("./docker-edge-executor-22IBXNYJ.js");
2336
+ const { NginxProvider: Provider } = await import("./nginx-KUFMLMAC.js");
2292
2337
  return new Provider({
2293
2338
  ...opts,
2294
2339
  paths: OPENRESTY_DEFAULT_PATHS,
2295
- executor: new DockerEdgeExecutor({ containerName: container }),
2340
+ // The one place the privilege gate is the WRONG instrument, so the reason is stated
2341
+ // instead: this executor's commands run inside the EDGE container, so probing
2342
+ // through it would measure that container's interior and brand the result with a
2343
+ // verdict about the wrong machine. Privilege here comes from the deployment shape —
2344
+ // the api process is root in its own container, the vhost dir is a bind mount it
2345
+ // owns, and the daemon socket is mounted in. There is no login to elevate.
2346
+ executor: rootChecked(
2347
+ new DockerEdgeExecutor({ containerName: container }),
2348
+ "compose: api is root in its own container, writing shared mounts over a mounted socket"
2349
+ ),
2296
2350
  // nginx.conf + the Lua are BAKED into the image — nothing to detect, install
2297
2351
  // or patch, and detection would answer from inside the container anyway.
2298
2352
  pinPaths: true,
@@ -2411,8 +2465,19 @@ async function ensureContainerEdge(executor, opts) {
2411
2465
  );
2412
2466
  }
2413
2467
  }
2468
+ const hostState = await rootOrDegrade(executor, {
2469
+ purpose: "Creating the edge's state directories",
2470
+ consequence: "Docker will create them empty, so vhosts and certificates may not persist.",
2471
+ report: (message) => onLog(log5(message, "warn"))
2472
+ });
2414
2473
  for (const mount of EDGE_CONTAINER_MOUNTS) {
2415
- await executor.exec(`mkdir -p ${sq(mount.host)}`).catch(() => {
2474
+ await hostState.exec(`mkdir -p ${sq(mount.host)}`).catch((err) => {
2475
+ onLog(
2476
+ log5(
2477
+ `Could not create the edge state directory ${mount.host}: ${safeErrorMessage(err)}. Docker will create it empty, so vhosts and certificates may not persist.`,
2478
+ "warn"
2479
+ )
2480
+ );
2416
2481
  });
2417
2482
  }
2418
2483
  const clear = await ensureEdgeClear(executor, opts.config, onLog);
@@ -2472,6 +2537,7 @@ export {
2472
2537
  verifyEdgeServing,
2473
2538
  ensureContainerEdge,
2474
2539
  checkGit,
2540
+ COMPONENT_CHECKS,
2475
2541
  checkAll,
2476
2542
  checkComponents
2477
2543
  };