openship 0.4.8 → 0.5.0

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 (42) hide show
  1. package/README.md +1 -0
  2. package/dist/{bare-JY5Y5ELU.js → bare-VRGE5MXH.js} +7 -7
  3. package/dist/{chunk-TGIINPVM.js → chunk-6LKHXB6T.js} +22 -7
  4. package/dist/{chunk-2D3I5G6P.js → chunk-6PIYRG5I.js} +23 -3
  5. package/dist/{chunk-BBHX7T4R.js → chunk-BS2AXYZX.js} +401 -76
  6. package/dist/{chunk-SJIFLLX4.js → chunk-CUJXGK7R.js} +321 -85
  7. package/dist/{chunk-VFINNTUL.js → chunk-EAAGSQXN.js} +23 -8
  8. package/dist/{chunk-4Z5QO7N6.js → chunk-I6PSTQIR.js} +2 -2
  9. package/dist/{chunk-5NYUCONN.js → chunk-JLKKYUZA.js} +31 -49
  10. package/dist/{chunk-XPPZ7B2J.js → chunk-K2RPVGPN.js} +2 -2
  11. package/dist/{chunk-MZ2WEHHY.js → chunk-KNTJ7PSF.js} +1 -1
  12. package/dist/{chunk-L2VOGB2X.js → chunk-KZOBYJUW.js} +1 -1
  13. package/dist/{chunk-GKHUSZCD.js → chunk-NNPEG6HM.js} +1 -1
  14. package/dist/{chunk-SLTYVHU7.js → chunk-NXFIRK2Z.js} +381 -72
  15. package/dist/{chunk-FXTBHTWY.js → chunk-RB35YN3J.js} +271 -35
  16. package/dist/{chunk-6APULWSU.js → chunk-UMQP7SYT.js} +429 -66
  17. package/dist/{chunk-V6AN6XLZ.js → chunk-V7BZMGOT.js} +123 -45
  18. package/dist/{chunk-VDMQJHPI.js → chunk-YWMODKWL.js} +67 -4
  19. package/dist/{chunk-OXWDY22G.js → chunk-ZCOQJCZO.js} +1 -1
  20. package/dist/{cloud-KYWY3RP4.js → cloud-3M2WZ624.js} +12 -12
  21. package/dist/{detect-AZLOHUFX.js → detect-BAJMY6KM.js} +3 -3
  22. package/dist/{docker-LODK5Y2S.js → docker-2NGBRJFJ.js} +9 -7
  23. package/dist/{docker-edge-executor-VHW3JPJS.js → docker-edge-executor-5I4M5QT7.js} +1 -1
  24. package/dist/{edge-import-ABUDKKJI.js → edge-import-RIWNEIXQ.js} +21 -19
  25. package/dist/{ensure-container-edge-SW6HENSG.js → ensure-container-edge-3NSYKHRX.js} +6 -6
  26. package/dist/{executor-XJ6RP4Q2.js → executor-LOYMPU67.js} +3 -3
  27. package/dist/index.js +269 -331
  28. package/dist/{nginx-3L4SIBJA.js → nginx-QE4DOM5J.js} +4 -4
  29. package/dist/{openresty-lua-4WLGDSER.js → openresty-lua-BTAKECCF.js} +2 -2
  30. package/dist/server/index.js +51643 -45322
  31. package/dist/server/migrations/0073_project_volumes_object_storage.sql +3 -0
  32. package/dist/server/migrations/0074_project_compose_path.sql +1 -0
  33. package/dist/server/migrations/0075_service_command_argv.sql +1 -0
  34. package/dist/server/migrations/0076_rollback_retention.sql +17 -0
  35. package/dist/server/migrations/0077_domain_redirect.sql +13 -0
  36. package/dist/server/migrations/0078_grant_source_scope.sql +30 -0
  37. package/dist/server/migrations/0079_project_readiness.sql +18 -0
  38. package/dist/server/migrations/0080_deployment_error_classification.sql +26 -0
  39. package/dist/server/migrations/0081_project_readiness_repair.sql +34 -0
  40. package/dist/server/migrations/meta/_journal.json +63 -0
  41. package/dist/{setup-NU6T2PDU.js → setup-KELALK7H.js} +7 -7
  42. package/package.json +1 -1
@@ -101,10 +101,10 @@ import {
101
101
  } from "./chunk-W73X43R7.js";
102
102
  import {
103
103
  CloudRuntime
104
- } from "./chunk-5NYUCONN.js";
104
+ } from "./chunk-JLKKYUZA.js";
105
105
  import {
106
106
  systemCatalog
107
- } from "./chunk-SLTYVHU7.js";
107
+ } from "./chunk-NXFIRK2Z.js";
108
108
  import {
109
109
  AwsCrc32,
110
110
  __awaiter,
@@ -139,10 +139,10 @@ import {
139
139
  DockerRuntime,
140
140
  isHostPathSource,
141
141
  scopedVolumeName
142
- } from "./chunk-SJIFLLX4.js";
142
+ } from "./chunk-CUJXGK7R.js";
143
143
  import {
144
144
  BareRuntime
145
- } from "./chunk-VDMQJHPI.js";
145
+ } from "./chunk-YWMODKWL.js";
146
146
  import {
147
147
  LocalExecutor
148
148
  } from "./chunk-TYVME3XD.js";
@@ -151,15 +151,16 @@ import {
151
151
  edgeCrashReason,
152
152
  invalidateEdgeContainer,
153
153
  sanitizeEdgeVhosts
154
- } from "./chunk-2D3I5G6P.js";
154
+ } from "./chunk-6PIYRG5I.js";
155
155
  import {
156
156
  EDGE_CONTAINER_MOUNTS,
157
157
  EDGE_HOST_STATE_DIR
158
- } from "./chunk-VFINNTUL.js";
158
+ } from "./chunk-EAAGSQXN.js";
159
159
  import {
160
160
  DEFAULT_IMAGE_REGISTRY,
161
+ isAwsS3Endpoint,
161
162
  safeErrorMessage
162
- } from "./chunk-6APULWSU.js";
163
+ } from "./chunk-UMQP7SYT.js";
163
164
 
164
165
  // src/lib/edge-import.ts
165
166
  import chalk from "chalk";
@@ -691,7 +692,15 @@ var DockerBackupExecutor = class {
691
692
  await this.ensureImage(helperImage);
692
693
  const hostConfig = {
693
694
  Binds: [`${source.source}:/mnt:ro`],
694
- AutoRemove: true
695
+ AutoRemove: true,
696
+ // The archive streams over `attach()` below — the daemon's default
697
+ // json-file driver would ALSO write every byte of it to
698
+ // /var/lib/docker/containers as a log. That's a second copy of the whole
699
+ // backup, and worse than a copy: json-file escapes binary, so a 4.6 GB
700
+ // volume ballooned to a 12 GB log file here. Backups then spike the
701
+ // host's disk by more than the data they read and only release it when
702
+ // the helper exits. Nothing ever reads these logs.
703
+ LogConfig: { Type: "none", Config: {} }
695
704
  };
696
705
  const helper = await this.dockerode.createContainer({
697
706
  Image: helperImage,
@@ -12191,7 +12200,7 @@ var S3DestinationImpl = class {
12191
12200
  if (!accessKeyId || !secretAccessKey) {
12192
12201
  throw new Error(`S3Destination "${row.name}" missing access credentials`);
12193
12202
  }
12194
- const isAws = !row.endpoint || row.endpoint.includes(".amazonaws.com") || row.endpoint === "https://s3.amazonaws.com";
12203
+ const isAws = isAwsS3Endpoint(row.endpoint);
12195
12204
  this.client = new S3Client({
12196
12205
  endpoint: row.endpoint ?? void 0,
12197
12206
  region: row.region ?? "auto",
@@ -12668,11 +12677,11 @@ registerDestination("sftp", (row) => new SftpDestinationImpl(row));
12668
12677
  registerDestination("openship_server", (row) => new SftpDestinationImpl(row));
12669
12678
 
12670
12679
  // src/lib/compose.ts
12671
- import { spawnSync as spawnSync3 } from "child_process";
12680
+ import { spawn as spawn2, spawnSync as spawnSync3 } from "child_process";
12672
12681
  import { randomBytes as randomBytes5 } from "crypto";
12673
- import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
12682
+ import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
12674
12683
  import { homedir as homedir2, userInfo } from "os";
12675
- import { join as join6 } from "path";
12684
+ import { join as join7 } from "path";
12676
12685
 
12677
12686
  // src/lib/paths.ts
12678
12687
  import { homedir } from "os";
@@ -12682,15 +12691,185 @@ var override = process.env.OPENSHIP_HOME?.trim();
12682
12691
  var OS_DIR = override ? resolve2(override) : DEFAULT_HOME;
12683
12692
  var IS_ALT_HOME = OS_DIR !== DEFAULT_HOME;
12684
12693
 
12694
+ // src/lib/ports.ts
12695
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
12696
+ import { join as join4 } from "path";
12697
+
12698
+ // ../../packages/core/src/ports.ts
12699
+ import { createServer } from "net";
12700
+ var DEFAULT_API_PORT = 4e3;
12701
+ var DEFAULT_DASHBOARD_PORT = 3001;
12702
+ var LOOPBACK = "127.0.0.1";
12703
+ var SCAN_SPAN = 20;
12704
+ var MAX_PORT = 65535;
12705
+ function isPortFree(port, host = LOOPBACK) {
12706
+ return new Promise((resolve4) => {
12707
+ const srv = createServer();
12708
+ srv.once("error", () => resolve4(false));
12709
+ srv.listen(port, host, () => srv.close(() => resolve4(true)));
12710
+ });
12711
+ }
12712
+ async function waitPortFree(port, opts = {}) {
12713
+ const timeoutMs = opts.timeoutMs ?? 6e3;
12714
+ const intervalMs = opts.intervalMs ?? 250;
12715
+ const host = opts.host ?? LOOPBACK;
12716
+ const deadline = Date.now() + timeoutMs;
12717
+ for (; ; ) {
12718
+ if (await isPortFree(port, host)) return true;
12719
+ if (Date.now() >= deadline) return false;
12720
+ await new Promise((r2) => setTimeout(r2, intervalMs));
12721
+ }
12722
+ }
12723
+ function getFreePort(host = LOOPBACK) {
12724
+ return new Promise((resolve4, reject) => {
12725
+ const srv = createServer();
12726
+ srv.once("error", reject);
12727
+ srv.listen(0, host, () => {
12728
+ const addr = srv.address();
12729
+ const port = addr && typeof addr === "object" ? addr.port : 0;
12730
+ srv.close(() => port ? resolve4(port) : reject(new Error("no free port")));
12731
+ });
12732
+ });
12733
+ }
12734
+ async function findPortNear(from, opts = {}) {
12735
+ const host = opts.host ?? LOOPBACK;
12736
+ const span = opts.span ?? SCAN_SPAN;
12737
+ const skip = opts.skip ?? (() => false);
12738
+ for (let port2 = from; port2 <= Math.min(from + span, MAX_PORT); port2++) {
12739
+ if (!skip(port2) && await isPortFree(port2, host)) return port2;
12740
+ }
12741
+ let port = await getFreePort(host);
12742
+ for (let tries = 0; tries < 10 && skip(port); tries++) port = await getFreePort(host);
12743
+ return port;
12744
+ }
12745
+ async function resolvePortPair(prefs = {}) {
12746
+ const host = prefs.bindAddr?.trim() || LOOPBACK;
12747
+ const ours = new Set(prefs.reclaimable ?? []);
12748
+ const prefFor = (kind) => prefs[kind] ?? prefs.previous?.[kind] ?? prefs.stored?.[kind] ?? prefs.defaults?.[kind];
12749
+ const remembered = (kind, pref) => prefs[kind] === void 0 && (prefs.previous?.[kind] === pref || prefs.stored?.[kind] === pref);
12750
+ const availableForPref = async (port) => ours.has(port) || await isPortFree(port, host);
12751
+ const apiPref = prefFor("api");
12752
+ const dashPref = prefFor("dashboard");
12753
+ let api;
12754
+ if (apiPref === void 0) api = await getFreePort(host);
12755
+ else if (await availableForPref(apiPref)) api = apiPref;
12756
+ else if (remembered("api", apiPref) && await waitPortFree(apiPref, { host })) api = apiPref;
12757
+ else {
12758
+ api = await findPortNear(apiPref + 1, {
12759
+ host,
12760
+ skip: (p2) => ours.has(p2) || p2 === dashPref
12761
+ });
12762
+ }
12763
+ const skipForDash = (p2) => ours.has(p2) || p2 === api;
12764
+ let dashboard;
12765
+ if (dashPref === void 0) {
12766
+ dashboard = await getFreePort(host);
12767
+ for (let tries = 0; tries < 10 && dashboard === api; tries++) {
12768
+ dashboard = await getFreePort(host);
12769
+ }
12770
+ } else if (dashPref !== api && await availableForPref(dashPref)) {
12771
+ dashboard = dashPref;
12772
+ } else if (dashPref !== api && remembered("dashboard", dashPref) && await waitPortFree(dashPref, { host })) {
12773
+ dashboard = dashPref;
12774
+ } else {
12775
+ dashboard = await findPortNear(dashPref + 1, { host, skip: skipForDash });
12776
+ }
12777
+ return {
12778
+ api,
12779
+ dashboard,
12780
+ switched: {
12781
+ api: apiPref !== void 0 && api !== apiPref,
12782
+ dashboard: dashPref !== void 0 && dashboard !== dashPref
12783
+ },
12784
+ preferred: { api: apiPref ?? api, dashboard: dashPref ?? dashboard }
12785
+ };
12786
+ }
12787
+
12788
+ // src/lib/ports.ts
12789
+ var PORTS_FILE = join4(OS_DIR, "ports.json");
12790
+ var INSTANCE_FILE = join4(OS_DIR, "instance.json");
12791
+ function saveInstanceUrl(publicUrl) {
12792
+ try {
12793
+ if (!existsSync(OS_DIR)) mkdirSync(OS_DIR, { recursive: true, mode: 448 });
12794
+ writeFileSync(INSTANCE_FILE, JSON.stringify({ publicUrl: publicUrl ?? null }));
12795
+ } catch {
12796
+ }
12797
+ }
12798
+ function readInstanceUrl() {
12799
+ try {
12800
+ return JSON.parse(readFileSync(INSTANCE_FILE, "utf8")).publicUrl ?? null;
12801
+ } catch {
12802
+ return null;
12803
+ }
12804
+ }
12805
+ function readStoredPorts() {
12806
+ try {
12807
+ return JSON.parse(readFileSync(PORTS_FILE, "utf-8"));
12808
+ } catch {
12809
+ return {};
12810
+ }
12811
+ }
12812
+ function storedApiPort() {
12813
+ return readStoredPorts().api ?? DEFAULT_API_PORT;
12814
+ }
12815
+ function storedDashboardPort() {
12816
+ return readStoredPorts().dashboard ?? DEFAULT_DASHBOARD_PORT;
12817
+ }
12818
+ function saveStoredPorts(api, dashboard) {
12819
+ try {
12820
+ if (!existsSync(OS_DIR)) mkdirSync(OS_DIR, { recursive: true, mode: 448 });
12821
+ writeFileSync(PORTS_FILE, JSON.stringify({ api, dashboard }));
12822
+ } catch {
12823
+ }
12824
+ }
12825
+ function portMoveNotice(resolved, trustedOriginUrls = []) {
12826
+ if (!resolved.switched.api && !resolved.switched.dashboard) return [];
12827
+ const lines = [
12828
+ `A preferred port was busy \u2014 using API ${resolved.api}, dashboard ${resolved.dashboard}.`
12829
+ ];
12830
+ const stale = [...new Set(stalePortOrigins(resolved.preferred.dashboard, trustedOriginUrls))];
12831
+ if (stale.length) {
12832
+ lines.push(
12833
+ `Heads up: ${stale.join(", ")} still names port ${resolved.preferred.dashboard}, so logins`,
12834
+ `from that origin will be rejected. Point it at :${resolved.dashboard} (or free port`,
12835
+ `${resolved.preferred.dashboard} and re-run to move back).`
12836
+ );
12837
+ }
12838
+ return lines;
12839
+ }
12840
+ function stalePortOrigins(movedFrom, urls) {
12841
+ const stale = [];
12842
+ for (const raw of urls) {
12843
+ for (const candidate of (raw ?? "").split(",")) {
12844
+ const value = candidate.trim();
12845
+ if (!value) continue;
12846
+ try {
12847
+ if (new URL(value).port === String(movedFrom)) stale.push(value);
12848
+ } catch {
12849
+ }
12850
+ }
12851
+ }
12852
+ return stale;
12853
+ }
12854
+ async function resolvePorts(prefs) {
12855
+ const resolved = await resolvePortPair({
12856
+ ...prefs,
12857
+ stored: readStoredPorts(),
12858
+ defaults: { api: DEFAULT_API_PORT, dashboard: DEFAULT_DASHBOARD_PORT }
12859
+ });
12860
+ saveStoredPorts(resolved.api, resolved.dashboard);
12861
+ return resolved;
12862
+ }
12863
+
12685
12864
  // src/lib/source-install.ts
12686
12865
  import { spawnSync as spawnSync2 } from "child_process";
12687
- import { existsSync as existsSync2, readFileSync } from "fs";
12688
- import { join as join5 } from "path";
12866
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
12867
+ import { join as join6 } from "path";
12689
12868
 
12690
12869
  // src/lib/from-source.ts
12691
12870
  import { spawn, spawnSync } from "child_process";
12692
- import { existsSync, mkdirSync } from "fs";
12693
- import { join as join4, resolve as resolve3 } from "path";
12871
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
12872
+ import { join as join5, resolve as resolve3 } from "path";
12694
12873
  var DEFAULT_REPO = "https://github.com/oblien/openship.git";
12695
12874
  function has(cmd) {
12696
12875
  try {
@@ -12720,7 +12899,7 @@ function shortSha(cwd) {
12720
12899
  return r2.status === 0 ? (r2.stdout ?? "").trim() || "unknown" : "unknown";
12721
12900
  }
12722
12901
  function isMonorepo(dir) {
12723
- return existsSync(join4(dir, "package.json")) && existsSync(join4(dir, "apps/api/package.json")) && existsSync(join4(dir, "apps/dashboard/package.json"));
12902
+ return existsSync2(join5(dir, "package.json")) && existsSync2(join5(dir, "apps/api/package.json")) && existsSync2(join5(dir, "apps/dashboard/package.json"));
12724
12903
  }
12725
12904
  async function prepareFromSource(opts) {
12726
12905
  if (!has("bun")) {
@@ -12744,9 +12923,9 @@ async function prepareFromSource(opts) {
12744
12923
  }
12745
12924
  ref = (opts.ref || "main").trim();
12746
12925
  const repoUrl = opts.repo || DEFAULT_REPO;
12747
- sourceDir = join4(OS_DIR, "src");
12748
- mkdirSync(OS_DIR, { recursive: true });
12749
- if (!existsSync(join4(sourceDir, ".git"))) {
12926
+ sourceDir = join5(OS_DIR, "src");
12927
+ mkdirSync2(OS_DIR, { recursive: true });
12928
+ if (!existsSync2(join5(sourceDir, ".git"))) {
12750
12929
  console.log(` Cloning ${repoUrl} \u2192 ${sourceDir}`);
12751
12930
  await run("git", ["clone", repoUrl, sourceDir], OS_DIR);
12752
12931
  }
@@ -12761,29 +12940,29 @@ async function prepareFromSource(opts) {
12761
12940
  console.log(` Source: ${sourceDir} @ ${ref} (${sha})`);
12762
12941
  console.log(" Installing workspace dependencies (bun install)\u2026");
12763
12942
  await run("bun", ["install"], sourceDir);
12764
- const distDir = join4(OS_DIR, "from-source-dist");
12943
+ const distDir = join5(OS_DIR, "from-source-dist");
12765
12944
  console.log(" Building release dist (compiles the dashboard \u2014 needs RAM/CPU)\u2026");
12766
12945
  await run(
12767
12946
  "bun",
12768
- ["run", join4(sourceDir, "apps/api/scripts/build-release.ts")],
12947
+ ["run", join5(sourceDir, "apps/api/scripts/build-release.ts")],
12769
12948
  sourceDir,
12770
12949
  { DIST_DIR: distDir, NODE_ENV: "production", CLOUD_MODE: "false", OPENSHIP_TARGET: "local" }
12771
12950
  );
12772
12951
  console.log(" Installing runtime dependencies in the dist\u2026");
12773
12952
  await run("bun", ["install", "--production", "--frozen-lockfile"], distDir);
12774
- const apiDir = join4(distDir, "api");
12775
- const dashboardDir = join4(distDir, "dashboard");
12776
- if (!existsSync(join4(apiDir, "src/index.ts"))) {
12953
+ const apiDir = join5(distDir, "api");
12954
+ const dashboardDir = join5(distDir, "dashboard");
12955
+ if (!existsSync2(join5(apiDir, "src/index.ts"))) {
12777
12956
  throw new Error(`Build produced no API at ${apiDir}/src/index.ts \u2014 build-release layout drift?`);
12778
12957
  }
12779
- if (!existsSync(join4(dashboardDir, "apps/dashboard/server.js"))) {
12958
+ if (!existsSync2(join5(dashboardDir, "apps/dashboard/server.js"))) {
12780
12959
  throw new Error(`Build produced no dashboard at ${dashboardDir}/apps/dashboard/server.js.`);
12781
12960
  }
12782
12961
  return { apiDir, dashboardDir, ref, sha, sourceDir };
12783
12962
  }
12784
12963
 
12785
12964
  // src/lib/source-install.ts
12786
- var MARKER = join5(OS_DIR, "source-install.json");
12965
+ var MARKER = join6(OS_DIR, "source-install.json");
12787
12966
  function parseSourceInstall(raw) {
12788
12967
  try {
12789
12968
  const obj = JSON.parse(raw);
@@ -12794,9 +12973,9 @@ function parseSourceInstall(raw) {
12794
12973
  }
12795
12974
  }
12796
12975
  function readSourceInstall() {
12797
- if (!existsSync2(MARKER)) return null;
12976
+ if (!existsSync3(MARKER)) return null;
12798
12977
  try {
12799
- return parseSourceInstall(readFileSync(MARKER, "utf8"));
12978
+ return parseSourceInstall(readFileSync2(MARKER, "utf8"));
12800
12979
  } catch {
12801
12980
  return null;
12802
12981
  }
@@ -12817,7 +12996,7 @@ async function rebuildFromSource(info) {
12817
12996
  if (!has("git")) {
12818
12997
  throw new Error("`git` is required to update the source checkout but wasn't found on PATH.");
12819
12998
  }
12820
- if (!existsSync2(join5(info.dir, ".git"))) {
12999
+ if (!existsSync3(join6(info.dir, ".git"))) {
12821
13000
  throw new Error(
12822
13001
  `Source checkout ${info.dir} is missing (no .git) \u2014 reinstall from source with scripts/install-source.sh.`
12823
13002
  );
@@ -12826,7 +13005,7 @@ async function rebuildFromSource(info) {
12826
13005
  await run("git", ["checkout", info.ref], info.dir);
12827
13006
  await run("git", ["pull", "--ff-only", "origin", info.ref], info.dir).catch(() => {
12828
13007
  });
12829
- const cliDir = join5(info.dir, "apps/cli");
13008
+ const cliDir = join6(info.dir, "apps/cli");
12830
13009
  await run("bun", ["install"], info.dir);
12831
13010
  await run("bun", ["run", "build"], cliDir);
12832
13011
  await run("bun", ["run", "build/stage-dashboard.ts"], cliDir);
@@ -12839,11 +13018,11 @@ var EDGE_ACME_HOST_DIR = `${EDGE_HOST_STATE_DIR}/acme`;
12839
13018
  function edgeVolumeYaml(indent) {
12840
13019
  return EDGE_CONTAINER_MOUNTS.map((m2) => `${indent}- ${m2.host}:${m2.container}:z`).join("\n");
12841
13020
  }
12842
- var COMPOSE_DIR = join6(OS_DIR, "compose");
12843
- var INSTALL_METHOD_FILE = join6(OS_DIR, "install-method");
12844
- var COMPOSE_FILE = join6(COMPOSE_DIR, "docker-compose.yml");
12845
- var BUILD_FILE = join6(COMPOSE_DIR, "docker-compose.build.yml");
12846
- var ENV_FILE = join6(COMPOSE_DIR, ".env");
13021
+ var COMPOSE_DIR = join7(OS_DIR, "compose");
13022
+ var INSTALL_METHOD_FILE = join7(OS_DIR, "install-method");
13023
+ var COMPOSE_FILE = join7(COMPOSE_DIR, "docker-compose.yml");
13024
+ var BUILD_FILE = join7(COMPOSE_DIR, "docker-compose.build.yml");
13025
+ var ENV_FILE = join7(COMPOSE_DIR, ".env");
12847
13026
  var BUILT_SERVICES = [
12848
13027
  { service: "api", dockerfile: "apps/api/Dockerfile" },
12849
13028
  { service: "dashboard", dockerfile: "apps/dashboard/Dockerfile" },
@@ -12852,21 +13031,45 @@ var BUILT_SERVICES = [
12852
13031
  var ENV_CONSUMING_SERVICES = ["api", "dashboard", "edge"];
12853
13032
  function readInstallMethod() {
12854
13033
  try {
12855
- const v2 = readFileSync2(INSTALL_METHOD_FILE, "utf8").trim();
13034
+ const v2 = readFileSync3(INSTALL_METHOD_FILE, "utf8").trim();
12856
13035
  return v2 === "compose" || v2 === "bare" ? v2 : null;
12857
13036
  } catch {
12858
13037
  return null;
12859
13038
  }
12860
13039
  }
12861
13040
  function writeInstallMethod(method) {
12862
- mkdirSync2(OS_DIR, { recursive: true, mode: 448 });
12863
- writeFileSync(INSTALL_METHOD_FILE, method, { mode: 384 });
13041
+ mkdirSync3(OS_DIR, { recursive: true, mode: 448 });
13042
+ writeFileSync2(INSTALL_METHOD_FILE, method, { mode: 384 });
13043
+ }
13044
+ function dockerState() {
13045
+ const ok = (args) => spawnSync3("docker", args, { stdio: "ignore" }).status === 0;
13046
+ if (!ok(["--version"])) return { binary: false, plugin: false, daemon: false };
13047
+ return { binary: true, plugin: ok(["compose", "version"]), daemon: ok(["version"]) };
13048
+ }
13049
+ function dockerGap(state = dockerState()) {
13050
+ if (!state.binary) {
13051
+ return { summary: "Docker isn't installed", installable: true };
13052
+ }
13053
+ if (!state.plugin) {
13054
+ return {
13055
+ summary: "Docker is installed but the Compose plugin (`docker compose`) is missing",
13056
+ installable: true
13057
+ };
13058
+ }
13059
+ if (!state.daemon) {
13060
+ const asRoot = typeof process.getuid === "function" && process.getuid() === 0;
13061
+ return {
13062
+ summary: "Docker is installed but its daemon isn't reachable",
13063
+ // Reinstalling changes nothing: a group change only applies to NEW logins,
13064
+ // and a stopped daemon needs starting, not installing.
13065
+ installable: false,
13066
+ hint: asRoot ? "Start it with: systemctl start docker" : `Add your user to the docker group: sudo usermod -aG docker ${userInfo().username} \u2014 then log out and back in (or run: newgrp docker). If the daemon is stopped: sudo systemctl start docker`
13067
+ };
13068
+ }
13069
+ return null;
12864
13070
  }
12865
13071
  function hasDockerCompose() {
12866
- const docker = spawnSync3("docker", ["version"], { stdio: "ignore" });
12867
- if (docker.status !== 0) return false;
12868
- const compose2 = spawnSync3("docker", ["compose", "version"], { stdio: "ignore" });
12869
- return compose2.status === 0;
13072
+ return dockerGap() === null;
12870
13073
  }
12871
13074
  function composeIsViableDefault() {
12872
13075
  return process.platform === "linux" && hasDockerCompose();
@@ -12877,9 +13080,18 @@ function hasCmd(cmd) {
12877
13080
  function shQuote(s2) {
12878
13081
  return `'${s2.replace(/'/g, `'\\''`)}'`;
12879
13082
  }
12880
- async function ensureDocker() {
12881
- if (hasDockerCompose()) return true;
13083
+ async function ensureDocker(opts = {}) {
13084
+ const notice = opts.onNotice ?? ((line) => process.stderr.write(` ${line}
13085
+ `));
13086
+ const state = dockerState();
13087
+ const gap = dockerGap(state);
13088
+ if (!gap) return true;
12882
13089
  if (process.platform !== "linux") return false;
13090
+ if (!gap.installable) {
13091
+ notice(gap.summary + ".");
13092
+ if (gap.hint) notice(gap.hint);
13093
+ return false;
13094
+ }
12883
13095
  const plan = systemCatalog.installs.docker({
12884
13096
  os: "linux",
12885
13097
  serviceManager: "systemd"
@@ -12887,12 +13099,36 @@ async function ensureDocker() {
12887
13099
  if (!plan.supported || !plan.installCommand) return false;
12888
13100
  const asRoot = typeof process.getuid === "function" && process.getuid() === 0;
12889
13101
  const sudo = !asRoot && hasCmd("sudo") ? "sudo " : "";
12890
- const sh = (script) => spawnSync3("sh", ["-c", sudo ? `${sudo}sh -c ${shQuote(script)}` : script], {
12891
- stdio: "inherit"
12892
- }).status ?? 1;
12893
- if (sh(plan.installCommand) !== 0) return false;
12894
- if (plan.startCommand) sh(plan.startCommand);
12895
- return hasDockerCompose();
13102
+ notice("This can take 2-5 minutes on a small VPS (~150 MB of packages) and stays quiet while apt works.");
13103
+ if (state.binary) {
13104
+ notice("Docker's installer will warn that docker already exists and pause ~20s before continuing \u2014 that's expected.");
13105
+ }
13106
+ const sh = (script) => new Promise((resolve4) => {
13107
+ const child = spawn2("sh", ["-c", sudo ? `${sudo}sh -c ${shQuote(script)}` : script], {
13108
+ stdio: "inherit"
13109
+ });
13110
+ const started = Date.now();
13111
+ const tick = setInterval(() => {
13112
+ const s2 = Math.round((Date.now() - started) / 1e3);
13113
+ notice(`still installing Docker \u2014 ${Math.floor(s2 / 60)}m${String(s2 % 60).padStart(2, "0")}s elapsed\u2026`);
13114
+ }, 3e4);
13115
+ const done = (code) => {
13116
+ clearInterval(tick);
13117
+ resolve4(code);
13118
+ };
13119
+ child.on("error", () => done(1));
13120
+ child.on("close", (code) => done(code ?? 1));
13121
+ });
13122
+ if (await sh(plan.installCommand) !== 0) return false;
13123
+ if (plan.startCommand) await sh(plan.startCommand);
13124
+ const after = dockerGap();
13125
+ if (!after) {
13126
+ notice("Docker ready.");
13127
+ return true;
13128
+ }
13129
+ notice(after.summary + ".");
13130
+ if (after.hint) notice(after.hint);
13131
+ return false;
12896
13132
  }
12897
13133
  var COMPOSE_YAML = `# Managed by \`openship up\` \u2014 do not edit; re-run \`openship up\` to regenerate.
12898
13134
  services:
@@ -12900,6 +13136,12 @@ services:
12900
13136
  image: postgres:16-alpine
12901
13137
  restart: unless-stopped
12902
13138
  environment:
13139
+ # Keep the data dir in a subdirectory of the volume so a fresh install never
13140
+ # runs initdb against a bare mount root (which fails on quirky host
13141
+ # filesystems with EPERM \u2014 #350). OPENSHIP_PGDATA is decided ONCE at install
13142
+ # by the CLI (fresh \u2192 subdir, pre-existing volume \u2192 root) and preserved in
13143
+ # .env, so this never moves an existing database.
13144
+ PGDATA: \${OPENSHIP_PGDATA:-/var/lib/postgresql/data/pgdata}
12903
13145
  POSTGRES_USER: \${POSTGRES_USER:-openship}
12904
13146
  POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:?missing from .env \u2014 re-run openship up to regenerate it}
12905
13147
  POSTGRES_DB: \${POSTGRES_DB:-openship}
@@ -12995,7 +13237,7 @@ function keepSecret(existing, key) {
12995
13237
  function readEnvFile() {
12996
13238
  const out = {};
12997
13239
  try {
12998
- for (const line of readFileSync2(ENV_FILE, "utf8").split("\n")) {
13240
+ for (const line of readFileSync3(ENV_FILE, "utf8").split("\n")) {
12999
13241
  const m2 = line.match(/^([A-Z0-9_]+)=(.*)$/);
13000
13242
  if (m2) out[m2[1]] = m2[2];
13001
13243
  }
@@ -13015,10 +13257,10 @@ function rewriteHostAuthorizedKeys(existing, pub) {
13015
13257
  function provisionHostSshChannel() {
13016
13258
  if (process.platform !== "linux") return null;
13017
13259
  try {
13018
- const sshDir = join6(COMPOSE_DIR, "host-ssh");
13019
- mkdirSync2(sshDir, { recursive: true, mode: 448 });
13020
- const keyPath = join6(sshDir, "id_ed25519");
13021
- if (!existsSync3(keyPath)) {
13260
+ const sshDir = join7(COMPOSE_DIR, "host-ssh");
13261
+ mkdirSync3(sshDir, { recursive: true, mode: 448 });
13262
+ const keyPath = join7(sshDir, "id_ed25519");
13263
+ if (!existsSync4(keyPath)) {
13022
13264
  const g2 = spawnSync3(
13023
13265
  "ssh-keygen",
13024
13266
  ["-t", "ed25519", "-N", "", "-q", "-f", keyPath, "-C", HOST_KEY_COMMENT],
@@ -13026,7 +13268,7 @@ function provisionHostSshChannel() {
13026
13268
  );
13027
13269
  if (g2.status !== 0) return null;
13028
13270
  }
13029
- const pub = readFileSync2(`${keyPath}.pub`, "utf8").trim();
13271
+ const pub = readFileSync3(`${keyPath}.pub`, "utf8").trim();
13030
13272
  if (!pub) return null;
13031
13273
  const user = (() => {
13032
13274
  try {
@@ -13035,12 +13277,12 @@ function provisionHostSshChannel() {
13035
13277
  return process.env.USER || process.env.LOGNAME || "root";
13036
13278
  }
13037
13279
  })();
13038
- const userSshDir = join6(homedir2(), ".ssh");
13039
- mkdirSync2(userSshDir, { recursive: true, mode: 448 });
13040
- const authKeys = join6(userSshDir, "authorized_keys");
13041
- const existing = existsSync3(authKeys) ? readFileSync2(authKeys, "utf8") : "";
13280
+ const userSshDir = join7(homedir2(), ".ssh");
13281
+ mkdirSync3(userSshDir, { recursive: true, mode: 448 });
13282
+ const authKeys = join7(userSshDir, "authorized_keys");
13283
+ const existing = existsSync4(authKeys) ? readFileSync3(authKeys, "utf8") : "";
13042
13284
  const next = rewriteHostAuthorizedKeys(existing, pub);
13043
- if (next !== existing) writeFileSync(authKeys, next, { mode: 384 });
13285
+ if (next !== existing) writeFileSync2(authKeys, next, { mode: 384 });
13044
13286
  chmodSync(authKeys, 384);
13045
13287
  return { user, keyPath };
13046
13288
  } catch {
@@ -13051,6 +13293,11 @@ function composeProjectName(prev) {
13051
13293
  if (prev.COMPOSE_PROJECT_NAME) return prev.COMPOSE_PROJECT_NAME;
13052
13294
  return Object.keys(prev).length > 0 ? "compose" : "openship";
13053
13295
  }
13296
+ var PGDATA_ROOT = "/var/lib/postgresql/data";
13297
+ function resolvePgData(prev) {
13298
+ if (prev.OPENSHIP_PGDATA) return prev.OPENSHIP_PGDATA;
13299
+ return dbVolumeExists(composeProjectName(prev)) ? PGDATA_ROOT : `${PGDATA_ROOT}/pgdata`;
13300
+ }
13054
13301
  function orphanedStackContainers(project) {
13055
13302
  const r2 = spawnSync3(
13056
13303
  "docker",
@@ -13150,7 +13397,7 @@ function migrateLegacyEdgeVolumes(project) {
13150
13397
  const existing = new Set(ls.stdout.split("\n").map((l2) => l2.trim()).filter(Boolean));
13151
13398
  for (const { volume, host } of pairs) {
13152
13399
  if (!existing.has(volume)) continue;
13153
- mkdirSync2(host, { recursive: true });
13400
+ mkdirSync3(host, { recursive: true });
13154
13401
  const alreadyThere = spawnSync3("sh", ["-c", `ls -A ${JSON.stringify(host)} 2>/dev/null | head -1`], {
13155
13402
  encoding: "utf8"
13156
13403
  });
@@ -13202,9 +13449,11 @@ function keepConfig(prev, key, explicit) {
13202
13449
  }
13203
13450
  function resolveEnvConfig(prev, opts) {
13204
13451
  const publicUrl = keepConfig(prev, "OPENSHIP_PUBLIC_URL", opts.publicUrl);
13452
+ const bindAddr = keepConfig(prev, "OPENSHIP_BIND_ADDR");
13205
13453
  return {
13206
- apiPort: keepConfig(prev, "API_PORT", opts.apiPort) ?? "4000",
13207
- dashPort: keepConfig(prev, "DASHBOARD_PORT", opts.dashboardPort) ?? "3001",
13454
+ apiPort: keepConfig(prev, "API_PORT", opts.apiPort) ?? String(DEFAULT_API_PORT),
13455
+ dashPort: keepConfig(prev, "DASHBOARD_PORT", opts.dashboardPort) ?? String(DEFAULT_DASHBOARD_PORT),
13456
+ ...bindAddr ? { bindAddr } : {},
13208
13457
  ...publicUrl ? { publicUrl } : {},
13209
13458
  // A public URL always implies a proxy in front; otherwise keep whatever the
13210
13459
  // install was configured with.
@@ -13222,6 +13471,47 @@ function resolveEnvConfig(prev, opts) {
13222
13471
  hostControl: opts.noHostControl === void 0 ? prev.OPENSHIP_HOST_CONTROL !== "false" : !opts.noHostControl
13223
13472
  };
13224
13473
  }
13474
+ function toPort(value) {
13475
+ const n2 = Number(value);
13476
+ return Number.isInteger(n2) && n2 > 0 && n2 <= 65535 ? n2 : void 0;
13477
+ }
13478
+ function composeEnvPorts() {
13479
+ const env = readEnvFile();
13480
+ return { api: toPort(env.API_PORT), dashboard: toPort(env.DASHBOARD_PORT) };
13481
+ }
13482
+ function composeBindAddr() {
13483
+ return readEnvFile().OPENSHIP_BIND_ADDR?.trim() || "0.0.0.0";
13484
+ }
13485
+ function composeTrustedOriginUrls() {
13486
+ const env = readEnvFile();
13487
+ return [env.OPENSHIP_PUBLIC_URL, env.OPENSHIP_EXTRA_TRUSTED_ORIGINS].filter(
13488
+ (v2) => !!v2?.trim()
13489
+ );
13490
+ }
13491
+ function composeHeldPorts() {
13492
+ const r2 = spawnSync3(
13493
+ "docker",
13494
+ ["ps", "--format", '{{.Label "com.docker.compose.project.config_files"}} {{.Ports}}'],
13495
+ { encoding: "utf8" }
13496
+ );
13497
+ if (r2.status !== 0 || !r2.stdout) return [];
13498
+ const ports = /* @__PURE__ */ new Set();
13499
+ for (const line of r2.stdout.split("\n")) {
13500
+ const [configFiles = "", published = ""] = line.split(" ");
13501
+ if (!configFiles.split(",").some((f2) => f2.trim() === COMPOSE_FILE)) continue;
13502
+ for (const m2 of published.matchAll(/:(\d+)->/g)) ports.add(Number(m2[1]));
13503
+ }
13504
+ return [...ports];
13505
+ }
13506
+ async function resolveComposePorts(prefs) {
13507
+ return resolvePorts({
13508
+ api: toPort(prefs.api),
13509
+ dashboard: toPort(prefs.dashboard),
13510
+ previous: composeEnvPorts(),
13511
+ bindAddr: composeBindAddr(),
13512
+ reclaimable: composeHeldPorts()
13513
+ });
13514
+ }
13225
13515
  function renderEnv(opts, host, cfg) {
13226
13516
  const prev = readEnvFile();
13227
13517
  const lines = [
@@ -13235,13 +13525,21 @@ function renderEnv(opts, host, cfg) {
13235
13525
  // (hides the local row). Written explicitly so the policy is visible in .env.
13236
13526
  `OPENSHIP_HOST_CONTROL=${cfg.hostControl ? "true" : "false"}`,
13237
13527
  `OPENSHIP_IMAGE_REGISTRY=${cfg.registry}`,
13238
- `OPENSHIP_VERSION=${opts.version || (true ? "0.4.8" : "latest")}`,
13528
+ `OPENSHIP_VERSION=${opts.version || (true ? "0.5.0" : "latest")}`,
13239
13529
  `POSTGRES_PASSWORD=${keepSecret(prev, "POSTGRES_PASSWORD")}`,
13530
+ // Pinned once (see resolvePgData): fresh install → subdir, existing volume → root.
13531
+ `OPENSHIP_PGDATA=${resolvePgData(prev)}`,
13240
13532
  `BETTER_AUTH_SECRET=${keepSecret(prev, "BETTER_AUTH_SECRET")}`,
13241
13533
  `INTERNAL_TOKEN=${keepSecret(prev, "INTERNAL_TOKEN")}`,
13242
13534
  `API_PORT=${cfg.apiPort}`,
13243
- `DASHBOARD_PORT=${cfg.dashPort}`
13535
+ `DASHBOARD_PORT=${cfg.dashPort}`,
13536
+ // The api's OWN view of the dashboard port: the self-app boot reconcile points
13537
+ // the operator's domain at the dashboard through this (self-deploy.ts), and it
13538
+ // silently defaults to 3001 when unset. Ports are dynamic now, so leaving it
13539
+ // out publishes a domain routed to a port nothing is listening on.
13540
+ `OPENSHIP_DASHBOARD_PORT=${cfg.dashPort}`
13244
13541
  ];
13542
+ if (cfg.bindAddr) lines.push(`OPENSHIP_BIND_ADDR=${cfg.bindAddr}`);
13245
13543
  if (cfg.publicUrl) lines.push(`OPENSHIP_PUBLIC_URL=${cfg.publicUrl}`);
13246
13544
  if (cfg.extraTrustedOrigins) {
13247
13545
  lines.push(`OPENSHIP_EXTRA_TRUSTED_ORIGINS=${cfg.extraTrustedOrigins}`);
@@ -13261,7 +13559,7 @@ function renderEnv(opts, host, cfg) {
13261
13559
  function sourceBuildDir() {
13262
13560
  const marker = readSourceInstall();
13263
13561
  if (!marker?.dir) return null;
13264
- const hasAll = BUILT_SERVICES.every((s2) => existsSync3(join6(marker.dir, s2.dockerfile)));
13562
+ const hasAll = BUILT_SERVICES.every((s2) => existsSync4(join7(marker.dir, s2.dockerfile)));
13265
13563
  return hasAll ? marker.dir : null;
13266
13564
  }
13267
13565
  function renderBuildOverride(repoDir) {
@@ -13277,20 +13575,20 @@ services:
13277
13575
  ${services}`;
13278
13576
  }
13279
13577
  function materialize(opts) {
13280
- mkdirSync2(COMPOSE_DIR, { recursive: true, mode: 448 });
13578
+ mkdirSync3(COMPOSE_DIR, { recursive: true, mode: 448 });
13281
13579
  const prev = readEnvFile();
13282
13580
  const cfg = resolveEnvConfig(prev, opts);
13283
13581
  const host = cfg.hostControl ? provisionHostSshChannel() : null;
13284
13582
  let before = "";
13285
13583
  try {
13286
- before = readFileSync2(ENV_FILE, "utf8");
13584
+ before = readFileSync3(ENV_FILE, "utf8");
13287
13585
  } catch {
13288
13586
  }
13289
13587
  const rendered = renderEnv(opts, host, cfg);
13290
- writeFileSync(COMPOSE_FILE, COMPOSE_YAML);
13291
- writeFileSync(ENV_FILE, rendered, { mode: 384 });
13588
+ writeFileSync2(COMPOSE_FILE, COMPOSE_YAML);
13589
+ writeFileSync2(ENV_FILE, rendered, { mode: 384 });
13292
13590
  const buildDir = opts.build === false ? null : sourceBuildDir();
13293
- if (buildDir) writeFileSync(BUILD_FILE, renderBuildOverride(buildDir));
13591
+ if (buildDir) writeFileSync2(BUILD_FILE, renderBuildOverride(buildDir));
13294
13592
  return { buildDir, cfg, envChanged: before !== "" && rendered !== before };
13295
13593
  }
13296
13594
  function dbVolumeExists(project) {
@@ -13365,12 +13663,12 @@ function onEdgeContainerChanged() {
13365
13663
  invalidateEdgeContainer();
13366
13664
  }
13367
13665
  function composeDown() {
13368
- if (!existsSync3(COMPOSE_FILE)) return false;
13666
+ if (!existsSync4(COMPOSE_FILE)) return false;
13369
13667
  return compose(["down"]) === 0;
13370
13668
  }
13371
13669
  function composeUninstall(opts = {}) {
13372
13670
  const removedImages = [];
13373
- const ok = existsSync3(COMPOSE_FILE) ? compose(["down", "-v", "--remove-orphans"]) === 0 : false;
13671
+ const ok = existsSync4(COMPOSE_FILE) ? compose(["down", "-v", "--remove-orphans"]) === 0 : false;
13374
13672
  if (opts.removeImages) {
13375
13673
  const env = readEnvFile();
13376
13674
  const registry = env.OPENSHIP_IMAGE_REGISTRY || DEFAULT_IMAGE_REGISTRY;
@@ -13385,7 +13683,7 @@ function composeUninstall(opts = {}) {
13385
13683
  return { ok, removedImages };
13386
13684
  }
13387
13685
  async function composeUpdate(version) {
13388
- if (!existsSync3(COMPOSE_FILE)) return false;
13686
+ if (!existsSync4(COMPOSE_FILE)) return false;
13389
13687
  return (await composeUp(version ? { version } : {})).ok;
13390
13688
  }
13391
13689
  function composePs() {
@@ -13421,6 +13719,15 @@ async function waitForApiHealth(port, tries) {
13421
13719
  }
13422
13720
  return false;
13423
13721
  }
13722
+ async function waitForEdgeRunning(tries) {
13723
+ const exec = new LocalExecutor();
13724
+ for (let i2 = 0; i2 < tries; i2++) {
13725
+ const out = await exec.exec(`docker inspect -f '{{.State.Running}}' ${EDGE_CONTAINER_NAME}`).catch(() => "");
13726
+ if (out.trim() === "true") return true;
13727
+ await new Promise((r2) => setTimeout(r2, 1e3));
13728
+ }
13729
+ return false;
13730
+ }
13424
13731
  async function importMigratedSites(apiPort, sites, certPems) {
13425
13732
  const token = composeInternalToken();
13426
13733
  if (!token) {
@@ -13437,6 +13744,11 @@ async function importMigratedSites(apiPort, sites, certPems) {
13437
13744
  spinner.warn(`${error} \u2014 migrated sites not imported. Re-run \`openship up\` to retry.`);
13438
13745
  return { ok: false, registered: [], error };
13439
13746
  }
13747
+ if (!await waitForEdgeRunning(60)) {
13748
+ const error = `the ${EDGE_CONTAINER_NAME} container isn't running yet`;
13749
+ spinner.warn(`${error} \u2014 migrated sites not imported. Re-run \`openship up\` to retry.`);
13750
+ return { ok: false, registered: [], error };
13751
+ }
13440
13752
  try {
13441
13753
  const r2 = await fetch(`http://127.0.0.1:${apiPort}/api/system/edge/import-sites`, {
13442
13754
  method: "POST",
@@ -13496,15 +13808,27 @@ async function importMigratedSites(apiPort, sites, certPems) {
13496
13808
  export {
13497
13809
  OS_DIR,
13498
13810
  IS_ALT_HOME,
13811
+ DEFAULT_API_PORT,
13812
+ DEFAULT_DASHBOARD_PORT,
13813
+ saveInstanceUrl,
13814
+ readInstanceUrl,
13815
+ readStoredPorts,
13816
+ storedApiPort,
13817
+ storedDashboardPort,
13818
+ portMoveNotice,
13819
+ resolvePorts,
13499
13820
  shortSha,
13500
13821
  prepareFromSource,
13501
13822
  readSourceInstall,
13502
13823
  remoteSha,
13503
13824
  rebuildFromSource,
13504
13825
  readInstallMethod,
13826
+ dockerGap,
13505
13827
  hasDockerCompose,
13506
13828
  composeIsViableDefault,
13507
13829
  ensureDocker,
13830
+ composeTrustedOriginUrls,
13831
+ resolveComposePorts,
13508
13832
  sourceBuildDir,
13509
13833
  composePrefetch,
13510
13834
  composeUp,
@@ -13517,5 +13841,6 @@ export {
13517
13841
  composeRestart,
13518
13842
  composeInternalToken,
13519
13843
  waitForApiHealth,
13844
+ waitForEdgeRunning,
13520
13845
  importMigratedSites
13521
13846
  };