openship 0.4.6 → 0.4.7

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.
@@ -101,10 +101,10 @@ import {
101
101
  } from "./chunk-W73X43R7.js";
102
102
  import {
103
103
  CloudRuntime
104
- } from "./chunk-5FNIWBFI.js";
104
+ } from "./chunk-XFCDYZUQ.js";
105
105
  import {
106
106
  systemCatalog
107
- } from "./chunk-SQ77DZOI.js";
107
+ } from "./chunk-UXWAVM4C.js";
108
108
  import {
109
109
  AwsCrc32,
110
110
  __awaiter,
@@ -139,13 +139,18 @@ import {
139
139
  DockerRuntime,
140
140
  isHostPathSource,
141
141
  scopedVolumeName
142
- } from "./chunk-2ZLR7ULL.js";
142
+ } from "./chunk-SJIFLLX4.js";
143
143
  import {
144
144
  BareRuntime
145
145
  } from "./chunk-VDMQJHPI.js";
146
146
  import {
147
+ LocalExecutor
148
+ } from "./chunk-TYVME3XD.js";
149
+ import {
150
+ EDGE_CONTAINER_NAME,
151
+ edgeCrashReason,
147
152
  invalidateEdgeContainer
148
- } from "./chunk-FQGJZ2HS.js";
153
+ } from "./chunk-R5BHFFAV.js";
149
154
  import {
150
155
  EDGE_CONTAINER_MOUNTS,
151
156
  EDGE_HOST_STATE_DIR
@@ -159,13 +164,6 @@ import {
159
164
  import chalk from "chalk";
160
165
  import ora from "ora";
161
166
 
162
- // src/lib/compose.ts
163
- import { spawnSync as spawnSync3 } from "child_process";
164
- import { randomBytes as randomBytes5 } from "crypto";
165
- import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
166
- import { homedir as homedir2, userInfo } from "os";
167
- import { join as join6 } from "path";
168
-
169
167
  // ../../packages/adapters/src/runtime/image-transfer.ts
170
168
  import { Transform } from "stream";
171
169
 
@@ -12668,6 +12666,13 @@ var SftpDestinationImpl = class {
12668
12666
  registerDestination("sftp", (row) => new SftpDestinationImpl(row));
12669
12667
  registerDestination("openship_server", (row) => new SftpDestinationImpl(row));
12670
12668
 
12669
+ // src/lib/compose.ts
12670
+ import { spawnSync as spawnSync3 } from "child_process";
12671
+ import { randomBytes as randomBytes5 } from "crypto";
12672
+ import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
12673
+ import { homedir as homedir2, userInfo } from "os";
12674
+ import { join as join6 } from "path";
12675
+
12671
12676
  // src/lib/paths.ts
12672
12677
  import { homedir } from "os";
12673
12678
  import { join as join3, resolve as resolve2 } from "path";
@@ -13229,7 +13234,7 @@ function renderEnv(opts, host, cfg) {
13229
13234
  // (hides the local row). Written explicitly so the policy is visible in .env.
13230
13235
  `OPENSHIP_HOST_CONTROL=${cfg.hostControl ? "true" : "false"}`,
13231
13236
  `OPENSHIP_IMAGE_REGISTRY=${cfg.registry}`,
13232
- `OPENSHIP_VERSION=${opts.version || (true ? "0.4.6" : "latest")}`,
13237
+ `OPENSHIP_VERSION=${opts.version || (true ? "0.4.7" : "latest")}`,
13233
13238
  `POSTGRES_PASSWORD=${keepSecret(prev, "POSTGRES_PASSWORD")}`,
13234
13239
  `BETTER_AUTH_SECRET=${keepSecret(prev, "BETTER_AUTH_SECRET")}`,
13235
13240
  `INTERNAL_TOKEN=${keepSecret(prev, "INTERNAL_TOKEN")}`,
@@ -13435,11 +13440,17 @@ async function importMigratedSites(apiPort, sites, certPems) {
13435
13440
  `Migrated NONE of the ${sites.length} site${sites.length === 1 ? "" : "s"} \u2014 Openship holds :80/:443 and ${sites.length === 1 ? "that hostname is" : "those hostnames are"} NOT being served.`
13436
13441
  );
13437
13442
  for (const w2 of warnings.slice(0, 8)) console.log(chalk.red(` \u2022 ${w2}`));
13443
+ const edgeLog = await edgeCrashReason(new LocalExecutor());
13444
+ if (edgeLog) {
13445
+ console.log(chalk.red(`
13446
+ The edge container is not running. Its log says:`));
13447
+ console.log(chalk.red(` ${edgeLog}`));
13448
+ }
13438
13449
  console.log(
13439
13450
  chalk.yellow(
13440
13451
  `
13441
13452
  Retry the import with \`openship up\` once the cause above is fixed, or put your
13442
- previous proxy back: docker stop openship-edge && sudo systemctl enable --now nginx
13453
+ previous proxy back: docker stop ${EDGE_CONTAINER_NAME} && sudo systemctl enable --now nginx
13443
13454
  `
13444
13455
  )
13445
13456
  );
@@ -6,7 +6,7 @@ import {
6
6
  COMPONENT_INSTALLERS,
7
7
  checkAll,
8
8
  checkComponents
9
- } from "./chunk-SQ77DZOI.js";
9
+ } from "./chunk-UXWAVM4C.js";
10
10
 
11
11
  // ../../packages/adapters/src/system/state.ts
12
12
  var DEFAULT_STATE_PATH = "/etc/openship/setup-state.json";
@@ -59,6 +59,36 @@ function sq(value) {
59
59
  function isOurEdgeContainer(name, image) {
60
60
  return /openship-edge/i.test(`${name ?? ""} ${image ?? ""}`);
61
61
  }
62
+ async function edgeIsServing(executor, port = 80) {
63
+ const running = await tryExec(
64
+ executor,
65
+ `docker inspect -f '{{.State.Running}}' ${sq(EDGE_CONTAINER_NAME)} 2>/dev/null`
66
+ );
67
+ if ((running ?? "").trim() !== "true") return false;
68
+ const probe = await tryExec(
69
+ executor,
70
+ containerShell(`wget -q -O /dev/null -T 3 http://127.0.0.1:${port}/ >/dev/null 2>&1; echo $?`)
71
+ );
72
+ const code = (probe ?? "").trim().split("\n").pop() ?? "1";
73
+ return code === "0" || code === "8";
74
+ }
75
+ function containerShell(command) {
76
+ return `docker exec ${sq(EDGE_CONTAINER_NAME)} sh -c ${sq(command)}`;
77
+ }
78
+ async function edgeCrashReason(executor) {
79
+ const logs = await tryExec(
80
+ executor,
81
+ `docker logs --tail 40 ${sq(EDGE_CONTAINER_NAME)} 2>&1`
82
+ );
83
+ return logs ? edgeFailureReason(logs) : null;
84
+ }
85
+ function edgeFailureReason(containerLog) {
86
+ const lines = containerLog.split("\n").map((l) => l.trim()).filter(Boolean);
87
+ const emerg = lines.find((l) => l.includes("[emerg]"));
88
+ if (emerg) return emerg.replace(/^.*\[emerg\]\s*\d*#\d*:\s*/, "");
89
+ return lines.at(-1) ?? null;
90
+ }
91
+ var EDGE_CONTAINER_NAME = "openship-edge";
62
92
  async function ourEdgeContainerRunning(executor) {
63
93
  return Boolean(await resolveOurEdgeContainer(executor));
64
94
  }
@@ -238,6 +268,10 @@ export {
238
268
  classifyProxy,
239
269
  sq,
240
270
  isOurEdgeContainer,
271
+ edgeIsServing,
272
+ edgeCrashReason,
273
+ edgeFailureReason,
274
+ EDGE_CONTAINER_NAME,
241
275
  ourEdgeContainerRunning,
242
276
  invalidateEdgeContainer,
243
277
  resolveOurEdgeContainer,
@@ -207,6 +207,8 @@ var DEFAULT_REMOTE_DOCKER_SOCKET_PATH = "/var/run/docker.sock";
207
207
  var resolvedDockerSocketPathCache = /* @__PURE__ */ new WeakMap();
208
208
  var DOCKER_DIAL_STDIO_COMMAND = "docker system dial-stdio";
209
209
  var STREAMLOCAL_PROBE_TIMEOUT_MS = 8e3;
210
+ var STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS = 3e3;
211
+ var VERIFY_BUFFER_CAP_BYTES = 8 * 1024 * 1024;
210
212
  function toSshConfig(opts) {
211
213
  return {
212
214
  host: opts.host ?? "",
@@ -304,13 +306,107 @@ async function openStreamlocalUpstream(opts) {
304
306
  throw error;
305
307
  }
306
308
  }
309
+ function pipeThrough(client, upstream) {
310
+ const teardown = () => {
311
+ client.destroy();
312
+ upstream.destroy();
313
+ };
314
+ client.on("error", teardown);
315
+ upstream.on("error", teardown);
316
+ client.once("close", () => upstream.destroy());
317
+ upstream.once("close", () => client.destroy());
318
+ client.pipe(upstream);
319
+ upstream.pipe(client);
320
+ }
321
+ function awaitFirstByte(upstream, ms) {
322
+ return new Promise((resolve) => {
323
+ const finish = (chunk) => {
324
+ clearTimeout(timer);
325
+ upstream.removeListener("data", onData);
326
+ upstream.removeListener("error", onFail);
327
+ upstream.removeListener("close", onFail);
328
+ resolve(chunk);
329
+ };
330
+ const onData = (chunk) => finish(chunk);
331
+ const onFail = () => finish(null);
332
+ const timer = setTimeout(() => finish(null), ms);
333
+ upstream.on("data", onData);
334
+ upstream.once("error", onFail);
335
+ upstream.once("close", onFail);
336
+ });
337
+ }
338
+ function captureClient(client) {
339
+ const chunks = [];
340
+ let bytes = 0;
341
+ let overflowed = false;
342
+ let closed = false;
343
+ let live = null;
344
+ const onData = (chunk) => {
345
+ if (live) live.write(chunk);
346
+ if (overflowed) return;
347
+ chunks.push(chunk);
348
+ bytes += chunk.length;
349
+ if (bytes >= VERIFY_BUFFER_CAP_BYTES) {
350
+ overflowed = true;
351
+ client.pause();
352
+ }
353
+ };
354
+ const onGone = () => {
355
+ closed = true;
356
+ };
357
+ client.on("data", onData);
358
+ client.once("error", onGone);
359
+ client.once("close", onGone);
360
+ const detach = () => {
361
+ client.removeListener("data", onData);
362
+ client.removeListener("error", onGone);
363
+ client.removeListener("close", onGone);
364
+ };
365
+ return {
366
+ get closed() {
367
+ return closed;
368
+ },
369
+ get overflowed() {
370
+ return overflowed;
371
+ },
372
+ /** Replay everything buffered so far onto `upstream` and forward the rest
373
+ * live. Safe to call again on a different upstream (the fallback): the full
374
+ * buffer is retained until `commit`, so the replay is complete each time. */
375
+ feed(upstream) {
376
+ for (const chunk of chunks) upstream.write(chunk);
377
+ live = upstream;
378
+ },
379
+ /** Stop forwarding to the current upstream (keep buffering) — used before
380
+ * destroying a dead channel so late bytes never hit a torn-down stream. */
381
+ stop() {
382
+ live = null;
383
+ },
384
+ /** Terminal: detach and hand the socket to the (already-fed) `upstream` via a
385
+ * bidi pipe, or destroy it if the client already went away. */
386
+ commit(upstream) {
387
+ detach();
388
+ chunks.length = 0;
389
+ if (closed) {
390
+ upstream.destroy();
391
+ return;
392
+ }
393
+ pipeThrough(client, upstream);
394
+ },
395
+ /** Terminal: no usable upstream — surface the error to dockerode. */
396
+ fail(err) {
397
+ detach();
398
+ client.destroy(err instanceof Error ? err : new Error(String(err)));
399
+ }
400
+ };
401
+ }
307
402
  function createDockerSshBridge(opts) {
308
403
  const clients = /* @__PURE__ */ new Set();
309
404
  let upstreamMode = null;
310
405
  let modeDecision = null;
311
406
  let dialClient = null;
312
- const openDialStdioUpstream = async () => {
313
- if (opts.executor?.openDockerDialStdio) {
407
+ let pooledUnreliable = false;
408
+ const openDialStdioUpstream = async (forceEphemeral = false) => {
409
+ if (!forceEphemeral && opts.executor?.openDockerDialStdio) {
314
410
  const stream = await opts.executor.openDockerDialStdio();
315
411
  stream.once(
316
412
  "error",
@@ -373,29 +469,57 @@ function createDockerSshBridge(opts) {
373
469
  modeDecision ??= decideMode().then((mode) => upstreamMode = mode);
374
470
  return modeDecision;
375
471
  };
376
- const openUpstream = async () => {
377
- const mode = await ensureMode();
378
- return mode === "dialstdio" ? openDialStdioUpstream() : openStreamlocalUpstream(opts);
472
+ const downgradeToDialStdio = async (capture, reason) => {
473
+ console.warn(
474
+ `[docker-ssh] streamlocal unusable (${opts.host ?? "?"}): ${reason} \u2014 downgrading this bridge to dial-stdio (fresh connection) and replaying the buffered request.`
475
+ );
476
+ upstreamMode = "dialstdio";
477
+ pooledUnreliable = true;
478
+ const upstream = await openDialStdioUpstream(true);
479
+ capture.feed(upstream);
480
+ capture.commit(upstream);
481
+ };
482
+ const bridgeClient = async (client) => {
483
+ const capture = captureClient(client);
484
+ try {
485
+ const mode = await ensureMode();
486
+ if (mode === "dialstdio") {
487
+ const upstream = await openDialStdioUpstream(pooledUnreliable);
488
+ capture.feed(upstream);
489
+ capture.commit(upstream);
490
+ return;
491
+ }
492
+ let channel;
493
+ try {
494
+ channel = await withTimeout(
495
+ openStreamlocalUpstream(opts),
496
+ STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS,
497
+ `channel open timed out after ${STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS / 1e3}s`
498
+ );
499
+ } catch (err) {
500
+ await downgradeToDialStdio(capture, safeErrorMessage(err));
501
+ return;
502
+ }
503
+ capture.feed(channel);
504
+ const firstByte = capture.overflowed ? null : await awaitFirstByte(channel, STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS);
505
+ if (capture.overflowed || firstByte) {
506
+ if (firstByte) client.write(firstByte);
507
+ capture.commit(channel);
508
+ return;
509
+ }
510
+ capture.stop();
511
+ channel.destroy();
512
+ await downgradeToDialStdio(capture, "channel opened but no data flowed");
513
+ } catch (err) {
514
+ console.warn(`[docker-ssh] bridge client failed (${opts.host ?? "?"}): ${safeErrorMessage(err)}`);
515
+ capture.fail(err);
516
+ }
379
517
  };
380
518
  const server = net2.createServer((client) => {
381
519
  clients.add(client);
382
520
  client.setNoDelay(true);
383
521
  client.once("close", () => clients.delete(client));
384
- openUpstream().then((upstream) => {
385
- const teardown = () => {
386
- client.destroy();
387
- upstream.destroy();
388
- };
389
- client.on("error", teardown);
390
- upstream.on("error", teardown);
391
- client.once("close", () => upstream.destroy());
392
- upstream.once("close", () => client.destroy());
393
- client.pipe(upstream);
394
- upstream.pipe(client);
395
- }).catch((error) => {
396
- console.warn(`[docker-ssh] bridge upstream open failed: ${safeErrorMessage(error)}`);
397
- client.destroy(error instanceof Error ? error : new Error(String(error)));
398
- });
522
+ void bridgeClient(client);
399
523
  });
400
524
  return {
401
525
  start: () => new Promise((resolve, reject) => {
@@ -22,8 +22,10 @@ import {
22
22
  sq
23
23
  } from "./chunk-FPRHYBY2.js";
24
24
  import {
25
+ EDGE_CONTAINER_NAME,
25
26
  EdgeConflictError,
26
27
  EdgeMigrateRequested,
28
+ edgeFailureReason,
27
29
  freeEdgeTargets,
28
30
  invalidateEdgeContainer,
29
31
  ourLuaOnHost,
@@ -31,7 +33,7 @@ import {
31
33
  resolveOurEdgeContainer,
32
34
  sq as sq2,
33
35
  stopTargetsForStatus
34
- } from "./chunk-FQGJZ2HS.js";
36
+ } from "./chunk-R5BHFFAV.js";
35
37
  import {
36
38
  waitForPortListening
37
39
  } from "./chunk-ILFETZOM.js";
@@ -976,6 +978,8 @@ async function readJournal(executor) {
976
978
  }
977
979
  async function rollback(executor, journal, onLog) {
978
980
  onLog(log("Rolling back \u2014 restoring the previous proxy...", "warn"));
981
+ await tryExec2(executor, `docker update --restart=no ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
982
+ await tryExec2(executor, `docker stop ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
979
983
  await tryExec2(
980
984
  executor,
981
985
  "systemctl disable --now openresty 2>/dev/null || systemctl stop openresty 2>/dev/null || true; systemctl reset-failed openresty 2>/dev/null || true"
@@ -1000,6 +1004,24 @@ async function rollback(executor, journal, onLog) {
1000
1004
  onLog(log(`Could not restore process ${p.pid} \u2014 no command captured.`, "warn"));
1001
1005
  }
1002
1006
  }
1007
+ const restored = await portIsServed(executor, 80);
1008
+ if (!restored) {
1009
+ onLog(
1010
+ log(
1011
+ "Nothing is listening on :80 after the restore \u2014 the box is NOT serving. Start your proxy by hand (e.g. `systemctl start nginx`, or `docker start <name>`).",
1012
+ "error"
1013
+ )
1014
+ );
1015
+ }
1016
+ return restored;
1017
+ }
1018
+ async function portIsServed(executor, port) {
1019
+ const hex = port.toString(16).toUpperCase().padStart(4, "0");
1020
+ const out = await tryExec2(
1021
+ executor,
1022
+ `(command -v ss >/dev/null 2>&1 && ss -ltn 2>/dev/null | grep -qE ':${port}[[:space:]]' && echo yes) || (grep -qiE '^[[:space:]]*[0-9]+:[[:space:]]*[0-9A-F]{8}:${hex}[[:space:]]+[0-9A-F]{8}:0000[[:space:]]+0A' /proc/net/tcp 2>/dev/null && echo yes) || true`
1023
+ );
1024
+ return (out ?? "").includes("yes");
1003
1025
  }
1004
1026
  async function beginEdgeTakeover(executor, status, onLog) {
1005
1027
  await writeJournal(executor, await buildJournal(executor, status));
@@ -1008,9 +1030,9 @@ async function beginEdgeTakeover(executor, status, onLog) {
1008
1030
  async function rollbackEdgeTakeover(executor, onLog) {
1009
1031
  const journal = await readJournal(executor);
1010
1032
  if (!journal || journal.completed) return false;
1011
- await rollback(executor, journal, onLog);
1033
+ const restored = await rollback(executor, journal, onLog);
1012
1034
  await clearJournal(executor);
1013
- return true;
1035
+ return restored;
1014
1036
  }
1015
1037
  async function completeEdgeTakeover(executor) {
1016
1038
  const journal = await readJournal(executor);
@@ -1802,7 +1824,6 @@ async function ensureEdgeClear(executor, config, onLog) {
1802
1824
  }
1803
1825
 
1804
1826
  // ../../packages/adapters/src/system/proxy/ensure-container-edge.ts
1805
- var EDGE_CONTAINER_NAME = "openship-edge";
1806
1827
  function log4(message, level = "info") {
1807
1828
  return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
1808
1829
  }
@@ -1821,7 +1842,7 @@ async function runningImage(executor, container) {
1821
1842
  return out.trim() || null;
1822
1843
  }
1823
1844
  async function containerEdgeProvider(executor, container, opts) {
1824
- const { NginxProvider: Provider } = await import("./nginx-2GTRUIDE.js");
1845
+ const { NginxProvider: Provider } = await import("./nginx-XWJAOGS4.js");
1825
1846
  return new Provider({
1826
1847
  ...opts,
1827
1848
  // Vhosts go to the BIND-MOUNTED HOST dir (so every host-side reader — the
@@ -1836,7 +1857,7 @@ async function containerEdgeProvider(executor, container, opts) {
1836
1857
  }
1837
1858
  async function localContainerEdgeProvider(container, opts) {
1838
1859
  const { DockerEdgeExecutor } = await import("./docker-edge-executor-VHW3JPJS.js");
1839
- const { NginxProvider: Provider } = await import("./nginx-2GTRUIDE.js");
1860
+ const { NginxProvider: Provider } = await import("./nginx-XWJAOGS4.js");
1840
1861
  return new Provider({
1841
1862
  ...opts,
1842
1863
  paths: OPENRESTY_DEFAULT_PATHS,
@@ -1893,6 +1914,35 @@ async function swapEdgeImage(executor, container, from, to, opts) {
1893
1914
  onLog(log4(`Rollback to ${from} ALSO failed \u2014 the edge is down on this server.`, "error"));
1894
1915
  return { swapped: false, edgeDown: true };
1895
1916
  }
1917
+ async function listDir(executor, dir) {
1918
+ const out = await executor.exec(`ls -1 ${sq(dir)} 2>/dev/null || true`).catch(() => "");
1919
+ return new Set(
1920
+ out.split("\n").map((l) => l.trim()).filter(Boolean)
1921
+ );
1922
+ }
1923
+ async function sanitizeCarriedVhosts(executor, sitesDir, onLog) {
1924
+ const script = [
1925
+ `for f in ${sq(sitesDir)}/*.conf; do`,
1926
+ ` [ -f "$f" ] || continue;`,
1927
+ // A real server_name is anything that isn't the `_` wildcard.
1928
+ ` if ! grep -qE '^[[:space:]]*server_name[[:space:]]+[^_;[:space:]]' "$f"; then`,
1929
+ ` echo "dropped-catchall $f"; rm -f "$f"; continue;`,
1930
+ ` fi;`,
1931
+ ` if grep -qE '[[:space:]]default_server' "$f"; then`,
1932
+ ` sed -i -E 's/([[:space:]]listen[^;]*)[[:space:]]+default_server/\\1/g' "$f" && echo "unset-default $f";`,
1933
+ ` fi;`,
1934
+ `done`
1935
+ ].join(" ");
1936
+ const out = await executor.exec(script).catch(() => "");
1937
+ for (const line of out.split("\n").map((l) => l.trim()).filter(Boolean)) {
1938
+ const [action, file] = [line.slice(0, line.indexOf(" ")), line.slice(line.indexOf(" ") + 1)];
1939
+ if (action === "dropped-catchall") {
1940
+ onLog(log4(`Dropped the bare edge's catch-all ${file} \u2014 the edge image provides it.`, "warn"));
1941
+ } else if (action === "unset-default") {
1942
+ onLog(log4(`Removed default_server from ${file} \u2014 the edge image owns it.`, "warn"));
1943
+ }
1944
+ }
1945
+ }
1896
1946
  async function ensureContainerEdge(executor, opts) {
1897
1947
  const { onLog } = opts;
1898
1948
  const container = opts.container?.trim() || EDGE_CONTAINER_NAME;
@@ -1931,16 +1981,28 @@ async function ensureContainerEdge(executor, opts) {
1931
1981
  });
1932
1982
  }
1933
1983
  const sitesTarget = EDGE_HOST_PATHS.sitesDir;
1984
+ let beforeCarry = null;
1934
1985
  if (bareWasOurs) {
1935
1986
  const barePaths = await detectOpenRestyPaths(executor).catch(() => OPENRESTY_DEFAULT_PATHS);
1936
1987
  if (barePaths.sitesDir !== sitesTarget && await executor.exists(barePaths.sitesDir)) {
1937
1988
  onLog(log4(`Carrying vhosts from ${barePaths.sitesDir}...`));
1989
+ beforeCarry = await listDir(executor, sitesTarget);
1938
1990
  await executor.exec(`cp -a ${sq(`${barePaths.sitesDir}/.`)} ${sq(`${sitesTarget}/`)} 2>/dev/null || true`).catch(() => {
1939
1991
  });
1992
+ await sanitizeCarriedVhosts(executor, sitesTarget, onLog);
1940
1993
  }
1941
1994
  }
1942
1995
  const restoreBare = async () => {
1943
1996
  if (!bareWasOurs) return;
1997
+ if (beforeCarry) {
1998
+ const now = await listDir(executor, sitesTarget);
1999
+ const added = [...now].filter((name) => !beforeCarry.has(name));
2000
+ if (added.length > 0) {
2001
+ await executor.exec(`rm -f ${added.map((n) => sq(`${sitesTarget}/${n}`)).join(" ")}`).catch(() => {
2002
+ });
2003
+ onLog(log4(`Removed ${added.length} carried vhost(s) so the next edge start is clean.`, "warn"));
2004
+ }
2005
+ }
1944
2006
  onLog(log4("Restoring the host OpenResty edge...", "warn"));
1945
2007
  await executor.exec("systemctl enable --now openresty 2>/dev/null || true").catch(() => {
1946
2008
  });
@@ -1982,7 +2044,8 @@ ${logs}`, "error"));
1982
2044
  });
1983
2045
  invalidateEdgeContainer(executor);
1984
2046
  await restoreBare();
1985
- throw new Error(`Edge container setup failed: ${msg}`);
2047
+ const reason = edgeFailureReason(logs);
2048
+ throw new Error(`Edge container setup failed: ${reason ? `${reason} (${msg})` : msg}`);
1986
2049
  }
1987
2050
  }
1988
2051
 
@@ -2000,7 +2063,6 @@ export {
2000
2063
  rollbackEdgeTakeover,
2001
2064
  completeEdgeTakeover,
2002
2065
  recoverInterruptedTakeover,
2003
- EDGE_CONTAINER_NAME,
2004
2066
  setDefaultEdgeImage,
2005
2067
  resolveEdgeImage,
2006
2068
  dockerAvailable,
@@ -5,7 +5,7 @@ const require = __ospCreateRequire(import.meta.url);
5
5
  import {
6
6
  checkGit,
7
7
  installGit
8
- } from "./chunk-SQ77DZOI.js";
8
+ } from "./chunk-UXWAVM4C.js";
9
9
  import {
10
10
  createDockerBuildContext,
11
11
  normalizeDockerRelativePath,
@@ -6,8 +6,8 @@ import {
6
6
  CloudRuntime,
7
7
  PAGE_CONTAINER_PREFIX,
8
8
  provisionCloudWorkspace
9
- } from "./chunk-5FNIWBFI.js";
10
- import "./chunk-SQ77DZOI.js";
9
+ } from "./chunk-XFCDYZUQ.js";
10
+ import "./chunk-UXWAVM4C.js";
11
11
  import "./chunk-V6AN6XLZ.js";
12
12
  import "./chunk-MZ2WEHHY.js";
13
13
  import "./chunk-L2VOGB2X.js";
@@ -18,7 +18,7 @@ import "./chunk-RPHRPFEH.js";
18
18
  import "./chunk-TYVME3XD.js";
19
19
  import "./chunk-FXTBHTWY.js";
20
20
  import "./chunk-FPRHYBY2.js";
21
- import "./chunk-FQGJZ2HS.js";
21
+ import "./chunk-R5BHFFAV.js";
22
22
  import "./chunk-ILFETZOM.js";
23
23
  import "./chunk-VFINNTUL.js";
24
24
  import "./chunk-6APULWSU.js";
@@ -3,9 +3,13 @@
3
3
  import { createRequire as __ospCreateRequire } from "node:module";
4
4
  const require = __ospCreateRequire(import.meta.url);
5
5
  import {
6
+ EDGE_CONTAINER_NAME,
6
7
  EdgeConflictError,
7
8
  EdgeMigrateRequested,
8
9
  classifyProxy,
10
+ edgeCrashReason,
11
+ edgeFailureReason,
12
+ edgeIsServing,
9
13
  freeEdgeTargets,
10
14
  invalidateEdgeContainer,
11
15
  isOurEdgeContainer,
@@ -15,15 +19,19 @@ import {
15
19
  resolveOurEdgeContainer,
16
20
  sq,
17
21
  stopTargetsForStatus
18
- } from "./chunk-FQGJZ2HS.js";
22
+ } from "./chunk-R5BHFFAV.js";
19
23
  import "./chunk-ILFETZOM.js";
20
24
  import "./chunk-VFINNTUL.js";
21
25
  import "./chunk-6APULWSU.js";
22
26
  import "./chunk-KI2EY3WB.js";
23
27
  export {
28
+ EDGE_CONTAINER_NAME,
24
29
  EdgeConflictError,
25
30
  EdgeMigrateRequested,
26
31
  classifyProxy,
32
+ edgeCrashReason,
33
+ edgeFailureReason,
34
+ edgeIsServing,
27
35
  freeEdgeTargets,
28
36
  invalidateEdgeContainer,
29
37
  isOurEdgeContainer,
@@ -4,7 +4,7 @@ import { createRequire as __ospCreateRequire } from "node:module";
4
4
  const require = __ospCreateRequire(import.meta.url);
5
5
  import {
6
6
  DockerRuntime
7
- } from "./chunk-2ZLR7ULL.js";
7
+ } from "./chunk-SJIFLLX4.js";
8
8
  import "./chunk-V6AN6XLZ.js";
9
9
  import "./chunk-4Z5QO7N6.js";
10
10
  import "./chunk-OXWDY22G.js";
@@ -5,7 +5,7 @@ const require = __ospCreateRequire(import.meta.url);
5
5
  import {
6
6
  importMigratedSites,
7
7
  waitForApiHealth
8
- } from "./chunk-3JQTL4R4.js";
8
+ } from "./chunk-IMBD5PZ5.js";
9
9
  import "./chunk-FRNAR33O.js";
10
10
  import "./chunk-EI575GHN.js";
11
11
  import "./chunk-6APIPIJY.js";
@@ -14,15 +14,15 @@ import "./chunk-OWED2GRM.js";
14
14
  import "./chunk-VWPEKOH6.js";
15
15
  import "./chunk-W73X43R7.js";
16
16
  import "./chunk-TGIINPVM.js";
17
- import "./chunk-5FNIWBFI.js";
17
+ import "./chunk-XFCDYZUQ.js";
18
18
  import "./chunk-AVIB7CT7.js";
19
19
  import "./chunk-5MZ624VX.js";
20
20
  import "./chunk-Q2CIRAOK.js";
21
- import "./chunk-C5OHTYEF.js";
22
- import "./chunk-SQ77DZOI.js";
21
+ import "./chunk-IPYPMI3S.js";
22
+ import "./chunk-UXWAVM4C.js";
23
23
  import "./chunk-O6ZZF52V.js";
24
24
  import "./chunk-JYGD5WSN.js";
25
- import "./chunk-2ZLR7ULL.js";
25
+ import "./chunk-SJIFLLX4.js";
26
26
  import "./chunk-V6AN6XLZ.js";
27
27
  import "./chunk-VDMQJHPI.js";
28
28
  import "./chunk-MZ2WEHHY.js";
@@ -34,7 +34,7 @@ import "./chunk-RPHRPFEH.js";
34
34
  import "./chunk-TYVME3XD.js";
35
35
  import "./chunk-FXTBHTWY.js";
36
36
  import "./chunk-FPRHYBY2.js";
37
- import "./chunk-FQGJZ2HS.js";
37
+ import "./chunk-R5BHFFAV.js";
38
38
  import "./chunk-ILFETZOM.js";
39
39
  import "./chunk-VFINNTUL.js";
40
40
  import "./chunk-6APULWSU.js";
@@ -3,7 +3,6 @@
3
3
  import { createRequire as __ospCreateRequire } from "node:module";
4
4
  const require = __ospCreateRequire(import.meta.url);
5
5
  import {
6
- EDGE_CONTAINER_NAME,
7
6
  buildEdgeRunCommand,
8
7
  containerEdgeProvider,
9
8
  dockerAvailable,
@@ -11,18 +10,17 @@ import {
11
10
  localContainerEdgeProvider,
12
11
  resolveEdgeImage,
13
12
  setDefaultEdgeImage
14
- } from "./chunk-SQ77DZOI.js";
13
+ } from "./chunk-UXWAVM4C.js";
15
14
  import "./chunk-L2VOGB2X.js";
16
15
  import "./chunk-RPHRPFEH.js";
17
16
  import "./chunk-FXTBHTWY.js";
18
17
  import "./chunk-FPRHYBY2.js";
19
- import "./chunk-FQGJZ2HS.js";
18
+ import "./chunk-R5BHFFAV.js";
20
19
  import "./chunk-ILFETZOM.js";
21
20
  import "./chunk-VFINNTUL.js";
22
21
  import "./chunk-6APULWSU.js";
23
22
  import "./chunk-KI2EY3WB.js";
24
23
  export {
25
- EDGE_CONTAINER_NAME,
26
24
  buildEdgeRunCommand,
27
25
  containerEdgeProvider,
28
26
  dockerAvailable,
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ import {
25
25
  remoteSha,
26
26
  shortSha,
27
27
  sourceBuildDir
28
- } from "./chunk-3JQTL4R4.js";
28
+ } from "./chunk-IMBD5PZ5.js";
29
29
  import "./chunk-FRNAR33O.js";
30
30
  import "./chunk-EI575GHN.js";
31
31
  import "./chunk-6APIPIJY.js";
@@ -34,11 +34,11 @@ import "./chunk-OWED2GRM.js";
34
34
  import "./chunk-VWPEKOH6.js";
35
35
  import "./chunk-W73X43R7.js";
36
36
  import "./chunk-TGIINPVM.js";
37
- import "./chunk-5FNIWBFI.js";
37
+ import "./chunk-XFCDYZUQ.js";
38
38
  import "./chunk-AVIB7CT7.js";
39
39
  import "./chunk-5MZ624VX.js";
40
40
  import "./chunk-Q2CIRAOK.js";
41
- import "./chunk-C5OHTYEF.js";
41
+ import "./chunk-IPYPMI3S.js";
42
42
  import {
43
43
  beginEdgeTakeover,
44
44
  collectProxyCerts,
@@ -51,10 +51,10 @@ import {
51
51
  recoverInterruptedTakeover,
52
52
  rollbackEdgeTakeover,
53
53
  scanImportableSites
54
- } from "./chunk-SQ77DZOI.js";
54
+ } from "./chunk-UXWAVM4C.js";
55
55
  import "./chunk-O6ZZF52V.js";
56
56
  import "./chunk-JYGD5WSN.js";
57
- import "./chunk-2ZLR7ULL.js";
57
+ import "./chunk-SJIFLLX4.js";
58
58
  import "./chunk-V6AN6XLZ.js";
59
59
  import "./chunk-VDMQJHPI.js";
60
60
  import "./chunk-MZ2WEHHY.js";
@@ -69,8 +69,10 @@ import {
69
69
  import "./chunk-FXTBHTWY.js";
70
70
  import "./chunk-FPRHYBY2.js";
71
71
  import {
72
+ edgeCrashReason,
73
+ edgeIsServing,
72
74
  ourEdgeContainerRunning
73
- } from "./chunk-FQGJZ2HS.js";
75
+ } from "./chunk-R5BHFFAV.js";
74
76
  import "./chunk-ILFETZOM.js";
75
77
  import "./chunk-VFINNTUL.js";
76
78
  import {
@@ -1722,6 +1724,27 @@ async function runCompose(opts) {
1722
1724
  process.exit(1);
1723
1725
  }
1724
1726
  spinner5.succeed("Openship is running via Docker Compose.");
1727
+ if (edgePlan.action && !await edgeIsServing(new LocalExecutor())) {
1728
+ const reason = await edgeCrashReason(new LocalExecutor());
1729
+ console.error(
1730
+ chalk6.red(`
1731
+ Openship's edge is not serving :80${reason ? ` \u2014 ${reason}` : "."}`)
1732
+ );
1733
+ const restored = await rollbackHostEdge();
1734
+ console.error(
1735
+ restored ? chalk6.yellow(
1736
+ ` Your previous proxy has been restarted \u2014 the box is serving again, on it.
1737
+ Nothing was migrated. Fix the cause above, then re-run \`openship up\`.
1738
+ `
1739
+ ) : chalk6.red(
1740
+ ` AND your previous proxy could NOT be restarted automatically \u2014 the box is
1741
+ serving nothing right now. Start it by hand:
1742
+ docker stop openship-edge && sudo systemctl enable --now nginx
1743
+ `
1744
+ )
1745
+ );
1746
+ process.exit(1);
1747
+ }
1725
1748
  let importedOk = true;
1726
1749
  if (edgePlan.action === "migrate" && edgePlan.sites?.length) {
1727
1750
  const outcome = await importMigratedSites(res.apiPort, edgePlan.sites, edgePlan.certPems);
@@ -1973,7 +1996,7 @@ async function runForeground(opts, source) {
1973
1996
  const uiSpinner = ora("Preparing the dashboard\u2026").start();
1974
1997
  try {
1975
1998
  const bundle = await ensureDashboard({
1976
- tag: source ? "local" : opts.uiVersion || `v${"0.4.6"}`,
1999
+ tag: source ? "local" : opts.uiVersion || `v${"0.4.7"}`,
1977
2000
  onProgress: (received, total) => {
1978
2001
  if (total) {
1979
2002
  uiSpinner.text = `Downloading dashboard\u2026 ${Math.round(received / total * 100)}%`;
@@ -6582,7 +6605,7 @@ function detectPackageManager2(override) {
6582
6605
  var updateCommand = new Command26("update").description("Update the Openship CLI + bundled server to the latest release").option("--check", "Only report the current + latest version; don't install").option("--via <manager>", "Package manager to update with: bun | npm").option("--rebuild", "From-source installs: rebuild even if already at the remote tip").action(async (opts) => {
6583
6606
  const source = readSourceInstall();
6584
6607
  if (source) return runSourceUpdate(source, opts);
6585
- const current = "0.4.6";
6608
+ const current = "0.4.7";
6586
6609
  let latest;
6587
6610
  try {
6588
6611
  latest = (await resolveLatestTag()).replace(/^v/, "");
@@ -7149,7 +7172,7 @@ async function runWizard() {
7149
7172
  }
7150
7173
  }
7151
7174
  }
7152
- const uiTag = `v${"0.4.6"}`;
7175
+ const uiTag = `v${"0.4.7"}`;
7153
7176
  if (method === "bare") {
7154
7177
  const dl = spinner4();
7155
7178
  dl.start("Pulling the Openship dist from GitHub");
@@ -7217,7 +7240,7 @@ async function runWizard() {
7217
7240
  const up = composeUp({
7218
7241
  publicUrl,
7219
7242
  trustProxy: behindProxy,
7220
- version: "0.4.6",
7243
+ version: "0.4.7",
7221
7244
  noHostControl: !allowHostControl
7222
7245
  });
7223
7246
  if (!up.ok) {
@@ -7554,7 +7577,7 @@ ${chalk20.dim("Dashboard".padEnd(11))}${dashUrl}
7554
7577
 
7555
7578
  // src/index.ts
7556
7579
  var program = new Command28();
7557
- program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.4.6").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
7580
+ program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.4.7").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
7558
7581
  if (thisCommand.opts().json) setJsonMode(true);
7559
7582
  }).action(async () => {
7560
7583
  const installed = serviceStatus().installed || readInstallMethod() === "compose";
@@ -10,7 +10,7 @@ import {
10
10
  summarizeCertbotFailure
11
11
  } from "./chunk-FXTBHTWY.js";
12
12
  import "./chunk-FPRHYBY2.js";
13
- import "./chunk-FQGJZ2HS.js";
13
+ import "./chunk-R5BHFFAV.js";
14
14
  import "./chunk-ILFETZOM.js";
15
15
  import "./chunk-VFINNTUL.js";
16
16
  import "./chunk-6APULWSU.js";
@@ -21998,13 +21998,102 @@ async function openStreamlocalUpstream(opts) {
21998
21998
  throw error51;
21999
21999
  }
22000
22000
  }
22001
+ function pipeThrough(client, upstream) {
22002
+ const teardown = () => {
22003
+ client.destroy();
22004
+ upstream.destroy();
22005
+ };
22006
+ client.on("error", teardown);
22007
+ upstream.on("error", teardown);
22008
+ client.once("close", () => upstream.destroy());
22009
+ upstream.once("close", () => client.destroy());
22010
+ client.pipe(upstream);
22011
+ upstream.pipe(client);
22012
+ }
22013
+ function awaitFirstByte(upstream, ms) {
22014
+ return new Promise((resolve2) => {
22015
+ const finish = (chunk) => {
22016
+ clearTimeout(timer);
22017
+ upstream.removeListener("data", onData);
22018
+ upstream.removeListener("error", onFail);
22019
+ upstream.removeListener("close", onFail);
22020
+ resolve2(chunk);
22021
+ };
22022
+ const onData = (chunk) => finish(chunk);
22023
+ const onFail = () => finish(null);
22024
+ const timer = setTimeout(() => finish(null), ms);
22025
+ upstream.on("data", onData);
22026
+ upstream.once("error", onFail);
22027
+ upstream.once("close", onFail);
22028
+ });
22029
+ }
22030
+ function captureClient(client) {
22031
+ const chunks = [];
22032
+ let bytes = 0;
22033
+ let overflowed = false;
22034
+ let closed = false;
22035
+ let live = null;
22036
+ const onData = (chunk) => {
22037
+ if (live)
22038
+ live.write(chunk);
22039
+ if (overflowed)
22040
+ return;
22041
+ chunks.push(chunk);
22042
+ bytes += chunk.length;
22043
+ if (bytes >= VERIFY_BUFFER_CAP_BYTES) {
22044
+ overflowed = true;
22045
+ client.pause();
22046
+ }
22047
+ };
22048
+ const onGone = () => {
22049
+ closed = true;
22050
+ };
22051
+ client.on("data", onData);
22052
+ client.once("error", onGone);
22053
+ client.once("close", onGone);
22054
+ const detach = () => {
22055
+ client.removeListener("data", onData);
22056
+ client.removeListener("error", onGone);
22057
+ client.removeListener("close", onGone);
22058
+ };
22059
+ return {
22060
+ get closed() {
22061
+ return closed;
22062
+ },
22063
+ get overflowed() {
22064
+ return overflowed;
22065
+ },
22066
+ feed(upstream) {
22067
+ for (const chunk of chunks)
22068
+ upstream.write(chunk);
22069
+ live = upstream;
22070
+ },
22071
+ stop() {
22072
+ live = null;
22073
+ },
22074
+ commit(upstream) {
22075
+ detach();
22076
+ chunks.length = 0;
22077
+ if (closed) {
22078
+ upstream.destroy();
22079
+ return;
22080
+ }
22081
+ pipeThrough(client, upstream);
22082
+ },
22083
+ fail(err2) {
22084
+ detach();
22085
+ client.destroy(err2 instanceof Error ? err2 : new Error(String(err2)));
22086
+ }
22087
+ };
22088
+ }
22001
22089
  function createDockerSshBridge(opts) {
22002
22090
  const clients = new Set;
22003
22091
  let upstreamMode = null;
22004
22092
  let modeDecision = null;
22005
22093
  let dialClient = null;
22006
- const openDialStdioUpstream = async () => {
22007
- if (opts.executor?.openDockerDialStdio) {
22094
+ let pooledUnreliable = false;
22095
+ const openDialStdioUpstream = async (forceEphemeral = false) => {
22096
+ if (!forceEphemeral && opts.executor?.openDockerDialStdio) {
22008
22097
  const stream = await opts.executor.openDockerDialStdio();
22009
22098
  stream.once("error", (e) => console.warn(`[docker-ssh] dial-stdio channel error: ${safeErrorMessage(e)}`));
22010
22099
  return stream;
@@ -22061,29 +22150,52 @@ Host: localhost\r
22061
22150
  modeDecision ??= decideMode().then((mode) => upstreamMode = mode);
22062
22151
  return modeDecision;
22063
22152
  };
22064
- const openUpstream = async () => {
22065
- const mode = await ensureMode();
22066
- return mode === "dialstdio" ? openDialStdioUpstream() : openStreamlocalUpstream(opts);
22153
+ const downgradeToDialStdio = async (capture, reason) => {
22154
+ console.warn(`[docker-ssh] streamlocal unusable (${opts.host ?? "?"}): ${reason} — ` + "downgrading this bridge to dial-stdio (fresh connection) and replaying the buffered request.");
22155
+ upstreamMode = "dialstdio";
22156
+ pooledUnreliable = true;
22157
+ const upstream = await openDialStdioUpstream(true);
22158
+ capture.feed(upstream);
22159
+ capture.commit(upstream);
22160
+ };
22161
+ const bridgeClient = async (client) => {
22162
+ const capture = captureClient(client);
22163
+ try {
22164
+ const mode = await ensureMode();
22165
+ if (mode === "dialstdio") {
22166
+ const upstream = await openDialStdioUpstream(pooledUnreliable);
22167
+ capture.feed(upstream);
22168
+ capture.commit(upstream);
22169
+ return;
22170
+ }
22171
+ let channel;
22172
+ try {
22173
+ channel = await withTimeout(openStreamlocalUpstream(opts), STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS, `channel open timed out after ${STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS / 1000}s`);
22174
+ } catch (err2) {
22175
+ await downgradeToDialStdio(capture, safeErrorMessage(err2));
22176
+ return;
22177
+ }
22178
+ capture.feed(channel);
22179
+ const firstByte = capture.overflowed ? null : await awaitFirstByte(channel, STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS);
22180
+ if (capture.overflowed || firstByte) {
22181
+ if (firstByte)
22182
+ client.write(firstByte);
22183
+ capture.commit(channel);
22184
+ return;
22185
+ }
22186
+ capture.stop();
22187
+ channel.destroy();
22188
+ await downgradeToDialStdio(capture, "channel opened but no data flowed");
22189
+ } catch (err2) {
22190
+ console.warn(`[docker-ssh] bridge client failed (${opts.host ?? "?"}): ${safeErrorMessage(err2)}`);
22191
+ capture.fail(err2);
22192
+ }
22067
22193
  };
22068
22194
  const server = net2.createServer((client) => {
22069
22195
  clients.add(client);
22070
22196
  client.setNoDelay(true);
22071
22197
  client.once("close", () => clients.delete(client));
22072
- openUpstream().then((upstream) => {
22073
- const teardown = () => {
22074
- client.destroy();
22075
- upstream.destroy();
22076
- };
22077
- client.on("error", teardown);
22078
- upstream.on("error", teardown);
22079
- client.once("close", () => upstream.destroy());
22080
- upstream.once("close", () => client.destroy());
22081
- client.pipe(upstream);
22082
- upstream.pipe(client);
22083
- }).catch((error51) => {
22084
- console.warn(`[docker-ssh] bridge upstream open failed: ${safeErrorMessage(error51)}`);
22085
- client.destroy(error51 instanceof Error ? error51 : new Error(String(error51)));
22086
- });
22198
+ bridgeClient(client);
22087
22199
  });
22088
22200
  return {
22089
22201
  start: () => new Promise((resolve2, reject) => {
@@ -22113,11 +22225,12 @@ Host: localhost\r
22113
22225
  }
22114
22226
  };
22115
22227
  }
22116
- var DEFAULT_REMOTE_DOCKER_SOCKET_PATH = "/var/run/docker.sock", resolvedDockerSocketPathCache, DOCKER_DIAL_STDIO_COMMAND = "docker system dial-stdio", STREAMLOCAL_PROBE_TIMEOUT_MS = 8000, DOCKER_SOCKET_DISCOVERY_SCRIPT;
22228
+ var DEFAULT_REMOTE_DOCKER_SOCKET_PATH = "/var/run/docker.sock", resolvedDockerSocketPathCache, DOCKER_DIAL_STDIO_COMMAND = "docker system dial-stdio", STREAMLOCAL_PROBE_TIMEOUT_MS = 8000, STREAMLOCAL_DATA_VERIFY_TIMEOUT_MS = 3000, VERIFY_BUFFER_CAP_BYTES, DOCKER_SOCKET_DISCOVERY_SCRIPT;
22117
22229
  var init_docker_ssh_agent = __esm(() => {
22118
22230
  init_ssh_client();
22119
22231
  init_src();
22120
22232
  resolvedDockerSocketPathCache = new WeakMap;
22233
+ VERIFY_BUFFER_CAP_BYTES = 8 * 1024 * 1024;
22121
22234
  DOCKER_SOCKET_DISCOVERY_SCRIPT = [
22122
22235
  "set -eu",
22123
22236
  'uid="$(id -u 2>/dev/null || printf 0)"',
@@ -29262,9 +29375,13 @@ __export(exports_detect, {
29262
29375
  isOurEdgeContainer: () => isOurEdgeContainer,
29263
29376
  invalidateEdgeContainer: () => invalidateEdgeContainer,
29264
29377
  freeEdgeTargets: () => freeEdgeTargets,
29378
+ edgeIsServing: () => edgeIsServing,
29379
+ edgeFailureReason: () => edgeFailureReason,
29380
+ edgeCrashReason: () => edgeCrashReason,
29265
29381
  classifyProxy: () => classifyProxy,
29266
29382
  EdgeMigrateRequested: () => EdgeMigrateRequested,
29267
- EdgeConflictError: () => EdgeConflictError
29383
+ EdgeConflictError: () => EdgeConflictError,
29384
+ EDGE_CONTAINER_NAME: () => EDGE_CONTAINER_NAME
29268
29385
  });
29269
29386
  async function tryExec3(executor, command) {
29270
29387
  try {
@@ -29297,6 +29414,30 @@ function sq3(value) {
29297
29414
  function isOurEdgeContainer(name2, image) {
29298
29415
  return /openship-edge/i.test(`${name2 ?? ""} ${image ?? ""}`);
29299
29416
  }
29417
+ async function edgeIsServing(executor, port = 80) {
29418
+ const running = await tryExec3(executor, `docker inspect -f '{{.State.Running}}' ${sq3(EDGE_CONTAINER_NAME)} 2>/dev/null`);
29419
+ if ((running ?? "").trim() !== "true")
29420
+ return false;
29421
+ const probe = await tryExec3(executor, containerShell(`wget -q -O /dev/null -T 3 http://127.0.0.1:${port}/ >/dev/null 2>&1; echo $?`));
29422
+ const code = (probe ?? "").trim().split(`
29423
+ `).pop() ?? "1";
29424
+ return code === "0" || code === "8";
29425
+ }
29426
+ function containerShell(command) {
29427
+ return `docker exec ${sq3(EDGE_CONTAINER_NAME)} sh -c ${sq3(command)}`;
29428
+ }
29429
+ async function edgeCrashReason(executor) {
29430
+ const logs = await tryExec3(executor, `docker logs --tail 40 ${sq3(EDGE_CONTAINER_NAME)} 2>&1`);
29431
+ return logs ? edgeFailureReason(logs) : null;
29432
+ }
29433
+ function edgeFailureReason(containerLog) {
29434
+ const lines = containerLog.split(`
29435
+ `).map((l) => l.trim()).filter(Boolean);
29436
+ const emerg = lines.find((l) => l.includes("[emerg]"));
29437
+ if (emerg)
29438
+ return emerg.replace(/^.*\[emerg\]\s*\d*#\d*:\s*/, "");
29439
+ return lines.at(-1) ?? null;
29440
+ }
29300
29441
  async function ourEdgeContainerRunning(executor) {
29301
29442
  return Boolean(await resolveOurEdgeContainer(executor));
29302
29443
  }
@@ -29466,7 +29607,7 @@ async function freeEdgeTargets(executor, targets, onLog) {
29466
29607
  }
29467
29608
  await new Promise((r) => setTimeout(r, 1000));
29468
29609
  }
29469
- var EDGE_PORTS, EdgeConflictError, EdgeMigrateRequested, EDGE_CONTAINER_TTL_MS = 20000, edgeContainerMemo, edgeMemoGeneration = 0, WORKER_RE, MASTER_RE;
29610
+ var EDGE_PORTS, EdgeConflictError, EdgeMigrateRequested, EDGE_CONTAINER_NAME = "openship-edge", EDGE_CONTAINER_TTL_MS = 20000, edgeContainerMemo, edgeMemoGeneration = 0, WORKER_RE, MASTER_RE;
29470
29611
  var init_detect2 = __esm(() => {
29471
29612
  init_src();
29472
29613
  init_port_conflict();
@@ -31222,6 +31363,8 @@ async function readJournal(executor) {
31222
31363
  }
31223
31364
  async function rollback(executor, journal, onLog) {
31224
31365
  onLog(log("Rolling back — restoring the previous proxy...", "warn"));
31366
+ await tryExec7(executor, `docker update --restart=no ${sq3(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
31367
+ await tryExec7(executor, `docker stop ${sq3(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
31225
31368
  await tryExec7(executor, "systemctl disable --now openresty 2>/dev/null || systemctl stop openresty 2>/dev/null || true; " + "systemctl reset-failed openresty 2>/dev/null || true");
31226
31369
  for (const u of journal.units) {
31227
31370
  await tryExec7(executor, u.wasEnabled ? `systemctl enable --now ${sq3(u.unit)} 2>/dev/null || true` : `systemctl start ${sq3(u.unit)} 2>/dev/null || true`);
@@ -31237,6 +31380,16 @@ async function rollback(executor, journal, onLog) {
31237
31380
  onLog(log(`Could not restore process ${p.pid} — no command captured.`, "warn"));
31238
31381
  }
31239
31382
  }
31383
+ const restored = await portIsServed(executor, 80);
31384
+ if (!restored) {
31385
+ onLog(log("Nothing is listening on :80 after the restore — the box is NOT serving. " + "Start your proxy by hand (e.g. `systemctl start nginx`, or `docker start <name>`).", "error"));
31386
+ }
31387
+ return restored;
31388
+ }
31389
+ async function portIsServed(executor, port) {
31390
+ const hex3 = port.toString(16).toUpperCase().padStart(4, "0");
31391
+ const out2 = await tryExec7(executor, `(command -v ss >/dev/null 2>&1 && ss -ltn 2>/dev/null | grep -qE ':${port}[[:space:]]' && echo yes) || ` + `(grep -qiE '^[[:space:]]*[0-9]+:[[:space:]]*[0-9A-F]{8}:${hex3}[[:space:]]+[0-9A-F]{8}:0000[[:space:]]+0A' /proc/net/tcp 2>/dev/null && echo yes) || true`);
31392
+ return (out2 ?? "").includes("yes");
31240
31393
  }
31241
31394
  async function beginEdgeTakeover(executor, status, onLog) {
31242
31395
  await writeJournal(executor, await buildJournal(executor, status));
@@ -31246,9 +31399,9 @@ async function rollbackEdgeTakeover(executor, onLog) {
31246
31399
  const journal = await readJournal(executor);
31247
31400
  if (!journal || journal.completed)
31248
31401
  return false;
31249
- await rollback(executor, journal, onLog);
31402
+ const restored = await rollback(executor, journal, onLog);
31250
31403
  await clearJournal(executor);
31251
- return true;
31404
+ return restored;
31252
31405
  }
31253
31406
  async function completeEdgeTakeover(executor) {
31254
31407
  const journal = await readJournal(executor);
@@ -32152,8 +32305,7 @@ __export(exports_ensure_container_edge, {
32152
32305
  ensureContainerEdge: () => ensureContainerEdge,
32153
32306
  dockerAvailable: () => dockerAvailable,
32154
32307
  containerEdgeProvider: () => containerEdgeProvider,
32155
- buildEdgeRunCommand: () => buildEdgeRunCommand,
32156
- EDGE_CONTAINER_NAME: () => EDGE_CONTAINER_NAME
32308
+ buildEdgeRunCommand: () => buildEdgeRunCommand
32157
32309
  });
32158
32310
  function log2(message, level = "info") {
32159
32311
  return { timestamp: new Date().toISOString(), message, level };
@@ -32231,6 +32383,34 @@ async function swapEdgeImage(executor, container, from, to, opts) {
32231
32383
  onLog(log2(`Rollback to ${from} ALSO failed — the edge is down on this server.`, "error"));
32232
32384
  return { swapped: false, edgeDown: true };
32233
32385
  }
32386
+ async function listDir(executor, dir) {
32387
+ const out2 = await executor.exec(`ls -1 ${sq2(dir)} 2>/dev/null || true`).catch(() => "");
32388
+ return new Set(out2.split(`
32389
+ `).map((l) => l.trim()).filter(Boolean));
32390
+ }
32391
+ async function sanitizeCarriedVhosts(executor, sitesDir, onLog) {
32392
+ const script = [
32393
+ `for f in ${sq2(sitesDir)}/*.conf; do`,
32394
+ ` [ -f "$f" ] || continue;`,
32395
+ ` if ! grep -qE '^[[:space:]]*server_name[[:space:]]+[^_;[:space:]]' "$f"; then`,
32396
+ ` echo "dropped-catchall $f"; rm -f "$f"; continue;`,
32397
+ ` fi;`,
32398
+ ` if grep -qE '[[:space:]]default_server' "$f"; then`,
32399
+ ` sed -i -E 's/([[:space:]]listen[^;]*)[[:space:]]+default_server/\\1/g' "$f" && echo "unset-default $f";`,
32400
+ ` fi;`,
32401
+ `done`
32402
+ ].join(" ");
32403
+ const out2 = await executor.exec(script).catch(() => "");
32404
+ for (const line of out2.split(`
32405
+ `).map((l) => l.trim()).filter(Boolean)) {
32406
+ const [action, file3] = [line.slice(0, line.indexOf(" ")), line.slice(line.indexOf(" ") + 1)];
32407
+ if (action === "dropped-catchall") {
32408
+ onLog(log2(`Dropped the bare edge's catch-all ${file3} — the edge image provides it.`, "warn"));
32409
+ } else if (action === "unset-default") {
32410
+ onLog(log2(`Removed default_server from ${file3} — the edge image owns it.`, "warn"));
32411
+ }
32412
+ }
32413
+ }
32234
32414
  async function ensureContainerEdge(executor, opts) {
32235
32415
  const { onLog } = opts;
32236
32416
  const container = opts.container?.trim() || EDGE_CONTAINER_NAME;
@@ -32264,16 +32444,27 @@ async function ensureContainerEdge(executor, opts) {
32264
32444
  await executor.exec(`mkdir -p ${sq2(mount.host)}`).catch(() => {});
32265
32445
  }
32266
32446
  const sitesTarget = EDGE_HOST_PATHS.sitesDir;
32447
+ let beforeCarry = null;
32267
32448
  if (bareWasOurs) {
32268
32449
  const barePaths = await detectOpenRestyPaths(executor).catch(() => OPENRESTY_DEFAULT_PATHS);
32269
32450
  if (barePaths.sitesDir !== sitesTarget && await executor.exists(barePaths.sitesDir)) {
32270
32451
  onLog(log2(`Carrying vhosts from ${barePaths.sitesDir}...`));
32452
+ beforeCarry = await listDir(executor, sitesTarget);
32271
32453
  await executor.exec(`cp -a ${sq2(`${barePaths.sitesDir}/.`)} ${sq2(`${sitesTarget}/`)} 2>/dev/null || true`).catch(() => {});
32454
+ await sanitizeCarriedVhosts(executor, sitesTarget, onLog);
32272
32455
  }
32273
32456
  }
32274
32457
  const restoreBare = async () => {
32275
32458
  if (!bareWasOurs)
32276
32459
  return;
32460
+ if (beforeCarry) {
32461
+ const now2 = await listDir(executor, sitesTarget);
32462
+ const added = [...now2].filter((name2) => !beforeCarry.has(name2));
32463
+ if (added.length > 0) {
32464
+ await executor.exec(`rm -f ${added.map((n) => sq2(`${sitesTarget}/${n}`)).join(" ")}`).catch(() => {});
32465
+ onLog(log2(`Removed ${added.length} carried vhost(s) so the next edge start is clean.`, "warn"));
32466
+ }
32467
+ }
32277
32468
  onLog(log2("Restoring the host OpenResty edge...", "warn"));
32278
32469
  await executor.exec("systemctl enable --now openresty 2>/dev/null || true").catch(() => {});
32279
32470
  };
@@ -32309,10 +32500,11 @@ ${logs}`, "error"));
32309
32500
  await executor.exec(`docker rm -f ${sq2(container)} 2>/dev/null || true`).catch(() => {});
32310
32501
  invalidateEdgeContainer(executor);
32311
32502
  await restoreBare();
32312
- throw new Error(`Edge container setup failed: ${msg}`);
32503
+ const reason = edgeFailureReason(logs);
32504
+ throw new Error(`Edge container setup failed: ${reason ? `${reason} (${msg})` : msg}`);
32313
32505
  }
32314
32506
  }
32315
- var EDGE_CONTAINER_NAME = "openship-edge", injectedDefaultImage;
32507
+ var injectedDefaultImage;
32316
32508
  var init_ensure_container_edge = __esm(() => {
32317
32509
  init_src();
32318
32510
  init_openresty_lua();
@@ -32417,9 +32609,9 @@ async function runEdgeTakeover(executor, opts, onLog) {
32417
32609
  edgeImage: opts.edgeImage
32418
32610
  });
32419
32611
  if (!install.success) {
32420
- await rollback(executor, journal, onLog);
32612
+ const rolledBack = await rollback(executor, journal, onLog);
32421
32613
  await clearJournal(executor);
32422
- return { ok: false, rolledBack: true, registered: [], warnings: [install.error ?? "Edge install failed"] };
32614
+ return { ok: false, rolledBack, registered: [], warnings: [install.error ?? "Edge install failed"] };
32423
32615
  }
32424
32616
  try {
32425
32617
  const container = await resolveOurEdgeContainer(executor, { fresh: true });
@@ -32454,9 +32646,11 @@ async function runEdgeTakeover(executor, opts, onLog) {
32454
32646
  return { ok: true, rolledBack: false, registered, warnings };
32455
32647
  } catch (err2) {
32456
32648
  warnings.push(safeErrorMessage(err2));
32457
- await rollback(executor, journal, onLog);
32649
+ const rolledBack = await rollback(executor, journal, onLog);
32458
32650
  await clearJournal(executor);
32459
- return { ok: false, rolledBack: true, registered: [], warnings };
32651
+ if (!rolledBack)
32652
+ warnings.push("The previous proxy did NOT come back — nothing is serving :80.");
32653
+ return { ok: false, rolledBack, registered: [], warnings };
32460
32654
  }
32461
32655
  }
32462
32656
  var DOMAIN_RE2;
@@ -80000,7 +80194,6 @@ __export(exports_src, {
80000
80194
  EdgeConflictError: () => EdgeConflictError,
80001
80195
  EDGE_HOST_STATE_DIR: () => EDGE_HOST_STATE_DIR,
80002
80196
  EDGE_HOST_PATHS: () => EDGE_HOST_PATHS,
80003
- EDGE_CONTAINER_NAME: () => EDGE_CONTAINER_NAME,
80004
80197
  EDGE_CONTAINER_MOUNTS: () => EDGE_CONTAINER_MOUNTS,
80005
80198
  DockerRuntime: () => DockerRuntime,
80006
80199
  DockerEdgeExecutor: () => DockerEdgeExecutor,
@@ -191353,7 +191546,7 @@ var package_default;
191353
191546
  var init_package = __esm(() => {
191354
191547
  package_default = {
191355
191548
  name: "@repo/api",
191356
- version: "0.4.6",
191549
+ version: "0.4.7",
191357
191550
  license: "Apache-2.0",
191358
191551
  private: true,
191359
191552
  type: "module",
@@ -4,13 +4,13 @@ import { createRequire as __ospCreateRequire } from "node:module";
4
4
  const require = __ospCreateRequire(import.meta.url);
5
5
  import {
6
6
  SystemManager
7
- } from "./chunk-C5OHTYEF.js";
8
- import "./chunk-SQ77DZOI.js";
7
+ } from "./chunk-IPYPMI3S.js";
8
+ import "./chunk-UXWAVM4C.js";
9
9
  import "./chunk-L2VOGB2X.js";
10
10
  import "./chunk-RPHRPFEH.js";
11
11
  import "./chunk-FXTBHTWY.js";
12
12
  import "./chunk-FPRHYBY2.js";
13
- import "./chunk-FQGJZ2HS.js";
13
+ import "./chunk-R5BHFFAV.js";
14
14
  import "./chunk-ILFETZOM.js";
15
15
  import "./chunk-VFINNTUL.js";
16
16
  import "./chunk-6APULWSU.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openship",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "Openship CLI - deploy, manage, and open your Openship platform from the terminal",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {