openship 0.6.6 → 0.6.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.
Files changed (58) hide show
  1. package/dist/bare-7QD623QV.js +29 -0
  2. package/dist/{chunk-Q7WGTQQ5.js → chunk-2ANV5ZQ5.js} +1 -1
  3. package/dist/{chunk-UJ6JZM65.js → chunk-2VSW6Q6C.js} +601 -382
  4. package/dist/{chunk-PCPVTH6M.js → chunk-5T4UVOM3.js} +6 -4
  5. package/dist/{chunk-YMMQRE5J.js → chunk-BLEXTYRK.js} +1 -1
  6. package/dist/{chunk-OBPKYXQL.js → chunk-CIJV43LH.js} +71 -32
  7. package/dist/{chunk-NK5N5LXN.js → chunk-DDFUESPJ.js} +24 -9
  8. package/dist/{chunk-6FH4AXVY.js → chunk-E2X26PXR.js} +1 -1
  9. package/dist/{chunk-HKRQXPTI.js → chunk-FYPFEDPZ.js} +1 -1
  10. package/dist/{chunk-UVNMQM7W.js → chunk-FZ6QCF6B.js} +8 -6
  11. package/dist/{chunk-MLYSEZQB.js → chunk-GGIAIGUL.js} +15 -66
  12. package/dist/{chunk-2YM3NI2E.js → chunk-GIPJNNI5.js} +38 -44
  13. package/dist/{chunk-ZFJXPCQP.js → chunk-H3WT3A6R.js} +4 -3
  14. package/dist/chunk-H4FS5BED.js +936 -0
  15. package/dist/{chunk-CBLD2HS5.js → chunk-J2XM3QSP.js} +8 -5
  16. package/dist/{chunk-FE7ZZQSE.js → chunk-J3K6XABC.js} +3 -3
  17. package/dist/{chunk-77QDF6UD.js → chunk-LHFXSC36.js} +428 -93
  18. package/dist/{chunk-YIMOCZBZ.js → chunk-MEE6OZFP.js} +3 -3
  19. package/dist/chunk-N5YZMO33.js +111 -0
  20. package/dist/{chunk-QVW76WDL.js → chunk-NRTJRNAE.js} +58 -16
  21. package/dist/{chunk-WVAML4H6.js → chunk-PPEBIY5F.js} +247 -61
  22. package/dist/{chunk-4I2BM2DT.js → chunk-PVP4RGWT.js} +3 -3
  23. package/dist/{chunk-RSR7PFIQ.js → chunk-QHMDCYZ3.js} +4 -4
  24. package/dist/chunk-R4VDPFLS.js +106 -0
  25. package/dist/{chunk-VTGPEC6B.js → chunk-RDXT4BQB.js} +6 -4
  26. package/dist/{chunk-LFE5KQ7Y.js → chunk-S6B2Z3OO.js} +142 -57
  27. package/dist/chunk-UWEV6XTS.js +249 -0
  28. package/dist/{chunk-5DMWSTOM.js → chunk-VQM6JHJB.js} +940 -224
  29. package/dist/{chunk-SM26HDS2.js → chunk-VSTFHUCV.js} +44 -50
  30. package/dist/chunk-WU54BXFE.js +70 -0
  31. package/dist/{chunk-LUKYPVJ4.js → chunk-XWHWTHEL.js} +313 -469
  32. package/dist/{chunk-A4GXYE4L.js → chunk-ZPJ74LXE.js} +22 -20
  33. package/dist/cloud-KNNABE3O.js +40 -0
  34. package/dist/{detect-OIE5V3VZ.js → detect-G4DR3U2N.js} +9 -6
  35. package/dist/{docker-VTR2GK5Y.js → docker-YOVPCIB4.js} +11 -8
  36. package/dist/{docker-edge-executor-22IBXNYJ.js → docker-edge-executor-QFXD3337.js} +8 -6
  37. package/dist/edge-import-CRFVSR6P.js +56 -0
  38. package/dist/{edge-preflight-GBNCPNVU.js → edge-preflight-MXERVAMG.js} +15 -12
  39. package/dist/{ensure-container-edge-KPI2CVXJ.js → ensure-container-edge-F4JUNQIK.js} +13 -10
  40. package/dist/{executor-7LXXZPBR.js → executor-CVPAYYHR.js} +8 -6
  41. package/dist/host-channel-preflight-QTDNGLCK.js +54 -0
  42. package/dist/index.js +84 -48
  43. package/dist/{local-executor-WOFZD72X.js → local-executor-ODC6RANU.js} +3 -2
  44. package/dist/{nginx-KUFMLMAC.js → nginx-KLFF43TC.js} +11 -8
  45. package/dist/{openresty-lua-ATTH35BE.js → openresty-lua-VKWUV4GB.js} +6 -5
  46. package/dist/{privilege-I4W3ZNZK.js → privilege-7D3SSOL3.js} +6 -5
  47. package/dist/server/index.js +8813 -5232
  48. package/dist/setup-I5AJC6NJ.js +25 -0
  49. package/dist/{ssh-client-JBMA7SN4.js → ssh-client-625VWGKP.js} +3 -2
  50. package/dist/up-QXTOOVXC.js +61 -0
  51. package/package.json +1 -1
  52. package/dist/bare-OXJVW3AK.js +0 -25
  53. package/dist/chunk-LH2PLRQO.js +0 -434
  54. package/dist/cloud-75VIT4KV.js +0 -37
  55. package/dist/edge-import-WJUB4M7F.js +0 -52
  56. package/dist/host-channel-preflight-KE6OONYK.js +0 -50
  57. package/dist/setup-XGLGUUFW.js +0 -22
  58. package/dist/up-K4BV7466.js +0 -57
@@ -7,42 +7,59 @@ import {
7
7
  resolveProjectDir,
8
8
  resolveStaticOutputPath,
9
9
  runLocalBuild
10
- } from "./chunk-YIMOCZBZ.js";
10
+ } from "./chunk-MEE6OZFP.js";
11
+ import {
12
+ isArtifactPathRef,
13
+ removeManagedArtifact
14
+ } from "./chunk-WU54BXFE.js";
11
15
  import {
12
16
  BuildCancelledError,
13
17
  BuildLogger,
14
18
  detectBuildKillHint,
19
+ droppedRuntimeEnvMessage,
20
+ isExcludedDocRootEntry,
15
21
  killProcessesUnderDir,
16
22
  parseLogLevel,
17
23
  runBuildPipeline,
24
+ splitRuntimeEnv,
18
25
  sq,
19
26
  transferLocalDirectory
20
- } from "./chunk-LH2PLRQO.js";
27
+ } from "./chunk-H4FS5BED.js";
21
28
  import {
22
29
  LocalExecutor
23
- } from "./chunk-6FH4AXVY.js";
30
+ } from "./chunk-E2X26PXR.js";
24
31
  import {
25
32
  privilegedExecutor
26
- } from "./chunk-4I2BM2DT.js";
33
+ } from "./chunk-PVP4RGWT.js";
27
34
  import {
28
35
  HOST_STATE_DIR,
29
36
  envOps,
30
37
  hostRefusal,
31
38
  opScript
32
- } from "./chunk-YMMQRE5J.js";
39
+ } from "./chunk-BLEXTYRK.js";
33
40
  import {
34
41
  ensureOwnedDir,
35
42
  resolveEnvironment
36
- } from "./chunk-UVNMQM7W.js";
43
+ } from "./chunk-FZ6QCF6B.js";
37
44
  import {
38
45
  sq as sq2,
39
46
  wrapLocalBuildCommand
40
- } from "./chunk-ZFJXPCQP.js";
47
+ } from "./chunk-H3WT3A6R.js";
48
+ import {
49
+ portOccupantDetails,
50
+ probeListeningPort
51
+ } from "./chunk-VSTFHUCV.js";
41
52
  import {
53
+ managedDeploymentUnitName,
42
54
  probeExec,
43
- probeListeningPort,
44
55
  withReason
45
- } from "./chunk-SM26HDS2.js";
56
+ } from "./chunk-UWEV6XTS.js";
57
+ import {
58
+ formatDuration,
59
+ isRemoteConnectionError,
60
+ isRetryableRemoteConnectionError,
61
+ systemDebug
62
+ } from "./chunk-N5YZMO33.js";
46
63
  import {
47
64
  DeployError,
48
65
  LANGUAGES,
@@ -50,16 +67,13 @@ import {
50
67
  answered,
51
68
  appVolumeTargets,
52
69
  buildOutputTransferExcludes,
53
- formatDuration,
54
- isRemoteConnectionError,
55
- isRetryableRemoteConnectionError,
56
70
  missingOutputDirectoryMessage,
71
+ nodeBinPathExport,
57
72
  packageManagerEnsureCommand,
58
73
  refused,
59
74
  safeErrorMessage,
60
- shellQuote,
61
- systemDebug
62
- } from "./chunk-UJ6JZM65.js";
75
+ shellQuote
76
+ } from "./chunk-2VSW6Q6C.js";
63
77
 
64
78
  // ../../packages/adapters/src/system/remote-journal.ts
65
79
  var OPSH_RUN_VERSION = 1;
@@ -1114,7 +1128,6 @@ async function sampleBareUsage(executor, pid, cgroupCandidates = []) {
1114
1128
  }
1115
1129
 
1116
1130
  // ../../packages/adapters/src/runtime/supervisor/systemd.ts
1117
- var UNIT_PREFIX = "openship";
1118
1131
  function escapeSystemdEnvValue(value) {
1119
1132
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
1120
1133
  }
@@ -1126,7 +1139,7 @@ var SystemdSupervisor = class {
1126
1139
  name = "systemd";
1127
1140
  // ── Helpers ──────────────────────────────────────────────────────────
1128
1141
  unitName(deploymentId) {
1129
- return `${UNIT_PREFIX}-${deploymentId}.service`;
1142
+ return managedDeploymentUnitName(deploymentId);
1130
1143
  }
1131
1144
  unitPath(deploymentId) {
1132
1145
  return `/etc/systemd/system/${this.unitName(deploymentId)}`;
@@ -1172,7 +1185,7 @@ Restart=on-failure
1172
1185
  RestartSec=3
1173
1186
  StandardOutput=journal
1174
1187
  StandardError=journal
1175
- SyslogIdentifier=${UNIT_PREFIX}-${opts.deploymentId}
1188
+ SyslogIdentifier=${this.unitName(opts.deploymentId).replace(/\.service$/, "")}
1176
1189
 
1177
1190
  [Install]
1178
1191
  WantedBy=multi-user.target
@@ -1206,16 +1219,7 @@ WantedBy=multi-user.target
1206
1219
  throw new DeployError(
1207
1220
  `Port ${opts.port} is already in use` + (occupant ? ` by ${occupant.command}` : "") + ". Stop the existing process before deploying.",
1208
1221
  "PORT_IN_USE",
1209
- {
1210
- port: opts.port,
1211
- pid: occupant?.pid,
1212
- command: occupant?.command,
1213
- rawCommand: occupant?.rawCommand,
1214
- systemdUnit: occupant?.systemdUnit,
1215
- systemdDescription: occupant?.systemdDescription,
1216
- deploymentId: occupant?.deploymentId,
1217
- isManagedDeployment: occupant?.isManagedDeployment
1218
- }
1222
+ portOccupantDetails(opts.port, occupant)
1219
1223
  );
1220
1224
  }
1221
1225
  const msg = hint ? `Process exited immediately after start. Last output:
@@ -1424,7 +1428,7 @@ var NohupSupervisor = class {
1424
1428
  throw new DeployError(
1425
1429
  `Port ${opts.port} is already in use` + (occupant ? ` by ${occupant.command}` : "") + ". Stop the existing process before deploying.",
1426
1430
  "PORT_IN_USE",
1427
- { port: opts.port, pid: occupant?.pid, command: occupant?.command }
1431
+ portOccupantDetails(opts.port, occupant)
1428
1432
  );
1429
1433
  }
1430
1434
  const msg = hint ? `Process exited immediately after spawn. Last output:
@@ -1690,6 +1694,7 @@ var BareRuntime = class {
1690
1694
  async promoteBuildArtifact(artifactPath, deploymentId, previousDeploymentId) {
1691
1695
  const releaseDir = this.releaseDir(deploymentId);
1692
1696
  if (artifactPath === releaseDir) return releaseDir;
1697
+ const consumeSource = !artifactPath.startsWith(`${this.workDir}/releases/`);
1693
1698
  await ensureOwnedDir(this.executor, `${this.workDir}/releases`);
1694
1699
  await this.executor.rm(releaseDir);
1695
1700
  const previousReleaseDir = previousDeploymentId ? this.releaseDir(previousDeploymentId) : void 0;
@@ -1700,7 +1705,7 @@ var BareRuntime = class {
1700
1705
  `deploy:${deploymentId}:promote-rsync`,
1701
1706
  `rsync -a --delete --link-dest=${sq(previousReleaseDir)} ${sq(artifactPath)}/ ${sq(releaseDir)}/`
1702
1707
  );
1703
- await this.executor.rm(artifactPath).catch(() => {
1708
+ if (consumeSource) await this.executor.rm(artifactPath).catch(() => {
1704
1709
  });
1705
1710
  return releaseDir;
1706
1711
  } catch {
@@ -1708,6 +1713,14 @@ var BareRuntime = class {
1708
1713
  });
1709
1714
  }
1710
1715
  }
1716
+ if (!consumeSource) {
1717
+ await execReliable(
1718
+ this.executor,
1719
+ `deploy:${deploymentId}:promote-copy`,
1720
+ `mkdir -p ${sq(releaseDir)} && cp -a ${sq(artifactPath)}/. ${sq(releaseDir)}/`
1721
+ );
1722
+ return releaseDir;
1723
+ }
1711
1724
  await execReliable(
1712
1725
  this.executor,
1713
1726
  `deploy:${deploymentId}:promote-mv`,
@@ -1947,19 +1960,29 @@ var BareRuntime = class {
1947
1960
  ) : stagedDir;
1948
1961
  await this.linkPersistentPaths(workDir, config.projectId, config.volumes, _onLog);
1949
1962
  const sv = await this.supervisor();
1963
+ const projectEnv = splitRuntimeEnv(
1964
+ Object.fromEntries(Object.entries(config.envVars ?? {}).map(([k, v]) => [k, String(v)]))
1965
+ );
1966
+ if (projectEnv.dropped.length > 0) {
1967
+ _onLog?.({
1968
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1969
+ level: "warn",
1970
+ message: droppedRuntimeEnvMessage(projectEnv.dropped)
1971
+ });
1972
+ }
1950
1973
  const env = {
1951
- ...Object.fromEntries(
1952
- Object.entries(config.envVars ?? {}).map(([k, v]) => [k, String(v)])
1953
- ),
1974
+ ...Object.fromEntries(projectEnv.entries),
1954
1975
  PORT: String(config.port),
1955
1976
  NODE_ENV: config.environment === "production" ? "production" : "development"
1956
1977
  };
1978
+ const startBinPath = nodeBinPathExport(config.packageManager, [workDir]);
1979
+ const resolvedStart = config.startCommand || "npm start";
1957
1980
  try {
1958
1981
  await sv.deploy({
1959
1982
  deploymentId: config.deploymentId,
1960
1983
  projectId: config.projectId,
1961
1984
  workDir,
1962
- startCommand: config.startCommand || "npm start",
1985
+ startCommand: startBinPath ? `${startBinPath} && ${resolvedStart}` : resolvedStart,
1963
1986
  port: config.port,
1964
1987
  env
1965
1988
  });
@@ -1979,13 +2002,40 @@ var BareRuntime = class {
1979
2002
  };
1980
2003
  }
1981
2004
  async deployStatic(config) {
1982
- const stagedDir = config.imageRef ?? this.projectDir(config.projectId);
1983
- const workDir = config.imageRef ? await this.promoteBuildArtifact(
1984
- stagedDir,
1985
- config.deploymentId,
1986
- config.previousDeploymentId
1987
- ) : stagedDir;
1988
- const staticRoot = resolveStaticOutputPath(workDir, config.outputDirectory);
2005
+ const workDir = await this.promoteStaticRelease({
2006
+ artifactPath: config.imageRef ?? this.projectDir(config.projectId),
2007
+ releaseId: config.deploymentId,
2008
+ previousReleaseId: config.previousDeploymentId,
2009
+ outputDirectory: config.outputDirectory,
2010
+ promote: Boolean(config.imageRef)
2011
+ });
2012
+ return {
2013
+ deploymentId: config.deploymentId,
2014
+ containerId: workDir,
2015
+ status: "running"
2016
+ };
2017
+ }
2018
+ /**
2019
+ * Promote an extracted static build into a stable release directory and PROVE it
2020
+ * can serve, returning the release dir.
2021
+ *
2022
+ * Split out of `deployStatic` so the compose/monorepo static path reuses the
2023
+ * identical promote + validation instead of registering a vhost straight at the
2024
+ * `.builds/<session>` staging dir. Two things came from that shortcut: the
2025
+ * public doc-root of a compose static sub-app was a per-build-session scratch
2026
+ * directory (nothing owned its lifetime, so superseded copies accumulated and
2027
+ * any sweep of `.builds` would take the live site down), and it skipped the only
2028
+ * HARD deploy-time output gate we have — so an extract that produced nothing
2029
+ * deployed green and 404'd.
2030
+ *
2031
+ * `releaseId` is the release's identity, not necessarily a deployment id: the
2032
+ * compose path passes `<deploymentId>-<serviceId>` so each static sub-app owns
2033
+ * its own release dir under one deployment.
2034
+ */
2035
+ async promoteStaticRelease(opts) {
2036
+ const stagedDir = opts.artifactPath;
2037
+ const workDir = opts.promote === false ? stagedDir : await this.promoteBuildArtifact(stagedDir, opts.releaseId, opts.previousReleaseId);
2038
+ const staticRoot = resolveStaticOutputPath(workDir, opts.outputDirectory);
1989
2039
  const abort = async (message) => {
1990
2040
  if (workDir !== stagedDir) {
1991
2041
  await this.executor.rm(workDir).catch(() => {
@@ -1994,18 +2044,47 @@ var BareRuntime = class {
1994
2044
  throw new Error(message);
1995
2045
  };
1996
2046
  if (!await this.executor.exists(staticRoot)) {
1997
- return abort(missingOutputDirectoryMessage(config.outputDirectory));
2047
+ return abort(missingOutputDirectoryMessage(opts.outputDirectory));
2048
+ }
2049
+ if (staticRoot === workDir) {
2050
+ const undeleted = await this.pruneBuildFilesFromDocRoot(staticRoot);
2051
+ if (undeleted.length > 0) {
2052
+ return abort(
2053
+ `Refusing to publish ${staticRoot}: could not remove ${undeleted.join(", ")} from the document root. Serving them would expose the repository (and, for a private repo, the access token in .git/config) at the site's public URL.`
2054
+ );
2055
+ }
1998
2056
  }
1999
2057
  if (await this.isEmptyDir(staticRoot)) {
2000
2058
  return abort(
2001
- `The output directory "${config.outputDirectory || "."}" is empty \u2014 the build produced no files to serve, so every request would 404. Check the build command and the Output Directory setting.`
2059
+ `The output directory "${opts.outputDirectory || "."}" is empty \u2014 the build produced no files to serve, so every request would 404. Check the build command and the Output Directory setting.`
2002
2060
  );
2003
2061
  }
2004
- return {
2005
- deploymentId: config.deploymentId,
2006
- containerId: workDir,
2007
- status: "running"
2008
- };
2062
+ return workDir;
2063
+ }
2064
+ /**
2065
+ * Remove top-level build inputs from a doc root, returning the names it could NOT
2066
+ * remove (empty on success, and on "there was nothing to remove").
2067
+ *
2068
+ * Reuses `isExcludedDocRootEntry` rather than re-listing the names, because the
2069
+ * Docker sandbox already owns that list and two copies of "what must not be
2070
+ * served" is precisely how the bare path came to serve `.git`.
2071
+ *
2072
+ * An unreadable listing returns empty — a probe that cannot read must not fail a
2073
+ * deploy — but a listed entry that survives its `rm` is reported, because the
2074
+ * caller must refuse to publish rather than expose a token.
2075
+ */
2076
+ async pruneBuildFilesFromDocRoot(root) {
2077
+ const listed = await this.executor.exec(`ls -A ${sq(root)} 2>/dev/null; true`).catch(() => null);
2078
+ if (listed === null) return [];
2079
+ const doomed = listed.split("\n").map((l) => l.trim()).filter(Boolean).filter(isExcludedDocRootEntry);
2080
+ if (doomed.length === 0) return [];
2081
+ await this.executor.exec(`rm -rf ${doomed.map((n) => sq(`${root}/${n}`)).join(" ")}`).catch(() => {
2082
+ });
2083
+ const survivors = [];
2084
+ for (const name of doomed) {
2085
+ if (await this.executor.exists(`${root}/${name}`)) survivors.push(name);
2086
+ }
2087
+ return survivors;
2009
2088
  }
2010
2089
  /**
2011
2090
  * Is this an existing directory with no entries? Any inconclusive answer (not a
@@ -2033,8 +2112,8 @@ var BareRuntime = class {
2033
2112
  await sv.restart(containerId);
2034
2113
  }
2035
2114
  async destroy(containerId) {
2036
- if (containerId.includes("/")) {
2037
- await this.executor.rm(containerId);
2115
+ if (isArtifactPathRef(containerId)) {
2116
+ await removeManagedArtifact(this.executor, containerId);
2038
2117
  return;
2039
2118
  }
2040
2119
  const sv = await this.supervisor();
@@ -2078,15 +2157,21 @@ var BareRuntime = class {
2078
2157
  }
2079
2158
  }
2080
2159
  async purge(deployment) {
2160
+ const failures = [];
2081
2161
  if (deployment.containerId) {
2082
- try {
2083
- await this.destroy(deployment.containerId);
2084
- } catch {
2085
- }
2162
+ await this.destroy(deployment.containerId).catch((err) => failures.push(err));
2086
2163
  }
2087
- try {
2088
- await this.executor.rm(this.releaseDir(deployment.id));
2089
- } catch {
2164
+ const releaseDir = this.releaseDir(deployment.id);
2165
+ if (releaseDir !== deployment.containerId) {
2166
+ await removeManagedArtifact(this.executor, releaseDir).catch(
2167
+ (err) => failures.push(err)
2168
+ );
2169
+ }
2170
+ if (failures.length === 1) throw failures[0];
2171
+ if (failures.length > 1) {
2172
+ throw new Error(
2173
+ failures.map((err) => err instanceof Error ? err.message : String(err)).join("; ")
2174
+ );
2090
2175
  }
2091
2176
  }
2092
2177
  // ── Observability ──────────────────────────────────────────────────────
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env sh
2
+ ":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
3
+ import { createRequire as __ospCreateRequire } from "node:module";
4
+ const require = __ospCreateRequire(import.meta.url);
5
+ import {
6
+ formatDuration,
7
+ isRemoteConnectionError,
8
+ systemDebug
9
+ } from "./chunk-N5YZMO33.js";
10
+ import {
11
+ safeErrorMessage,
12
+ shellQuote
13
+ } from "./chunk-2VSW6Q6C.js";
14
+
15
+ // ../../packages/adapters/src/system/probe-exec.ts
16
+ async function tryExec(executor, command, opts) {
17
+ try {
18
+ return opts ? await executor.exec(command, opts) : await executor.exec(command);
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+ async function probeExec(executor, command, scope) {
24
+ const startedAt = Date.now();
25
+ systemDebug(scope, `exec:start ${command}`);
26
+ try {
27
+ const result = await executor.exec(command, { timeout: 1e4 });
28
+ systemDebug(scope, `exec:ok ${command} (${formatDuration(startedAt)})`);
29
+ return { output: result.trim(), error: null };
30
+ } catch (err) {
31
+ const msg = safeErrorMessage(err);
32
+ if (isRemoteConnectionError(err)) {
33
+ systemDebug(scope, `exec:abort ${command} (${formatDuration(startedAt)}) ${msg}`);
34
+ throw err;
35
+ }
36
+ systemDebug(scope, `exec:fail ${command} (${formatDuration(startedAt)}) ${msg}`);
37
+ return { output: "", error: msg || `\`${command}\` failed` };
38
+ }
39
+ }
40
+ function withReason(message, error) {
41
+ if (!error) return message;
42
+ const first = error.split("\n").map((line) => line.trim()).find(Boolean);
43
+ if (!first) return message;
44
+ return `${message} \u2014 ${first.length > 200 ? `${first.slice(0, 199)}\u2026` : first}`;
45
+ }
46
+
47
+ // ../../packages/adapters/src/infra/mail-container.ts
48
+ var MAIL_HOST_STATE_DIR = "/var/lib/openship/mail";
49
+ var MAIL_CONTAINER_MOUNTS = [
50
+ { host: `${MAIL_HOST_STATE_DIR}/vmail`, container: "/var/vmail" },
51
+ // The Postfix queue. A recreate on an ephemeral layer would drop in-flight and
52
+ // deferred mail without this — keep it on the host.
53
+ { host: `${MAIL_HOST_STATE_DIR}/spool`, container: "/var/spool/postfix" },
54
+ // DKIM private keys. NEVER baked, never regenerated on pull — a fresh selector
55
+ // breaks DMARC until DNS repropagates.
56
+ { host: `${MAIL_HOST_STATE_DIR}/dkim`, container: "/var/lib/dkim" },
57
+ // Mutable daemon config the admin layer edits (sasl_passwd, relayhost maps,
58
+ // amavis 50-user). Seeded from baked defaults on first boot.
59
+ { host: `${MAIL_HOST_STATE_DIR}/config/postfix`, container: "/etc/postfix", seed: true },
60
+ { host: `${MAIL_HOST_STATE_DIR}/config/dovecot`, container: "/etc/dovecot", seed: true },
61
+ { host: `${MAIL_HOST_STATE_DIR}/config/amavis`, container: "/etc/amavis/conf.d", seed: true },
62
+ // ClamAV signatures — bind-mounted so a pull doesn't force a multi-hundred-MB
63
+ // freshclam re-download and delay readiness. `seed: true` because that same mount
64
+ // HIDES the database baked into the image: with nothing copying it across, clamd
65
+ // finds an empty database directory and exits (issue #565).
66
+ { host: `${MAIL_HOST_STATE_DIR}/clamav`, container: "/var/lib/clamav", seed: true },
67
+ { host: "/etc/letsencrypt", container: "/etc/letsencrypt", readonly: true }
68
+ ];
69
+ var MAIL_DB_HOST_DATA_DIR = `${MAIL_HOST_STATE_DIR}/pgdata`;
70
+ var MAIL_DB_CONTAINER_DATA_DIR = "/var/lib/postgresql/data";
71
+ var MAIL_DB_PGDATA = `${MAIL_DB_CONTAINER_DATA_DIR}/pgdata`;
72
+ var MAIL_HOST_PATHS = {
73
+ saslPasswd: `${MAIL_HOST_STATE_DIR}/config/postfix/sasl_passwd`,
74
+ senderRelayhost: `${MAIL_HOST_STATE_DIR}/config/postfix/sender_relayhost`,
75
+ // Per-nexthop TLS policy. Only split ("selected"-scope) relaying writes it: the
76
+ // relay hop must be `encrypt` so SASL credentials never cross a plaintext
77
+ // connection, while direct-to-MX delivery for the un-relayed domains stays
78
+ // opportunistic. A GLOBAL smtp_tls_security_level can't express both.
79
+ relayTlsPolicy: `${MAIL_HOST_STATE_DIR}/config/postfix/openship_tls_policy`,
80
+ amavisUserConf: `${MAIL_HOST_STATE_DIR}/config/amavis/50-user`
81
+ };
82
+
83
+ // ../../packages/adapters/src/system/port-owner.ts
84
+ var OPENSHIP_LABEL = {
85
+ project: "openship.project",
86
+ deployment: "openship.deployment",
87
+ service: "openship.service",
88
+ build: "openship.build"
89
+ };
90
+ var PS_FIELDS = [
91
+ "{{.ID}}",
92
+ "{{.Names}}",
93
+ "{{.Image}}",
94
+ `{{.Label "${OPENSHIP_LABEL.project}"}}`,
95
+ `{{.Label "${OPENSHIP_LABEL.deployment}"}}`,
96
+ `{{.Label "${OPENSHIP_LABEL.service}"}}`,
97
+ `{{.Label "${OPENSHIP_LABEL.build}"}}`,
98
+ '{{.Label "com.docker.compose.project"}}',
99
+ '{{.Label "com.docker.compose.service"}}'
100
+ ];
101
+ var PS_FORMAT = PS_FIELDS.join("\\t");
102
+ function parsePsLines(out) {
103
+ const containers = [];
104
+ for (const line of out.split("\n")) {
105
+ if (!line.trim()) continue;
106
+ const f = line.split(" ");
107
+ const id = f[0]?.trim();
108
+ const name = f[1]?.trim();
109
+ if (!id || !name || !/^[0-9a-f]{12,64}$/i.test(id)) continue;
110
+ containers.push({
111
+ id,
112
+ name: name.replace(/^\//, ""),
113
+ image: f[2]?.trim() ?? "",
114
+ ...f[3]?.trim() ? { projectId: f[3].trim() } : {},
115
+ ...f[4]?.trim() ? { deploymentId: f[4].trim() } : {},
116
+ ...f[5]?.trim() ? { serviceName: f[5].trim() } : {},
117
+ ...f[6]?.trim() ? { buildSessionId: f[6].trim() } : {},
118
+ ...f[7]?.trim() ? { composeProject: f[7].trim() } : {},
119
+ ...f[8]?.trim() ? { composeService: f[8].trim() } : {}
120
+ });
121
+ }
122
+ return containers;
123
+ }
124
+ async function queryContainers(executor, filter) {
125
+ const out = await tryExec(
126
+ executor,
127
+ `docker ps --no-trunc --filter ${filter} --format '${PS_FORMAT}' 2>/dev/null`
128
+ );
129
+ return out === null ? null : parsePsLines(out);
130
+ }
131
+ async function resolveContainerPublishingPort(executor, port) {
132
+ if (!Number.isInteger(port) || port < 1 || port > 65535) return { kind: "unknown" };
133
+ const containers = await queryContainers(executor, `publish=${port}`);
134
+ if (containers === null) return { kind: "unknown" };
135
+ if (containers.length === 0) return { kind: "none" };
136
+ if (containers.length === 1) return { kind: "one", container: containers[0] };
137
+ return { kind: "ambiguous", containers };
138
+ }
139
+ function containerIdFromCgroup(cgroup) {
140
+ const match = cgroup?.match(/(?:^|[^0-9a-f])([0-9a-f]{64})(?:[^0-9a-f]|$)/);
141
+ return match?.[1] ?? null;
142
+ }
143
+ async function stopContainerDurably(executor, ref) {
144
+ await tryExec(executor, `docker update --restart=no ${shellQuote(ref)} 2>/dev/null || true`);
145
+ await tryExec(executor, `docker stop ${shellQuote(ref)} 2>/dev/null || true`);
146
+ }
147
+ async function describeContainerById(executor, containerId) {
148
+ if (!/^[0-9a-f]{12,64}$/i.test(containerId)) return null;
149
+ const containers = await queryContainers(executor, `id=${shellQuote(containerId)}`);
150
+ return containers?.[0] ?? null;
151
+ }
152
+ var EDGE_CONTAINER_NAME = "openship-edge";
153
+ function isOurEdgeContainer(name, image) {
154
+ return /openship-edge/i.test(`${name ?? ""} ${image ?? ""}`);
155
+ }
156
+ var PORT_FORWARDER_NAMES = [
157
+ "docker-proxy",
158
+ "rootlesskit",
159
+ "rootlesskit-docker-proxy",
160
+ "slirp4netns",
161
+ "pasta",
162
+ // LXD/Incus publish a container port with `forkproxy`, whose cgroup is the snap's
163
+ // daemon unit — the same misattribution with a different daemon.
164
+ "forkproxy"
165
+ ];
166
+ function isContainerPortForwarder(command) {
167
+ const argv0 = command?.trim().split(/\s+/)[0];
168
+ if (!argv0) return false;
169
+ const base = argv0.replace(/.*\//, "");
170
+ return PORT_FORWARDER_NAMES.some(
171
+ (name) => base === name || base.startsWith(`${name}-`) || base.length >= 15 && name.startsWith(base)
172
+ );
173
+ }
174
+ var MANAGED_DEPLOYMENT_UNIT_RE = /^openship-(dep_[A-Za-z0-9_-]+)\.service$/;
175
+ function managedDeploymentUnitName(deploymentId) {
176
+ return `openship-${deploymentId}.service`;
177
+ }
178
+ function managedDeploymentIdFromUnit(unit) {
179
+ return unit?.match(MANAGED_DEPLOYMENT_UNIT_RE)?.[1] ?? null;
180
+ }
181
+ var PROTECTED_UNITS = /* @__PURE__ */ new Set([
182
+ "docker",
183
+ "docker.socket",
184
+ "dockerd",
185
+ "snap.docker.dockerd",
186
+ "containerd",
187
+ "containerd.socket",
188
+ "podman",
189
+ // Other container/VM supervisors whose forwarders land on their daemon unit.
190
+ "lxd",
191
+ "snap.lxd.daemon",
192
+ "incus",
193
+ "k3s",
194
+ "k3s-agent",
195
+ "kubelet",
196
+ "libvirtd",
197
+ "ssh",
198
+ "sshd",
199
+ "dbus",
200
+ "systemd-resolved",
201
+ "systemd-networkd",
202
+ "systemd-logind",
203
+ // The control plane itself, and a from-source dev install of it.
204
+ "openship",
205
+ "openship-dev"
206
+ ]);
207
+ var USER_MANAGER_UNIT_RE = /^user@\d+$/;
208
+ function classifyProxy(text) {
209
+ if (!text) return void 0;
210
+ const t = text.toLowerCase();
211
+ if (t.includes("openresty")) return "openresty";
212
+ if (/(^|[\s/:])nginx/.test(t)) return "nginx";
213
+ if (/(^|[\s/:])caddy/.test(t)) return "caddy";
214
+ if (/(apache2|httpd)/.test(t)) return "apache";
215
+ if (/(^|[\s/:])traefik/.test(t)) return "traefik";
216
+ if (/(^|[\s/:])haproxy/.test(t)) return "haproxy";
217
+ return void 0;
218
+ }
219
+ function bareUnit(unit) {
220
+ return unit.replace(/\.service$/, "");
221
+ }
222
+ function isProtectedUnit(unit) {
223
+ const bare = bareUnit(unit);
224
+ return PROTECTED_UNITS.has(bare) || USER_MANAGER_UNIT_RE.test(bare);
225
+ }
226
+ function mayStopUnitForEdgeTakeover(unit) {
227
+ if (!unit) return false;
228
+ if (managedDeploymentIdFromUnit(unit)) return true;
229
+ return !isProtectedUnit(unit);
230
+ }
231
+
232
+ export {
233
+ MAIL_HOST_STATE_DIR,
234
+ tryExec,
235
+ probeExec,
236
+ withReason,
237
+ OPENSHIP_LABEL,
238
+ resolveContainerPublishingPort,
239
+ containerIdFromCgroup,
240
+ stopContainerDurably,
241
+ describeContainerById,
242
+ EDGE_CONTAINER_NAME,
243
+ isOurEdgeContainer,
244
+ isContainerPortForwarder,
245
+ managedDeploymentUnitName,
246
+ managedDeploymentIdFromUnit,
247
+ classifyProxy,
248
+ mayStopUnitForEdgeTakeover
249
+ };