openship 0.4.7 → 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-2HYDXQEH.js → bare-VRGE5MXH.js} +7 -7
  3. package/dist/{chunk-TGIINPVM.js → chunk-6LKHXB6T.js} +22 -7
  4. package/dist/{chunk-R5BHFFAV.js → chunk-6PIYRG5I.js} +56 -17
  5. package/dist/{chunk-IMBD5PZ5.js → chunk-BS2AXYZX.js} +430 -86
  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-XFCDYZUQ.js → chunk-JLKKYUZA.js} +31 -49
  10. package/dist/{chunk-4TJC4JZQ.js → chunk-K2RPVGPN.js} +23 -4
  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-IPYPMI3S.js → chunk-NNPEG6HM.js} +1 -1
  14. package/dist/{chunk-UXWAVM4C.js → chunk-NXFIRK2Z.js} +398 -112
  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-2KXREXMO.js → cloud-3M2WZ624.js} +12 -12
  21. package/dist/{detect-2XSCFFTF.js → detect-BAJMY6KM.js} +7 -5
  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-ZQ4Y5HEA.js → edge-import-RIWNEIXQ.js} +21 -19
  25. package/dist/{ensure-container-edge-ED25XL3Z.js → ensure-container-edge-3NSYKHRX.js} +6 -6
  26. package/dist/{executor-YPT6RH6O.js → executor-LOYMPU67.js} +3 -3
  27. package/dist/index.js +309 -338
  28. package/dist/{nginx-XWJAOGS4.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 +51705 -45369
  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-2DMFIA74.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-XFCDYZUQ.js";
104
+ } from "./chunk-JLKKYUZA.js";
105
105
  import {
106
106
  systemCatalog
107
- } from "./chunk-UXWAVM4C.js";
107
+ } from "./chunk-NXFIRK2Z.js";
108
108
  import {
109
109
  AwsCrc32,
110
110
  __awaiter,
@@ -139,26 +139,28 @@ 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";
149
149
  import {
150
150
  EDGE_CONTAINER_NAME,
151
151
  edgeCrashReason,
152
- invalidateEdgeContainer
153
- } from "./chunk-R5BHFFAV.js";
152
+ invalidateEdgeContainer,
153
+ sanitizeEdgeVhosts
154
+ } from "./chunk-6PIYRG5I.js";
154
155
  import {
155
156
  EDGE_CONTAINER_MOUNTS,
156
157
  EDGE_HOST_STATE_DIR
157
- } from "./chunk-VFINNTUL.js";
158
+ } from "./chunk-EAAGSQXN.js";
158
159
  import {
159
160
  DEFAULT_IMAGE_REGISTRY,
161
+ isAwsS3Endpoint,
160
162
  safeErrorMessage
161
- } from "./chunk-6APULWSU.js";
163
+ } from "./chunk-UMQP7SYT.js";
162
164
 
163
165
  // src/lib/edge-import.ts
164
166
  import chalk from "chalk";
@@ -690,7 +692,15 @@ var DockerBackupExecutor = class {
690
692
  await this.ensureImage(helperImage);
691
693
  const hostConfig = {
692
694
  Binds: [`${source.source}:/mnt:ro`],
693
- 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: {} }
694
704
  };
695
705
  const helper = await this.dockerode.createContainer({
696
706
  Image: helperImage,
@@ -12190,7 +12200,7 @@ var S3DestinationImpl = class {
12190
12200
  if (!accessKeyId || !secretAccessKey) {
12191
12201
  throw new Error(`S3Destination "${row.name}" missing access credentials`);
12192
12202
  }
12193
- const isAws = !row.endpoint || row.endpoint.includes(".amazonaws.com") || row.endpoint === "https://s3.amazonaws.com";
12203
+ const isAws = isAwsS3Endpoint(row.endpoint);
12194
12204
  this.client = new S3Client({
12195
12205
  endpoint: row.endpoint ?? void 0,
12196
12206
  region: row.region ?? "auto",
@@ -12667,11 +12677,11 @@ registerDestination("sftp", (row) => new SftpDestinationImpl(row));
12667
12677
  registerDestination("openship_server", (row) => new SftpDestinationImpl(row));
12668
12678
 
12669
12679
  // src/lib/compose.ts
12670
- import { spawnSync as spawnSync3 } from "child_process";
12680
+ import { spawn as spawn2, spawnSync as spawnSync3 } from "child_process";
12671
12681
  import { randomBytes as randomBytes5 } from "crypto";
12672
- 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";
12673
12683
  import { homedir as homedir2, userInfo } from "os";
12674
- import { join as join6 } from "path";
12684
+ import { join as join7 } from "path";
12675
12685
 
12676
12686
  // src/lib/paths.ts
12677
12687
  import { homedir } from "os";
@@ -12681,15 +12691,185 @@ var override = process.env.OPENSHIP_HOME?.trim();
12681
12691
  var OS_DIR = override ? resolve2(override) : DEFAULT_HOME;
12682
12692
  var IS_ALT_HOME = OS_DIR !== DEFAULT_HOME;
12683
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
+
12684
12864
  // src/lib/source-install.ts
12685
12865
  import { spawnSync as spawnSync2 } from "child_process";
12686
- import { existsSync as existsSync2, readFileSync } from "fs";
12687
- import { join as join5 } from "path";
12866
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
12867
+ import { join as join6 } from "path";
12688
12868
 
12689
12869
  // src/lib/from-source.ts
12690
12870
  import { spawn, spawnSync } from "child_process";
12691
- import { existsSync, mkdirSync } from "fs";
12692
- 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";
12693
12873
  var DEFAULT_REPO = "https://github.com/oblien/openship.git";
12694
12874
  function has(cmd) {
12695
12875
  try {
@@ -12719,7 +12899,7 @@ function shortSha(cwd) {
12719
12899
  return r2.status === 0 ? (r2.stdout ?? "").trim() || "unknown" : "unknown";
12720
12900
  }
12721
12901
  function isMonorepo(dir) {
12722
- 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"));
12723
12903
  }
12724
12904
  async function prepareFromSource(opts) {
12725
12905
  if (!has("bun")) {
@@ -12743,9 +12923,9 @@ async function prepareFromSource(opts) {
12743
12923
  }
12744
12924
  ref = (opts.ref || "main").trim();
12745
12925
  const repoUrl = opts.repo || DEFAULT_REPO;
12746
- sourceDir = join4(OS_DIR, "src");
12747
- mkdirSync(OS_DIR, { recursive: true });
12748
- if (!existsSync(join4(sourceDir, ".git"))) {
12926
+ sourceDir = join5(OS_DIR, "src");
12927
+ mkdirSync2(OS_DIR, { recursive: true });
12928
+ if (!existsSync2(join5(sourceDir, ".git"))) {
12749
12929
  console.log(` Cloning ${repoUrl} \u2192 ${sourceDir}`);
12750
12930
  await run("git", ["clone", repoUrl, sourceDir], OS_DIR);
12751
12931
  }
@@ -12760,29 +12940,29 @@ async function prepareFromSource(opts) {
12760
12940
  console.log(` Source: ${sourceDir} @ ${ref} (${sha})`);
12761
12941
  console.log(" Installing workspace dependencies (bun install)\u2026");
12762
12942
  await run("bun", ["install"], sourceDir);
12763
- const distDir = join4(OS_DIR, "from-source-dist");
12943
+ const distDir = join5(OS_DIR, "from-source-dist");
12764
12944
  console.log(" Building release dist (compiles the dashboard \u2014 needs RAM/CPU)\u2026");
12765
12945
  await run(
12766
12946
  "bun",
12767
- ["run", join4(sourceDir, "apps/api/scripts/build-release.ts")],
12947
+ ["run", join5(sourceDir, "apps/api/scripts/build-release.ts")],
12768
12948
  sourceDir,
12769
12949
  { DIST_DIR: distDir, NODE_ENV: "production", CLOUD_MODE: "false", OPENSHIP_TARGET: "local" }
12770
12950
  );
12771
12951
  console.log(" Installing runtime dependencies in the dist\u2026");
12772
12952
  await run("bun", ["install", "--production", "--frozen-lockfile"], distDir);
12773
- const apiDir = join4(distDir, "api");
12774
- const dashboardDir = join4(distDir, "dashboard");
12775
- 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"))) {
12776
12956
  throw new Error(`Build produced no API at ${apiDir}/src/index.ts \u2014 build-release layout drift?`);
12777
12957
  }
12778
- if (!existsSync(join4(dashboardDir, "apps/dashboard/server.js"))) {
12958
+ if (!existsSync2(join5(dashboardDir, "apps/dashboard/server.js"))) {
12779
12959
  throw new Error(`Build produced no dashboard at ${dashboardDir}/apps/dashboard/server.js.`);
12780
12960
  }
12781
12961
  return { apiDir, dashboardDir, ref, sha, sourceDir };
12782
12962
  }
12783
12963
 
12784
12964
  // src/lib/source-install.ts
12785
- var MARKER = join5(OS_DIR, "source-install.json");
12965
+ var MARKER = join6(OS_DIR, "source-install.json");
12786
12966
  function parseSourceInstall(raw) {
12787
12967
  try {
12788
12968
  const obj = JSON.parse(raw);
@@ -12793,9 +12973,9 @@ function parseSourceInstall(raw) {
12793
12973
  }
12794
12974
  }
12795
12975
  function readSourceInstall() {
12796
- if (!existsSync2(MARKER)) return null;
12976
+ if (!existsSync3(MARKER)) return null;
12797
12977
  try {
12798
- return parseSourceInstall(readFileSync(MARKER, "utf8"));
12978
+ return parseSourceInstall(readFileSync2(MARKER, "utf8"));
12799
12979
  } catch {
12800
12980
  return null;
12801
12981
  }
@@ -12816,7 +12996,7 @@ async function rebuildFromSource(info) {
12816
12996
  if (!has("git")) {
12817
12997
  throw new Error("`git` is required to update the source checkout but wasn't found on PATH.");
12818
12998
  }
12819
- if (!existsSync2(join5(info.dir, ".git"))) {
12999
+ if (!existsSync3(join6(info.dir, ".git"))) {
12820
13000
  throw new Error(
12821
13001
  `Source checkout ${info.dir} is missing (no .git) \u2014 reinstall from source with scripts/install-source.sh.`
12822
13002
  );
@@ -12825,7 +13005,7 @@ async function rebuildFromSource(info) {
12825
13005
  await run("git", ["checkout", info.ref], info.dir);
12826
13006
  await run("git", ["pull", "--ff-only", "origin", info.ref], info.dir).catch(() => {
12827
13007
  });
12828
- const cliDir = join5(info.dir, "apps/cli");
13008
+ const cliDir = join6(info.dir, "apps/cli");
12829
13009
  await run("bun", ["install"], info.dir);
12830
13010
  await run("bun", ["run", "build"], cliDir);
12831
13011
  await run("bun", ["run", "build/stage-dashboard.ts"], cliDir);
@@ -12838,11 +13018,11 @@ var EDGE_ACME_HOST_DIR = `${EDGE_HOST_STATE_DIR}/acme`;
12838
13018
  function edgeVolumeYaml(indent) {
12839
13019
  return EDGE_CONTAINER_MOUNTS.map((m2) => `${indent}- ${m2.host}:${m2.container}:z`).join("\n");
12840
13020
  }
12841
- var COMPOSE_DIR = join6(OS_DIR, "compose");
12842
- var INSTALL_METHOD_FILE = join6(OS_DIR, "install-method");
12843
- var COMPOSE_FILE = join6(COMPOSE_DIR, "docker-compose.yml");
12844
- var BUILD_FILE = join6(COMPOSE_DIR, "docker-compose.build.yml");
12845
- 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");
12846
13026
  var BUILT_SERVICES = [
12847
13027
  { service: "api", dockerfile: "apps/api/Dockerfile" },
12848
13028
  { service: "dashboard", dockerfile: "apps/dashboard/Dockerfile" },
@@ -12851,21 +13031,45 @@ var BUILT_SERVICES = [
12851
13031
  var ENV_CONSUMING_SERVICES = ["api", "dashboard", "edge"];
12852
13032
  function readInstallMethod() {
12853
13033
  try {
12854
- const v2 = readFileSync2(INSTALL_METHOD_FILE, "utf8").trim();
13034
+ const v2 = readFileSync3(INSTALL_METHOD_FILE, "utf8").trim();
12855
13035
  return v2 === "compose" || v2 === "bare" ? v2 : null;
12856
13036
  } catch {
12857
13037
  return null;
12858
13038
  }
12859
13039
  }
12860
13040
  function writeInstallMethod(method) {
12861
- mkdirSync2(OS_DIR, { recursive: true, mode: 448 });
12862
- 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;
12863
13070
  }
12864
13071
  function hasDockerCompose() {
12865
- const docker = spawnSync3("docker", ["version"], { stdio: "ignore" });
12866
- if (docker.status !== 0) return false;
12867
- const compose2 = spawnSync3("docker", ["compose", "version"], { stdio: "ignore" });
12868
- return compose2.status === 0;
13072
+ return dockerGap() === null;
12869
13073
  }
12870
13074
  function composeIsViableDefault() {
12871
13075
  return process.platform === "linux" && hasDockerCompose();
@@ -12876,9 +13080,18 @@ function hasCmd(cmd) {
12876
13080
  function shQuote(s2) {
12877
13081
  return `'${s2.replace(/'/g, `'\\''`)}'`;
12878
13082
  }
12879
- async function ensureDocker() {
12880
- 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;
12881
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
+ }
12882
13095
  const plan = systemCatalog.installs.docker({
12883
13096
  os: "linux",
12884
13097
  serviceManager: "systemd"
@@ -12886,12 +13099,36 @@ async function ensureDocker() {
12886
13099
  if (!plan.supported || !plan.installCommand) return false;
12887
13100
  const asRoot = typeof process.getuid === "function" && process.getuid() === 0;
12888
13101
  const sudo = !asRoot && hasCmd("sudo") ? "sudo " : "";
12889
- const sh = (script) => spawnSync3("sh", ["-c", sudo ? `${sudo}sh -c ${shQuote(script)}` : script], {
12890
- stdio: "inherit"
12891
- }).status ?? 1;
12892
- if (sh(plan.installCommand) !== 0) return false;
12893
- if (plan.startCommand) sh(plan.startCommand);
12894
- 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;
12895
13132
  }
12896
13133
  var COMPOSE_YAML = `# Managed by \`openship up\` \u2014 do not edit; re-run \`openship up\` to regenerate.
12897
13134
  services:
@@ -12899,6 +13136,12 @@ services:
12899
13136
  image: postgres:16-alpine
12900
13137
  restart: unless-stopped
12901
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}
12902
13145
  POSTGRES_USER: \${POSTGRES_USER:-openship}
12903
13146
  POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:?missing from .env \u2014 re-run openship up to regenerate it}
12904
13147
  POSTGRES_DB: \${POSTGRES_DB:-openship}
@@ -12994,7 +13237,7 @@ function keepSecret(existing, key) {
12994
13237
  function readEnvFile() {
12995
13238
  const out = {};
12996
13239
  try {
12997
- for (const line of readFileSync2(ENV_FILE, "utf8").split("\n")) {
13240
+ for (const line of readFileSync3(ENV_FILE, "utf8").split("\n")) {
12998
13241
  const m2 = line.match(/^([A-Z0-9_]+)=(.*)$/);
12999
13242
  if (m2) out[m2[1]] = m2[2];
13000
13243
  }
@@ -13014,10 +13257,10 @@ function rewriteHostAuthorizedKeys(existing, pub) {
13014
13257
  function provisionHostSshChannel() {
13015
13258
  if (process.platform !== "linux") return null;
13016
13259
  try {
13017
- const sshDir = join6(COMPOSE_DIR, "host-ssh");
13018
- mkdirSync2(sshDir, { recursive: true, mode: 448 });
13019
- const keyPath = join6(sshDir, "id_ed25519");
13020
- 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)) {
13021
13264
  const g2 = spawnSync3(
13022
13265
  "ssh-keygen",
13023
13266
  ["-t", "ed25519", "-N", "", "-q", "-f", keyPath, "-C", HOST_KEY_COMMENT],
@@ -13025,7 +13268,7 @@ function provisionHostSshChannel() {
13025
13268
  );
13026
13269
  if (g2.status !== 0) return null;
13027
13270
  }
13028
- const pub = readFileSync2(`${keyPath}.pub`, "utf8").trim();
13271
+ const pub = readFileSync3(`${keyPath}.pub`, "utf8").trim();
13029
13272
  if (!pub) return null;
13030
13273
  const user = (() => {
13031
13274
  try {
@@ -13034,12 +13277,12 @@ function provisionHostSshChannel() {
13034
13277
  return process.env.USER || process.env.LOGNAME || "root";
13035
13278
  }
13036
13279
  })();
13037
- const userSshDir = join6(homedir2(), ".ssh");
13038
- mkdirSync2(userSshDir, { recursive: true, mode: 448 });
13039
- const authKeys = join6(userSshDir, "authorized_keys");
13040
- 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") : "";
13041
13284
  const next = rewriteHostAuthorizedKeys(existing, pub);
13042
- if (next !== existing) writeFileSync(authKeys, next, { mode: 384 });
13285
+ if (next !== existing) writeFileSync2(authKeys, next, { mode: 384 });
13043
13286
  chmodSync(authKeys, 384);
13044
13287
  return { user, keyPath };
13045
13288
  } catch {
@@ -13050,6 +13293,11 @@ function composeProjectName(prev) {
13050
13293
  if (prev.COMPOSE_PROJECT_NAME) return prev.COMPOSE_PROJECT_NAME;
13051
13294
  return Object.keys(prev).length > 0 ? "compose" : "openship";
13052
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
+ }
13053
13301
  function orphanedStackContainers(project) {
13054
13302
  const r2 = spawnSync3(
13055
13303
  "docker",
@@ -13149,7 +13397,7 @@ function migrateLegacyEdgeVolumes(project) {
13149
13397
  const existing = new Set(ls.stdout.split("\n").map((l2) => l2.trim()).filter(Boolean));
13150
13398
  for (const { volume, host } of pairs) {
13151
13399
  if (!existing.has(volume)) continue;
13152
- mkdirSync2(host, { recursive: true });
13400
+ mkdirSync3(host, { recursive: true });
13153
13401
  const alreadyThere = spawnSync3("sh", ["-c", `ls -A ${JSON.stringify(host)} 2>/dev/null | head -1`], {
13154
13402
  encoding: "utf8"
13155
13403
  });
@@ -13201,9 +13449,11 @@ function keepConfig(prev, key, explicit) {
13201
13449
  }
13202
13450
  function resolveEnvConfig(prev, opts) {
13203
13451
  const publicUrl = keepConfig(prev, "OPENSHIP_PUBLIC_URL", opts.publicUrl);
13452
+ const bindAddr = keepConfig(prev, "OPENSHIP_BIND_ADDR");
13204
13453
  return {
13205
- apiPort: keepConfig(prev, "API_PORT", opts.apiPort) ?? "4000",
13206
- 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 } : {},
13207
13457
  ...publicUrl ? { publicUrl } : {},
13208
13458
  // A public URL always implies a proxy in front; otherwise keep whatever the
13209
13459
  // install was configured with.
@@ -13221,6 +13471,47 @@ function resolveEnvConfig(prev, opts) {
13221
13471
  hostControl: opts.noHostControl === void 0 ? prev.OPENSHIP_HOST_CONTROL !== "false" : !opts.noHostControl
13222
13472
  };
13223
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
+ }
13224
13515
  function renderEnv(opts, host, cfg) {
13225
13516
  const prev = readEnvFile();
13226
13517
  const lines = [
@@ -13234,13 +13525,21 @@ function renderEnv(opts, host, cfg) {
13234
13525
  // (hides the local row). Written explicitly so the policy is visible in .env.
13235
13526
  `OPENSHIP_HOST_CONTROL=${cfg.hostControl ? "true" : "false"}`,
13236
13527
  `OPENSHIP_IMAGE_REGISTRY=${cfg.registry}`,
13237
- `OPENSHIP_VERSION=${opts.version || (true ? "0.4.7" : "latest")}`,
13528
+ `OPENSHIP_VERSION=${opts.version || (true ? "0.5.0" : "latest")}`,
13238
13529
  `POSTGRES_PASSWORD=${keepSecret(prev, "POSTGRES_PASSWORD")}`,
13530
+ // Pinned once (see resolvePgData): fresh install → subdir, existing volume → root.
13531
+ `OPENSHIP_PGDATA=${resolvePgData(prev)}`,
13239
13532
  `BETTER_AUTH_SECRET=${keepSecret(prev, "BETTER_AUTH_SECRET")}`,
13240
13533
  `INTERNAL_TOKEN=${keepSecret(prev, "INTERNAL_TOKEN")}`,
13241
13534
  `API_PORT=${cfg.apiPort}`,
13242
- `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}`
13243
13541
  ];
13542
+ if (cfg.bindAddr) lines.push(`OPENSHIP_BIND_ADDR=${cfg.bindAddr}`);
13244
13543
  if (cfg.publicUrl) lines.push(`OPENSHIP_PUBLIC_URL=${cfg.publicUrl}`);
13245
13544
  if (cfg.extraTrustedOrigins) {
13246
13545
  lines.push(`OPENSHIP_EXTRA_TRUSTED_ORIGINS=${cfg.extraTrustedOrigins}`);
@@ -13260,7 +13559,7 @@ function renderEnv(opts, host, cfg) {
13260
13559
  function sourceBuildDir() {
13261
13560
  const marker = readSourceInstall();
13262
13561
  if (!marker?.dir) return null;
13263
- 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)));
13264
13563
  return hasAll ? marker.dir : null;
13265
13564
  }
13266
13565
  function renderBuildOverride(repoDir) {
@@ -13276,20 +13575,20 @@ services:
13276
13575
  ${services}`;
13277
13576
  }
13278
13577
  function materialize(opts) {
13279
- mkdirSync2(COMPOSE_DIR, { recursive: true, mode: 448 });
13578
+ mkdirSync3(COMPOSE_DIR, { recursive: true, mode: 448 });
13280
13579
  const prev = readEnvFile();
13281
13580
  const cfg = resolveEnvConfig(prev, opts);
13282
13581
  const host = cfg.hostControl ? provisionHostSshChannel() : null;
13283
13582
  let before = "";
13284
13583
  try {
13285
- before = readFileSync2(ENV_FILE, "utf8");
13584
+ before = readFileSync3(ENV_FILE, "utf8");
13286
13585
  } catch {
13287
13586
  }
13288
13587
  const rendered = renderEnv(opts, host, cfg);
13289
- writeFileSync(COMPOSE_FILE, COMPOSE_YAML);
13290
- writeFileSync(ENV_FILE, rendered, { mode: 384 });
13588
+ writeFileSync2(COMPOSE_FILE, COMPOSE_YAML);
13589
+ writeFileSync2(ENV_FILE, rendered, { mode: 384 });
13291
13590
  const buildDir = opts.build === false ? null : sourceBuildDir();
13292
- if (buildDir) writeFileSync(BUILD_FILE, renderBuildOverride(buildDir));
13591
+ if (buildDir) writeFileSync2(BUILD_FILE, renderBuildOverride(buildDir));
13293
13592
  return { buildDir, cfg, envChanged: before !== "" && rendered !== before };
13294
13593
  }
13295
13594
  function dbVolumeExists(project) {
@@ -13307,7 +13606,14 @@ function compose(args, opts) {
13307
13606
  });
13308
13607
  return r2.status ?? 1;
13309
13608
  }
13310
- function composeUp(opts) {
13609
+ function composePrefetch(opts) {
13610
+ const { buildDir } = materialize(opts);
13611
+ if (buildDir) {
13612
+ return compose(["pull", "postgres", "redis"], { withBuildOverride: true }) === 0 && compose(["build"], { withBuildOverride: true }) === 0;
13613
+ }
13614
+ return compose(["pull"]) === 0;
13615
+ }
13616
+ async function composeUp(opts) {
13311
13617
  const { buildDir, cfg, envChanged } = materialize(opts);
13312
13618
  const apiPort = cfg.apiPort;
13313
13619
  const dashPort = cfg.dashPort;
@@ -13323,12 +13629,20 @@ function composeUp(opts) {
13323
13629
  if (dbVolumeExists(project)) {
13324
13630
  reconcileDbPassword(env.POSTGRES_USER || "openship", env.POSTGRES_PASSWORD ?? "");
13325
13631
  }
13632
+ await sanitizeEdgeVhosts(
13633
+ new LocalExecutor(),
13634
+ EDGE_SITES_HOST_DIR,
13635
+ (l2) => console.log(` ${l2.message}`)
13636
+ ).catch(() => {
13637
+ });
13326
13638
  if (buildDir) {
13327
- if (compose(["pull", "postgres", "redis"], { withBuildOverride: true }) !== 0) {
13328
- return { ok: false, apiPort, dashPort };
13329
- }
13330
- if (compose(["build"], { withBuildOverride: true }) !== 0) {
13331
- return { ok: false, apiPort, dashPort };
13639
+ if (!opts.alreadyFetched) {
13640
+ if (compose(["pull", "postgres", "redis"], { withBuildOverride: true }) !== 0) {
13641
+ return { ok: false, apiPort, dashPort };
13642
+ }
13643
+ if (compose(["build"], { withBuildOverride: true }) !== 0) {
13644
+ return { ok: false, apiPort, dashPort };
13645
+ }
13332
13646
  }
13333
13647
  if (up({ withBuildOverride: true }) !== 0) {
13334
13648
  return { ok: false, apiPort, dashPort };
@@ -13337,7 +13651,9 @@ function composeUp(opts) {
13337
13651
  writeInstallMethod("compose");
13338
13652
  return { ok: true, apiPort, dashPort };
13339
13653
  }
13340
- if (compose(["pull"]) !== 0) return { ok: false, apiPort, dashPort };
13654
+ if (!opts.alreadyFetched && compose(["pull"]) !== 0) {
13655
+ return { ok: false, apiPort, dashPort };
13656
+ }
13341
13657
  if (up() !== 0) return { ok: false, apiPort, dashPort };
13342
13658
  onEdgeContainerChanged();
13343
13659
  writeInstallMethod("compose");
@@ -13347,12 +13663,12 @@ function onEdgeContainerChanged() {
13347
13663
  invalidateEdgeContainer();
13348
13664
  }
13349
13665
  function composeDown() {
13350
- if (!existsSync3(COMPOSE_FILE)) return false;
13666
+ if (!existsSync4(COMPOSE_FILE)) return false;
13351
13667
  return compose(["down"]) === 0;
13352
13668
  }
13353
13669
  function composeUninstall(opts = {}) {
13354
13670
  const removedImages = [];
13355
- 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;
13356
13672
  if (opts.removeImages) {
13357
13673
  const env = readEnvFile();
13358
13674
  const registry = env.OPENSHIP_IMAGE_REGISTRY || DEFAULT_IMAGE_REGISTRY;
@@ -13366,9 +13682,9 @@ function composeUninstall(opts = {}) {
13366
13682
  }
13367
13683
  return { ok, removedImages };
13368
13684
  }
13369
- function composeUpdate(version) {
13370
- if (!existsSync3(COMPOSE_FILE)) return false;
13371
- return composeUp(version ? { version } : {}).ok;
13685
+ async function composeUpdate(version) {
13686
+ if (!existsSync4(COMPOSE_FILE)) return false;
13687
+ return (await composeUp(version ? { version } : {})).ok;
13372
13688
  }
13373
13689
  function composePs() {
13374
13690
  return compose(["ps"]);
@@ -13403,6 +13719,15 @@ async function waitForApiHealth(port, tries) {
13403
13719
  }
13404
13720
  return false;
13405
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
+ }
13406
13731
  async function importMigratedSites(apiPort, sites, certPems) {
13407
13732
  const token = composeInternalToken();
13408
13733
  if (!token) {
@@ -13419,6 +13744,11 @@ async function importMigratedSites(apiPort, sites, certPems) {
13419
13744
  spinner.warn(`${error} \u2014 migrated sites not imported. Re-run \`openship up\` to retry.`);
13420
13745
  return { ok: false, registered: [], error };
13421
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
+ }
13422
13752
  try {
13423
13753
  const r2 = await fetch(`http://127.0.0.1:${apiPort}/api/system/edge/import-sites`, {
13424
13754
  method: "POST",
@@ -13478,16 +13808,29 @@ async function importMigratedSites(apiPort, sites, certPems) {
13478
13808
  export {
13479
13809
  OS_DIR,
13480
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,
13481
13820
  shortSha,
13482
13821
  prepareFromSource,
13483
13822
  readSourceInstall,
13484
13823
  remoteSha,
13485
13824
  rebuildFromSource,
13486
13825
  readInstallMethod,
13826
+ dockerGap,
13487
13827
  hasDockerCompose,
13488
13828
  composeIsViableDefault,
13489
13829
  ensureDocker,
13830
+ composeTrustedOriginUrls,
13831
+ resolveComposePorts,
13490
13832
  sourceBuildDir,
13833
+ composePrefetch,
13491
13834
  composeUp,
13492
13835
  composeDown,
13493
13836
  composeUninstall,
@@ -13498,5 +13841,6 @@ export {
13498
13841
  composeRestart,
13499
13842
  composeInternalToken,
13500
13843
  waitForApiHealth,
13844
+ waitForEdgeRunning,
13501
13845
  importMigratedSites
13502
13846
  };